Erstellen intelligenter Anwendungslayouts mit Windows Forms 2.0

Veröffentlicht: 14. Aug 2006

Von Jay Allen

Sie erfahren hier, wie Sie mit den neuen in Windows Forms 2.0 enthaltenen Steuerelementen intelligente und erweiterungsfähige Anwendungslayouts erstellen (15 gedruckte Seiten).

Laden Sie Codebeispiele in C# und Visual Basic (903 KB) vom Microsoft Download Center herunter.

Auf dieser Seite

 Einführung
 Die Grenzen des Registersteuerelements
 ToolStrip mit Registern
 ToolStrip im Outlook-Stil
 Reduzierbare Menüs
 Flyout-Panel
 Schlussbemerkung

Einführung

Microsoft Windows Forms 2.0 ermöglicht Ihnen, die Funktionalität Ihrer Anwendung so zu strukturieren, dass sie für Ihre Kunden einfacher zu bedienen ist. Mit den neuen Steuerelementen, z. B. ToolStrip, FlowLayoutPanel und TableLayoutPanel, können Sie intelligente und erweiterungsfähige Anwendungslayouts erstellen. Dieser Artikel beschreibt vier Anwendungslayouts: eine Toolleiste mit Registern, eine Toolleiste im Outlook-Stil, reduzierbare Menüs und einen Flyout-Panel. Alle diese Layouts sind mit Windows Forms 2.0 einfach zu erstellen. In diesem Artikel wird davon ausgegangen, dass Sie die Grundlagen von Windows Forms kennen und mit UserControls vertraut sind.

 

Die Grenzen des Registersteuerelements

Unlängst wollte ich eine einfache Anwendung schreiben, die als Countdown-Timer für verschiedene Aktivitäten, z. B. Gymnastik oder Meditation, fungieren sollte. Die Kernfunktionalität sollte aus einem Timer bestehen, der nach Ablauf eines festgelegten Zeitraums (beispielsweise 30 Minuten) ein akustisches Signal wiederholt wiedergibt. Diese einfache Anwendung umfasst drei Hauptabschnitte: Den Bereich "Home", der nach dem Start angezeigt wird, den Bereich "Countdown", in dem die Countdown-Uhr eingestellt und ausgeführt wird, und den Bereich "Settings", in dem der Benutzer ein akustisches Signal festlegt und angibt, ob die Sitzungen in einem Protokoll verzeichnet werden sollen.

Ich entschied mich für die Verwendung von Registerkarten zur Navigation, da dies für meine Zwecke am praktikabelsten erschien. Ich definierte die Hauptfunktionsbereiche auf drei verschiedenen Registerkarten. Zuerst schien das TabControl-Steuerelement (Registersteuerelement), das zum Lieferumfang von Windows Forms gehört, hierfür prädestiniert zu sein. Dann merkte ich, dass diese Lösung einen Haken hat. Meine Anwendung sollte nicht wie eine normale Windows-Anwendung aussehen und die vorgegebenen grauen Farbschemas verwenden. Ich wollte auch keine am oberen Rand ausgerichteten Registerkarten, die sonst Standard sind. Die Registerkarten sollten sich am rechten Rand des TabControl-Steuerelements befinden. Sie sollten weiß sein, ohne störende Rahmenlinien oder Kanten, und die drei Registerkarten sollten sich vom oberen bis zum unteren Rand des Anwendungsfensters erstrecken.

Ich erkannte bald, dass das TabControl-Steuerelement dies nicht leisten konnte. Das TabControl-Steuerelement ließ zwar die Anordnung von Registerkarten am rechten und linken Rand zu, stellte den Text standardmäßig aber vertikal statt horizontal dar. Ich stellte fest, dass auf den seitlich angeordneten Registerkarten überhaupt kein Text angezeigt wurde, wenn visuelle Stile aktiviert waren.

Eine Möglichkeit, in etwa den gewünschten Effekt zu erzielen, fand ich durch den Einsatz des Besitzerzeichnungsmodus. (Nähere Informationen hierzu enthält mein Eintrag Window Forms TabControl: Using Right-Aligned or Left-Aligned Tabs (in englischer Sprache) im Blog "Windows Forms Documentation Updates".) Es gab allerdings noch andere Hindernisse, die mich vom Einsatz des TabControl-Steuerelements abhielten. So konnte im Besitzerzeichnungsmodus beispielsweise nur der Inhalt der Registerkarten neu gezeichnet werden. Ich konnte die Rahmenlinien weder bei den Registerkarten noch bei den Bereichen, welche die Register umgeben, anpassen oder entfernen. Dadurch wurden meine Anpassungsmöglichkeiten eingeschränkt. Überdies werden die Steuerelemente auf den Registerkarten manchmal nicht richtig angeordnet, wenn visuelle Stile verwendet werden. Angesichts dieser Probleme und der mangelnden Anpassungsfähigkeit war ich vom Einsatz des TabControl-Steuerelements nicht mehr so angetan.

 

ToolStrip mit Registern

Ich ging noch einmal einen Schritt zurück und erkannte, dass sich dieses Problem auf eine andere, originellere Weise lösen lässt. Aus meinen vergangenen Versuchen wusste ich, dass das neue ToolStrip-Steuerelement von Windows Forms sehr flexibel ist. Vielleicht, so dachte ich, war es möglich, damit das gewünschte Erscheinungsbild und Verhalten zu erzielen.

Wie sich herausstellte, war es möglich - und zudem mit nur minimalem Programmieraufwand. Abbildung1 zeigt das Endergebnis.

Die fertige Anwendung mit einem ToolStrip-Steuerelement mit Registern
Abbildung 1: Die fertige Anwendung mit einem ToolStrip-Steuerelement mit Registern

