Documente Academic
Documente Profesional
Documente Cultură
0
Visual Studio 2005
https://msdn.microsoft.com/en-us/library/aa730847%28v=vs.80%29.aspx?
f=255&MSPPError=-2147217396
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.
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.
Aa730847.laywf201(en-US,VS.80).gif
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
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.
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.
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
' 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.
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.
To implement this in my main form, I added some logic to the MouseDown event
handler I defined for all three ToolStripButton objects.
Aa730847.laywf203(en-US,VS.80).gif
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
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.
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.
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.
Aa730847.laywf206(en-US,VS.80).gif
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
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
' 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
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
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.