发布日期: 2006-08-14 | 更新日期: 2006-08-14

Jay Allen

Microsoft Corporation

适用于:

Microsoft Visual Studio 2005

Microsoft .NET Framework 2.0

Microsoft Windows Forms 2.0

摘要:学习如何使用 Windows Forms 2.0 中的新控件创建智能化和可扩展的应用程序布局。

Microsoft 下载中心下载 C# 和 Visual Basic 代码示例 (903 KB)。

*
本页内容
简介 简介
TabControl 的局限 TabControl 的局限
选项卡样式的工具条 选项卡样式的工具条
Outlook 样式的工具条 Outlook 样式的工具条
可折叠菜单 可折叠菜单
飞出式面板 飞出式面板
结论 结论

简介

Microsoft Windows Forms 2.0 允许您以独特的方式组织应用程序的功能,以便于客户使用。通过使用新的控件(比如 ToolStrip、FlowLayoutPanel 和 TableLayoutPanel),您可以创建智能化和可扩展的应用程序布局。本文介绍四种应用程序布局:选项卡样式的工具条、Outlook 样式的工具条、可折叠菜单和飞出式面板。所有这些布局用 Windows Forms 2.0 创建起来都很简单。本文假定您了解 Windows Forms 的基础知识并熟悉 UserControls。

TabControl 的局限

最近,我想要构建一个可作为各种活动(比如锻炼或思考)的倒计时器的简单应用程序。核心功能是可在设定的时间(比如 30 分钟)结束后反复播放一种声音。我知道这个简单的应用程序应当由三个主要部分构成:启动时显示的“Home”(主)面板、在其上设置和运行倒计时时钟的“Countdown”(倒计时)面板和在其上设置自定义声音并选择是否要保存会话记录的“Settings”(设置)面板。

我认定最好的入手方法是使用基于选项卡的导航方法。我在三个不同的选项卡上定义了主要的功能区域。最初,在 Windows Forms 中免费提供的 TabControl 似乎是自然而然的选择。但问题来了。我不想让此应用程序看上去像一个普通 Windows 应用程序,采用其默认的灰色配色方案。我也不想使用默认情况下的顶部对齐的选项卡。我想让各个选项卡在 TabControl 的右侧向下延伸排列。我希望所有内容均为白色,没有分散注意力的边框,并且我想让三个选项卡从应用程序的顶部延伸到底部。

我很快发现 TabControl 不能满足我的要求。虽然 TabControl 支持右对齐和左对齐选项卡,但默认情况下它会垂直而非水平呈现文本。我发现,如果启用“视觉样式”,则侧向对齐的选项卡上根本不会呈现任何文本!

我发现,通过使用 owner-draw(自绘)功能,可以获得与我的需求接近的效果(有关详细信息,请参阅我在 Windows Forms Documentation Updates 博客上发布的 Windows Forms TabControl:Using Right-Aligned or Left-Aligned Tabs(英文))。不过,还有其他的障碍阻止我使用 TabControl。特别是,自绘功能只能重绘选项卡的内容。我无法自定义或完全消除选项卡上或选项卡面板周围的选项卡边框。这限制了我的自由发挥。更有甚者,选项卡本身的控件在您应用“视觉样式”时有时布局不正确。鉴于所有这些问题以及缺乏可自定义性,我不再热衷于使用 TabControl 了。

选项卡样式的工具条

我停下来考虑了一下,意识到另有一种更具创造性的方法可以解决这个问题。我记得在以前的摸索练习中感到 Windows Forms 的新的 ToolStrip 控件极其灵活。我能否使用它来实现恰如所需要的感观呢?

结果表明可以这样做,并且只需要极少量的编程工作。图 1 显示了最终的结果。

a

图 1. 完成后的应用程序(使用了选项卡样式的工具条)。

下面介绍我创建该应用程序的过程。主窗体拆分为两部分。左侧为我编写的名为“Slideshow”的自定义 UserControl(包括在 Countdown 示例中)。这只不过是一个“卖弄一下”的控件,它使用托管的 WebBrowser 包装控件在一系列使用动态 HTML 的图像之间淡入淡出。