Ich habe diese Anwendung wie folgt erstellt: Das Hauptformular ist in zwei Bereiche unterteilt. Auf der linken Seite befindet sich ein benutzerdefiniertes UserControl-Steuerelement, das ich programmiert und Slideshow genannt habe (es ist im Beispiel Countdown enthalten). Es handelt sich hierbei einfach um ein "auffälliges" Steuerelement, das mithilfe des verwalteten Wrapper-Steuerelements WebBrowser und Dynamic HTML eine Reihe von Bildern ein- und ausblendet.

Für diesen Artikel ist die rechte Seite von besonderem Interesse, auf der ich ein SplitContainer-Steuerelement platziert habe. Auch SplitContainer ist ein neues Windows Forms-Steuerelement. Es ersetzt das alte Splitter-Steuerelement und ist leistungsfähiger in Bezug auf die Anzahl und Bündelung unterteilter Bereiche. Das SplitContainer-Steuerelement umfasst zwei Bereiche namens Panel1 und Panel2. Panel1 ist der Inhaltsbereich, der über verschiedene Steuerelemente für jede Schaltfläche verfügt. Panel2 enthält Schaltflächen zur Navigation zwischen den drei Bereichen: "Home", "Countdown" und "Settings". Panel2 wird das ToolStrip-Steuerelement aufnehmen. In meiner Anwendung habe ich für die Orientation-Eigenschaft des SplitContainer-Steuerelements die Einstellung Vertical (die Standardeinstellung) übernommen und die Größe der Bereiche in Microsoft Visual Studio so geändert, dass der linke Bereich größer als der rechte Bereich ist. Abbildung 2 zeigt die Anwendung Countdown, bevor das ToolStrip-Steuerelement zu Panel2 hinzugefügt wurde.

Die Anwendung Countdown mit dem benutzerdefinierten UserControl-Element Slideshow und einem SplitContainer-Steuerelement
Abbildung 2: Die Anwendung Countdown mit dem benutzerdefinierten UserControl-Element Slideshow und einem SplitContainer-Steuerelement

Nachdem ich das benutzerdefinierte UserControl-Steuerelement Slideshow und das SplitContainer-Steuerelement hinzugefügt hatte, fügte ich ein ToolStrip-Steuerelement in Panel2 ein. In der Voreinstellung ist die Dock-Eigenschaft des ToolStrip-Steuerelements auf den Wert Top (Oben) festgelegt. Dies ist sinnvoll, denn in den meisten Anwendungen wird das ToolStrip-Steuerelement zum Erstellen von Symbolleisten für Befehle eingesetzt, die den Symbolleisten von Microsoft Word oder anderen Anwendungen ähneln. Für meine Zwecke musste ich die Dock-Eigenschaft jedoch auf den Wert Fill festlegen. Ich wollt auch nicht die Darstellung im Office-Stil, die in der Voreinstellung für das ToolStrip-Steuerelement benutzt wird, und daher wählte ich für RenderMode die Einstellung System und für BackColor die Einstellung White. Die GripStyle-Eigenschaft legte ich auf Hidden fest, damit der Ziehpunkt nicht sichtbar ist, mit dem das ToolStrip-Steuerelement auf dem Bildschirm verschoben werden kann. Schließlich sollten die Navigationsschaltflächen in einer senkrechten Linie von oben nach unten verlaufen, und daher änderte ich den Wert der LayoutStyle-Eigenschaft in VerticalStackWithOverflow.

Als Nächstes kamen die eigentlichen Navigationsschaltflächen an die Reihe. Ich erstellte drei Schaltflächen (eine pro Bereich). Mit dem Elementauflistungs-Editor von Visual Designer erstellte ich im ToolStrip-Steuerelement drei ToolStripButton-Objekte. Um den Elementauflistungs-Editor zu öffnen, klickte ich auf die Schaltfläche mit den drei Punkten neben der Items-Eigenschaft des ToolStrip-Steuerelements. (Ich hätte den Elementauflistungs-Editor auch öffnen können, indem ich auf das Smarttag des ToolStrip-Steuerelements und im Menü "ToolStrip-Aufgaben" auf "Einträge bearbeiten" geklickt hätte.)

Als Nächstes war die Gestaltung der Schaltflächen an der Reihe. Ich erstellte aus vorhandenen Fotografien für jede Schaltfläche ein Bild und änderte die Größe der Bilder, sodass sie die gleiche Höhe und Breite hatten. Bei jedem ToolStripButton-Steuerelement klickte ich auf die Ellipsen-Schaltfläche neben der Image-Eigenschaft und importierte über das Dialogfeld "Ressource auswählen" das Bild für die einzelnen Schaltflächen. In der Voreinstellung des ToolStripButton-Steuerelements werden die Bilder auf die Standardgröße der Schaltfläche skaliert. Da die Bilder in der tatsächlichen Größe angezeigt werden sollten, legte ich die ImageScaling-Eigenschaft jeder Schaltfläche auf None fest.

Ich gab in der Text-Eigenschaft jedes ToolStripButton-Steuerelements den passenden Text ein. Damit der Text unter dem Bild angezeigt wird, musste ich zwei Eigenschaften ändern. Zuerst musste ich die DisplayStyle-Eigenschaft auf ImageAndText festlegen. Dies bewirkte, dass der Text mittig rechts neben dem Bild angezeigt wird. Um den Text unter das Bild zu verschieben, legte ich die TextImageRelation-Eigenschaft auf ImageAboveText fest. Anschließend änderte ich die Schriftart des ToolStripButton-Steuerelements, sodass die Textdarstellung meinen Wünschen entsprach.

Als letzten Effekt wollte ich, dass die Schaltflächen "aktiviert" erscheinen, wenn auf sie geklickt wird, damit der Benutzer auf einen Blick erkennen kann, welcher Bereich gegenwärtig aktiv ist. (Der Aktiviert-Effekt wird in Abbildung 1 deutlich: die zweite Schaltfläche ist umrahmt.) Um dies zu erreichen, legte ich für die CheckOnClick-Eigenschaft jeder Schaltfläche den Wert True fest.

