Artikel

November 2003 | Artikel

Das Komponenten-Brevier

(Link zum Artikel: http://www.it-republik.de/dotnet/artikel/0349)

Fünfte Lieferung: Designer für Windows-Controls

Text: von Peter Pohmann
Mit einem eigenen Designer können Sie die grafische Darstellung Ihrer Komponente im Formular-Entwurfsfenster beeinflussen, eigene Befehle zum Eigenschaftsfenster hinzufügen und Eigenschaften anbieten, die ausschließlich zur Entwurfszeit vorhanden sind. Als Anwendungsbeispiel erstellen wir ein Label, dessen Höhe sich beim Eingeben des Texts automatisch an die Anzahl der Zeilen anpasst. Dabei werden auch Eigenschafts-Deskriptoren eine große Rolle spielen.

Neben dem UITypeEditor und dem TypeConverter ist der Designer die dritte Möglichkeit der Entwurfszeitunterstützung für selbst geschriebene Komponenten. In allen drei Fällen verbindet man eine Entwurfszeit-Klasse mit der Komponente über ein Attribut und beeinflusst damit deren Verhalten in der Visual Studio IDE. Aus diesem Trio sind Designer die mächtigste Schnittstelle, die das .NET Framework zur Steuerung des Entwurfszeitverhaltens bereitstellt. Auch die grafischen Editoren in Visual Studio selbst sind solche Designer und theoretisch könnten Sie auf diese Weise einen eigenen Formular-Designer oder Ähnliches implementieren. Doch wahrscheinlich wollen Sie gar nicht so weit hinaus. Auch die häufiger benutzten Fähigkeiten der Designer-Schnittstelle sind schon interessant genug. Sehen wir uns zuerst einmal die Vererbungshierarchie an (siehe Abb. 1).

Die grundlegende Schnittstelle ist wie so oft im .NET Framework von beeindruckender Schlichtheit. Ein Designer kennt selbstverständlich die Komponente, die er gerade bearbeitet (Component) und kann bei einem Doppelklick auf diese Komponente eine Standard-Aktion ausführen (DoDefaultAction). Darüber hinaus verfügt er über eine Initialisierungs-Methode, die ihm die zu bearbeitende Komponente zuweist (Initialize). Das Spannendste an dieser Schnittstelle ist die Eigenschaft Verbs. Hier wird eine Kollektion von Menübefehlen zurückgeliefert, die dann im Kontextmenü und im Eigenschaftsfenster erscheinen.
Neue Befehle für die Komponente
Sie haben sicher schon bemerkt, dass beispielweise beim DataAdapter das Eigenschaftsfenster nicht nur eine Liste der Eigenschaften inklusive Erklärung für die aktuelle Auswahl enthält, sondern auch Befehle wie Datenadapter konfigurieren..., DataSet generieren..., die als Hyperlinks in einem eigenen Feld auftauchen. Die gleichen Befehle sieht man auch im Kontext-Menü des DataAdapters. Diese Menübefehle stellt der Designer des DataAdapters über die Eigenschaft Verbs bereit. Streng genommen ist ein solches Verb (DesignerVerb) sogar ein bisschen mehr als ein Menübefehl (MenuCommand), weil es im Gegensatz zu diesem auch noch einen eigenen Text für den Menüpunkt definieren kann. Für die Muster-Freunde unter unseren Lesern: An dieser Stelle implementiert das .NET Framework das Command Pattern und erreicht so seine hervorragende Erweiterbarkeit.

In den von IDesigner abgeleiteten Klassen kommen zuerst allgemein verwendbare Fähigkeiten für Komponenten hinzu und schließlich spezielle Features für Steuerelemente. Wobei dann schon wieder zwischen Win Forms und Web Forms unterschieden werden muss. In dieser Ausgabe des Komponenten-Breviers konzentrieren wir uns auf die Win Forms und besprechen die wichtigsten Möglichkeiten des ControlDesigners.

Als Anschauungsmaterial soll uns diesmal eine eigene Label-Komponente mit verbesserter automatischer Größenanpassung dienen. Das Standard-Label kann ja seine Breite an die Menge des eingetragenen Texts anpassen, nicht jedoch seine Höhe. Deshalb erstellen wir mit Datei | Neu | Projekt...| Visual C#-Projekte | Klassenbibliothek unter dem Namen KB5Components eine neue Assembly. Fügen Sie dem Projekt einen Verweis auf System.Windows.Forms hinzu, benennen Sie die Klasse in AutoHeightLabel um und leiten Sie sie von Label ab. Dann fügen Sie die neue Methode zum Anpassen der Höhe hinzu. Glücklicherweise verfügt die Graphics-Klasse über genau die Methode, die wir brauchen. Mit MeasureString kann die Höhe eines mehrzeiligen Strings bei vorgegebener Breite und Schriftart berechnet werden:
  1. internal void ResizeHeight()
  2. {
  3. Graphics graphics = CreateGraphics();
  4. SizeF size = graphics.MeasureString(Text, Font, Width);
  5. Height = (int)size.Height;
  6. graphics.Dispose();
  7. }
Nun kommt der Designer ins Spiel: Eine von ControlDesigner abgeleitete Klasse, für die man auch den Verweis auf System.Design zum Projekt hinzufügen muss. Der Designer überschreibt die Eigenschaft Verbs und erstellt darin eine neue DesignerVerbCollection, der er dann auch gleich das gewünschte DesignerVerb hinzufügt. In der einfachen Ausführung besteht es aus dem Text für den Menüpunkt und einem Delegaten, der die Ausführung des Kommandos übernimmt:
  1. public override DesignerVerbCollection Verbs
  2. {
  3. get {
  4. DesignerVerbCollection result = new DesignerVerbCollection();
  5. result.Add(new DesignerVerb("Höhe anpassen", new EventHandler(AdjustHeightHandler)));
  6. return result;
  7. }
  8. }
Der zugehörige Handler hat nicht viel zu tun. Es genügt, die obige Methode ResizeHeight beim AutoHeightLabel aufzurufen und anschließend dafür zu sorgen, dass sich der Positionsrahmen auf dem Formular an die neue Höhe anpasst.
Der TypeDescriptor weiß alles
Dies erreicht man dadurch, dass der Designer seine eigene Methode RaiseComponentChanged aufruft und ihr sowohl die geänderte Eigenschaft als auch deren vorherigen und neuen Wert übergibt. Die Eigenschaft wird an dieser Stelle durch ihre Beschreibung in Form eines PropertyDescriptors angegeben. Ein PropertyDescriptor enthält alle Angaben über eine Eigenschaft wie zum Beispiel den Namen, den Typ, die angegebenen Attribute, den zugeordneten Typ-Konverter und so weiter. Noch interessanter sind die Methoden: Hier kann man unter anderem in generischer Weise den aktuellen Wert abfragen sowie Handler anmelden, die bei einer Änderung des Eigenschaftswerts aufgerufen werden. Diese letzte Fähigkeit werden wir etwas später noch ausnutzen. Für den Augenblick genügt jedoch der PropertyDescriptor als solcher. Man kann ihn sich vom TypeDescriptor abholen, der ja die Grundlage jeglicher Reflektion in .NET ist. Mit
  1. PropertyDescriptorCollection pdc = TypeDescriptor.GetProperties(Control);
  2. PropertyDescriptor pd = pdc.Find("Height", false);
sucht man aus der Liste aller Eigenschafts-Beschreibungen des Controls diejenige heraus, die zum Eigenschaftsnamen Height gehört. Mit dem so ermittelten PropertyDescriptor ruft man dann RaiseComponentChanged auf und gibt zusätzlich den vorherigen Wert der Höhe sowie den neuen Wert an:
  1. RaiseComponentChanged(pd, oldHeight, MyControl.Height);
Das neue Steuerelement können Sie jetzt übersetzen und in einer Testanwendung einsetzen. (Hinweise zum Debuggen finden Sie im Kasten Die eigene Anwendung entwanzen.) Der neue Befehl taucht sowohl im Kontextmenü als auch im Eigenschaftsfenster auf und verhält sich wie geplant.

Eigene Komponente entwanzen
Um eine eigene Komponente zu debuggen, startet man am besten eine zweite Instanz von Visual Studio .NET. Dazu geht man zu Projekt | Eigenschaften | Konfigurationseigenschaften und setzt DebugModus auf Programm sowie Anwendung starten auf C:\Programme\Microsoft Visual Studio .NET\Common7\IDE\devenv.exe oder den entsprechenden Pfad.

Dann erstellt man eine neue Windows-Anwendung und nimmt die Komponente in die Toolbox auf, indem man im Kontextmenü der gewünschten Toolbox-Seite Toolbox anpassen... auswählt und im entsprechenden Dialogfeld im Register .NET Framework-Komponenten mit Durchsuchen die KB5Components.dll aufnimmt.
Eigenschaften aus der Luft gegriffen
Schöner wäre es allerdings noch, wenn sich die Höhe des Labels beim Ändern des Texts automatisch anpassen würde, so wie das auch die geerbte Eigenschaft AutoSize macht. Um diese Anforderung umzusetzen, muss AutoHeightLabel etliche neue Merkmale erhalten:
  • Eine Eigenschaft, mit der man die automatische Höhenanpassung ein- und ausschalten kann. Anders als bei AutoSize wird diese Eigenschaft nur zur Entwurfszeit existieren.
  • Eine visuelle Darstellung der Tatsache, dass das Label gerade im Modus für die Höhenanpassung ist.
  • Blockierung der manuellen Größenänderung per Drag&Drop, während die automatische Höhenanpassung eingeschaltet ist.
  • Automatische Neuberechnung der Höhe beim Ändern der Text-Eigenschaft
Beginnen wir mit der neuen Eigenschaft, die den Namen AutoAdjustHeight erhalten soll. Wir könnten sie der Label-Komponente hinzufügen, aber das macht wenig Sinn. Zur Laufzeit würde ein sich automatisch vergrößerndes Steuerelement früher oder später das gesamte Formular-Layout über den Haufen werfen. Wir nutzen deshalb die Fähigkeit des Designers, eigene Eigenschaften in das Eigenschaftsfenster der Komponente einzublenden, solange diese sich im Entwurfsmodus befindet. Dadurch bleibt das Steuerelement selbst von solcher Logik befreit.

Alle Komponenten-Designer bieten einen Satz von Filter-Funktionen an, mit denen die Liste der im Eigenschaftsfenster sichtbaren Elemente beeinflusst werden kann:
  1. protected virtual void PreFilterEvents(IDictionary events);
  2. protected virtual void PostFilterEvents(IDictionary events);
  3. protected virtual void PreFilterProperties(IDictionary properties);
  4. protected virtual void PostFilterProperties(IDictionary properties);
Diese Methoden werden aufgerufen, wenn sich jemand beim TypeDescriptor (der ja alles weiß) die Eigenschafts- oder die Ereignisliste einer Komponente holt. Im Dictionary stecken dann die Eigenschafts-Deskriptoren und sind über ihren jeweiligen Namen zu finden. Die Pre-Versionen dieser Methoden werden vor den jeweiligen Post-Versionen aufgerufen, und die .NET Framework-Dokumentation empfiehlt, die Pre-Versionen für das Hinzufügen und die Post-Versionen für das Entfernen von Elementen zu nutzen.

Das weiter oben erworbene Wissen um den PropertyDescriptor ist also auch hier von Bedeutung. Diesmal erzeugen wir eine künstliche Eigenschaft durch Hinzufügen eines solchen Objekts zum Dictionary. Es wäre denkbar, einen neuen Eigenschafts-Deskriptor zu erstellen und ihn mit allen nötigen Informationen, insbesondere auch den Delegaten zum Lesen und Schreiben, zu versehen. Aber es geht auch einfacher: Warum nicht eine passende Eigenschaft für den Designer erstellen und deren PropertyDescriptor in das Dictionary der Label-Komponenten einfügen? Hier passt wieder einmal das Motto eines japanischen Fahrzeugherstellers: Nichts ist unmöglich!

Die gesamte Implementierung der Entwurfszeit-Eigenschaft passt auf diese Weise in zehn Zeilen, wie in Listing 1 zu sehen.

Listing 1
  1. bool autoAdjustHeight;
  2. // Falls true, wird während der Eingabe des Textes die Höhe des Labels angepasst
  3. [Category("Design")]
  4. public bool AutoAdjustHeight
  5. {
  6. get {return autoAdjustHeight;}
  7. set {
  8. autoAdjustHeight = value;
  9. MyControl.Invalidate();
  10. }
  11. }
  12. protected override void PreFilterProperties(IDictionary properties)<br></br>{
  13. base.PreFilterProperties(properties);
  14. properties.Add("AutoAdjustHeight", TypeDescriptor.GetProperties(this).Find("AutoAdjustHeight", false));
  15. }
Zwei kleine Details sind hier noch zu erwähnen: Mit dem Attribut Category("Design") sorgen wir dafür, dass die neue Eigenschaft im Eigenschaftsfenster unter der Kategorie Entwurf aufscheint. Und der Aufruf von Invalidate beim Ändern der Eigenschaft AutoAdjustHeight soll dafür sorgen, dass die visuelle Kennzeichnung auch korrekt aktualisiert wird. Visuelle Kennzeichnung? Etwas weniger geschwollen ausgedrückt soll das Steuerelement zur Entwurfszeit einen blauen senkrechten Strich links und rechts anzeigen und damit ausdrücken, dass die automatische Höhenanpassung aktiviert ist.
Die Komponente wird verziert
Solche grafischen Zusätze, die nur beim Entwurf zu sehen sind, heißen in .NET Adornment, was so viel wie Verzierung bedeutet. Es wird Sie kaum überraschen, dass auch hierfür der Designer zuständig ist. Jedes Mal wenn das Steuerelement sich zeichnet, ruft die Umgebung anschließend die Designer-Methode
  1. protected virtual void OnPaintAdornments(PaintEventArgs ps);
auf. In den PaintEventArgs findet man ein Graphics-Objekt, das auf den Bereich des Steuerelements beschränkt ist und somit leicht die gewünschten senkrechten Striche malen kann:
  1. protected override void OnPaintAdornments(PaintEventArgs pe)
  2. {
  3. if (autoAdjustHeight) {
  4. Pen pen = new Pen(Color.Blue);
  5. pe.Graphics.DrawLine(pen, 0, 0, 0, MyControl.Height);
  6. pe.Graphics.DrawLine(pen, MyControl.Width-1, 0, MyControl.Width-1, MyControl.Height);
  7. }
  8. }
Damit haben wir die gewünschte visuelle Darstellung auch schon realisiert. Noch aber kann man das Steuerelement mit der Maus niedriger oder höher ziehen obwohl die automatische Anpassung aktiv ist. Welcher leidgeprüfte Entwickler hört da nicht schon die Stimme des späteren Anwenders: Ich habe doch auf Automatik gestellt und trotzdem passt der Text nicht in den Kasten! Um solche gern fälschlicherweise als Anwender-Fehler disqualifizierten Rückmeldungen zu vermeiden, verhindern wir einfach die Höhenverstellung des Steuerelements, wenn die Automatik eingeschaltet ist.

Die Eigenschaft SelectionRules des ControlDesigners hilft uns dabei. Diese Menge von Flags teilt der Umgebung mit, ob man das Steuerelement verschieben oder in der Größe verändern kann. Und hierbei sogar, auf welcher Seite dies möglich ist. Für das AutoHeightLabel entfernen wir mit
  1. public override SelectionRules SelectionRules
  2. {
  3. get {
  4. SelectionRules result = base.SelectionRules;
  5. if (autoAdjustHeight) result &= ~(SelectionRules.BottomSizeable | SelectionRules.TopSizeable);
  6. return result;
  7. }
  8. }
die Flags für das Ziehen am oberen und unteren Griff und verhindern so eine überflüssige Fehlermeldung von unseren Anwendern.
Auf Text-Änderungen reagieren
Nun fehlt eigentlich nur noch die Dynamik. Die Höhenanpassung soll ja ausgelöst werden, wenn sich die Eigenschaft Text des Labels ändert. Ein letztes Mal kommt hierzu der PropertyDescriptor zum Einsatz. Der verfügt wie schon erwähnt über ein Methode zum Anmelden von Delegaten. Weist jemand der Eigenschaft einen neuen Wert zu, wird der Delegat aufgerufen - et voilà! Der Rest ist ein Kinderspiel, weil der eigentliche Anpassungs-Mechanismus ja schon steht.
  1. public virtual void AddValueChanged(object component, EventHandler handler);
Diese Methode braucht den Verweis auf die Komponente, also in unserem Fall das AutoHeightLabel, sowie einen ganz normalen Delegaten. Der Designer hängt sich auf diese Weise bei seiner Komponente ein und kann so auf Eigenschaftsänderungen reagieren. Das Einhängen macht man am besten in der Methode Initialize. Hier erhält der Designer seine Komponente und kann sich gleich anmelden. Wer sich anmeldet, sollte sich auch einmal wieder abmelden und dies kann man gut in der Methode Dispose erledigen (siehe Listing 2).

Listing 2
  1. public override void Initialize(IComponent component)
  2. {
  3. base.Initialize(component);
  4. TypeDescriptor.GetProperties(MyControl).Find("Text", false).AddValueChanged(MyControl, new EventHandler(TextChangedHandler));
  5. }
  6. private void TextChangedHandler(object sender, EventArgs e)
  7. {
  8. if (autoAdjustHeight) AdjustHeightHandler(sender, e);
  9. }
  10. protected override void Dispose(bool disposing)
  11. {
  12. TypeDescriptor.GetProperties(MyControl).Find("Text", false).RemoveValueChanged(MyControl, new EventHandler(TextChangedHandler));
  13. base.Dispose(disposing);
  14. }
In Abpictureung 2 sehen Sie - soweit dies möglich ist - das Ergebnis unserer Bemühungen. Das Eigenschaftsfenster zeigt sowohl die zusätzliche Entwurfszeit-Eigenschaft AutoAdjustHeight als auch den Befehl Höhe anpassen an. Auf dem Formular hat das Steuerelement die beiden blauen Striche, um anzuzeigen, dass die Höhenanpassung aktiviert ist. Der Mauszeiger auf dem unteren Griff signalisiert, dass man hier nicht ziehen kann und das Label hat die korrekt Höhe für den angezeigten Text. Die Dynamik können wir hier leider nicht demonstrieren, aber dafür haben Sie ja den Quellcode der Komponente auf der Heft-CD.
Die verhältnismäßig einfache Umsetzung all dieser Features verdanken wir dem Designer-Konzept im Zusammenspiel mit .NET Reflection insbesondere durch den PropertyDescriptor. Weil es sich dabei um zwei generelle .NET-Konzepte handelt und nicht um spezielle Visual Studio .NET-Objektmodelle, sollte sich die hier vorgestellte Komponente auch in anderen .NET-Entwicklungsumgebungen wie beispielsweise Borland Sidewinder ebenso verhalten. Wenn Sie damit Erfahrung haben, würde ich mich über ein Feedback freuen.
Peter Pohmann ist Geschäftsführer von pohmann & partner, einem Ingenieurbüro für Software-Entwicklung in Niederbayern. Er ist seit über 15 Jahren als Entwickler, Fachautor, Coach, Referent und Berater für Objektorientierte Technologien und Windows-Programmierung tätig. Er freut sich über Feedback zu diesem und verwandten Themen an pohmann@pohmannundpartner.de.


Anzeige

Kommentare

zurück zum Seitenanfang