对于本文,有趣之处在于右侧,我在右侧放置了一个 SplitContainer 控件。SplitContainer 也是 Windows Forms 中的一个新控件。它替代了旧的 Splitter 控件,为拆分面板的数量和方向性提供了更强的功能。SplitContainer 具有两个拆分面板,即 Panel1 和 Panel2。Panel1 是包含每个按钮的各个控件的内容面板。Panel2 包括允许在以下三个面板之间进行导航的按钮:Home、Countdown 和 Settings。Panel2 将承载 ToolStrip 控件。对于我的应用程序,我将 SplitContainer 上的 Orientation 属性保持为默认值 Vertical,然后在 Microsoft Visual Studio 中调整面板的大小,使左侧的面板比右侧的面板大。图 2 显示了向 Panel2 中添加 ToolStrip 控件之前的 Countdown 应用程序。

a

图 2. 具有一个自定义 Slideshow UserControl 和一个 SplitContainer 的 Countdown 应用程序。

添加自定义 Slideshow UserControl 和 SplitContainer 后,我向 Panel2 中添加了一个 ToolStrip 控件。默认情况下,ToolStrip 的 Dock 属性设置为 Top。这样做是有道理的;大多数应用程序默认情况下都使用 ToolStrip 来创建命令工具栏,比如在 Microsoft Word 和其他应用程序中那样。但对于我的应用程序,我需要将 Dock 属性设置为 Fill。我也不希望使用 ToolStrip 默认情况下使用的 Office 式的老套呈现方式,因此,我将 RenderMode 设置为 System,并将 BackColor 设置为 White。我将 GripStyle 设置为 Hidden,以便让用于抓取 ToolStrip 并将其重新定位到屏幕其他区域的手柄不可见。最后,由于我想让导航按钮从上至下排列,即其中一个位于另一个的上方,因此我将 LayoutStyle 的值改为 VerticalStackWithOverflow。

接下来就是真正的导航按钮了。由于我将有三个面板,因此我创建了三个按钮。我在可视设计器中使用 Items Collection Editor(项集合编辑器)在 ToolStrip 中创建了三个 ToolStripButton 对象。为了打开 Items Collection Editor,我单击 ToolStrip 的 Items 属性旁边的省略号按钮(要打开 Items Collection Editor,也可以单击 ToolStrip 的智能标记,然后在“ToolStrip Tasks”(ToolStrip 任务)菜单中单击“Edit Items”(编辑项))。

接下来,我需要设置按钮的感观样式。我用库存的照片为每个按钮创建了一个图像,并将图像的大小调整到相同的高度和宽度。我单击每个 ToolStripButton 的 Image 属性旁边的省略号按钮,并使用“Select Resource”(选择资源)对话框导入了每个按钮的图像。默认情况下,ToolStripButton 会将图像缩小到按钮的默认大小。由于我需要实际大小的图像,因此我将每个按钮上的 ImageScaling 属性设置为 None。

我在每个 ToolStripButton 的 Text 属性中输入了相应的文本。为了使该文本显示在图像的下面,我不得不更改两个属性。首先,我需要将 DisplayStyle 属性设置为 ImageAndText。这使文本显示在图像上中间偏右的位置。为了使文本显示在图像的下方,我将 TextImageRelation 属性设置为 ImageAboveText。之后我更改了 ToolStripButton 的字体,以便让文本按照我的想法呈现。

我最后的一招是,决定让按钮在受到单击后显示为“选中”状态,以便用户可以一目了然地看到哪个模板当前处于活动状态(在图 1 中,通过第二个按钮周围出现的边框来表示选中)。这了实现这一目标,我将每个按钮的 CheckOnClick 属性设置为 True。

使工具条选项卡可以工作

这是使用该设计器所能实现的极限操作。您可以看出,我完成了大量工作却未编写一点代码。现在的任务是设计各个面板,并将所有一切合并在一起。对于选项卡样式的工具条,我设法实现了以下逻辑:

应用程序启动时默认显示“Home”面板。

单击 ToolStrip 按钮时进行检测。如果尚未显示与该按钮相对应的面板,则创建并显示该面板。