Die Toolleistenregister mit Leben füllen

Hier war die Grenze dessen erreicht, was ich mit dem Designer entwickeln konnte. Wie Sie sehen, bin ich sehr weit gekommen, ohne auch nur eine Zeile Code zu schreiben. Jetzt war es an der Zeit, die einzelnen Bereiche zu entwerfen und die einzelnen Komponenten miteinander zu verknüpfen. Für meine Toolleiste mit Registerkarte wollte ich die folgende Logik implementieren:

  • Den Bereich "Home" beim Anwendungsstart standardmäßig anzeigen.

  • Erkennen, wenn auf eine ToolStrip-Schaltfläche geklickt wird. Wenn der zu dieser Schaltfläche gehörige Bereich nicht bereits angezeigt wird, ihn erstellen und anzeigen.

  • Die zuvor aktivierte Schaltfläche deaktivieren.

Für jeden Bereich erstellte ich ein eigenes UserControl-Steuerelement: eines für den Bereich "Home", eines für den Hauptbereich "Countdown" und eines für den Bereich "Settings". Ich nannte diese Bereiche CDHomePanel, CDCountdownPanel bzw. CDSettingsPanel.

Nachdem ich das Grundgerüst für diese UserControl-Steuerelemente implementiert hatte, begann ich mit der Programmierung der Logik, um sie in Panel1 des SplitContainer-Steuerelements anzuzeigen. Da ich nur drei Bereiche hatte, hätte ich für jede Schaltfläche einen eigenen festcodierten Ereignishandler benutzen können, mit dem der entsprechende Bereich instantiiert und angezeigt wird. Dies ist allerdings kein sehr erweiterungsfähiger Ansatz. Er funktioniert bei drei Schaltflächen - aber wie sieht die Sache aus, wenn ich sechs Schaltflächen verwende? Was passiert, wenn ich diesen Ansatz in einer umfangreicheren Anwendung verfolge, bei der ich 10 oder 20 Schaltflächen benötige? Ich beschloss, einen einzigen Ereignishandler zu schreiben, der für alle Schaltflächen zuständig ist, und hierbei keine Werte fest zu codieren. Der Code sollte so abstrakt sein, damit ich in Zukunft weitere Schaltflächen oder Bereiche hinzufügen kann, ohne den Code des ToolStripButton-Ereignishandlers groß ändern zu müssen.

Zuerst wechselte ich wieder zum Designer und legte als Wert der Tag-Eigenschaft jedes ToolStripButton-Steuerelements den Namen des zur betreffenden Schaltfläche gehörigen UserControl-Steuerelements fest, wobei ich dem Namen jeweils den zugehörigen Namespace voranstellte. Ich ging so vor, damit der Ereignishandler aus der Tag-Eigenschaft der Schaltfläche, auf die geklickt wurde, schließen konnte, welches UserControl-Steuerelement instantiiert werden muss. Beispielsweise lautet der voll qualifizierte Name meines UserControl-Steuerelements CDHomePanel, das im Namespace Countdown vorhanden ist, Countdown.CDHomePanel. Ich wies der Tag-Eigenschaft der Schaltfläche "Home" daher den Wert "Countdown.CDHomePanel" zu. Analog hierzu legte ich für die Tag-Eigenschaft der Schaltfläche "Countdown" den Wert "Countdown.CDCountdownPanel" und für die Tag-Eigenschaft der Schaltfläche "Settings" den Wert "Countdown.CDSettingsPanel" fest.

Als Nächstes fügte ich Code hinzu, den mein Formular für den Ereignishandler benötigte. Da die UserControl-Bereiche dynamisch instatiiert werden sollten, musste ich in den Namespaces System.Reflection und System.Runtime.Remoting definierte Klassen benutzen. Ich definierte auch zwei private Variablen auf Klassenebene für meinen Ereignishandler: _CurrentControl, ein Verweis auf den Bereich, der aktuell für den Benutzer sichtbar ist, und _CurrentClickedButton, ein Verweis auf das zum sichtbaren Bereich gehörige ToolStripButton-Steuerelement.

Imports System.Threading
Imports System.Drawing.Drawing2D
Imports System.Reflection
Imports System.Runtime.Remoting

Public Class Form1
    Private _CurrentControl As Control
    Private _CurrentClickedButton As ToolStripButton = Nothing

Danach fügte ich dem Load-Ereignishandler des Formulars Code hinzu, um den Bereich "Home" zu instantiieren, ihn für die Benutzer anzuzeigen und die Variablen _CurrentControl und _CurrentClickedButton mit gültigen Objektverweisen zu initialisieren. Da dies während der Nutzungsdauer der Anwendung nur einmal geschehen musste, erstellte ich gleich eine Instanz der Klasse CDHomePanel. Nachdem der Bereich instantiiert worden war, fügte ich ihn zu Panel1 meines SplitContainer-Steuerelements hinzu.

Dim HomePanel As New CDHomePanel()
HomePanel.Tag = "Countdown.CDHomePanel"
HomePanel.Dock = DockStyle.Fill
SplitContainer1.Panel1.Controls.Add(HomePanel)
_CurrentControl = HomePanel
_CurrentClickedButton = HomeButton

Schließlich schrieb ich eine Methode namens Navigate, die vom MouseDown-Ereignishandler jedes ToolStripButton-Steuerelements aufgerufen werden sollte. (Ich erkläre weiter unten im Abschnitt Anzeigen von Eingabeaufforderungen beim Wechseln von Bereichen, warum ich diesen Code in einer eigenen Methode untergebracht habe.) Da ich in Visual Basic programmierte, konnte ich mithilfe des Schlüsselworts Handles angeben, dass dieser Ereignishandler für alle drei Schaltflächen gilt. Der Handler überprüft die Controls-Auflistung von Panel1 meines SplitContainer-Steuerelements daraufhin, ob das UserControl-Steuerelement bereits erstellt wurde. Anderenfalls wird es erstellt, indem die Tag-Eigenschaft des ToolStripButton-Steuerelements der CreateInstance-Methode übergeben, die in der AppDomain-Klasse definiert ist.

