Export (0) Print
Expand All
Debugger Visualizations, Garbage Collection
Use the Visual Studio 2005 Bootstrapper to Kick-Start Your Installation
CLR Inside Out: Improving Application Startup Time
Unit Testing Tips: Write Maintainable Unit Tests That Will Save You Time And Tears
Program Customized Testing Environments Without Trashing Your Machine
Bridge the Gap Between Development and Operations with Whitehorse
Visual Studio 2005: Create Reusable Project And Item Templates For Your Development Team
Expand Minimize

Creating Smart Application Layouts with Windows Forms 2.0

Visual Studio 2005
 

Jay Allen
Microsoft Corporation

July 2006

Applies to:
   Microsoft Visual Studio 2005
   Microsoft .NET Framework 2.0
   Microsoft Windows Forms 2.0

Summary: Learn how to use new controls in Windows Forms 2.0 to create smart and extensible application layouts. (15 printed pages)

Download code samples in C# and Visual Basic (903 KB) at the Microsoft Download Center.

Contents

Introduction
The Limits of TabControl
Tab-Style Tool Strip
Outlook-Style Tool Strip
Collapsible Menus
Flyout Panel
Conclusion

Introduction

Microsoft Windows Forms 2.0 allows you to organize the functionality of your application in unique ways that are easy for your customers to use. Using new controls, such as ToolStrip, FlowLayoutPanel, and TableLayoutPanel, you can create smart and extensible application layouts. This article describes four application layouts: a tab-style tool strip, an Outlook-style tool strip, collapsible menus, and a flyout panel. All of these layouts are simple to create with Windows Forms 2.0. This article assumes you know the basics of Windows Forms and are familiar with UserControls.

The Limits of TabControl

Recently, I wanted to build a simple application that served as a countdown timer for various activities, such as exercising or meditating. The core functionality would be a timer that would play a sound repeatedly after the set time (say, 30 minutes) had expired. I knew that this simple application would have three major sections: A Home panel that displays at start up, a Countdown panel on which you set and run the countdown clock, and a Settings panel on which you set a custom sound and check whether you want to keep a record of your sessions.

I decided that the best way to go about this was a tab-based approach to navigation. I defined the major areas of functionality on three different tabs. At first, the TabControl, which you get for free with Windows Forms, seemed like a natural choice. But then came the catch. I didn't want this application to look like a normal Windows application, with the default gray color schemes. And I didn't want to use top-aligned tabs, which are the default. I wanted the tabs to stretch down the right side of the TabControl. I wanted everything to be white, without distracting borders, and I wanted the three tabs to stretch from the top to the bottom of the application.

I soon discovered that the TabControl wouldn't do what I wanted. While TabControl supported right-aligning and left-aligning tabs, by default it would render the text vertically instead of horizontally. I found that if Visual Styles were enabled, no text was rendered in the side-aligned tabs at all!

I found a way to get close to the effect that I wanted by using owner-draw. (For details, see my Window Forms TabControl: Using Right-Aligned or Left-Aligned Tabs post on the Windows Forms Documentation Updates blog.) However, there were other obstacles that prevented me from using TabControl. In particular, owner-draw could only redraw the contents of the tabs. I could not customize or eliminate altogether the tab borders, either on the tabs or surrounding the tab panels. This limited the customization I could accomplish. To top it all off, the controls on the tabs themselves are sometimes not laid out correctly when you apply Visual Styles. Given all these problems and the lack of customizability, I was no longer jazzed about using TabControl.

Tab-Style Tool Strip

I took a step back and realized that there was another, more inventive way to approach this problem. I remembered from my past tinkering that the new ToolStrip control in Windows Forms was very flexible. Could I use it to create exactly the look and feel I was aiming for?

As it turned out, I could—and with only a minimum of programming. Figure 1 shows the finished result.

Aa730847.laywf201(en-US,VS.80).gif

Figure 1. The completed application, which uses a tab-style tool strip.

Here's how I created the application. The main form is split into two parts. On the left side is a custom UserControl I wrote called Slideshow (included with the Countdown sample). This is just a "showy" control that uses the managed WebBrowser wrapper control to fade between a series of images using Dynamic HTML.

