Creating Smart Application Layouts with Windows Forms 2.0
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.
The Limits of TabControl
Tab-Style Tool Strip
Outlook-Style Tool Strip
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.
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.
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
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
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
_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.
- Author the UserControl.
- Create the ToolStripButton.
- Assign the name of the UserControl to the Tag property.
- 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.
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.
Figure 4. The simple mock layout from Figure 3, with ToolStripDropDownButton controls that display submenus.
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.
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
ListHeaderto represent the shaded user interface element that displays the header text and the plus/minus collapsible indicator on the left side.
ListHeaderdefines an event that it raises whenever the collapsible indicator is clicked.
- A control named
ListHeaderwith another arbitrary control (which I'll call the "content control").
CollapsibleControlcontains 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 is an enumeration with two possible values:
ListHeader complete, I set about creating
CollapsibleControl is divided into two parts.
First, I created a
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
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
ListHeaderfor the content control, and adds it and the content control to the end of the FlowLayoutPanel.
- Uses the
Tagproperty of the
ListHeadercontrol to associate the content control with its
ListHeadercontrol, so that
CollapsibleControlknows which content control to show and hide.
- Adds a handler for the
ListHeaderStateChangedevent, so that it can show or hide the appropriate content panel when the user clicks the collapsible indicator on
The following is the code for the
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.
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.
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
ToolboxPanelcontrol 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.
MouseLeaveTimerprovides 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.
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.
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: