====== 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.