Private Sub Navigate(ByVal sender As Object)
    Dim _NewControl As Control
    Dim _CurrentButton As ToolStripButton = CType(sender, ToolStripButton)
    Dim _ControlName As String = _CurrentButton.Tag.ToString()

    ' Uncheck the previous clicked button.
    _CurrentClickedButton.Checked = False
    _CurrentClickedButton = _CurrentButton

    ' First, make sure this isn't a redundant event - are we already 
    ' displaying the control?
    If (Not (_ControlName = _CurrentControl.Name)) Then
        ' Get the control to use, and instantiate it dynamically 
        ' if it isn't already defined.
        _NewControl = SplitContainer1.Panel1.Controls(_ControlName)
        If (_NewControl Is Nothing) Then
            ' Control not found - instantiate it.
            Dim _Oh As ObjectHandle = _
                AppDomain.CurrentDomain.CreateInstance( _
                Assembly.GetExecutingAssembly().FullName, _ControlName)
            _NewControl = _Oh.Unwrap()
            _NewControl.Name = _ControlName
            _NewControl.Dock = DockStyle.Fill
            SplitContainer1.Panel1.Controls.Add(_NewControl)
        End If

        ' Hide the old control, and show the new one. 
        _CurrentControl.Visible = False
        _NewControl.Visible = True
        _CurrentControl = _NewControl
    End If
End Sub

Dies war der letzte Schritt, nun konnte ich die Anwendung kompilieren und erfolgreich ausführen. Der Code ist so strukturiert, dass in vier Schritten mühelos eine neue Schaltfläche und ein neuer Bereich hinzugefügt werden kann.

  1. Das UserControl-Steuerelement entwickeln.

  2. Das ToolStripButton-Steuerelement erstellen.

  3. Den Namen des UserControl-Steuerelements der Tag-Eigenschaft zuweisen.

  4. Das ToolStripButton-Steuerelement der Handles-Klausel des Ereignishandlers hinzufügen.

Wie Sie sehen, besteht der schwierigste Schritt in der Erstellung des neuen UserControl -Steuerelements. Wenn das erledigt ist, müssen Sie nur die Schaltfläche wie oben beschrieben konfigurieren und eine Codezeile ändern. Sie verfügen jetzt über einen Ersatz für das TabControl-Steuerelement, der einfach zu erweitern und zudem anpassungsfähiger ist.

Anzeigen von Eingabeaufforderungen beim Wechseln von Bereichen

Nachdem ich all das fertig gestellt hatte, kamen mir mehrere Fragen in den Sinn. Was passiert, wenn der Benutzer auf die Schaltfläche "Settings" klickt, während der Bereich "Countdown" aktiv ist und die Uhr läuft? Und was passiert, wenn der Benutzer den Bereich "Settings" anzeigt und auf die Schaltfläche "Countdown" klickt, bevor er seine Einstellungen gespeichert hat? Sollen die Änderungen gespeichert werden? Sollen sie verworfen werden? Soll der Benutzer gefragt werden?

Es war offensichtlich, dass für die UserControl-Bereiche ein Mechanismus erforderlich war, mit dem das übergeordnete Formular die UserControl-Bereiche über eine bevorstehende Navigation informieren kann. Ebenso benötigten die einzelnen Bereiche eine Möglichkeit, eine solche Navigation abhängig vom Status des Bereichs zuzulassen oder zu verbieten. Hierzu definierte ich eine Schnittstelle namens IPanelNavigating und deklarierte eine Methode namens CanNavigate, die von den Klassen implementiert werden sollte. Die CanNavigate-Methode gibt einen Wert des Typs Boolean zurück, der angibt, ob der Bereich gewechselt werden kann.

Public Interface IPanelNavigating
    Function CanNavigate() As Boolean
End Interface

Da die Navigation im Bereich "Home" nie gestoppt werden muss, implementierte ich die Methode nicht für diese Klasse. Ich implementierte sie für den Bereich "Countdown" und benutzte ein Meldungsfeld, um den Benutzer zu fragen, ob der Countdown beendet werden soll. Wenn der Benutzer den Countdown beenden möchte, wird der Timer zurückgesetzt und die CanNavigate-Methode gibt True zurück. Wenn die Sitzung nicht beendet werden soll, gibt die Methode False zurück.

Um dies im Hauptformular zu implementieren, fügte ich dem MouseDown-Ereignishandler, den ich für alle drei ToolStripButton-Objekte definiert hatte, einigen Code hinzu.

Private Sub ToolStripButton_MouseDown(ByVal sender As System.Object, _
ByVal e As MouseEventArgs) Handles HomeButton.MouseDown, _
CountdownButton.MouseDown, SettingsButton.MouseDown
    ' Attempt to cast to an IPanelNavigating. If not implemented, 
    ' just navigate.
    If (TypeOf _CurrentControl Is IPanelNavigating) Then
        Dim _CanNavigate As IPanelNavigating = _
            CType(_CurrentControl, IPanelNavigating)
        If (_CanNavigate.CanNavigate()) Then
            Navigate(sender)
        End If
    Else
        ' Just navigate.
        Navigate(sender)
    End If
End Sub

Ich stellte die Anwendung fertig, indem ich die Programmierung der UserControl-Bereiche ausarbeitete und Unterstützung für eine benutzerdefinierte Titelleiste hinzufügte, was mir die Gelegenheit gab, das ToolStrip-Steuerelement auf eine andere einzigartige Weise einzusetzen. (Nähere Informationen zur Implementierung finden Sie unter meinem Eintrag Using ToolStrip to Create a Custom Title Bar im MSDN-Blog "Windows Forms Documentation Updates".)

 

ToolStrip im Outlook-Stil

