====== Vega — Eigene Steuerelemente ====== Reicht der Vorrat nicht, baut man sich eins. Ein eigenes Steuerelement ist eine Klasse, die von [[lyx_-_programmiersprache:vega:controls:tcontrol|TControl]] erbt: Zustand als Felder, Darstellung in ''Paint'', Bedienung in den ''Do*''-Methoden. Mehr Mechanik gibt es nicht — dieselbe, die alle mitgelieferten Elemente benutzen. → [[lyx_-_programmiersprache:vega:guides|Vega-Guides]] · [[lyx_-_programmiersprache:vega:guides:zeichnen|Zeichnen]] · [[lyx_-_programmiersprache:vega:themes|Themes]] ---- ===== Das Gerüst ===== pub type TMeins = class extends TControl { Wert: int64; // eigener Zustand fn Create(): void { self.Init(); // IMMER zuerst self.Wert := 0; self.Height := 28; self.CanFocus := true; } override fn Paint(canvas: TCanvas): void { … } override fn DoMouseDown(x: int64, y: int64, button: int64): void { … } } pub fn NewMeins(): TMeins { return new TMeins(); } ''self.Init()'' setzt die Vorgaben von ''TControl'' — ohne diesen Aufruf sind Sichtbarkeit, Farben und Zustandsfelder undefiniert. ---- ===== Was sich überschreiben lässt ===== ^ Methode ^ Aufgabe ^ | ''Paint(canvas)'' | Die Darstellung | | ''PaintChildren(canvas)'' | Zeichnen der Kinder (selten nötig) | | ''DoMouseDown(x, y, button)'' / ''DoMouseUp'' / ''DoMouseMove'' | Maus | | ''DoClick()'' / ''DoDblClick()'' | Klick, Doppelklick | | ''DoMouseEnter()'' / ''DoMouseLeave()'' | Zeiger betritt/verlässt die Fläche | | ''DoKeyDown(key, mods)'' / ''DoKeyUp'' | Tastatur | | ''DoMouseWheel(delta)'' | Mausrad | | ''DoStartDrag()'', ''DoDragOver'', ''DoDragDrop'', ''DoEndDrag'' | Ziehen und Ablegen | | ''HitTest(x, y)'' | Welches Element liegt an dieser Stelle? | | ''StyleStates()'' | Welche Theme-Zustände gelten gerade (''ST_CHECKED'' usw.) | Die ''Do*''-Methoden sind der Weg für **eingebautes** Verhalten, die ''On*''-Felder der Weg für die **Anwendung**. Ein eigenes Element bedient beides: Es reagiert selbst (Griff verschieben) und meldet die Änderung nach außen (''OnChange''). ---- ===== Beispiel: ein Schieberegler ===== unit Main; import std.io; import Vega.Types; import Vega.Events; import Vega.Canvas; import Vega.Theme; import Vega.Control; import Vega.Font; import Vega.Strings; import Vega.Lists; import Vega.Style; import Vega.StdCtrls; import Vega.Backend; import Vega.BackendAuto; import Vega.Forms; var g_app: TApplication; var g_anzeige: TLabel; // Ein eigenes Steuerelement: waagerechter Schieberegler. // Zustand als Felder, Darstellung in Paint, Bedienung in den Do*-Methoden. pub type TRegler = class extends TControl { Wert: int64; // 0..100 Ziehen: bool; OnChange: TNotifyEvent; fn Create(): void { self.Init(); // IMMER zuerst: setzt die Vorgaben von TControl self.Wert := 50; self.Ziehen := false; self.Height := 28; self.CanFocus := true; // nimmt Tastatureingaben an self.TabStop := true; } fn SetWert(v: int64): void { var n: int64 := v; if (n < 0) { n := 0; } if (n > 100) { n := 100; } if (n == self.Wert) { return; } self.Wert := n; self.Invalidate(); // Setter zeichnet neu if (self.OnChange != 0) { self.OnChange(self as TControl); } } // Wert aus einer x-Position im Control fn WertAus(x: int64): int64 { var w: int64 := self.Width - 8; if (w <= 0) { return 0; } return ((x - 4) * 100) / w; } override fn Paint(canvas: TCanvas): void { var r: TRect := self.ClientRect(); canvas.FillRect(r, RGB(240, 240, 240)); canvas.FrameRect(r, RGB(150, 150, 150)); // Rille var mitte: int64 := RectHeight(r) / 2; canvas.FillRect(Rect(4, mitte - 2, RectWidth(r) - 4, mitte + 2), RGB(200, 200, 200)); // Griff — Farbe zeigt Fokus und Ziehen var gx: int64 := 4 + (self.Wert * (RectWidth(r) - 8)) / 100; var farbe: TColor := RGB(70, 130, 180); if (self.Hovered) { farbe := RGB(90, 150, 200); } if (self.Ziehen) { farbe := RGB(40, 90, 140); } canvas.FillRoundRect(Rect(gx - 5, 3, gx + 5, RectHeight(r) - 3), 3, farbe); if (self.Focused) { canvas.FrameRect(r, RGB(70, 130, 180)); } } override fn DoMouseDown(x: int64, y: int64, button: int64): void { self.Ziehen := true; self.SetWert(self.WertAus(x)); } override fn DoMouseMove(x: int64, y: int64, button: int64): void { if (self.Ziehen) { self.SetWert(self.WertAus(x)); } } override fn DoMouseUp(x: int64, y: int64, button: int64): void { self.Ziehen := false; self.Invalidate(); } override fn DoKeyDown(key: int64, modifiers: int64): void { if (key == KEY_LEFT) { self.SetWert(self.Wert - 1); } if (key == KEY_RIGHT) { self.SetWert(self.Wert + 1); } if (key == KEY_HOME) { self.SetWert(0); } if (key == KEY_END) { self.SetWert(100); } } override fn DoMouseWheel(delta: int64): void { self.SetWert(self.Wert + delta * 5); } } pub fn NewRegler(): TRegler { return new TRegler(); } fn OnReglerAenderung(sender: TControl): void { var r: TRegler := sender as TRegler; g_anzeige.SetCaption(StrConcat("Wert: ", IntToStr(r.Wert))); } fn OnBeenden(sender: TControl): void { g_app.Terminate(); } fn main(): int64 { g_app := new TApplication(NewPlatformBackend()); if (!g_app.Ready) { PrintLn("Kein Fenstersystem erreichbar."); return 1; } var f: TForm := new TForm(360, 180, "Eigenes Steuerelement"); g_anzeige := new TLabel("Wert: 50"); g_anzeige.SetBounds(16, 16, 200, 18); g_anzeige.SetParent(f); var reg: TRegler := NewRegler(); reg.SetBounds(16, 48, 320, 28); reg.OnChange := OnReglerAenderung; reg.SetParent(f); var b: TButton := new TButton("Beenden"); b.SetBounds(216, 110, 120, 28); b.OnClick := OnBeenden; b.SetParent(f); if (!g_app.ShowForm(f)) { return 2; } f.Dispatcher.SetFocus(reg as TControl); g_app.Run(); g_app.Shutdown(); return 0; } Übersetzen: ''lyxc regler.lyx -I . -I platform/linux -o regler''. Nachgemessen mit ''lyxc 1.1.3I'', übersetzt fehlerfrei. ---- ===== Die fünf Regeln ===== - **''self.Init()'' als erste Zeile im ''Create''.** - **''Paint'' rechnet nicht, es zeichnet.** Alles, was aus Ereignissen kommt, steht vorher in Feldern. - **Setter statt nackter Felder.** ''SetWert'' begrenzt den Bereich, zeichnet neu und ruft ''OnChange'' — wer ''reg.Wert := 200'' schreibt, umgeht all das. Genau dafür gibt es in Delphi ''property''; hier ist es Disziplin. - **Zustand nach außen melden.** Ein Element ohne ''On*''-Feld lässt sich nicht in eine Anwendung einbauen. - **Nach jeder Änderung ''Invalidate()''** — am besten im Setter, dann kann es niemand vergessen. ---- ===== Wer über die eigenen Grenzen zeichnet, muss ''HitTest'' überschreiben ===== Ein aufklappender Teil — eine Liste, ein Kalender — liegt **unterhalb** des Controls und damit außerhalb von ''ClientRect()''. Klicks kommen dort trotzdem nicht an, solange der Treffertest sie nicht durchlässt: * [[lyx_-_programmiersprache:vega:units:dispatch|Vega.Dispatch]] fragt ''Root.HitTest(x, y)''. * Die Basisfassung in [[lyx_-_programmiersprache:vega:units:control|Vega.Control]] beginnt mit ''if (!RectContains(self.ClientRect(), x, y)) { return null; }''. Ohne eigenes ''HitTest'' ist der Auswahlpfad im aufgeklappten Teil also **toter Code** — er sieht richtig aus, wird aber nie erreicht. Das Muster: override fn HitTest(x: int64, y: int64): TControl { if (!self.Visible) { return null; } if (self.DroppedDown) { if (RectContains(self.DropRect(), x, y)) { return self as TControl; } } return super.HitTest(x, y); } So arbeiten [[lyx_-_programmiersprache:vega:controls:tdatetimepicker|TDateTimePicker]] und [[lyx_-_programmiersprache:vega:controls:tcombobox|TComboBox]]. **Menüs sind nicht betroffen**: sie laufen über ''TForm.ActivePopup'' und das Overlay, nicht über den Treffertest. **Geometrie nur einmal ausrechnen.** ''DropRect()'' gehört in eine Methode, die Zeichnen **und** Treffertest fragen. Zwei getrennte Rechnungen für dasselbe Rechteck laufen früher oder später auseinander — dann trifft der Klick eine andere Zeile als die, die unter dem Zeiger steht. ---- ===== Aussehen an das Theme hängen ===== Wer sein Element themefähig machen will, holt die Farben aus dem Stylesheet statt sie festzuschreiben: self.StyleType := StyleRegistry().TypeId("meins"); // im Create … var st: TStyleSet := self.ResolvedStyle(); // im Paint canvas.FillRect(r, st.GetOr(SP_BACKGROUND, self.Color)); st.Destroy(); Zusätzlich meldet ''override fn StyleStates()'', welche Zustände gerade gelten (''ST_CHECKED'', ''ST_HOVER'' …) — damit greifen Regeln wie ''checked { … }'' aus einem ''.vtheme'' auch am eigenen Element. Einzelheiten in der [[lyx_-_programmiersprache:vega:themes|Themes-Referenz]]. ---- ===== Wann ein eigenes Element, wann eine PaintBox ===== ^ Fall ^ Weg ^ | Nur zeichnen, kein Zustand, kein Verhalten | [[lyx_-_programmiersprache:vega:controls:tpaintbox|TPaintBox]] mit ''OnPaint'' | | Zustand plus Bedienung, einmal gebraucht | ''TPaintBox'' plus Modulvariablen — geht, bleibt aber unübersichtlich | | Zustand plus Bedienung, mehrfach gebraucht | eigene Klasse | | Vorhandenes Element, nur andere Optik | Theme oder ''SetRole'' | ---- ===== Fallstricke ===== * **''self.Init()'' vergessen.** Das Element verhält sich zufällig — Sichtbarkeit und Zustandsfelder sind nicht gesetzt. * **''CanFocus'' nicht gesetzt**, aber ''DoKeyDown'' überschrieben: Es kommen nie Tasten an. * **Direkt in ''Width''/''Height'' gerechnet** statt in ''ClientRect()'' — dann zeichnet man über den Rahmen. * **Kein ''Invalidate''** nach der Zustandsänderung: Das Bild bleibt stehen. * **Über die eigenen Grenzen gezeichnet, ohne ''HitTest'' zu überschreiben** — siehe oben; der Klick kommt nie an. * **Dieselbe Geometrie an zwei Stellen gerechnet** (Zeichnen und Treffertest): Sie laufen auseinander, sobald sich eine der beiden ändert. * **''DoAccept'' auf einem [[lyx_-_programmiersprache:vega:controls:tmonthcalendar|TMonthCalendar]] gerufen** — die Methode gibt es nicht. Die Wahl kommt über Doppelklick oder ''KEY_RETURN'', beides feuert ''OnAccept''. * **Inline-Array von Kindern** (''%%[N]TControl%%'') — hielt bis lyxc 1.1.2G Kopien statt Referenzen ([[https://github.com/SEOLizer/LyX-Compiler/issues/1646|#1646]]); [[lyx_-_programmiersprache:vega:controls:tobjectlist|TObjectList]] ist der ruhigere Weg. Letzte Aktualisierung: 2026-08-20 — überschreibbare Methoden aus ''Vega/Control.lyx'' erhoben; Beispiel mit ''lyxc 1.1.3I'' übersetzt, der ''HitTest''-Abschnitt gegen ''Vega/DateTimePicker.lyx'' und ''Vega/ListCtrls.lyx'' (Stand 2026-08-20) geprüft.