65.9K
CodeProject 正在变化。 阅读更多。
Home

类似 MS Office 的任务窗格控件

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.85/5 (23投票s)

2007年4月25日

CPOL

10分钟阅读

viewsIcon

131487

downloadIcon

8429

一个 .NET TaskPane 控件,具有完整的运行时支持。

Screenshot - TaskPane.jpg

目录

引言

本文介绍如何创建一个 TaskPane 控件,该控件模仿 Microsoft Office XP 和 2003 中的 TaskPane(我没有 Office 2007 的副本,因此不知道此界面是否被保留)。我将描述该控件的实现,但我将特别关注该控件中包含的设计时功能,因为这是此项目更具挑战性的方面。

由于项目限制,我使用 VB.NET 编写了此组件。如果兴趣足够,我将将其移植到我更擅长的语言 C#。

介绍完毕,让我们开始吧。

必备组件

此项目使用 Ascend.NET 控件套件创建渐变面板。是的,我可以自己创建它们,但我想发现使用可用工具创建功能齐全、特性丰富的控件的速度有多快。您可以在此处下载 Ascend.NET 控件套件,或者下载包含相关 DLL 的演示项目。请注意,Ascend.NET 套件会将自身安装到您的 Visual Studio 工具箱中,因此如果这对您来说是不可取的行为,只需使用我演示项目中的 DLL 即可。

TaskPane 设计

创建 TaskPane 控件的第一步是确定如何设计其视觉元素。任务窗格有三个基本元素:

  1. 标题栏
  2. 导航按钮
  3. 内容窗格区域
Screenshot - Scrolling.jpg

请注意,当内容窗格小于可用控件区域时,滚动条会出现。这与真正的 TaskPane 行为略有不同,真正的 TaskPane 行为会在内容区域的顶部和底部生成滚动条拇指,宽度与内容区域相同。我想,除非有人真的想要这种行为,否则将其省略是可以的。

我认为呈现这三个元素的简单方法是使用以下控件:

  1. 一个工具栏,包含两个 Button(一个后退,一个前进)、一个 Label、一个 DropDownButton 和一个用于关闭的 Button。当 TaskPanePage 添加到控件时,一个新的 ToolStripMenuItem 会添加到 DropDownButton 中。当 SelectedIndexSelectedTab 更改时,将设置相应的标题和标题图像。CaptionStyle 可以通过设计器中的相应属性进行更改。
  2. 对于 Office 2003 设计,一个小的 Ascend.NET 渐变面板停靠在内容区域的顶部。NavigationStyle 属性决定导航按钮是出现在标题区域还是内容窗格中。
  3. 一个称为 TaskPanePage 的特殊面板,它继承自 Ascend.NET 渐变面板,具有额外的数据属性和一个自定义设计器。这些页面在设计器中可用。大多数相关属性都可以使用 SmartTag 面板进行设置。

下图展示了这三个基本功能。

Screenshot - DesignTimeOfficeXP_notated.jpg

我希望 TaskPane 同时支持 Office XP 样式(将导航按钮放在标题区域内)和 Office 2003 样式(将导航按钮放在内容窗格内)。

TaskPane 标题,Office 2003 样式
Screenshot - DropDown.jpg
TaskPane 标题,Office XP 样式
Screenshot - DropDownOfficeXPStyle.jpg

设计器支持

我为 TaskPaneTaskPanePage 都包含了一个 Designer 和一个 ActionList。这些对象提供了对控件的完整运行时支持——实际上,在设计器中,您很难分辨出您正在设计控件——设计时行为与运行时行为完全相同。

设计师

TaskPaneDesigner

为了让 TaskPane 在设计时表现得像运行时一样,我必须让设计器了解我自定义控件中包含的控件。这可以通过创建一个继承自 ParentControlDesigner 而非仅仅 ControlDesigner 的设计器来实现——ParentControlDesigner 允许您为控件指定父规则。例如,没有必要允许控件拖放到 TaskPane 本身——控件只能添加到 TaskPanePage。因此,我在 TaskPaneDesigner 中有以下重写:

TaskPaneDesigner.cs

' No dragging of any tool is allowed on the taskpane itself.
' Tools can only be dragged onto the TaskPanePage panels themselves,
' which manage their own designers, so I don't need to control those.
Public Overrides Function CanParent(ByVal control As _
                 System.Windows.Forms.Control) As Boolean
    Return False
End Function

Public Overrides Function CanParent(ByVal controlDesigner _
                 As ControlDesigner) As Boolean
    Return False
End Function

Protected Overrides Sub OnDragOver(ByVal de As _
          System.Windows.Forms.DragEventArgs)
    MyBase.OnDragOver(de)

    ' Reject all drags
    de.Effect = DragDropEffects.None