取消选中以前选中的按钮。

我为每个面板单独创建了一个 UserControl:即分别为“Home”面板、主“Countdown”面板和“Settings”面板各创建了一个。我将这些面板分别命名为 CDHomePanel、CDCountdownPanel 和 CDSettingsPanel。

在实现了这些 UserControls 的空外壳后,我开始着手编写在 SplitContainer 的 Panel1 中显示它们的逻辑。由于我仅有三个面板,因此我可以为每个按钮都进行单独的事件处理程序硬编码,用以实例化及显示相应的面板。但这并不是一种扩展性良好的方法。这种方法对于三个按钮没有问题,但如果最终有六个按钮,情况又会如何呢?如果要在需要 10 个或 20 个按钮的更大应用程序中使用这种方法,情况又当如何呢?我决定只编写一个事件处理程序来处理所有按钮,而不对任何值进行硬编码。我想让代码足够抽象,以便将来添加其他按钮和其他面板时,可以尽可能少地更改 ToolStripButton 事件处理程序代码。

首先,我回到了设计器中,将每个 ToolStripButton 的 Tag 属性都设置为 UserControl 的名称,并以对应于该按钮的命名空间作为前缀。我之所以这样做,是为了使我的事件处理程序能够使用受到单击的按钮的 Tag(标记)属性来推断出应该实例化哪个 UserControl。例如,对于 CDHomePanel UserControl,由于它存在于命令空间 Countdown 中,因此其完全限定的名称 Countdown.CDHomePanel。我将 "Countdown.CDHomePanel" 指定给“Home”按钮的 Tag 属性。类似地,我将“Countdown”按钮的 Tag 属性设置为 "Countdown.CDCountdownPanel",将“Settings”按钮的 Tag 属性设置为 "Countdown.CDSettingsPanel"。

接下来,我添加了一些我的窗体将需要的用于事件处理程序的代码。由于我打算以动态方式实例化 UserControl 面板,因此我需要使用 System.Reflection 和 System.Runtime.Remoting 命名空间中所定义的类。我还定义了两个我的事件处理程序将会需要的类级别的私有变量:_CurrentControl,它是对用户当前可见面板的引用;_CurrentClickedButton,它是对与可见面板相对应的 ToolStripButton 的引用。

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

接下来,我向窗体的 Load 事件处理程序添加了代码,以便实例化“Home”面板、向用户显示该面板并用有效的对象引用初始化 _CurrentControl 和 _CurrentClickedButton 变量。由于我只需要在应用程序的生命周期内进行一次此操作,因此我直接创建了 CDHomePanel 的一个实例。实例化该面板后,我将其添加到了 SplitContainer 的 Panel1 中。

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

最后,我编写了一个名为 Navigate 的方法,它设计成从每个 ToolStripButton 的 MouseDown 事件进行调用。(我将在后面的切换面板时显示提示一节中解释为什么会将此代码分解到一个单独的方法中。)由于我是在 Visual Basic 中进行创作,因此可以使用 Handles 关键字来指定此事件处理程序适用于所有三个按钮。处理程序会查看 SplitContainer 的 Panel1 的 Controls 集合,并会查看是否已经创建与此按钮相对应的 UserControl。如果没有,它会将 ToolStripButton 的 Tag 属性传递给 AppDomain 类中定义的 CreateInstance 方法,从而进行创建。

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()

    ' 取消选中以前单击的按钮。
    _CurrentClickedButton.Checked = False
    _CurrentClickedButton = _CurrentButton

    ' 首先,确保它不是冗余事件 - 我们是否已经
    ' 显示该控件?
    If (Not (_ControlName = _CurrentControl.Name)) Then
        ' 开始使用该控件,如果尚未定义,
        ' 则动态实例化该控件。
        _NewControl = SplitContainer1.Panel1.Controls(_ControlName)
        If (_NewControl Is Nothing) Then
            ' 未找到控件 - 实例化该控件。
            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

        ' 隐藏旧的控件并显示新的控件。 
        _CurrentControl.Visible = False
        _NewControl.Visible = True
        _CurrentControl = _NewControl
    End If
End Sub

