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:
Richard
2026-07-02 10:47:14 +02:00
co-authored by Claude Opus 4.8
parent 26dd68a550
commit 3503bbb5d4
6 changed files with 409 additions and 2 deletions
+99
View File
@@ -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;
}
}
+115
View File
@@ -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();
}
}
}