diff --git a/Program.cs b/Program.cs index 4a3cd94..112e2c2 100644 --- a/Program.cs +++ b/Program.cs @@ -91,6 +91,8 @@ internal static class Program services.AddHostedService((IServiceProvider sp) => sp.GetRequiredService()); services.AddHostedService((IServiceProvider sp) => sp.GetRequiredService()); services.AddTransient(); + services.AddSingleton(); + services.AddTransient(); }).Build(); try @@ -122,8 +124,29 @@ internal static class Program catch { } AppHost.Start(); - frm_main requiredService = AppHost.Services.GetRequiredService(); - Application.Run(requiredService); + + // Shell-Views registrieren (Core-App-Views; Module folgen via module.RegisterUi). + var uiHost = AppHost.Services.GetRequiredService(); + var viewServices = AppHost.Services; + uiHost.RegisterView(new PolyTrader.Core.Modularity.ModuleView + { + Id = "core.terminal", + Title = "Terminal", + Group = "Core", + Order = 10, + PreferredWidth = 1000, + PreferredHeight = 650, + CreateControl = () => + { + var view = new PolyTraderSharp.Ui.Views.TerminalView(); + view.Initialize(viewServices.GetRequiredService()); + return view; + } + }); + // TODO Phase 5-UI: registrierte IPolyTraderModule durchlaufen und module.RegisterUi(uiHost) aufrufen. + + var launcher = AppHost.Services.GetRequiredService(); + Application.Run(launcher); AppHost.StopAsync().GetAwaiter().GetResult(); } } diff --git a/Ui/LauncherForm.cs b/Ui/LauncherForm.cs new file mode 100644 index 0000000..9567c8b --- /dev/null +++ b/Ui/LauncherForm.cs @@ -0,0 +1,105 @@ +using System; +using System.Drawing; +using System.Linq; +using System.Windows.Forms; +using Microsoft.Extensions.DependencyInjection; + +namespace PolyTraderSharp.Ui +{ + /// + /// 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. + /// + 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(); + _legacyForm.FormClosed += (_, _) => _legacyForm = null; + _legacyForm.Show(); + } + } +} diff --git a/Ui/ShellUiHost.cs b/Ui/ShellUiHost.cs new file mode 100644 index 0000000..a501509 --- /dev/null +++ b/Ui/ShellUiHost.cs @@ -0,0 +1,37 @@ +using System.Collections.Generic; +using System.Windows.Forms; +using PolyTrader.Core.Modularity; + +namespace PolyTraderSharp.Ui +{ + /// + /// Sammelt die von Core-App und Modulen registrierten Views und öffnet sie als + /// eigenständige Host-Fenster (Einzelinstanz je View). + /// + public class ShellUiHost : IModuleUiHost + { + private readonly List _views = new(); + private readonly Dictionary _open = new(); + + public IReadOnlyList 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(); + } + } +} diff --git a/Ui/ViewHostForm.cs b/Ui/ViewHostForm.cs new file mode 100644 index 0000000..4bb5443 --- /dev/null +++ b/Ui/ViewHostForm.cs @@ -0,0 +1,28 @@ +using System.Windows.Forms; +using PolyTrader.Core.Modularity; + +namespace PolyTraderSharp.Ui +{ + /// + /// Generisches Host-Fenster für genau eine . Erzeugt das + /// (designbare) Control der View und dockt es formfüllend. Die Shell öffnet je View + /// höchstens eine Instanz (siehe ). + /// + 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); + } + } +} diff --git a/Ui/Views/TerminalView.Designer.cs b/Ui/Views/TerminalView.Designer.cs new file mode 100644 index 0000000..dd67abd --- /dev/null +++ b/Ui/Views/TerminalView.Designer.cs @@ -0,0 +1,99 @@ +namespace PolyTraderSharp.Ui.Views +{ + partial class TerminalView + { + /// Erforderliche Designer-Variable. + 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; + } +} diff --git a/Ui/Views/TerminalView.cs b/Ui/Views/TerminalView.cs new file mode 100644 index 0000000..52d7b61 --- /dev/null +++ b/Ui/Views/TerminalView.cs @@ -0,0 +1,115 @@ +using System; +using System.Collections.Concurrent; +using System.Windows.Forms; +using PolyTraderSharp.Services; + +namespace PolyTraderSharp.Ui.Views +{ + /// + /// Terminal-/Log-Ansicht. Designbar (siehe TerminalView.Designer.cs). Die Laufzeit- + /// Abhängigkeit (TerminalLogger) wird per injiziert, damit der + /// parameterlose Konstruktor für den VS-Designer nutzbar bleibt. + /// + public partial class TerminalView : UserControl + { + private TerminalLogger? _logger; + private readonly ConcurrentQueue _logQueue = new(); + private readonly System.Windows.Forms.Timer _uiLogTimer = new() { Interval = 250 }; + private bool _autoScroll = true; + private EventHandler? _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; + } + + /// Verbindet die View mit dem Logger (Laufzeit-DI). + 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(); + } + } +}