Die Architektur der Anwendung "Countdown" hat den großen Vorteil, dass sie sich in anderen Anwendungen einfach wiederverwenden lässt. Dank der Leistungsfähigkeit des ToolStrip-Steuerelements können Erscheinungsbild und Verhalten grundlegend geändert werden, wenn eine Anwendung dies erfordert. Abbildung 3 zeigt als weiteres Beispiel das Modell einer Banking-Anwendung. Hier erfolgt die Navigation über eine Toolleiste im Outlook-Stil.

Ein einfaches Beispiellayout mit einer Toolleiste im Outlook-Stil
Abbildung 3: Ein einfaches Beispiellayout mit einer Toolleiste im Outlook-Stil

In dieser Anwendung platzierte ich das ToolStrip-Steuerelement auf der linken Seite und ließ den rechten Bereich des SplitContainer-Steuerelements für die UserControl-Bereiche frei. Ich formatierte die ToolStripButton-Objekte, indem ich der DisplayStyle-Eigenschaft den Wert ImageAndText, der ImageAlign-Eigenschaft den Wert MiddleCenter, der TextAlign-Eigenschaft den Wert MiddleRight und der TextImageRelation-Eigenschaft den Wert Overlay zuwies. Der gesamte Code, den ich für die Anwendung Countdown geschrieben hatte, funktioniert auch in dieser Anwendung. Er muss nur minimal angepasst werden, damit eine beliebige Anzahl von ToolStripButton-Steuerelementen unterstützt werden.

Sie können dieses Konzept weiter ausbauen, indem Sie Schaltflächen mit Untermenüs hinzufügen. Das ToolStrip-Steuerelement unterstützt ein Steuerelement namens ToolStripDropDownButton, das es Ihnen ermöglicht, untergeordnete Steuerelemente hinzuzufügen, die als Untermenü angezeigt werden, wenn der Benutzer auf die Schaltfläche klickt. Abbildung 4 zeigt das modifizierte Banking-Beispiel, in dem für die Schaltfläche "Settings" ein ToolStripDropDownButton-Steuerelement mit mehreren Untermenüeinträgen verwendet wurde, die anzeigen, welche Art von Einstellungen der Benutzer ändern kann.

Einfaches Beispiellayout aus Abbildung 3 mit ToolStripDropDownButton-Steuerelementen, die Untermenüs anzeigen
Abbildung 4: Einfaches Beispiellayout aus Abbildung 3 mit ToolStripDropDownButton-Steuerelementen, die Untermenüs anzeigen

 

Reduzierbare Menüs

Nachdem ich die Anwendung Countdown fertig gestellt hatte, wollte ich untersuchen, welche neuen Möglichkeiten zur Implementierung komplexer Menüs und Navigationssysteme sich durch den Einsatz der neuen Windows Forms-Steuerelemente eröffnen. Als Erstes ist mir aufgefallen, dass das Fenster "Toolbox" in Visual Studio "ähnlich, aber anders" aussieht. Wenn Sie schon einmal in Visual Studio mit Windows Forms oder ASP.NET gearbeitet haben, dann wissen Sie, dass die Steuerelemente in der Toolbox in einer Reihe von reduzierbaren Menüs in Gruppen zusammengefasst sind. Abbildung 5 enthält ein Beispiel für die reduzierbaren Menüs der Toolbox.

Reduzierbare Menüs im Fenster "Toolbox" von Visual Studio
Abbidung 5: Reduzierbare Menüs im Fenster "Toolbox" von Visual Studio

In den Vorgängerversionen von Windows Forms wäre für eine Simulation der Toolbox eine Menge Code zur Neupositionierung der reduzierbaren Menüs notwendig gewesen. In Windows Forms wird diese Aufgabe durch das FlowLayoutPanel-Steuerelement außerordentlich vereinfacht. Das FlowLayoutPanel-Steuerelement kann eine beliebige Anzahl von Windows Forms-Steuerelementen aufnehmen, die in einer sequenziellen Reihenfolge angeordnet werden. Das FlowLayoutPanel-Steuerelement ist dynamisch, und das ist sein großer Pluspunkt. Wenn ein Steuerelement zur Laufzeit entfernt oder ausgeblendet wird, werden die nachfolgenden Steuerelemente so verschoben, dass der frei gewordene Platz ausgefüllt wird. Genau das brauchen wir zur Implementierung reduzierbarer Menüs.

Ich unterteilte das reduzierbare Steuerelement in zwei getrennte Steuerelemente:

  • Ein Steuerelement namens ListHeader, das das schattierte Benutzeroberflächenelement repräsentiert, in dem der Kopftext und links daneben der Plus-/Minus-Indikator zum Erweitern bzw. Reduzieren der Anzeige angezeigt wird. In ListHeader ist ein Ereignis definiert, das ausgelöst wird, sobald auf den Plus-/Minus-Indikator geklickt wird.

  • Ein Steuerelement namens CollapsibleControl, welches das ListHeader-Steuerelement mit einem anderen beliebigen Steuerelement (das ich "Inhaltssteuerelement" nenne) kombiniert. In CollapsibleControl ist das FlowLayoutPanel-Objekt definiert, das eine beliebige Anzahl von Inhaltsabschnitten anzeigt, die reduziert und erweitert werden können.

Ich werde hier nicht näher auf die Implementierungsdetails des ListHeader-Steuerelements eingehen. (Wer mag, kann einen Blick auf den Code werfen.) Wichtig ist hier die Tatsache, dass ich ein ListHeaderStateChanged-Ereignis definierte, um CollapsibleControl zu signalisieren, ob das Inhaltssteuerelement reduziert oder erweitert werden soll. ListHeaderStateChanged übergibt ein Objekt des Typs ListHeaderEventArgs, das eine State-Eigenschaft des Typs ListHeaderState definiert. ListHeaderState ist eine Enumeration mit zwei möglichen Werten: ListHeaderState.Expanded oder ListHeaderState.Collapsed.

