Phase 5-UI (Proof-of-Pattern): Launcher-Shell + erste extrahierte View (Terminal)
- Ui/LauncherForm: schlanke Startleiste, rendert Buttons dynamisch aus den registrierten Views (gruppiert) + Legacy-Button fuer das alte Tab-frm_main. - Ui/ViewHostForm: generisches Host-Fenster fuer eine View (Control docked fill). - Ui/ShellUiHost : IModuleUiHost: sammelt Views, oeffnet Einzelinstanz-Fenster. - Ui/Views/TerminalView: erste designbare View (UserControl + Designer.cs); Terminal-Logik aus frm_main extrahiert; DI via Initialize(TerminalLogger) -> parameterloser Ctor bleibt fuer den VS-Designer nutzbar. - Program.cs: startet jetzt LauncherForm (statt frm_main), registriert die Terminal-View; App bleibt via Legacy-Button voll bedienbar. - Build 0 Fehler. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
26dd68a550
commit
3503bbb5d4
@@ -0,0 +1,105 @@
|
||||
using System;
|
||||
using System.Drawing;
|
||||
using System.Linq;
|
||||
using System.Windows.Forms;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
namespace PolyTraderSharp.Ui
|
||||
{
|
||||
/// <summary>
|
||||
/// Schlanke „Startleiste" der PolyTrader.App. Rendert Buttons aus den registrierten
|
||||
/// Views (gruppiert) und öffnet je View ein eigenständiges Fenster. Kennt selbst
|
||||
/// kein Modul. Enthält übergangsweise einen Legacy-Button für das alte Tab-frm_main.
|
||||
/// </summary>
|
||||
public class LauncherForm : Form
|
||||
{
|
||||
private readonly ShellUiHost _uiHost;
|
||||
private readonly IServiceProvider _services;
|
||||
private frm_main? _legacyForm;
|
||||
|
||||
public LauncherForm(ShellUiHost uiHost, IServiceProvider services)
|
||||
{
|
||||
_uiHost = uiHost;
|
||||
_services = services;
|
||||
|
||||
Text = "PolyTrader";
|
||||
Width = 360;
|
||||
Height = 520;
|
||||
StartPosition = FormStartPosition.CenterScreen;
|
||||
MinimizeBox = true;
|
||||
MaximizeBox = false;
|
||||
FormBorderStyle = FormBorderStyle.FixedSingle;
|
||||
|
||||
BuildUi();
|
||||
}
|
||||
|
||||
private void BuildUi()
|
||||
{
|
||||
var flow = new FlowLayoutPanel
|
||||
{
|
||||
Dock = DockStyle.Fill,
|
||||
FlowDirection = FlowDirection.TopDown,
|
||||
WrapContents = false,
|
||||
AutoScroll = true,
|
||||
Padding = new Padding(12)
|
||||
};
|
||||
|
||||
foreach (var group in _uiHost.Views
|
||||
.GroupBy(v => string.IsNullOrEmpty(v.Group) ? "Allgemein" : v.Group)
|
||||
.OrderBy(g => g.Key))
|
||||
{
|
||||
flow.Controls.Add(MakeHeader(group.Key));
|
||||
foreach (var view in group.OrderBy(v => v.Order).ThenBy(v => v.Title))
|
||||
{
|
||||
var captured = view;
|
||||
flow.Controls.Add(MakeButton(view.Title, (_, _) => _uiHost.OpenView(captured)));
|
||||
}
|
||||
}
|
||||
|
||||
// Übergangs-Zugang zur alten Tab-UI, bis alle Views extrahiert sind.
|
||||
flow.Controls.Add(MakeHeader("Übergang"));
|
||||
flow.Controls.Add(MakeButton("Legacy-UI (alte Tabs)", (_, _) => OpenLegacy()));
|
||||
|
||||
Controls.Add(flow);
|
||||
}
|
||||
|
||||
private Label MakeHeader(string text) => new()
|
||||
{
|
||||
Text = text,
|
||||
AutoSize = true,
|
||||
Font = new Font(Font, FontStyle.Bold),
|
||||
Margin = new Padding(0, 10, 0, 4)
|
||||
};
|
||||
|
||||
private Button MakeButton(string text, EventHandler onClick)
|
||||
{
|
||||
var btn = new Button
|
||||
{
|
||||
Text = text,
|
||||
Width = 310,
|
||||
Height = 34,
|
||||
Margin = new Padding(0, 2, 0, 2),
|
||||
TextAlign = ContentAlignment.MiddleLeft,
|
||||
UseVisualStyleBackColor = true
|
||||
};
|
||||
btn.Click += onClick;
|
||||
return btn;
|
||||
}
|
||||
|
||||
private void OpenLegacy()
|
||||
{
|
||||
if (_legacyForm != null && !_legacyForm.IsDisposed)
|
||||
{
|
||||
if (_legacyForm.WindowState == FormWindowState.Minimized)
|
||||
_legacyForm.WindowState = FormWindowState.Normal;
|
||||
_legacyForm.BringToFront();
|
||||
_legacyForm.Activate();
|
||||
return;
|
||||
}
|
||||
|
||||
_legacyForm = _services.GetRequiredService<frm_main>();
|
||||
_legacyForm.FormClosed += (_, _) => _legacyForm = null;
|
||||
_legacyForm.Show();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Windows.Forms;
|
||||
using PolyTrader.Core.Modularity;
|
||||
|
||||
namespace PolyTraderSharp.Ui
|
||||
{
|
||||
/// <summary>
|
||||
/// Sammelt die von Core-App und Modulen registrierten Views und öffnet sie als
|
||||
/// eigenständige Host-Fenster (Einzelinstanz je View).
|
||||
/// </summary>
|
||||
public class ShellUiHost : IModuleUiHost
|
||||
{
|
||||
private readonly List<ModuleView> _views = new();
|
||||
private readonly Dictionary<string, ViewHostForm> _open = new();
|
||||
|
||||
public IReadOnlyList<ModuleView> Views => _views;
|
||||
|
||||
public void RegisterView(ModuleView view) => _views.Add(view);
|
||||
|
||||
public void OpenView(ModuleView view)
|
||||
{
|
||||
if (_open.TryGetValue(view.Id, out var existing) && !existing.IsDisposed)
|
||||
{
|
||||
if (existing.WindowState == FormWindowState.Minimized)
|
||||
existing.WindowState = FormWindowState.Normal;
|
||||
existing.BringToFront();
|
||||
existing.Activate();
|
||||
return;
|
||||
}
|
||||
|
||||
var host = new ViewHostForm(view);
|
||||
host.FormClosed += (_, _) => _open.Remove(view.Id);
|
||||
_open[view.Id] = host;
|
||||
host.Show();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
using System.Windows.Forms;
|
||||
using PolyTrader.Core.Modularity;
|
||||
|
||||
namespace PolyTraderSharp.Ui
|
||||
{
|
||||
/// <summary>
|
||||
/// Generisches Host-Fenster für genau eine <see cref="ModuleView"/>. Erzeugt das
|
||||
/// (designbare) Control der View und dockt es formfüllend. Die Shell öffnet je View
|
||||
/// höchstens eine Instanz (siehe <see cref="ShellUiHost"/>).
|
||||
/// </summary>
|
||||
public class ViewHostForm : Form
|
||||
{
|
||||
public string ViewId { get; }
|
||||
|
||||
public ViewHostForm(ModuleView view)
|
||||
{
|
||||
ViewId = view.Id;
|
||||
Text = view.Title;
|
||||
Width = view.PreferredWidth;
|
||||
Height = view.PreferredHeight;
|
||||
StartPosition = FormStartPosition.CenterScreen;
|
||||
|
||||
var control = view.CreateControl();
|
||||
control.Dock = DockStyle.Fill;
|
||||
Controls.Add(control);
|
||||
}
|
||||
}
|
||||
}
|
||||
Generated
+99
@@ -0,0 +1,99 @@
|
||||
namespace PolyTraderSharp.Ui.Views
|
||||
{
|
||||
partial class TerminalView
|
||||
{
|
||||
/// <summary>Erforderliche Designer-Variable.</summary>
|
||||
private System.ComponentModel.IContainer components = null;
|
||||
|
||||
protected override void Dispose(bool disposing)
|
||||
{
|
||||
if (disposing && (components != null))
|
||||
{
|
||||
components.Dispose();
|
||||
}
|
||||
base.Dispose(disposing);
|
||||
}
|
||||
|
||||
#region Vom Komponenten-Designer generierter Code
|
||||
|
||||
private void InitializeComponent()
|
||||
{
|
||||
this.pnlTop = new System.Windows.Forms.Panel();
|
||||
this.btnAutoscroll = new System.Windows.Forms.Button();
|
||||
this.cbLogLevel = new System.Windows.Forms.ComboBox();
|
||||
this.lblFilter = new System.Windows.Forms.Label();
|
||||
this.rtbTerminal = new System.Windows.Forms.RichTextBox();
|
||||
this.pnlTop.SuspendLayout();
|
||||
this.SuspendLayout();
|
||||
//
|
||||
// pnlTop
|
||||
//
|
||||
this.pnlTop.Controls.Add(this.btnAutoscroll);
|
||||
this.pnlTop.Controls.Add(this.cbLogLevel);
|
||||
this.pnlTop.Controls.Add(this.lblFilter);
|
||||
this.pnlTop.Dock = System.Windows.Forms.DockStyle.Top;
|
||||
this.pnlTop.Location = new System.Drawing.Point(0, 0);
|
||||
this.pnlTop.Name = "pnlTop";
|
||||
this.pnlTop.Size = new System.Drawing.Size(900, 36);
|
||||
this.pnlTop.TabIndex = 0;
|
||||
//
|
||||
// btnAutoscroll
|
||||
//
|
||||
this.btnAutoscroll.Location = new System.Drawing.Point(224, 5);
|
||||
this.btnAutoscroll.Name = "btnAutoscroll";
|
||||
this.btnAutoscroll.Size = new System.Drawing.Size(150, 26);
|
||||
this.btnAutoscroll.TabIndex = 2;
|
||||
this.btnAutoscroll.Text = "Stop Autoscroll";
|
||||
this.btnAutoscroll.UseVisualStyleBackColor = true;
|
||||
//
|
||||
// cbLogLevel
|
||||
//
|
||||
this.cbLogLevel.DropDownStyle = System.Windows.Forms.ComboBoxStyle.DropDownList;
|
||||
this.cbLogLevel.Location = new System.Drawing.Point(52, 6);
|
||||
this.cbLogLevel.Name = "cbLogLevel";
|
||||
this.cbLogLevel.Size = new System.Drawing.Size(160, 23);
|
||||
this.cbLogLevel.TabIndex = 1;
|
||||
//
|
||||
// lblFilter
|
||||
//
|
||||
this.lblFilter.AutoSize = true;
|
||||
this.lblFilter.Location = new System.Drawing.Point(8, 10);
|
||||
this.lblFilter.Name = "lblFilter";
|
||||
this.lblFilter.Size = new System.Drawing.Size(38, 15);
|
||||
this.lblFilter.TabIndex = 0;
|
||||
this.lblFilter.Text = "Level:";
|
||||
//
|
||||
// rtbTerminal
|
||||
//
|
||||
this.rtbTerminal.BackColor = System.Drawing.Color.Black;
|
||||
this.rtbTerminal.Dock = System.Windows.Forms.DockStyle.Fill;
|
||||
this.rtbTerminal.ForeColor = System.Drawing.Color.White;
|
||||
this.rtbTerminal.Location = new System.Drawing.Point(0, 36);
|
||||
this.rtbTerminal.Name = "rtbTerminal";
|
||||
this.rtbTerminal.ReadOnly = true;
|
||||
this.rtbTerminal.Size = new System.Drawing.Size(900, 464);
|
||||
this.rtbTerminal.TabIndex = 1;
|
||||
this.rtbTerminal.Text = "";
|
||||
//
|
||||
// TerminalView
|
||||
//
|
||||
this.AutoScaleDimensions = new System.Drawing.SizeF(7F, 15F);
|
||||
this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
|
||||
this.Controls.Add(this.rtbTerminal);
|
||||
this.Controls.Add(this.pnlTop);
|
||||
this.Name = "TerminalView";
|
||||
this.Size = new System.Drawing.Size(900, 500);
|
||||
this.pnlTop.ResumeLayout(false);
|
||||
this.pnlTop.PerformLayout();
|
||||
this.ResumeLayout(false);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
private System.Windows.Forms.Panel pnlTop;
|
||||
private System.Windows.Forms.Label lblFilter;
|
||||
private System.Windows.Forms.ComboBox cbLogLevel;
|
||||
private System.Windows.Forms.Button btnAutoscroll;
|
||||
private System.Windows.Forms.RichTextBox rtbTerminal;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,115 @@
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Windows.Forms;
|
||||
using PolyTraderSharp.Services;
|
||||
|
||||
namespace PolyTraderSharp.Ui.Views
|
||||
{
|
||||
/// <summary>
|
||||
/// Terminal-/Log-Ansicht. Designbar (siehe TerminalView.Designer.cs). Die Laufzeit-
|
||||
/// Abhängigkeit (TerminalLogger) wird per <see cref="Initialize"/> injiziert, damit der
|
||||
/// parameterlose Konstruktor für den VS-Designer nutzbar bleibt.
|
||||
/// </summary>
|
||||
public partial class TerminalView : UserControl
|
||||
{
|
||||
private TerminalLogger? _logger;
|
||||
private readonly ConcurrentQueue<LogMessageEventArgs> _logQueue = new();
|
||||
private readonly System.Windows.Forms.Timer _uiLogTimer = new() { Interval = 250 };
|
||||
private bool _autoScroll = true;
|
||||
private EventHandler<LogMessageEventArgs>? _logHandler;
|
||||
|
||||
public TerminalView()
|
||||
{
|
||||
InitializeComponent();
|
||||
|
||||
cbLogLevel.Items.AddRange(new object[] { "Alle", "Debug", "Info", "Warning", "Error", "Trade", "TradeReasoning" });
|
||||
cbLogLevel.SelectedIndex = 0;
|
||||
|
||||
btnAutoscroll.BackColor = System.Drawing.Color.LightGreen;
|
||||
btnAutoscroll.Text = "Stop Autoscroll";
|
||||
btnAutoscroll.Click += (_, _) => ToggleAutoscroll();
|
||||
|
||||
_uiLogTimer.Tick += ProcessLogQueue;
|
||||
}
|
||||
|
||||
/// <summary>Verbindet die View mit dem Logger (Laufzeit-DI).</summary>
|
||||
public void Initialize(TerminalLogger logger)
|
||||
{
|
||||
_logger = logger;
|
||||
|
||||
// Jüngste Historie vorladen, damit das Fenster beim Öffnen nicht leer ist.
|
||||
foreach (var e in _logger.GetHistory(TimeSpan.FromMinutes(10)))
|
||||
_logQueue.Enqueue(e);
|
||||
|
||||
_logHandler = (_, e) => _logQueue.Enqueue(e);
|
||||
_logger.OnLogMessage += _logHandler;
|
||||
|
||||
_uiLogTimer.Start();
|
||||
Disposed += OnDisposed;
|
||||
}
|
||||
|
||||
private void OnDisposed(object? sender, EventArgs e)
|
||||
{
|
||||
_uiLogTimer.Stop();
|
||||
if (_logger != null && _logHandler != null)
|
||||
_logger.OnLogMessage -= _logHandler;
|
||||
}
|
||||
|
||||
private void ToggleAutoscroll()
|
||||
{
|
||||
_autoScroll = !_autoScroll;
|
||||
btnAutoscroll.BackColor = _autoScroll ? System.Drawing.Color.LightGreen : System.Drawing.Color.IndianRed;
|
||||
btnAutoscroll.Text = _autoScroll ? "Stop Autoscroll" : "Start Autoscroll";
|
||||
}
|
||||
|
||||
private void ProcessLogQueue(object? sender, EventArgs e)
|
||||
{
|
||||
if (_logQueue.IsEmpty || !IsHandleCreated) return;
|
||||
|
||||
string filter = cbLogLevel.SelectedItem?.ToString() ?? "Alle";
|
||||
TimeZoneInfo berlinTz = TimeZoneInfo.FindSystemTimeZoneById("W. Europe Standard Time");
|
||||
bool appended = false;
|
||||
int count = 0;
|
||||
const int maxProcess = 500;
|
||||
|
||||
rtbTerminal.SuspendLayout();
|
||||
|
||||
while (count < maxProcess && _logQueue.TryDequeue(out var logEvent))
|
||||
{
|
||||
count++;
|
||||
if (filter != "Alle" && logEvent.Level.ToString() != filter) continue;
|
||||
|
||||
DateTime logTime = logEvent.Timestamp.Kind == DateTimeKind.Utc
|
||||
? TimeZoneInfo.ConvertTimeFromUtc(logEvent.Timestamp, berlinTz)
|
||||
: TimeZoneInfo.ConvertTime(logEvent.Timestamp, berlinTz);
|
||||
string timeStr = $"[{logTime:HH:mm:ss}]";
|
||||
|
||||
System.Drawing.Color c = System.Drawing.Color.White;
|
||||
if (logEvent.Level == LogLevel.Error) c = System.Drawing.Color.Red;
|
||||
else if (logEvent.Level == LogLevel.Warning) c = System.Drawing.Color.Yellow;
|
||||
else if (logEvent.Level == LogLevel.Trade) c = System.Drawing.Color.LightGreen;
|
||||
else if (logEvent.Level == LogLevel.TradeReasoning) c = System.Drawing.Color.Orange;
|
||||
|
||||
rtbTerminal.SelectionStart = rtbTerminal.TextLength;
|
||||
rtbTerminal.SelectionLength = 0;
|
||||
rtbTerminal.SelectionColor = c;
|
||||
rtbTerminal.AppendText($"{timeStr} [{logEvent.Level}] {logEvent.Message}\n");
|
||||
appended = true;
|
||||
}
|
||||
|
||||
if (appended)
|
||||
{
|
||||
if (rtbTerminal.TextLength > 80000)
|
||||
{
|
||||
rtbTerminal.Clear();
|
||||
rtbTerminal.SelectionColor = System.Drawing.Color.LightPink;
|
||||
rtbTerminal.AppendText($"[{DateTime.Now:HH:mm:ss}] [System] Terminal Auto-Clear (RAM Limit erreicht). Vollständige Logs im Ordner /Logs.\n");
|
||||
}
|
||||
|
||||
if (_autoScroll) rtbTerminal.ScrollToCaret();
|
||||
}
|
||||
|
||||
rtbTerminal.ResumeLayout();
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user