于是,我的操作完成,并可以成功编译和运行该应用程序了。按照我编写代码的方法,可以通过以下四个步骤轻松地添加新按钮和面板。

1.

创作 UserControl。

2.

创建 ToolStripButton。

3.

将 UserControl 的名称指定给 Tag 属性。

4.

将 ToolStripButton 添加到事件处理程序的 Handles 子句中。

您可以看出,此处最难的步骤是创建新的 UserControl。这一步完成后,需要做的就是像我以前那样配置按钮并修改一行代码。您现在有了 TabControl 的替代控件,该控件易于扩展并更易于进行自定义。

切换面板时显示提示

完成所有这些工作之后,我发现了一些问题。当用户处于“Countdown”面板上并且时钟正在运行时,如果用户单击“Settings”按钮,将会发生什么情况?如果用户处于“Settings”面板上并在保存其设置前单击“Countdown”按钮,又会发生什么情况?我应该保留更改?放弃更改?还是询问用户?

显然,UserControl 面板需要一种机制,以便其父窗体可以向 UserControl 面板通知即将发生的导航。同样,每个面板都需要一种根据面板的状态允许或禁止这种导航的方法。为了实现这一目的,我定义了一个名为 IPanelNavigating 的接口,并为要实现的类声明了一个名为 CanNavigate 的单一方法。CanNavigate 方法将返回一个 Boolean 值,指示是否可以切换面板。

Public Interface IPanelNavigating
    Function CanNavigate() As Boolean
End Interface

由于我永远也不需要在“Home”面板上停止导航,因此我没有为这个类实现这一方法。我为“Countdown”面板实现了这个方法,并使用一个消息框询问用户是否要结束倒计时。如果要结束倒计时,我会重置计时器并从 CanNavigate 返回 True。如果他们不希望结束会话,我会返回 False。

为了在主窗体中实现这一功能,我向为所有三个 ToolStripButton 对象定义的 MouseDown 事件处理程序添加了一些逻辑。

Private Sub ToolStripButton_MouseDown(ByVal sender As System.Object, _
ByVal e As MouseEventArgs) Handles HomeButton.MouseDown, _
CountdownButton.MouseDown, SettingsButton.MouseDown
    ' 尝试转换为 IPanelNavigating。如果未实现, 
    ' 就会导航。
    If (TypeOf _CurrentControl Is IPanelNavigating) Then
        Dim _CanNavigate As IPanelNavigating = _
            CType(_CurrentControl, IPanelNavigating)
        If (_CanNavigate.CanNavigate()) Then
            Navigate(sender)
        End If
    Else
        ' 就会导航。
        Navigate(sender)
    End If
End Sub

最后,通过完善对 UserControl 面板的编程,并添加对自定义标题栏(为我提供了 ToolStrip 控件的另一种独特的使用方法)的支持,我完成了该应用程序的创作。(有关如何实现此功能的详细信息,请参阅我在 Windows Forms Documentation Updates 博客上发布的 Using ToolStrip to Create a Custom Title Bar(英文)。)

Outlook 样式的工具条

Countdown 应用程序体系结构的最大优势在于它易于在其他应用程序中重复使用。借助于 ToolStrip 控件的强大功能,您可以对感观进行彻底的自定义,以满足您的应用程序的需要。图 3 显示了另一个示例:银行业务应用程序的模拟。其中的导航使用 Outlook 样式的工具条。

a

图 3. 具有 Outlook 样式工具条的简单模拟布局。

对于这个应用程序,我将 ToolStrip 放在了左侧,并保留了 SplitContainer 的右侧面板供 UserControl 面板使用。通过将 DisplayStyle 设置为 ImageAndText、将 ImageAlign 设置为 MiddleCenter、将 TextAlign 设置为 MiddleRight 并将 TextImageRelation 设置为 Overlay,我设置了 ToolStripButton 对象的格式。我为 Countdown 应用程序编写的所有逻辑在这个应用程序都能很好地工作,并且只需要极少量的调整即可以用于任意数量的 ToolStripButton 控件。