Nachdem ListHeader fertig gestellt war, begann ich mit der Erstellung von CollapsibleControl. CollapsibleControl ist zweigeteilt.

Zuerst erstellte ich eine Klasse namens CollapsibleControlSection . CollapsibleControlSection verknüpft ein Inhaltssteuerelement mit einem Kopftext-Namen.

Public Class CollapsingControlSection
    Inherits Object

    Private _SectionName As String = Nothing
    Private _Control As Control = Nothing


    Public Sub New(ByVal sectionName As String, ByVal control As Control)
        _SectionName = sectionName
        _Control = control
    End Sub


    Public Property SectionName() As String
        Get
            Return _SectionName
        End Get
        Set(ByVal value As String)
            _SectionName = value
        End Set
    End Property

    Public Property SectionControl() As Control
        Get
            Return _Control
        End Get
        Set(ByVal value As Control)
            _Control = value
        End Set
    End Property
End Class

Die CollapsibleControl-Klasse zeigt unter Verwendung des ListHeader-Steuerelements und eines oder mehrerer CollapsibleControlSection-Objekte eine Reihe von Inhaltssteuerelementen an, die durch verschiedene Kopftexte voneinander getrennt sind. Wenn ein Inhaltssteuerelement reduziert oder ausgeblendet wird, füllen die anderen Steuerelemente den verbliebenen Leerraum aus. Um dies zu erreichen, öffnete ich zuerst CollapsibleControlSection im Visual Studio-Designer und fügte dem Steuerelement ein FlowLayoutPanel-Element hinzu. Ich legte für die Dock-Eigenschaft des FlowLayoutPanel-Elements die Einstellung Fill fest, damit es die gesamte Fläche des Steuerelements ausfüllt.

Anschließend erstellte ich das eigentliche CollapsibleControl-Objekt. Die für das Funktionieren dieses Steuerelements erforderliche Programmierung ist überraschend einfach. Ich importierte den Namespace System.Collections.Generic, damit ich die generische List(Of T)-Klasse zum Speichern der CollapsibleControlSection-Objekte benutzen konnte. Dieses List-Objekt wird von der AddSection-Methode mit Einträgen gefüllt, die folgende Operationen ausführt:

  • Sie erstellt ein ListHeader-Objekt für das Inhaltssteuerelement und fügt es und das Inhaltssteuerelement am Ende des FlowLayoutPanel-Steuerelements hinzu.

  • Sie verwendet die Tag-Eigenschaft des ListHeader-Steuerelements, um das Inhaltssteuerelement dem passenden ListHeader-Steuerelement zuzuordnen, damit das CollapsibleControl-Objekt weiß, welches Inhaltssteuerelement angezeigt bzw. ausgeblendet werden soll.

  • Sie fügt einen Handler für das ListHeaderStateChanged-Ereignis des ListHeader-Steuerelements hinzu, damit der passende Inhaltsbereich angezeigt oder ausgeblendet werden kann, wenn der Benutzer im ListHeader-Objekt auf den Plus-/Minus-Indikator klickt.

Es folgt der Code der Klasse CollapsibleControl.

Public Class CollapsibleControl
    Dim SectionList As List(Of CollapsibleControlSection)

    Public Sub New()
        ' This call is required by the Windows Form Designer.
        InitializeComponent()

        SectionList = New List(Of CollapsibleControlSection)()
    End Sub

    Public Sub AddSection(ByVal NewSection As CollapsibleControlSection)
        SectionList.Add(NewSection)
        ' Add a new row. 
        Dim Header As New ListHeader()
        Header.Text = NewSection.SectionName
        Header.Width = Me.Width
        AddHandler Header.ListHeaderStateChanged, _
            AddressOf Header_ListHeaderStateChanged
        FlowLayoutPanel1.Controls.Add(Header)
        ' Get the position of the control we're going to add, 
        ' so that we can show and hide it when needed.
        Header.Tag = FlowLayoutPanel1.Controls.Count

        Dim c As Control = NewSection.SectionControl
        c.Width = Me.Width
        FlowLayoutPanel1.Controls.Add(c)
    End Sub

    Sub Header_ListHeaderStateChanged(ByVal sender As Object, _
    ByVal e As ListHeaderStateChangedEventArgs)
        Dim header As ListHeader = CType(sender, ListHeader)
        Dim c As Control = FlowLayoutPanel1.Controls(CInt(header.Tag))

        If e.State = ListHeaderState.Collapsed Then
            c.Hide()
        Else
            c.Show()
        End If
    End Sub
End Class

Da es mir nicht darum ging, das Steuerelement im Designer bearbeiten zu können, implementierte ich keine RemoveSection-Methode und stellte auch keine anderen Infrastrukturelemente bereit, um das Hinzufügen oder Entfernen von Abschnitten im Visual Studio-Designer zu ermöglichen. So wie es hier vorliegt, muss das Steuerelement im Code erstellt und mit Inhalten gefüllt werden.

Um diesen Code zu testen, erstellte ich ein UserControl-Steuerelement namens ToolboxContentControl, das als Inhaltssteuerelement fungieren sollte. Das ToolboxContentControl-Steuerelement enthält ein ToolStrip-Steuerelement mit zwei ToolStripButton-Steuerelementen, die so konfiguriert sind, dass sie wie Einträge in der Toolbox von Visual Studio aussehen. (Würde ich eine reale Anwendung mit CollapsibleControl erstellen, dann hätte ich einige Inhaltssteuerelemente mit jeweils einer Reihe von Optionen verwendet. Für meine Testanwendung verwendete ich dagegen vier Instanzen von ToolboxContentControl.) Ich fügte dann der Hauptfomularklasse der Anwendung ein CollapsibleControl-Objekt und etwas Code hinzu, um es mit vier Abschnitten zu füllen:

Private Sub Form1_Load(ByVal sender As System.Object, _
ByVal e As System.EventArgs) Handles MyBase.Load
    Dim NewSection As New CollapsibleControlSection( _
        "Toolbox Controls", New ToolboxContentControl())
    Me.CollapsibleControl1.AddSection(NewSection)

    Dim NewSection2 As New CollapsibleControlSection( _
        "Toolbox Controls 2", New ToolboxContentControl())
    Me.CollapsibleControl1.AddSection(NewSection2)

    Dim NewSection3 As New CollapsibleControlSection( _
        "Toolbox Controls 3", New ToolboxContentControl())
    Me.CollapsibleControl1.AddSection(NewSection3)

    Dim NewSection4 As New CollapsibleControlSection( _
        "Toolbox Controls 4", New ToolboxContentControl())
    Me.CollapsibleControl1.AddSection(NewSection4)
End Sub

Das Ergebnis entsprach genau meinen Vorstellungen: eine Reihe reduzierbarer Menüs, die dank der Leistungsfähigkeit von FlowLayoutPanel neu angeordnet werden, wenn ein oder mehrere Menüs reduziert werden. Abbildung 6 enthält ein Beispiel für die reduzierbaren Menüs.

Reduzierbares Beispielmenü in Aktion
Abbildung 6: Reduzierbares Beispielmenü in Aktion

Das reduzierbare Beispielmenü ist den Menüs der Toolbox von Visual Studio nachempfunden. Allerdings kann das Toolbox-Fenster auch noch in der Länge nach auf- und zugeklappt werden. Diese Art von Fenster wird gelegentlich auch als Flyout-Panel bezeichnet.

 

Flyout-Panel

Ein Flyout-Panel ist ein Bereich, den der Benutzer einblenden kann. Der Bereich wird ausgeblendet, wenn er nicht verwendet wird. Das Fenster "Toolbox" von Visual Studio wird beispielsweise eingeblendet, wenn Sie den Mauszeiger über das Symbol "Toolbox" halten. Sobald der Mauszeiger aus dem Symbol "Toolbox" und dem Toolbox-Bereich herausbewegt wird, wird dieser Bereich automatisch ausgeblendet.

Sie können dieses Verhalten in Windows Forms implementieren, indem Sie wieder unseren alten Bekannten, das ToolStrip-Steuerelement, einsetzen. Im ersten Schritt erstellte ich ein neues UserControl-Element namens ToolboxPanel. Ich ging bei diesem ToolboxPanel-Objekt genauso vor wie beim Testformular mit reduzierbaren Menüs. Ich fügte ein CollapsibleControl-Element und etwas Code hinzu, um es zu Testzwecken mit vier Abschnitten zu füllen.

Als Nächstes erstellte ich ein neues Formular für mein Projekt und legte es als Startformular fest. Dann fügte ich ein ToolStrip-Steuerelement hinzu und legte dessen Dock-Eigenschaft auf den Wert Left fest. Ich erstellte ein ToolStripLabel-Symbol, wies ihm ein Bild zu, das der Toolbox ähnelte, und wies der Text-Eigenschaft das Wort "Toolbox" zu. Damit der Text vertikal angezeigt wurde, legte ich die für die TextDirection-Eigenschaft der ToolStrip-Schaltfläche die Einstellung Vertical90 fest.

Mein Testformular sollte lediglich einen einzigen Flyout-Panel anzeigen. In der Praxis werden Sie wahrscheinlich mehrere Flyout-Panels verwenden, wie in Visual Studio. Ich schrieb den Code so, dass schließlich mehrere Flyout-Panels unterstützt werden (allerdings erst nach entsprechenden Erweiterungen). Ich wiederholte einen Trick, den ich schon in der Anwendung Countdown benutzt hatte, und wies der Tag-Eigenschaft meines ToolStripLabel-Steuerelements die Zeichenfolge "TestPanelFlyoutVB.ToolboxPanel" zu, also den voll qualifizierten Namen des zuvor erstellten ToolboxPanel-Steuerelements. Der voll qualifizierte Name wird im MouseEnter-Ereignishandler des ToolStripLabel-Steuerelements benutzt, um das ToolboxPanel-Steuerelement zu instantiieren, falls es noch nicht vorhanden ist.

Ich fügte zwei Timer-Steuerelemente zur Unterstützung der Flyout-Animation in das Formular ein. Das Steuerelement PanelTimer wird aktiviert, wenn der Flyout-Panel angezeigt oder ausgeblendet werden muss. MouseLeaveTimer sorgt für eine Verzögerung von einer Sekunde beim Ausblenden des Bereichs, wenn der Mauszeiger aus dem ToolStripLabel-Element herausbewegt wird. Der Grund hierfür ist, dass in unserem Flyout-Panel-Code zwei Möglichkeiten zu berücksichtigen sind:

  • Der Benutzer platziert den Mauszeiger gleich wieder über dem ToolStripLabel-Steuerelement. In diesem Fall sollte der Flayout-Panel weiterhin angezeigt werden.

  • Der Mauszeiger wird aus dem ToolStripLabel-Steuerelement herausbewegt, verbleibt aber über dem Flyout-Panel. In diesem Fall sollte der Flyout-Panel weiterhin angezeigt werden, damit der Benutzer ihn verwenden kann. Es gibt einen interessanten Extremfall, in dem der Mauszeiger kurzzeitig über dem dünnen Rahmen des ToolStrip-Steuerelements platziert werden kann, das sich zwischen dem ToolStripLabel-Steuerelement und dem Flyout-Panel befindet. Die Verzögerung von einer Sekunde gibt dem Benutzer Zeit, den Mauszeiger in den Flyout-Panel zu bewegen.

Nachdem diese Elemente implementiert waren, schrieb ich folgenden Code, um die verschiedenen Teile miteinander zu verknüpfen.

Imports System.Runtime.Remoting
Imports System.Reflection