End Sub

如果没有 OnDragOver 重写,您将看不到控件上的“禁止”符号——您尝试将任何控件拖到 TaskPane 上只会消失。

然而,这个设计器更有趣的一个方面是它如何让设计器感知子控件,并允许开发人员像运行时一样与它们交互。为了实现这一点,设计器需要知道鼠标点击点上有一个控件。为了提供此信息,我们必须为我们的设计器重写 GetHitTest 方法。

Protected Overrides Function GetHitTest(ByVal point As System.Drawing.Point) As Boolean
 Dim pane As TaskPane = TryCast(Me.Control, TaskPane)
 If pane Is Nothing Then Return False

 If pane.btnNavBack.Enabled AndAlso _
  pane.btnNavBack.ClientRectangle.Contains(pane.btnNavBack.PointToClient(point)) Then
    Return True
 End If

 If pane.btnNavForward.Enabled AndAlso _
  pane.btnNavForward.ClientRectangle.Contains(pane.btnNavForward.PointToClient(point)) Then
    Return True
 End If

 If pane.btnNavHome.Enabled AndAlso _
  pane.btnNavHome.ClientRectangle.Contains(pane.btnNavHome.PointToClient(point)) Then
    Return True
 End If

 If pane.btnToolBack.Enabled AndAlso _
  pane.btnToolBack.Bounds.Contains(pane.toolStripToolsButtons.PointToClient(point)) Then
    Return True
 End If

 If pane.btnToolForward.Enabled AndAlso _
  pane.btnToolForward.Bounds.Contains(pane.toolStripToolsButtons.PointToClient(point)) Then
    Return True
 End If

 If pane.btnToolTaskTools.Enabled AndAlso _
  pane.btnToolTaskTools.Bounds.Contains(pane.toolStripToolsButtons.PointToClient(point)) Then
    Return True
 End If

 If pane.btnClose.Enabled AndAlso _
  pane.btnClose.Bounds.Contains(pane.toolStripToolsButtons.PointToClient(point)) Then
    Return True
 End If

 Return False
End Function