通过添加具有子菜单的按钮,您可以进一步扩展这一概念。ToolStrip 支持一个名为 ToolStripDropDownButton 的控件,该控件允许您添加子控件,这些子控件会在用户单击按钮时显示为子菜单。图 4 显示了修改后的银行业务模拟示例,其“Settings”按钮现在是一个具有多个子菜单项的 ToolStripDropDownButton,这些子菜单项表示用户可以进行修改的不同类型的设置。

A

图 4. 从图 3 得到的简单模拟布局,具有显示子菜单的 ToolStripDropDownButton 控件。

可折叠菜单

在我完成 Countdown 应用程序后,我决定寻找使用 Windows Forms 中的新控件实现复杂的菜单和导航系统的其他方法。最先引起我注意并让我产生“似曾相识”感觉的,是 Visual Studio 中的“Toolbox”(工具箱)窗口。如果您曾经使用 Visual Studio 在 Windows Forms 或 ASP.NET 中进行过任何编程的话,您一定会熟悉“Toolbox”(工具箱)将各种控件分类为一系列可折叠菜单的方式。图 5 显示了“Toolbox”(工具箱)中可折叠菜单的示例。

a

图 5. Visual Studio 的“Toolbox”(工具箱)窗口中的可折叠菜单。

在 Windows Forms 的早期版本中,模仿“Toolbox”(工具箱)将会涉及到添加大量代码以重新定位可折叠菜单。在 Windows Forms 中,通过使用 FlowLayoutPanel 控件,这一任务大大得以简化。FlowLayoutPanel 以顺序流的形式承载任意数量的 Windows Forms 控件,各个控件依次排列。FlowLayoutPanel 的最大优势在于它是一个动态控件。如果在运行时删除或隐藏一个控件,则其后的其他控件将会“折叠”到它所留下的空间。这正是实现可折叠菜单时所需要的特性。

我将可折叠控件本身拆分成两个单独的控件:

一个名为 ListHeader 的控件,它表示带阴影的用户界面元素,该元素在左侧显示标题文本和加/减折叠指示符。ListHeader 定义一个事件,每当单击折叠指示符时,它都会触发该事件。

一个名为 CollapsibleControl 的控件,它会将 ListHeader 与另一个任意控件(我称之为“内容控件”)相结合。CollapsibleControl 包含 FlowLayoutPanel,可以显示任意数量的可折叠和展开的内容部分。

我现在不想详细说明实现 ListHeader 控件的过程;有兴趣的读者可以查看代码。其中重要的事实是,我定义了一个 ListHeaderStateChanged 事件,以便告诉 CollapsibleControl 应该折叠还是显示内容控件。ListHeaderStateChanged 将传递一个 ListHeaderEventArgs,后者可以定义一个类型为 ListHeaderState 的 State 属性。ListHeaderState 是一个具有以下两个可能值的枚举:ListHeaderState.Expanded 或 ListHeaderState.Collapsed。

在完成 ListHeader 之后,我开始着手创建 CollapsibleControl。CollapsibleControl 分为两个部分。

首先,我创建了一个 CollapsibleControlSection 类。CollapsibleControlSection 可以将内容控件与标题名称相关联。

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 类使用 ListHeader 控件和一个或多个 CollapsibleControlSection 对象来显示一组内容控件,中间用标题分隔。在折叠或隐藏其中一个内容控件时,其他控件会填充留下的空间。为实现这一技巧,我首先在 Visual Studio 设计器中打开了 CollapsibleControlSection,并向该控件添加了一个 FlowLayoutPanel。我将 FlowLayoutPanel 的 Dock 属性设置为 Fill,以便其占据该控件的整个区域。

然后,我创建了 CollapsibleControl 本身。为使这个控件可以工作所需的逻辑出奇地简单。我导入了命名空间 System.Collections.Generic,以便可以使用通用 List(Of T) 类来存储 CollapsibleControlSection 对象。此 List 由 AddSection 方法进行填充,该方法将会执行以下操作:

为内容控件创建一个 ListHeader,并将其连同内容控件一起添加到 FlowLayoutPanel 的末尾。

使用 ListHeader 控件的 Tag 属性将内容控件与其 ListHeader 控件相关联,以使 CollapsibleControl 知道要显示和隐藏哪个内容控件。

