Visuelles Drag & Drop

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.

Demo zum Visual Drag & Drop Project

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