构建 WPF 的停靠窗口管理解决方案






4.28/5 (24投票s)
使用 WPF 作为 Synergy 工具包一部分的停靠窗口解决方案。
引言
窗口停靠是多窗口应用程序中常见的行为。作为一名用户界面开发者,这种行为一直让我着迷,因此我考虑在自己的 WPF 工具包中开发类似的功能。我知道市面上已经有很多类似的解决方案实现,有些甚至是开源的,但我的目标是把这个个人项目作为一个挑战和学习的机会。
我还应该提到,我实现的停靠解决方案是我在我公司 MixModes Inc 正在持续构建的一个更大的 WPF 工具包的一部分,但我打算将其保持为开源且免费的项目。要预览此库的完整功能集,您可以访问我的博客 这里。
许多现有的窗口停靠解决方案都有浮动窗口作为 MDI 父窗口下管理的独立窗口。但我将浮动窗口严格包含在父窗口内,因为我打算很快将此代码移植到 Silverlight。
我还在 CodePlex 上托管了这个项目:http://mixmodessynergy.codeplex.com/。
Synergy 工具包的结构
MixModes Synergy 工具包包含以下顶级项目:
MixModes.Synergy.Resources
– 包含图像和语言资源MixModes.Synergy.Themes
– 定义用户控件、自定义窗口、颜色、画笔和文本主题的默认主题MixModes.Synergy.Utilities
– 公共实用工具MixModes.Synergy.VisualFramework
– 包含行为、装饰器、命令、用户控件、停靠框架和其他 WPF 特定功能Synergy
– 突出显示工具包功能的示例项目
可停靠窗口的结构
要理解窗口停靠解决方案,有必要理解驱动这一切的最基本控件——可停靠窗口。可停靠窗口是一个特殊的窗口,除了内容和标题之外,还可以处于以下几种状态:
- 固定状态(Pinned state)– 可停靠窗口可以固定在父窗口的侧边,以保持一致的可见性。通常,常用内容会被固定以便于访问。
- 自动隐藏状态(Auto hidden state)– 不常用窗口可以自动隐藏,这样当鼠标不悬停在它们上面时,它们会折叠成一个紧凑的形式(我称之为标题栏)。当鼠标悬停在标题栏上时,完整窗口会从停靠的一侧滑出。
- 文档状态(Document state)– 当用作文档时,可停靠窗口可以作为选项卡项合并到选项卡控件中。
- 浮动(Floating)– 常规浮动窗口
停靠窗口支持由 DockPane
控件提供,该控件是 HeaderedContentControl
的派生类。除了它从 HeaderedContentControl
继承的 Header
和 Content
属性外,它还包含以下属性:
Icon
– 停靠窗口的图标CondencedDockPanelTemplate
– 紧凑型DockPane
的模板CondencedDockPanel
–DockPane
的紧凑形式DockPaneState
– 停靠窗格的状态
DockPane
还包含以下事件:
Close
– 关闭事件TogglePin
– 切换固定/自动隐藏按钮HeaderDrag
– 通知用户已开始拖动DockPane
的标题栏
DockPane
的默认主题定义在 MixModes.Synergy.Themes
项目中的 DockPane.xaml 资源字典中。
文档容器
DockPane
(s) 包含在文档容器中。文档容器通过 DocumentContainer
类建模,该类是 ContentControl
的派生类。DocumentContainer
可以处于以下几种(互斥的)状态之一:
Empty
–DocumentContainer
不包含任何DockPane
ContainsDocuments
–DocumentContainer
包含一个或多个DockPane
(s) 作为文档SplitHorizontally
–DocumentContainer
被水平分割SplitVertically
-DocumentContainer
被垂直分割
DocumentContainer
的模板很复杂,因为它需要表示分割视图或选项卡视图。我在模板中使用了持久的 TabControl
,如果 Content
属性始终为 null
,则隐藏该 TabControl
。Content
当然是专门用于通过带有两个 DocumentContainer
子项的 Grid
来包含分割视图。
DocumentContainer
包含以下属性:
State
–DocumentContainer
的状态Documents
– 包含在TabControl
中显示的文档DocumentsTab
– 包含文档的TabControl
DockIllustrationPanel
– 这是一个包含停靠说明点(docking illustration points)的面板,指示用户内容应该停靠在哪里。如果用户将DockPane
拖动到其中一个点,它就会被停靠在相应的位置。下图显示了DockIllustrationPanel
的内容。
DocumentContainer
包含以下方法:
AddDockPane(DockPane, ContentDockPoint)
– 将DockPane
添加为停靠窗口AddDocumentContainers(IEnumerable<DocumentContainer>, bool)
– 分割并添加子DocumentContainers
AddDocument(DockPane)
– 将DockPane
添加为选项卡式文档RemoveDocument(DockPane)
– 将DockPane
从选项卡式文档中移除
DocumentContainer
的模板包含以下分层视觉元素(底部视觉元素 Z 顺序递增):
TabControl (PART_DOCUMENTS)
– 绑定到DocumentContainer
的Documents
属性ContentPresenter
– 分割子项将添加到此处Grid (PART_DOCK_POINTS)
– 用于容纳停靠说明点的面板Grid (PART_DOCK_ILLUSTRATION)
–DockIllustrationPanel
,用于通过提示说明未来的停靠
窗口管理器
窗口管理器是连接 DockPanel
(s) 和 DocumentContainer
(s) 以在应用程序中提供窗口管理功能的组件。此外,窗口管理器在所有四个窗口侧边包含自动隐藏和固定的停靠点,因此 DockPane
(s) 可以固定或自动隐藏在 DocumentContainer
(s) 之外。WindowsManager
还包含根 DocumentContainer
,它可以托管选项卡控件中的文档,或托管嵌套分割的 DocumentContainer
实例。
WindowsManager
具有以下属性:
DockPaneIllustrationStyle
– 在WindowsManager
或DocumentsContainer
中停靠窗口的说明样式DockIllustrationContentStyle
– 在拖动DockPane
到TabControl
中合并文档时的说明样式ActiveWindowsManager
– 指示正在进行拖动操作的WindowsManager
的静态属性DraggedPane
– 正在被拖动的DockPane
<Orientation>WindowHeaders
– 包含自动隐藏的紧凑型DockPane
(s) 的StackPanel
<Orientation>PinnedWindows
– 包含固定的DockPane
(s) 的DockPanel
DocumentContainer
– 根文档容器DockingIllustrationPanel
– 用于将来固定DockPanel
(s) 的停靠说明面板PopupArea
– 当鼠标悬停在紧凑型标题栏上时,自动隐藏的DockPane
(s) 会滑出的DockPanel
FloatingPanel
– 包含浮动DockPane
(s) 的Canvas
DockingPanel
– 包含固定窗口停靠点的DockPanel
,如下面的图像所示
WindowsManager
具有以下方法:
AddPinnedWindow(DockPane, Dock)
– 添加一个固定的DockPane
AddAutoHideWindow(DockPane, Dock)
– 添加一个自动隐藏的DockPane
AddFloatingWindow(DockPane)
– 添加一个浮动的DockPane
RemoveDockPane(DockPane)
– 从WindowsManager
的(固定、自动隐藏或浮动部分)移除DockPane
Clear
– 清除WindowManager
中的所有DockPane
(s)StartDockPaneStateChangeDetection
– 开始监视DraggedPane
的状态StopDockPaneStateChangeDetection
– 停止监视DraggedPane
的状态
所有组件如何协同工作
下图说明了停靠解决方案中各个组件之间的关系。
结构上,WindowsManager
是包含固定和自动隐藏 DockPane
(s) 的包容组件。它还包含根 DocumentContainer
。另一方面,DocumentContainer
可以通过将 DockPane
包装在 DocumentContent
实例中来包含选项卡控件中的文档,或者它可以包含分割窗口,其中一个网格包含子 DocumentContainer
(s),每个子 DocumentContainer
都可以递归地包含文档或更多的子 DocumentContainer
(s)。
WindowsManager
会持续监视 DockPane
的状态变化。当检测到 DockPane
拖动时,它会被放置在 WindowsManager
的 FloatingPanel
画布上,作为一个可以四处拖动的浮动窗口。在拖动 DockPane
的过程中,DockPane
上的命中测试会被关闭,以便鼠标事件能够向下传递到下面的控件,例如 WindowsManager
和 DocumentContainer
。
为了协调拖放和停靠功能,我采用了基于行为的方法。其思想是在视觉实体(如 DockPane
、DocumentContainer
和 WindowsManager
)上公开功能端点(如方法和属性),并使用行为来协调和调用这些功能端点。这种方法也产生了易于管理和封装的组件,更易于追踪和测试。
DockPointBehavior
在 WindowsManager
上监视拖动的 DockPane
并弹出用于固定的停靠点。而 ContentPointBehavior
在 DocumentContainer
中注入类似的功能,用于分割和选项卡合并目的。
WindowsManager
和 DocumentContainer
都拥有包含停靠行为的停靠说明网格。DockPointBehavior
在 WindowsManager
上说明固定停靠,而 ContentDockBehavior
在 DocumentContainer
上说明分割和选项卡合并。
Using the Code
使用 WindowsManager
非常简单:
- 在 XAML 中导入命名空间
xmlns:visualFx="http://mixmodes.com/visualFx"
- 将
WindowsManager
放入 XAML 中
<visualFx:WindowsManager x:Name="WindowsManager"/>
- 开始创建
DockPane
并将它们插入WindowsManager
/DocumentContainer
中DockPane pane = new DockPane(); pane.Header = … pane.Content = …. WindowsManager.AddPinnedWindow(pane, Dock.Top); // OR WindowsManager.AddAutoHideWindow(pane, Dock.Left); // OR // Assuming DocumentContainer is either in Empty or ContainsDocuments state WindowsManager.DocumentContainer.AddDocument(pane);
序列化窗口状态
Synergy 开箱即用,通过 XmlWindowsManagerSerializer
和 XmlWindowsManagerDeserializer
类提供窗口状态的 XML 序列化。通过分别特化基类 WindowsManagerSerializer
和 WindowsManagerDeserializer
来支持自定义序列化。
使用 XmlWindowsManagerSerializer
进行 WindowsManager
的 XML 序列化需要在构造时提供两类信息:
-
DockPane
写入器 – 一个Action<XmlElement, DockPane>
实例,可以将DockPane
的额外元数据写入XmlElement
。 -
Document
写入器 – 一个Func<DocumentContent, string>
实例,它接受一个DocumentContent
并返回内容的字符串表示。注意:DocumentContent.DockPane
属性返回关联的DockPane
,但DockPane
的Header
和Content
属性设置为null
。要访问Header
和Content
属性,请直接使用DocumentContent
实例的Header
和Content
属性。
一旦创建了 XmlWindowsManagerSerializer
实例,调用 Serialize(Stream, WindowsManager)
方法即可将 WindowsManager
序列化到 stream
。
与序列化过程类似,反序列化过程需要在 XmlWindowsManagerDeserializer
的构造函数中提供一个 Action<DockPane, string>
实例,以便从先前保存的字符串表示中反序列化 DockPane
。反序列化不需要额外的 Action
来实现 DocumentContent
,因为 DocumentContent
本质上是 DockPane
的序列化包装器。
一旦创建了 XmlWindowsManagerDeserializer
实例,调用 Deserialize(Stream, WindowsManager)
即可将 WindowsManager
反序列化到先前保存的状态。
所有停靠功能都可以通过使用带有源代码的示例应用程序(Synergy 项目)来练习。一如既往,欢迎任何评论、建议或 bug 报告。祝编码愉快!