Public Class FlyoutForm

    Dim _DoFade As Boolean = False
    Dim _HaveProcessedMouseEnter As Boolean = False
    Dim _CurrentControl As Control = Nothing
    Dim _CachedControlPoint As Point = Nothing

    Sub ToolBoxLabel_MouseEnter(ByVal sender As Object, _
    ByVal e As EventArgs) Handles ToolBoxLabel.MouseEnter
        If Not _HaveProcessedMouseEnter Then
            _HaveProcessedMouseEnter = True

            If (_DoFade) Then
                PanelTimer.Stop()
                _DoFade = False
            ElseIf (_CurrentControl Is Nothing) Then
                ' Get the control to use, and instantiate it dynamically.
                Dim controlName As String = CType(sender, _
                    ToolStripLabel).Tag.ToString()
                Dim oh As ObjectHandle = _
                    AppDomain.CurrentDomain.CreateInstance( _
                    Assembly.GetExecutingAssembly().FullName, controlName)
                _CurrentControl = CType(oh.Unwrap(), Control)
                _CurrentControl.Height = Me.Height
                _CurrentControl.Location = New Point( _
                    toolStrip1.Location.X + toolStrip1.Width - _
                    _CurrentControl.Width, 0)
                _CachedControlPoint = _CurrentControl.Location
                Me.Controls.Add(_CurrentControl)

                ' The following calls make the panel appear above all 
                ' other controls on the form,
                ' *except* the ToolStrip with the panel buttons.
                _CurrentControl.BringToFront()
                toolStrip1.BringToFront()
            End If

            PanelTimer.Start()
        End If
    End Sub 'toolBoxLabel_MouseEnter

    Sub ToolBoxLabel_MouseLeave(ByVal sender As Object, _
    ByVal e As EventArgs) Handles ToolBoxLabel.MouseLeave
        ' Slight problem: We could be transitioning between the ToolStrip 
        'and the flyout panel. If the
        ' mouse is in an inbetween state where it's not over the 
        ' ToolStripLabel but *is* over the
        ' thin wedge of the ToolStrip itself, we'll fade the panel 
        ' erroneously. So let's kick off this
        ' timer to give the user time to transition. The delay built into 
        ' VS is about 1 second; that 
        ' should work for us.
        MouseLeaveTimer.Start()
    End Sub


    Private Sub PanelTimer_Tick(ByVal sender As Object, _
    ByVal e As EventArgs) Handles PanelTimer.Tick
        If _DoFade Then
            ' Hide the panel.
            If _CachedControlPoint.X + _CurrentControl.Size.Width > _
            toolStrip1.Location.X + toolStrip1.Width Then
                _CachedControlPoint.Offset(-20, 0)
                _CurrentControl.Location = _CachedControlPoint
            Else
                PanelTimer.Stop()
                _HaveProcessedMouseEnter = False
                _DoFade = False
            End If
        Else
            ' Show the panel.
            If _CachedControlPoint.X < toolStrip1.Location.X + _
            toolStrip1.Width Then
                _CachedControlPoint.Offset(20, 0)
                _CurrentControl.Location = _CachedControlPoint
            Else
                PanelTimer.Stop()
                _HaveProcessedMouseEnter = False
                _DoFade = True
            End If
        End If
    End Sub

    Private Sub MouseLeaveTimer_Tick(ByVal sender As Object, _
    ByVal e As EventArgs) Handles MouseLeaveTimer.Tick
        ' If we're over the flyout panel, don't trigger the leave event. 
        Dim controlUnderMouse As Control = _
            Me.GetChildAtPoint(Me.PointToClient( _
            System.Windows.Forms.Cursor.Position))

        ' This may get us whatever child control is under the ToolStrip. 
        ' We need to inspect
        ' parent controls until we reach the Form. If we don't find a 
        ' control that matches
        ' _CurrentControl in the parenting chain, we fade.
        Dim overPanel As Boolean = False
        While controlUnderMouse IsNot Me
            ' If we're over the blank form area, controlOverMouse 
            ' will be null.
            If controlUnderMouse Is Nothing Then
                overPanel = False
                Exit While
            End If

            If controlUnderMouse Is _CurrentControl Then
                overPanel = True
                Exit While
            End If

            controlUnderMouse = controlUnderMouse.Parent
        End While
        If Not overPanel Then
            ' Since the mouse must leave the panel area SOMETIME, 
            ' keep checking until we've left. 
            MouseLeaveTimer.Stop()
            PanelTimer.Start()
        End If
    End Sub
End Class

Nachdem dieser Code eingebaut war, funktionierte mein Beispiel für einen Flyout-Panel reibungslos. Abbildung 7 zeigt den Flyout-Panel in voller Größe.

Beispiel für Flyout-Panel in Aktion
Abbildung 7: Beispiel für Flyout-Panel in Aktion

Hinweis   Die Beispielanwendung zeigt ein schlechteres Leistungsverhalten, wenn sie unter dem Visual Studio-Debugger ausgeführt wird. Der Flyout-Panel arbeitet halb so schnell wie erwartet. Das Beispiel zeigt das erwartete Leistungsverhalten, wenn es nicht im Debugger ausgeführt wird.

Offensichtlich lässt sich diese Anwendung noch weiter verbessern: Die Gestaltung des Flyout-Panels könnte weiter ausgearbeitet werden, es könnten mehrere Bereiche eingebunden und Features wie Pinning implementiert werden (sodass der Flyout-Panel sichtbar bleibt, statt ein- und ausgeblendet zu werden). Damit andere Registerkarten mit Flyout-Panels unterstützt werden, müsste ich zudem Programmcode hinzufügen, in dem überprüft wird, ob ein Flyout-Panel geöffnet ist, und der geöffnete Panel geschlossen wird, bevor ein neuer Panel geöffnet wird.

 

Schlussbemerkung

Wie in diesem Artikel veranschaulicht wurde, erleichtert Windows Forms 2.0 die Erstellung von fortgeschrittenen dynamischen Layouts und Navigationslayouts. Weitere Informationen zu Windows Forms finden Sie unter: