Beispiel für selbstgezeichnete Controls
Eigentlich eine Jugendsünde von mir: Ohne zu wissen, wie Controls eigentlich funktionieren, habe ich mit dem Graphics-Objekt experimentiert. Ist aber was ganz cooles rausgekommen. Dieses Beispiel habe ich noch keinem Refactoring unterzogen, inzwischen bin ich wohl dazu in der Lage, "richtige Controls" daraus zu machen - aber alles braucht seine Zeit. So sind die folgenden Kommentare noch aus VB2003-Zeiten.
Arbeiten mit Windows soll einfach sein und Spaß machen. Die Steuerelemente, die im
.NET Framework schon fertig definiert sind, erfüllen zwar die meisten Aufgaben, sind aber, was die
Gestaltung betrifft, ziemlich nüchtern.
So brauchte ich ein Control, das Daten aus
einem TreeView anzeigen kann - mit Icon und Text. Damit wollte ich Zeilen und Spalten für Tabellen
definieren, die dann automatisch aus einer Datenbank berechnet werden.
Die Klasse
zum Speichern meiner TreeNode - Informationen hab ich VisibleNode genannt, sie ist vom TreeNode
abgeleitet und enthält zusätzliche von mir benötigte Variablen.
Ein kleiner Code-Ausschnitt:
Public ClassVisibleNode
InheritsTreeNode
"damit wird die Klasse vom TreeNode abgeleitet
Public
NodeData As
String
Public Sub New
(ByVal
xText As String,
ByValxNodeData
As String,…)
MyBase.New(xText)
NodeData =
xNodeData
"natürlich wird hier außerdem noch der
ImageIndex fesgelegt…
End
Sub
End Class
So hab ich also ne Klasse, die alles kann, was ein TreeNode kann undzusätzlich noch
beliebige Daten enthält. Sie ist ganz normal in einemTreeView-Steuerelement verwendbar.
Nun zum wichtigsten - dem Control. Es soll einen VisibleNode aufnehmen und
anzeigen können. Ich nenne es malInfoBox.
Zunächst vom Control ableiten, und eine
private Variable + Property für den enthaltenen VisibleNode hinzufügen:
Public ClassInfoBox
Inherits Control
Private _usedNode
As VisibleNode…
Public Property usedNode()
As VisibleNode
Get
Return _usedNode
End Get
Set
(ByVal
Value As VisibleNode)
_usedNode = Value
End Set
End Property
End
Class
Real hab ichs in meinem Control ein bisschen anders gemacht, unter anderem gehört noch Code dazu, dass jedes mal, wenn der _usedNode sich ändert, die Anzeige des Steuerelementes aktualisiert wird.
Was das Control anzeigt, wird beim Ausführen der Methode "OnPaint" festgelegt. Dazu wird diese Methode in meinem Control überschrieben.
Protected Overrides Sub OnPaint(
ByVal e
AsSystem.Windows.Forms.PaintEventArgs)
MyBase.OnPaint(e)
DrawControl(e.Graphics)
End Sub
Die Prozedur DrawControl enthält dann meine eigenen Vorgaben zum Zeichnendes
Controls.
Hier kann ich den Hintergrund meines Controls mit einer Farbe ausfüllen,
das gewünschte Icon zeichnen und natürlich den Text, den mein VisibleNode enthält. Dann kommt noch
ein Rahmen für das Control. (da mein Steuerelementviel können soll, ist der Code natürlich zu
umfangreich, um hier aufgeführt zu werden)
Was gäbe es dafür für Tipps?
(1) Wichtig ist die Reihenfolge des Zeichnens: Was zu sehen sein soll, muss zuletzt
gezeichnet werden. (ich kann nicht zuerst einen Text zeichnen und danach die Fläche mit einer Farbe
ausfüllen - dann sehe ich nichts mehr vom Text)
(2) Wenn man selbst einen Fehler im
Code zum Zeichnen des Controls gemacht hat, kann es passieren, dass der Debugger die zugehörige
Zeile nicht findet. Es wird nur die erste Code-Zeile einer Klasse grün markiert, und man weiss dann
gar nicht, was man nun schon wieder falsch gemacht hat. Meine Technik zum Debuggen eines solchen
Fehlers: Den nach OnPaint aufgerufenen Code bis auf den zum Rahmenzeichnen deaktivieren, (dann wird
das Control wenigstens als Rahmen angezeigt)und dann schrittweise die einzelnen Code-Fragmente
wieder aktivieren. Irgendwann wird dann wieder ne Fehlermeldung angezeigt - und diese Zeile enthält
den Fehler.
(3) Für verwendete Pens und Brushes sollte, wenn sie nicht mehr
benötigt werden, die Dispose-Methode aufgerufen werden. Das spart Ressourcen.
(4)
Die OnPaint-Prozedur wird ziemlich häufig wieder aufgerufen, auch wenn sich der Inhalt, den das
Control anzeigen soll, gar nicht geändert hat. Und wenn das Control dann jedesmal mit allen Details
im Code neu gezeichnet werden muss (die Prozeduren brauchen Zeit und Speicher), fängt es entweder an
zu flimmern, oder das Programm wird insgesamt langsamer. Ein übliches Vorgehen, um das zu vermeiden,
ist das Zwischenspeichern des angezeigten Controls als Bitmap. Beim ersten Zeichnen des
Steuerelementes wird dieses Bitmap angelegt, und jedes Mal, wenn die OnPaint-Methode wieder
aufgerufen wird, wird eigentlich nur das Bitmap gezeichnet. Erst wenn das Steuerelement was neues
anzeigen soll, wird das Bitmap gelöscht. Wie man das machen kann, ist dem Demo-Projekt zu entnehmen.
(5) Als zusätzliche Möglichkeit zum Vermeiden von Flimmern (das gerade auf
langsamen Rechnern oder bei großen Controls auftritt) besteht die Aktivierung der Doppelpufferung.
Dies geschieht bei mir in der Klassendeklaration.
Public Sub New()
MyBase.New()
SetStyle(ControlStyles.DoubleBuffer, True
)
SetStyle(ControlStyles.UserPaint,
True)
SetStyle(ControlStyles.AllPaintingInWmPaint, True
)
Me.BackColor = Color.White
End Sub
(6) Wann muss das Neuzeichnen des Controls erzwungen werden?
· jedes Mal, wenn
seine Größe sich ändert, "Überschreiben der Methode OnResize
· jedes Mal, wenn es enabled/disabled wird,
"Überschreiben der Methode OnEnabledChanged
· immer, wenn sich der
Inhalt des Controls (bei mir der VisibleNode) ändert. "z.B.
beim Ändern der Property-Eigenschaft usedNode()
Um das Neuzeichnen des Controls zu erzwingen, wird es als ungültig erklärt
(Invalidate())
Da ich den Inhalt meines Controls als
Bitmap zwischenspeichere, muss vorher das Bitmap als ungültig erklärt werden.
(_bitmap = Nothing
)
(7) Wie legt man ein Projekt am besten an, um ein Control zu entwerfen?
· Erst
ein beliebiges Windows-Forms-Projekt anlegen
· Dann über den Menüpunkt Datei ein
neues Projekt vom Typ "Klassenbibliothek" hinzufügen. In dem
Klassenbibliotheks-Projektlegt man eine neue Klasse an, die mindestens den folgenden Code enthält:
Imports System.Windows.Forms
Imports System.Drawing
Imports System.Drawing.Drawing2D
Public Class EigenesControl
Inherits Control
Protected Overrides SubOnPaint(
ByVal e
As System.Windows.Forms.PaintEventArgs)
MyBase.OnPaint(e)
ControlPaint.DrawBorder3D(e.Graphics, Me
.ClientRectangle, Border3DStyle.Sunken)
End Sub
End Class
· Man wird merken, dass im Klassenbibliotheks-Projekt noch Verweise zu
System.Windows.Forms und System.Drawing hinzugefügt werden müssen.
· Dann im
Menüpunkt "Erstellen" "Projektmappe neuerstellen"
· Dann das
vorher angelegte Windows-Forms-Projekt aktivieren, das Formular, auf dem das eigene Control
angezeigt werden soll, im Projektmappen-Explorer auswählen.
· Dann auf der Toolbox,
in der die normalen Windows-Steuerelemente vorhandensind, Klick mit rechter Maustaste, "Toolbox
anpassen" und im folgenden Fenster den Reiter ".NET Framework Komponenten" auswählen,
dann "Durchsuchen". Hier den Ort auswählen, an dem die vorhin angelegte Klassenbibliothek
befindet. Im bin-oder debug-Verzeichnisbefindet sich die benötigte DLL. Diese mit Doppelklick
auswählen. Nun befindet sich an unterster Stelle unserer Toolbox ein neues Control, das beliebig in
das Windows-Formulareingefügt werden kann.
· Fertig: Das Control (erst mal ein
leerer Rahmen) wird angezeigt, und nun kann man es in der Klassenbibliothek nach Bedarf abwandeln.
Um die Ergebnisse zu überprüfen, kann man immer wieder mal das Windows-Forms-Projekt starten,
Änderungen am Control werden dann angezeigt.
Mein Beispielprojekt enthält noch mehr selbstgezeichnete Controls, z.B. eine "InfoParkBox", die mehrere VisibleNodes speichern und anzeigen kann, und ein DummyGrid-Control, das hier nur das Vorhandensein eines Grid-Steuerelementes simulieren soll. Wer eigene Controls zeichnen will, sollte sich vielleicht zuerst den Code des DummyGrid ansehen, er ist kurz und einfachverständlich.
Daneben hab ich in das Testformular noch ein paar Methoden eingebaut, um eine InfoBox
dem Mauszeiger folgen zu lassen. Damit kann sichtbar ein Drag &Drop-Vorgang simuliert werden.
In der Klasse VisibleDragControls sind für die Funktion wichtige Controls in einer
Arraylist aufgelistet, die Klasse macht Daten zur Position und Größe des Controls verfügbar, über
dem sich die Maus gerade befindet. Daneben enthält sie noch Methoden, die die Weitergabe der
VisibleNodes von Control zu Control regulieren.
Nun wünsche ich erst mal viel Spaß beim Ausprobieren, wenn jemand dabei ein interessantes neues Control fertig hat, wäre ich sehr daran interessiert, es mal anzusehen.
Thomas Bergner