类似 MS Office 的任务窗格控件






4.85/5 (23投票s)
一个 .NET TaskPane 控件,具有完整的运行时支持。

目录
引言
本文介绍如何创建一个 TaskPane
控件,该控件模仿 Microsoft Office XP 和 2003 中的 TaskPane(我没有 Office 2007 的副本,因此不知道此界面是否被保留)。我将描述该控件的实现,但我将特别关注该控件中包含的设计时功能,因为这是此项目更具挑战性的方面。
由于项目限制,我使用 VB.NET 编写了此组件。如果兴趣足够,我将将其移植到我更擅长的语言 C#。
介绍完毕,让我们开始吧。
必备组件
此项目使用 Ascend.NET 控件套件创建渐变面板。是的,我可以自己创建它们,但我想发现使用可用工具创建功能齐全、特性丰富的控件的速度有多快。您可以在此处下载 Ascend.NET 控件套件,或者下载包含相关 DLL 的演示项目。请注意,Ascend.NET 套件会将自身安装到您的 Visual Studio 工具箱中,因此如果这对您来说是不可取的行为,只需使用我演示项目中的 DLL 即可。
TaskPane 设计
创建 TaskPane
控件的第一步是确定如何设计其视觉元素。任务窗格有三个基本元素:
|
![]() |
请注意,当内容窗格小于可用控件区域时,滚动条会出现。这与真正的 TaskPane 行为略有不同,真正的 TaskPane 行为会在内容区域的顶部和底部生成滚动条拇指,宽度与内容区域相同。我想,除非有人真的想要这种行为,否则将其省略是可以的。
我认为呈现这三个元素的简单方法是使用以下控件:
- 一个工具栏,包含两个
Button
(一个后退,一个前进)、一个Label
、一个DropDownButton
和一个用于关闭的Button
。当TaskPanePage
添加到控件时,一个新的ToolStripMenuItem
会添加到DropDownButton
中。当SelectedIndex
或SelectedTab
更改时,将设置相应的标题和标题图像。CaptionStyle
可以通过设计器中的相应属性进行更改。 - 对于 Office 2003 设计,一个小的 Ascend.NET 渐变面板停靠在内容区域的顶部。
NavigationStyle
属性决定导航按钮是出现在标题区域还是内容窗格中。 - 一个称为
TaskPanePage
的特殊面板,它继承自 Ascend.NET 渐变面板,具有额外的数据属性和一个自定义设计器。这些页面在设计器中可用。大多数相关属性都可以使用SmartTag
面板进行设置。
下图展示了这三个基本功能。
我希望 TaskPane
同时支持 Office XP 样式(将导航按钮放在标题区域内)和 Office 2003 样式(将导航按钮放在内容窗格内)。
TaskPane 标题,Office 2003 样式
![]() |
TaskPane 标题,Office XP 样式
![]() |
设计器支持
我为 TaskPane
和 TaskPanePage
都包含了一个 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 模式下,我们不希望用户将控件放置在导航按钮上方。最后,我们只希望允许 TaskPanePage
s 作为 TaskPane
的子项。
为此,我为 TaskPanePageDesigner
提供了两项功能:
- 自定义“装饰”以直观地指示有效的控件区域,以及
- 控制拖放和命中测试,以防止控件被拖放到导航按钮上方。显然,运行时没有任何东西可以阻止开发人员这样做,但至少,他们会非常确定他们确实想要这样做。
这些是通过设计器中的以下方法实现的:
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
我们也不希望 TaskPanePage
s 在设计时可移动:这些控件必须始终停靠以填充内容区域。这就导致了以下重写:
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
许多 .NET 控件的一个便捷功能是能够拥有一个智能标签,允许您快速向控件的集合添加新项——例如,TabControl
有一个智能标签允许添加新选项卡。我认为为 TaskPane
添加类似的功能以添加新的 TaskPanePage
s 会很棒,而且这出奇地容易。在 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
”方法——这是因为如果您向设计器同时添加 Verb
s 和 ActionList
,ActionList
会导致 Verb
s 不再用于智能标签(Verb
s 仍会保留在上下文菜单中,但不会放在智能标签上)。因此,添加页面项也必须添加到操作列表中。
TaskPanePage ActionList
这个操作列表的构建方式与 TaskPane
操作列表非常相似,因此我将让您查看代码以了解具体细节。我只是想展示 TaskPanePage
s 的设计器中可用的项,并演示一个更复杂的智能标签。
一个有趣的“陷阱”
在设计 TaskPanePage
s 时,我遇到了一件相当烦人的事情,直到我找到了一个有趣的解决方案:哪个 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
通过在 TaskPaneDesigner
的 Initialize()
方法中添加 TaskPane
的 SelectedIndexChanged
事件的事件处理程序,我能够让设计器感知所选页面的更改。然后,我调用设计器的 ISelectionService
来告诉它要选择哪个控件,从而解决了我的问题。
待办事项列表
还有一些尚未实现的功能。我没有实现它们有两个原因:
- 它们对这个项目没有迫切的需要(应用程序层已经编写完成,这个控件是现有控件的即插即用替换件,所以我已经知道它的用例),并且
- 我不太确定 **应该** 实现什么。
我已经实现了显而易见的事件:
TaskPaneCloseClick
SelectedIndexChanging
(用于取消更改)SelectedIndexChanged
我认为前进、后退和首页的导航事件会很有用。我还认为在下拉菜单项之间添加分隔符来创建像 Office TaskPane 那样的分组会很有用。
但是,还有哪些事件和属性真正必要呢?欢迎反馈。
关于演示项目
演示项目包含一些与 TaskPane
控件不直接相关的内容,但我觉得提供一个完整的示例而不是一个仅仅包含控件而没有任何其他内容的存根会很有帮助。因此,演示包含了我在这里 CodeProject 上最喜欢的一些控件,我想在此提及它们:
- MdiTabStrip - 由 **crcrites** 编写的我最喜欢的自定义标签式 MDI 界面。
- Windows XP 风格的 Explorer Bar - 一个有趣的带主题的控件,对于
TaskPane
界面来说似乎非常合适,由 **Mathew Hall** 编写。
对于编辑器界面,我选择在一个称为 DocumentForm
的子窗体中使用 RichTextBox
。TaskPane
中的搜索框允许您实际搜索当前选定的标签式文档中的文本,并且找到的任何文本都会被适当地突出显示。
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()
,但由于工具栏和用于导航按钮的渐变面板而放弃了这种方法。
当 TaskPane
的 SelectedIndex
更改时,会显示相应的 TaskPanePage
,并将其对应的 ToolStripMenuItem
设置为当前标题和图像。它确实没有比这更复杂的了。
结论
好了,我想就到这里了。如果我没有完全解释清楚任何事情,或者您有 bug 修复或关于可以为该控件添加什么的建议,请告诉我。
资源
以下是我用于学习如何为自定义控件提供完整运行时支持的几个资源:
历史
- 2007 年 4 月 26 日 - 初始提交。