您会注意到我使用了对 TaskPane 中对象的直接引用。这些对象的保护级别设置为 Protected Friend(在 C# 中为 protected internal),以便它们可以对设计器可见,但对控件的消费者不可见。

TaskPanePageDesigner

对于 TaskPanePage 对象,我希望提供丰富的运行时体验,并指示正在设计哪个页面,以及有效的控件放置区域。显然,在 Office 2003 模式下,我们不希望用户将控件放置在导航按钮上方。最后,我们只希望允许 TaskPanePages 作为 TaskPane 的子项。

为此,我为 TaskPanePageDesigner 提供了两项功能:

  1. 自定义“装饰”以直观地指示有效的控件区域,以及
  2. 控制拖放和命中测试,以防止控件被拖放到导航按钮上方。显然,运行时没有任何东西可以阻止开发人员这样做,但至少,他们会非常确定他们确实想要这样做。

这些是通过设计器中的以下方法实现的:

Public Overrides Function CanBeParentedTo(ByVal parentDesigner _
                          As IDesigner) As Boolean
    Return TypeOf (parentDesigner) Is TaskPaneDesigner
End Function

Protected Overrides Sub OnPaintAdornments(ByVal pe As _
                    System.Windows.Forms.PaintEventArgs)
    MyBase.OnPaintAdornments(pe)

    PaintDesignerInfo(pe.Graphics)
End Sub

Protected Overrides Sub OnDragOver(ByVal de As System.Windows.Forms.DragEventArgs)
    MyBase.OnDragOver(de)

    If GetHitTest(New Point(de.X, de.Y)) Then
        de.Effect = DragDropEffects.None
    End If
End Sub

Protected Overrides Function GetHitTest(ByVal point As _
                    System.Drawing.Point) As Boolean
    ' pnlNavButtons
    ' need to check parent control to see if pnlNavButtons
    ' contains point and reject a drag
    ' if it does
    Dim prnt As TaskPane = TryCast(Me.Control.Parent, TaskPane)
    If prnt Is Nothing Then Return False

    If prnt.pnlNavButtons.ClientRectangle.Contains(
            prnt.pnlNavButtons.PointToClient(point)) Then
        Return True
    End If

    Return False
End Function

PaintDesignerInfo 方法创建一个斜纹填充画笔并勾勒出 TaskPanePage 的边框。有关该方法如何进行绘制的更多详细信息,请参阅源代码。

而且,我再次重写了 CanParent 方法:

Public Overrides Function CanBeParentedTo(ByVal parentDesigner As IDesigner) As Boolean
    Return TypeOf (parentDesigner) Is TaskPaneDesigner
End Function

我们也不希望 TaskPanePages 在设计时可移动:这些控件必须始终停靠以填充内容区域。这就导致了以下重写:

Protected Overrides ReadOnly Property EnableDragRect() As Boolean
    Get
        Return False
    End Get
End Property

Public Overrides ReadOnly Property SelectionRules() As SelectionRules
    Get
        Return Windows.Forms.Design.SelectionRules.Locked
    End Get
End Property

Verb 和 Action Lists (智能标签)

TaskPane Verb 和 ActionList

Screenshot - TaskPaneSmartTags.jpg

许多 .NET 控件的一个便捷功能是能够拥有一个智能标签,允许您快速向控件的集合添加新项——例如,TabControl 有一个智能标签允许添加新选项卡。我认为为 TaskPane 添加类似的功能以添加新的 TaskPanePages 会很棒,而且这出奇地容易。在 TaskPaneDesigner 中,只需添加以下方法:

Public Overrides ReadOnly Property Verbs() As _
       System.ComponentModel.Design.DesignerVerbCollection
    Get
        Dim vbs As New DesignerVerbCollection
        vbs.Add(New DesignerVerb("Add Task Page", _
                New EventHandler(AddressOf handleAddPage)))

        Return vbs
    End Get
End Property

Private Sub handleAddPage(ByVal sender As Object, ByVal e As EventArgs)
    Dim pane As TaskPane = CType(Me.Control, TaskPane)
    Dim h As IDesignerHost = CType(GetService(GetType(IDesignerHost)), IDesignerHost)
    Dim c As IComponentChangeService = _
        CType(GetService(GetType(IComponentChangeService)), IComponentChangeService)
    Dim dt As DesignerTransaction = h.CreateTransaction("Add Task Page")
    Dim page As TaskPanePage = _
        CType(h.CreateComponent(GetType(TaskPanePage)), TaskPanePage)
    c.OnComponentChanging(pane, Nothing)

    'Add a new page to the collection
    pane.TaskPanePages.Add(page)

    ' Commit the change
    c.OnComponentChanged(pane, Nothing, Nothing, Nothing)
    dt.Commit()
End Sub

组件事务处理允许设计器注册撤销/重做信息。

我还认为允许用户在设计器中设置 TaskPane 的停靠行为会很方便。这需要添加一个 ActionList。实际的 ActionList 对象本身具有更多功能,但关键部分是:

Public Overrides Function GetSortedActionItems() As DesignerActionItemCollection
    Dim items As DesignerActionItemCollection = New DesignerActionItemCollection

    items.Add(New DesignerActionMethodItem(Me, "handleAddPage", _
              "Add TaskPane Page", "Actions", _
              "Adds a new TaskPanePage to the TaskPane", False))
    items.Add(New DesignerActionPropertyItem("Dock", "Dock", _
              "Appearance", "Docking position of the TaskPane"))
    Return items
End Function

这是实际显示在智能标签上的项。您会注意到我再次添加了“handleAddPage”方法——这是因为如果您向设计器同时添加 Verbs 和 ActionListActionList 会导致 Verbs 不再用于智能标签(Verbs 仍会保留在上下文菜单中,但不会放在智能标签上)。因此,添加页面项也必须添加到操作列表中。

TaskPanePage ActionList

Screenshot - TaskPanePageSmartTags.jpg

这个操作列表的构建方式与 TaskPane 操作列表非常相似,因此我将让您查看代码以了解具体细节。我只是想展示 TaskPanePages 的设计器中可用的项,并演示一个更复杂的智能标签。

一个有趣的“陷阱”

在设计 TaskPanePages 时,我遇到了一件相当烦人的事情,直到我找到了一个有趣的解决方案:哪个 TaskPanePage 当前在设计器中被选中。

在切换页面之间的事件处理程序(使用 DropDownList 或导航按钮)中,我调用 BringToFront(),这可以使页面置于 Z 顺序的前面;然而,它不会改变设计器选择的控件。因此,如果您有一个为 TaskPanePage 打开的智能标签,然后切换页面,则旧页面仍被选中。单击内容区域会在设计器中选择新页面,但我想要一种以编程方式实现这一点的方法。我几乎要放弃了,直到我在 MSDN 论坛上找到了以下解决方案:

Private Sub SelectedPageChanged(ByVal sender As Object, ByVal e As EventArgs)
    ' What we're doing here is causing the designer to select the desired task pane page.
    ' This is so that the Smart Tag arrow will popup for the correct page.
    ' Without this little piece of code, it will continue to select the wrong page,
    ' until the user themselves selects the correct one by clicking on it.
    Dim dHost As IDesignerHost = CType(GetService(GetType(IDesignerHost)), IDesignerHost)
    Dim selectionService As ISelectionService = _
        CType(dHost.GetService(GetType(ISelectionService)), ISelectionService)
    selectionService.SetSelectedComponents(New Object() {Me.Pane.SelectedPage})
End Sub

通过在 TaskPaneDesignerInitialize() 方法中添加 TaskPaneSelectedIndexChanged 事件的事件处理程序,我能够让设计器感知所选页面的更改。然后,我调用设计器的 ISelectionService 来告诉它要选择哪个控件,从而解决了我的问题。

待办事项列表

还有一些尚未实现的功能。我没有实现它们有两个原因:

  1. 它们对这个项目没有迫切的需要(应用程序层已经编写完成,这个控件是现有控件的即插即用替换件,所以我已经知道它的用例),并且
  2. 我不太确定 **应该** 实现什么。

我已经实现了显而易见的事件:

  • TaskPaneCloseClick
  • SelectedIndexChanging(用于取消更改)
  • SelectedIndexChanged

我认为前进、后退和首页的导航事件会很有用。我还认为在下拉菜单项之间添加分隔符来创建像 Office TaskPane 那样的分组会很有用。

但是,还有哪些事件和属性真正必要呢?欢迎反馈。

关于演示项目

演示项目包含一些与 TaskPane 控件不直接相关的内容,但我觉得提供一个完整的示例而不是一个仅仅包含控件而没有任何其他内容的存根会很有帮助。因此,演示包含了我在这里 CodeProject 上最喜欢的一些控件,我想在此提及它们:

  1. MdiTabStrip - 由 **crcrites** 编写的我最喜欢的自定义标签式 MDI 界面。
  2. Windows XP 风格的 Explorer Bar - 一个有趣的带主题的控件,对于 TaskPane 界面来说似乎非常合适,由 **Mathew Hall** 编写。

对于编辑器界面,我选择在一个称为 DocumentForm 的子窗体中使用 RichTextBoxTaskPane 中的搜索框允许您实际搜索当前选定的标签式文档中的文本,并且找到的任何文本都会被适当地突出显示。

TaskPaneExampleForm.vb

Private ReadOnly Property SelectedDocument() As DocumentForm
    Get
        Dim frm As DocumentForm = Nothing
        Dim tab As MdiTabStrip.MdiTab = MdiTabStrip1.ActiveTab

        If tab IsNot Nothing Then
            frm = TryCast(tab.Form, DocumentForm)
        End If

        If frm Is Nothing Then
            frm = New DocumentForm ' just to avoid null ref exceptions
        End If
        Return frm
    End Get
End Property

...

Private Sub btnSearch_Click(ByVal sender As System.Object, _
        ByVal e As System.EventArgs) Handles btnSearch.Click
    Me.SelectedDocument.FindText(txtSearch.Text)
End Sub

文件 DocumentForm.vb

Public Sub FindText(ByVal argText As String)
    If argText.Length = 0 Then Return

    rtbDocument.Find(argText, m_CurrentStart, RichTextBoxFinds.None)
End Sub

Private Sub rtbDocument_SelectionChanged(ByVal sender As System.Object, _
        ByVal e As System.EventArgs) Handles rtbDocument.SelectionChanged
    m_CurrentStart = rtbDocument.SelectionStart

    If rtbDocument.SelectionLength > 0 Then
        m_CurrentStart += 1 ' make sure it finds the NEXT match, not the current one
    End If
End Sub

TaskPane 实现

TaskPane 的实现相对直接,这就是为什么我不太关注它的原因。

TaskPane 包含一个 TaskPanePageCollection(继承自 CollectionBase)的实例。我处理集合中控件的添加、删除和设置事件,并添加、删除和设置相应的 TaskPanePage 和标题的 ToolStripMenuItem

我考虑继承 Control.ControlCollection 并重写 TaskPane 中的 CreateControlInstance(),但由于工具栏和用于导航按钮的渐变面板而放弃了这种方法。

TaskPaneSelectedIndex 更改时,会显示相应的 TaskPanePage,并将其对应的 ToolStripMenuItem 设置为当前标题和图像。它确实没有比这更复杂的了。

结论

好了,我想就到这里了。如果我没有完全解释清楚任何事情,或者您有 bug 修复或关于可以为该控件添加什么的建议,请告诉我。

资源

以下是我用于学习如何为自定义控件提供完整运行时支持的几个资源:

  1. 使用 Visual Studio 中的自定义设计器操作简化 UI 开发
  2. 定位用户控件的设计时事件
  3. 创建自定义控件——提供设计时支持,第一部分
  4. 创建自定义控件——提供设计时支持,第二部分

历史

  • 2007 年 4 月 26 日 - 初始提交。
© . All rights reserved.