为 ListHeader 控件的 ListHeaderStateChanged 事件添加一个处理程序,以使其在用户单击 ListHeader 上的折叠指示符时显示或隐藏相应的内容面板。

下面是 CollapsibleControl 类的代码。

Public Class CollapsibleControl
    Dim SectionList As List(Of CollapsibleControlSection)

    Public Sub New()
        ' 此调用是 Windows 窗体设计器所要求的。
        InitializeComponent()

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

    Public Sub AddSection(ByVal NewSection As CollapsibleControlSection)
        SectionList.Add(NewSection)
        ' 添加一个新行。 
        Dim Header As New ListHeader()
        Header.Text = NewSection.SectionName
        Header.Width = Me.Width
        AddHandler Header.ListHeaderStateChanged, _
            AddressOf Header_ListHeaderStateChanged
        FlowLayoutPanel1.Controls.Add(Header)
        ' 获取要添加的控件的位置,
        ' 以便在需要时可以予以显示和隐藏。
        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

由于我并不想让该控件成为可设计的控件,因此我没有实现 RemoveSection 方法,也没有提供为在 Visual Studio 设计器中添加或删除各个部分所必需的任何其他基础结构。所编写的控件必须使用代码进行创建和填充。

为了测试此代码,我创建了一个名为 ToolboxContentControl 的 UserControl,它将作为我的内容控件。ToolboxContentControl 包含一个 ToolStrip 控件,该控件有两个 ToolStripButton 控件,这两个控件的外观配置与 Visual Studio“Toolbox”(工具箱)中的项相似。(如果我使用 CollapsibleControl 创建实际的应用程序,我将会使用一些内容控件,每个内容控件都具有多个选项。但对于我的测试应用程序,我使用了 ToolboxContentControl 的四个实例。)然后我向应用程序的主窗体类中添加了一个 CollapsibleControl,并添加了少量代码,以便向其中填充四个部分:

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

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

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

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

结果正如我希望的那样:获得了一组可折叠菜单,折叠其中一个或多个菜单时,这组菜单会重新定位。这应归功于 FlowLayoutPanel 的强大功能。图 6 显示了可折叠菜单的示例。

a

图 6. 使用中的可折叠菜单的示例。

可折叠菜单示例虽然模仿了 Visual Studio“Toolbox”(工具箱)中的菜单,但“Toolbox”(工具箱)窗口还具有滑入和滑出视图的功能。这种类型的窗口有时称为飞出式面板。

飞出式面板

飞出式面板是一种可在用户请求时滑入视图,并在不使用时滑出视图的面板。Visual Studio“Toolbox”(工具箱)窗口就可以在将鼠标指针放在工具箱图标上时滑入视图。当鼠标指针离开工具箱图标和面板区域时,该面板会自动滑出视图。

在 Windows Forms 中,您也可以通过再次使用我们的好朋友 ToolStrip 控件实现这一行为。作为我的第一步,我创建了一个名为 ToolboxPanel 的新 UserControl。我对 ToolboxPanel 进行了与可折叠菜单测试窗体相同的操作:为了进行测试,我添加了一个 CollapsibleControl,并添加了少量代码以便向其中填充四个部分。

接下来,我为项目创建了一个新窗体,并将其设置为启动窗体。然后,我添加了一个 ToolStrip,并将 Dock 属性设置为 Left。我创建了一个单一的 ToolStripLabel 图标,为其指定了一个类似于工作箱的图像,并将 Text 属性设置为 "Toolbox" 一词。为了让文本垂直显示,我将 ToolStrip 按钮的 TextDirection 属性设置为 Vertical90。

对于我的测试窗体,我想要做的就是显示单一的飞出式面板。但在实际情况下,您可能会需要一系列飞出式面板,就像 Visual Studio 使用的那样。我编写代码时,采用了最终将会支持多个飞出式面板的形式(也许需要进一步增强)。借鉴我在前面的 Countdown 应用程序中所使用的技巧,我将 ToolStripLabel 的 Tag 属性设置为 "TestPanelFlyoutVB.ToolboxPanel",它是我先前创建的 ToolboxPanel 控件的完全限定名称。ToolStripLabel 的 MouseEnter 事件中使用该完全限定名称来实例化 ToolboxPanel 控件(如果它尚不存在)。