What's interesting for this article is the right side, on which I positioned a SplitContainer control. SplitContainer is also a new control in Windows Forms. It replaces the old Splitter control, providing greater functionality for the number and directionality of split panels. SplitContainer has two split panels, Panel1 and Panel2. Panel1 is the content panel that will contain various controls for each button. Panel2 will include buttons that will allow navigation between the three panels: Home, Countdown, and Settings. Panel2 will host the ToolStrip control. For my application, I kept the Orientation property on SplitContainer set to Vertical (the default), and resized the panels in Microsoft Visual Studio to make the left one larger than the right one. Figure 2 shows the Countdown application prior to adding the ToolStrip control to Panel2.

Aa730847.laywf202(en-US,VS.80).gif

Figure 2. The Countdown application with a custom Slideshow UserControl and a SplitContainer.

After adding the custom Slideshow UserControl and SplitContainer, I added a ToolStrip control to Panel2. By default, the Dock property of ToolStrip is set to Top. This makes sense; the default for most applications is to use ToolStrip to create command toolbars, like you find in Microsoft Word and other applications. But for my purposes, I needed to set the Dock property to Fill. I also didn't want the slick Office-like rendering that ToolStrip uses by default, so I set RenderMode to System and BackColor to White. I set GripStyle to Hidden, so that the grip used to grab the ToolStrip and relocate it to another area of the screen wasn't visible. Finally, since I wanted the navigation buttons to flow from top to bottom, one on top of the other, I changed the value of LayoutStyle to VerticalStackWithOverflow.