我在窗体上放置了两个 Timer 控件,以帮助实现飞出式动画。当需要显示或隐藏飞出式面板时,即会激活 PanelTimer。当鼠标离开 ToolStripLabel 时,MouseLeaveTimer 会在收回面板的过程中提供一秒钟的延迟。这是因为飞出式面板逻辑必须适应以下两种可能性:

用户几乎立即将鼠标放回到 ToolStripLabel 上,在这种情况下,飞出式面板应保持显示状态。

鼠标离开 ToolStripLabel,但进入飞出式面板区域,在这种情况下,飞出式面板应保持显示状态,以便用户可以与其进行交互。此处存在一种有趣的边缘情况,即鼠标可能会短暂地放在 ToolStripLabel 和飞出式面板本身之间的 ToolStrip 控件的细边框上。一秒钟的延迟可以让用户有时间将光标移动到飞出式面板上。

在处理完这些问题后,我编写了以下代码以便将所有内容整合在一起。

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
                ' 获取要使用的控件,并以动态方式对其实例化。
                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)

                ' 以下调用会使面板显示在  
                ' 窗体上的所有其他控件上面,
                ' 但具有面板按钮的 ToolStrip *除外*。
                _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
        ' 小问题:我们可能正在 ToolStrip 
        '和飞出式面板之间进行转换。如果
        ' 鼠标处于一种中间状态,即不在 
        ' ToolStripLabel 上,而是 *在*
        ' ToolStrip 本身的细边框上,则将会不正确地  
        ' 淡化面板。因此,让我们启动
        ' 计时器,以便让用户有时间进行过渡。VS 中内置的延迟时间
        ' 约为 1 秒;这  
        ' 对于我们应当足够了。
        MouseLeaveTimer.Start()
    End Sub


    Private Sub PanelTimer_Tick(ByVal sender As Object, _
    ByVal e As EventArgs) Handles PanelTimer.Tick
        If _DoFade Then
            ' 隐藏面板。
            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
            ' 显示面板。
            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
        ' 如果在飞出式面板上,则不触发离开事件。
        Dim controlUnderMouse As Control = _
            Me.GetChildAtPoint(Me.PointToClient( _
            System.Windows.Forms.Cursor.Position))
        ' 这可能会为我们获取 ToolStrip 下的任何子控件。
        ' 我们需要检查
        ' 父控件,直到到达窗体。如果未在父级链中找到 
        ' 与 _CurrentControl
        ' 相匹配的控件,则将淡化。
        Dim overPanel As Boolean = False
        While controlUnderMouse IsNot Me
            ' 如果处于窗体的空白区域,controlOverMouse  
            ' 将为 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
            ' 由于鼠标必然会在某一时间离开面板区域,
            ' 因此保持检查,直到已经离开。 
            MouseLeaveTimer.Stop()
            PanelTimer.Start()
        End If
    End Sub
End Class

在采用了此代码后,我的飞出式面板示例可以顺畅地运行。图 7 显示了完整视图中的飞出式面板。

a

图 7. 使用中的飞出式面板的示例。

注意 该示例应用程序在 Visual Studio 调试器下运行较慢。特别是,飞出式面板的速度会降低到预期速度的一半。在调试器外部执行时,该示例可以按预期的那样运行。

在设置飞出式面板的样式、采用多个面板及实现诸如固定(此时飞出式面板始终可见,而不会飞入和飞出)等功能方面,显然还可以进行更多的工作。另外,为了对添加其他飞出式面板选项卡提供支持,我需要添加逻辑以发现打开的飞出式面板,并在显示新面板前收回该面板。

结论

正如本文所显示的那样,使用 Windows Forms 2.0 可以更加轻松地创建高级的动态和导航布局。有关 Windows Forms 的详细信息,请参阅:

Windows Forms on Microsoft .NET Framework Developer Center(英文)

WindowsForms.NET(英文)

Windows Forms Documentation Updates Blog(英文)