Next came the actual navigation buttons. Since I would have three panels, I created three buttons. I used the Items Collection Editor in the visual designer to create three ToolStripButton objects in the ToolStrip. To open the Items Collection Editor, I clicked the ellipsis button next to the Items property for the ToolStrip. (To open the Items Collection Editor, I could have also clicked the ToolStrip's smart tag and in the ToolStrip Tasks menu, clicked Edit Items.)

Next, I needed to stylize the buttons' look and feel. I created an image for each button with stock photography, and resized the images to the same height and width. I clicked the ellipsis button next to the Image property for each ToolStripButton and used the Select Resource dialog box to import the image for each button. By default, ToolStripButton will scale the image down to the default size of the button. Since I wanted the image at its actual size, I set the ImageScaling property on each button to None.

I entered the appropriate text in the Text property of each ToolStripButton. In order to get the text to appear below the image, I had to change two properties. First, I had to set the DisplayStyle property to ImageAndText. This made the text appear to the center right of the image. To put the text underneath the image, I set the TextImageRelation property to ImageAboveText. I then changed the font of ToolStripButton so that the text rendered to my liking.

For my last magic trick, I decided that I wanted the buttons to appear "checked" when they were clicked, so that the user had a quick visual indicator as to which panel was currently active. (The checked effect appears as a border around the second button in Figure 1.) To accomplish this, I set the CheckOnClick property for each button to True.

Making the Tool Strip Tabs Functional

This was the limit of what I could do with the designer. As you can see, I accomplished a lot without writing a lick of code. Now, it was time to design the individual panels and hook everything together. For my tab-style tool strip, I sought to implement the following logic:

  • Display the Home panel by default when the application starts.
  • Detect when a ToolStrip button is clicked. If the corresponding panel for that button is not already displayed, create and display it.
  • Uncheck the previously checked button.

For each of the panels, I created a separate UserControl: one for the Home panel, one for the main Countdown panel, and one for the Settings panel. I named these panels CDHomePanel, CDCountdownPanel, and CDSettingsPanel, respectively.

Once I had implemented the bare shell of these UserControls, I set about writing the logic to display them in Panel1 of the SplitContainer. Since I only had three panels, I could have hard-coded a separate event handler for each button that instantiated and displayed the appropriate panel. However, that isn't a very extensible approach. It's fine for three buttons—but what if I eventually have six buttons? What if I want to use this same approach in a larger application that requires 10 or 20 buttons? I decided to write a single event handler to handle all of the buttons without hard-coding any values. I wanted the code to be abstract enough that, in the future, I could add another button and another panel with minimal changes to the ToolStripButton event handler code.

First, I went back to the designer and, for each ToolStripButton, set the Tag property to the name of the UserControl, prefixed with its namespace, to which the button corresponds. I did this so that my event handler could use the Tag property of the button that was clicked to deduce which UserControl it should instantiate. For example, for my CDHomePanel UserControl, which exists in the namespace Countdown, the fully qualified name is Countdown.CDHomePanel. I assigned "Countdown.CDHomePanel" to the Tag property of the Home button. Similarly, I set the Tag property of the Countdown button to "Countdown.CDCountdownPanel", and the Tag property of the Settings button to "Countdown.CDSettingsPanel".

Next, I added some code that my form would require for the event handler. Since I was going to instantiate the UserControl panels dynamically, I would need to use classes defined in the System.Reflection and System.Runtime.Remoting namespaces. I also defined two class-level private variables my event handler would need: _CurrentControl, a reference to the panel currently visible to the user; and _CurrentClickedButton, a reference to the ToolStripButton corresponding to the visible panel.

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

Next, I added code to the Load event handler of my form to instantiate the Home panel, display it to the user, and initialize the _CurrentControl and _CurrentClickedButton variables with valid object references. Since I only have to do this once in the lifetime of the application, I directly created an instance of CDHomePanel. Once the panel was instantiated, I added it to Panel1 of my SplitContainer.

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

Finally, I wrote a method called Navigate, which is designed to be called from the MouseDown event of each ToolStripButton. (I'll explain later in the section Incorporating Prompts when Switching Panels why I factored this code out into a separate method.) Since I was authoring in Visual Basic, I could use the Handles keyword to specify that this event handler applied to all three buttons. The handler looks at the Controls collection of Panel1 of my SplitContainer, and sees if the UserControl corresponding to this button has been created yet. If not, it creates it by passing the Tag property of the ToolStripButton to the CreateInstance method, which is defined on the AppDomain class.

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

With that, I was finished, and could compile and run the application successfully. Given the way I wrote the code, it would be easy to add a new button and panel in four steps.

  1. Author the UserControl.
  2. Create the ToolStripButton.
  3. Assign the name of the UserControl to the Tag property.
  4. Add the ToolStripButton to the Handles clause of the event handler.

As you can see, the most difficult step here is creating the new UserControl. Once that is done, all that is required is to configure the button as I did earlier and modify one line of code. You now have a replacement for the TabControl that is easy to extend and more open to customization.

Incorporating Prompts when Switching Panels

Once I finished all of this, some questions occurred to me. What happens when users click the Settings button when they are on the Countdown panel and the clock is running? And what happens if they are on the Settings panel and click the Countdown button before saving their settings? Should I keep the changes? Throw them away? Ask the user?

It became clear that the UserControl panels needed a mechanism by which their parent form could inform the UserControl panels that a navigation was about to occur. Likewise, the panels each needed a way to either allow or forbid such navigation, based on the panel's state. To achieve this, I defined an interface called IPanelNavigating, and declared a single method named CanNavigate for classes to implement. The CanNavigate method returns a Boolean value that indicates whether the panel can be switched.

Public Interface IPanelNavigating
    Function CanNavigate() As Boolean
End Interface

Since I would never have a need to stop navigation on the Home panel, I didn't implement it for this class. I implemented it for my Countdown panel, and use a message box to ask users if they want to end the countdown. If they do, I reset the timer and return True from CanNavigate. If they do not wish to end the session, I return False.

To implement this in my main form, I added some logic to the MouseDown event handler I defined for all three ToolStripButton objects.

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

I finished up the application by fleshing out the programming of the UserControl panels, and adding support for a custom title bar, which provided me another unique use of the ToolStrip control. (For details on how to implement this, see my Using ToolStrip to Create a Custom Title Bar post on the Windows Forms Documentation Updates blog.)

Outlook-Style Tool Strip

The great thing about the Countdown application architecture is that it is easy to reuse in other applications. Thanks to the power of the ToolStrip control, you can customize the look and feel radically to fit your application's needs. Figure 3 shows another example, a mock-up of a banking application. Here the navigation has an Outlook-style tool strip.

Aa730847.laywf203(en-US,VS.80).gif

Figure 3. A simple mock layout that has an Outlook-style tool strip.

For this application, I placed the ToolStrip on the left, and left the right panel of the SplitContainer available for the UserControl panels. I formatted the ToolStripButton objects by setting DisplayStyle to ImageAndText, setting ImageAlign to MiddleCenter, setting TextAlign to MiddleRight, and setting TextImageRelation to Overlay. All of the logic I wrote for the Countdown application works fine in this application, and only needs minimal adjustment to work for an arbitrary number of ToolStripButton controls.

You can extend this concept further by adding buttons with submenus. ToolStrip supports a control called ToolStripDropDownButton, which allows you to add child controls that are displayed as a submenu when the user clicks the button. Figure 4 shows the mock banking sample modified so that the Settings button is a ToolStripDropDownButton with several submenu items denoting the different types of settings the user can modify.

Aa730847.laywf204(en-US,VS.80).gif

Figure 4. The simple mock layout from Figure 3, with ToolStripDropDownButton controls that display submenus.

Collapsible Menus

After I had finished my Countdown application, I decided to look at other ways to implement complex menus and navigation systems using the new controls in Windows Forms. The first thing that caught my eye as "similar, yet different" was the Toolbox window in Visual Studio. If you have done any programming in Windows Forms or ASP.NET with Visual Studio, you're familiar with the way in which the Toolbox categorizes controls in a series of collapsible menus. Figure 5 shows an example of the collapsible menus in the Toolbox.

Aa730847.laywf205(en-US,VS.80).gif

Figure 5. Collapsible menus in the Toolbox window of Visual Studio.

In previous versions of Windows Forms, emulating the Toolbox would have involved adding a lot of code for repositioning the collapsible menus. In Windows Forms, the task is greatly simplified by the FlowLayoutPanel control. FlowLayoutPanel hosts an arbitrary number of Windows Forms controls in a sequential flow, one control positioned after the other. The great thing about FlowLayoutPanel is that it is dynamic. If you remove or hide a control at run time, the other controls after it will "collapse" into the space it leaves. This is just what you need to implement collapsible menus.

I broke the collapsible control itself into two separate controls:

  • A control named ListHeader to represent the shaded user interface element that displays the header text and the plus/minus collapsible indicator on the left side. ListHeader defines an event that it raises whenever the collapsible indicator is clicked.
  • A control named CollapsibleControl that combines ListHeader with another arbitrary control (which I'll call the "content control"). CollapsibleControl contains the FlowLayoutPanel, which displays an arbitrary number of content sections that can be collapsed and expanded.

I won't get into the details of how I implemented the ListHeader control; interested readers can look at the code. The important fact here is that I defined a ListHeaderStateChanged event to signal to CollapsibleControl whether the content control should be collapsed or displayed. ListHeaderStateChanged passes a ListHeaderEventArgs, which defines a State property of type ListHeaderState. ListHeaderState is an enumeration with two possible values: ListHeaderState.Expanded or ListHeaderState.Collapsed.

With ListHeader complete, I set about creating CollapsibleControl. CollapsibleControl is divided into two parts.

First, I created a CollapsibleControlSection class. CollapsibleControlSection associates a content control with a header name.

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

The CollapsibleControl class uses the ListHeader control and one or more CollapsibleControlSection objects to display a set of content controls separated by headers. When one content control is collapsed, or hidden, the other controls will fill the space left behind. To pull off this trick, I first opened CollapsibleControlSection in the Visual Studio designer and added a FlowLayoutPanel to the control. I set the Dock property of my FlowLayoutPanel to Fill, so that it occupied the entire area of the control.

Second, I created the CollapsibleControl itself. The logic needed to make this control work is surprisingly simple. I imported the namespace System.Collections.Generic so that I could use the generic List(Of T) class to store CollapsibleControlSection objects. This List is populated by the AddSection method, which performs the following operations:

  • Creates a ListHeader for the content control, and adds it and the content control to the end of the FlowLayoutPanel.
  • Uses the Tag property of the ListHeader control to associate the content control with its ListHeader control, so that CollapsibleControl knows which content control to show and hide.
  • Adds a handler for the ListHeader control's ListHeaderStateChanged event, so that it can show or hide the appropriate content panel when the user clicks the collapsible indicator on ListHeader.

The following is the code for the CollapsibleControl class.

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

Since I wasn't concerned about making the control designable, I didn't implement a RemoveSection method, or provide any of the other infrastructure necessary to enable adding or removing sections in the Visual Studio designer. The control, as written, must be created and populated using code.

To test this code, I created a UserControl called ToolboxContentControl, which will serve as my content control. ToolboxContentControl contains a ToolStrip control that has two ToolStripButton controls configured to look like items in the Visual Studio Toolbox. (If I were to build a real world application using CollapsibleControl, I would have a number of content controls, each with a number of options. For my test application, however, I used four instances of ToolboxContentControl.) Then I added a CollapsibleControl to my application's main form class, and a little code to populate it with four sections:

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

The result was exactly what I was aiming for; a set of collapsible menus that were re-positioned when one or more of the menus were collapsed—thanks to the power of FlowLayoutPanel. Figure 6 shows a sample of the collapsible menus.

Aa730847.laywf206(en-US,VS.80).gif

Figure 6. Collapsible menus sample in action.

The collapsible menu sample imitates the menus in the Visual Studio Toolbox, but the Toolbox window also has the ability to slide in and out of view. This type of window is sometimes called a flyout panel.

Flyout Panel

A flyout panel is a panel that slides into view when requested by the user and slides out of view when not used. The Visual Studio Toolbox window slides into view when you position the mouse pointer over the Toolbox icon. When the mouse pointer leaves the vicinity of both the Toolbox icon and the panel, the panel automatically slides out of view.

You can implement this same behavior in Windows Forms by once again employing our good friend, the ToolStrip control. For my first step, I created a new UserControl called ToolboxPanel. I did the same thing for ToolboxPanel that I did for the collapsible menu test form; I added a CollapsibleControl, and added some code to populate it with four sections for testing purposes.

Next, I created a new form for my project, and set it as the startup form. Then I added a ToolStrip, and set the Dock property to Left. I created a single ToolStripLabel icon, assigned it a Toolbox-like image, and set the Text property to the word "Toolbox". To make the text display vertically, I set the TextDirection property of the ToolStrip button to Vertical90.

For my test form, all I wanted to do was show a single flyout panel. However, in the real world, you would probably want a series of flyout panels, like Visual Studio uses. I wrote my code in a way that would eventually support multiple flyout panels (albeit not without further enhancements). Borrowing from a trick I used earlier with my Countdown application, I set the Tag property of my ToolStripLabel to "TestPanelFlyoutVB.ToolboxPanel", which is the fully qualified name of the ToolboxPanel control I created earlier. The fully qualified name is used in the MouseEnter event of the ToolStripLabel to instantiate the ToolboxPanel control if it doesn't already exist.

I placed two Timer controls on the form to help with the flyout animation. PanelTimer is activated when the flyout panel needs to be shown or hidden. MouseLeaveTimer provides a one-second delay in retracting the panel when the mouse leaves the ToolStripLabel. This is because our flyout panel logic has to accommodate two possibilities:

  • The user places the mouse back over the ToolStripLabel almost immediately, in which case the flyout panel should remain displayed.
  • The mouse leaves the ToolStripLabel, but enters the area of the flyout panel, in which case the flyout panel should remain displayed so that the user can interact with it. There is an interesting edge case here in which the mouse can be momentarily positioned over the thin border of the ToolStrip control that resides between the ToolStripLabel and the flyout panel itself. The one-second delay gives the user time to finish moving the cursor onto the flyout panel.

With these in place, I wrote the following code to tie everything together.

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

With this code incorporated, my flyout panel sample worked smoothly. Figure 7 shows the flyout panel in full view.

Aa730847.laywf207(en-US,VS.80).gif

Figure 7. Flyout panel sample in action.

Note   The sample application runs slower under the Visual Studio debugger. In particular, the flyout panel slows down to about half its expected speed. The sample runs as expected when it is executed outside of the debugger.

There is obviously more work that can be done here in terms of stylizing the flyout panel, incorporating multiple panels, and implementing such features as pinning (where the flyout panel remains visible instead of flying in and out). Also, in order to support adding other flyout panel tabs, I would need to add logic to check for an open flyout panel and retract it before displaying the new panel.

Conclusion

As demonstrated in this article, Windows Forms 2.0 makes it easier to create advanced dynamic and navigation layouts. For more information on Windows Forms, check out:

Windows Forms on Microsoft .NET Framework Developer Center

WindowsForms.NET

Windows Forms Documentation Updates Blog

Show:
© 2014 Microsoft