WPF 停靠窗口管理库






4.86/5 (21投票s)
WPF 停靠窗口管理器库
引言
几年前,我开发了一个商业应用程序,其中包含大量的文档窗口和工具窗口。我希望用户能够像 Microsoft Visual Studio 一样,以最方便的方式排列这些窗口。我选择基于免费版的 AvalonDock 来构建该应用程序。我选择 AvalonDock 是因为它免费且声誉良好。我发现它确实是一个非常好的控件,具有极其强大和灵活的用户界面。我也了解到,由于它是一个复杂的软件,定制起来可能非常困难。我曾收到用户关于添加或更改功能的请求,但很多时候,我都无法找到实现这些更改的方法。最常见的请求是改进当多个工具组合在一起时,选项卡控件的表现。我们有太多的工具面板,以至于很难弄清楚哪个选项卡是哪个。我们还收到了添加一个关闭按钮到每个选项卡式工具或文档面板的请求。我在网上找到了一些解决方案,但很多时候都无法奏效。这可能是因为这些解决方案是针对早期版本的 AvalonDock。
最近,我有一些空闲时间,我决定创建自己的停靠窗口管理器库,并设定了以下关键设计目标:
- 它应该能够创建具有像 Microsoft Visual Studio 这样领先的应用程序的外观和感觉的应用程序。
- 它应该允许开发人员通过主题轻松定制外观。
- 代码应该相对简单,以便开发人员可以在内置行为不足的情况下进行修改以满足自己的需求。
- 它应该完全符合 MVVM 模式。
- 它应该允许将布局序列化到文件并从文件反序列化。
停靠窗口控件的关键特性如下:
- 支持文档和工具。
- 文档代表可编辑内容,例如文本文档,或计算结果,例如 3D 曲面图。
- 工具代表一组控件,这些控件在某种程度上影响文档的内容,例如 3D 曲面图的设置。
- 文档可以以平铺模式布局。两个或多个文档可以组合成一个选项卡组。文档占据一个矩形区域(文档区域)。
- 工具可以以平铺模式布局。两个或多个工具可以组合成一个选项卡组。工具占据文档区域两侧的区域。
- 工具可以被“取消固定”,这样面板会从平铺区域移除,并在边距处显示一个标题栏。
- 单击取消固定工具的标题栏会显示先前隐藏的内容。单击内容外部会再次隐藏它。
- 可以通过单击工具标题栏中的固定按钮来重新固定取消固定的工具。
- 文档和工具都可以被“浮动”。它们可以单独浮动,也可以作为一个选项卡组浮动。
- 可以通过拖动每个选项卡标题来重新排序选项卡组中的工具或文档。
- 选项卡组有一个下拉菜单项,其中列出了所有文档/工具,允许用户快速选择所需内容。
- 可以通过单击选项卡标题中的关闭按钮来关闭工具和文档。如果存在未保存的数据,这会提示用户保存未保存的数据、放弃未保存的数据或取消操作。
- 该控件维护一个文档视图模型列表和一个工具视图模型列表。
- 视图和视图模型之间存在一对一的对应关系。因此,删除一个视图模型将自动关闭关联的视图。反之,添加一个视图模型将自动显示关联的视图。
以下内容可以定制:
- 侧面工具栏的样式(显示取消固定工具的标题)
- 选定和未选定选项卡标题的样式
- 所有按钮的样式
- 所有字体
- 选项卡组中的滚动箭头
- 工具面板的标题栏
- 每个工具和每个文档周围的边框
请注意,示例应用程序中的按钮是使用路径而不是位图(或类似物)定义的,这使得根据主题更改颜色和大小变得微不足道。
我将该组件命名为 Open Controls Dock Manager,“open”表示该控件是免费的。本文附带一个 zip 文件,其中包含源代码和几个示例应用程序,演示如何使用该库和创建主题。
示例应用程序
第一个示例应用程序使用了一个模仿 Visual Studio 2019 的主题,如下所示:
主窗口视图模型有五个文档和五个工具。这五个文档组合在一个面板中。在这五个工具中,有两个在 UI 的右侧被取消固定,一个停靠在文档的左侧,还有两个被组合停靠在文档的下方。
第二个示例应用程序使用了一个我称之为“现代”的不同主题,如下所示:
可以通过单击面板中的关闭按钮图标,或通过 **文档** 和 **工具** 应用程序菜单来关闭给定的文档或视图。
可以通过单击边距中的项目来显示取消固定的工具面板。
可以通过单击标题栏中的固定图标,将取消固定的工具面板(以及同一组中的其他工具面板)重新添加到停靠面板布局中。
可以通过拖动标题栏来浮动一组工具。
主窗口两侧的小窗口图标是位置指示器。要将浮动工具面板组重新插入到停靠面板布局中,请拖动窗口直到光标位于适当的位置指示器上方。由于位置指示器是在主题中定义的,因此它们是完全可定制的。
类似地,可以通过拖动其选项卡标题来浮动单个工具(或文档)面板。
可以通过相同的方式将工具或文档从包含多个工具或文档的浮动面板中分离出来,使其成为一个只包含一个视图的浮动面板。
每个面板组都有一个文件列表按钮,该按钮会显示组内工具/文档的列表,允许用户快速选择所需的项。当组包含大量工具/文档时,这尤其有用。例如:
可以通过将项目拖动到新位置来更改选项卡顺序。
运行演示应用程序
运行一个演示应用程序。单击窗口下拉菜单(右上角),然后单击加载布局菜单项。
选择所需的布局文件。源代码的顶层文件夹中有五个示例布局。
背景
为了使用该库,您需要了解如何使用 MVVM 模式创建 WPF 应用程序,并对 C# 有深入的了解。您不需要深入了解 WPF,尽管这样做会有帮助。
Using the Code
每个示例应用程序的主窗口 XAML 都包含一个 `LayoutManager` 类的实例,如下所示:
<dockManager:LayoutManager x:Name="_layoutManager"
Grid.Row="1" Grid.Column="0" DocumentsSource="{Binding Documents}"
ToolsSource="{Binding Tools}" VerticalAlignment="Stretch"
HorizontalAlignment="Stretch" Background="Gray" >
<dockManager:LayoutManager.Theme>
<themes:ModernTheme/>
</dockManager:LayoutManager.Theme>
<dockManager:LayoutManager.DocumentTemplates>
<DataTemplate DataType="{x:Type viewModel:DocumentOneViewModel}">
<view:DocumentOneView x:Name="_documentOneView"/>
</DataTemplate>
<DataTemplate DataType="{x:Type viewModel:DocumentTwoViewModel}">
<view:DocumentTwoView x:Name="_documentTwoView"/>
</DataTemplate>
<DataTemplate DataType="{x:Type viewModel:DocumentThreeViewModel}">
<view:DocumentThreeView x:Name="_documentThreeView"/>
</DataTemplate>
</dockManager:LayoutManager.DocumentTemplates>
<dockManager:LayoutManager.ToolTemplates>
<DataTemplate DataType="{x:Type viewModel:ToolOneViewModel}">
<view:ToolOneView x:Name="_toolOneView"/>
</DataTemplate>
<DataTemplate DataType="{x:Type viewModel:ToolTwoViewModel}">
<view:ToolTwoView x:Name="_toolTwoView"/>
</DataTemplate>
<DataTemplate DataType="{x:Type viewModel:ToolThreeViewModel}">
<view:ToolThreeView x:Name="_toolThreeView"/>
</DataTemplate>
<DataTemplate DataType="{x:Type viewModel:ToolFourViewModel}">
<view:ToolFourView x:Name="_toolFourView"/>
</DataTemplate>
<DataTemplate DataType="{x:Type viewModel:ToolFiveViewModel}">
<view:ToolFiveView x:Name="_toolFiveView"/>
</DataTemplate>
</dockManager:LayoutManager.ToolTemplates>
</dockManager:LayoutManager>
`DocumentsSource` 属性绑定到视图模型上的 `Documents` 属性。这是一个视图模型的可观察集合,每个视图模型都派生自 `IViewModel` 类。
`ToolsSource` 属性绑定到视图模型上的 `Tools` 属性。这是一个视图模型的可观察集合,每个视图模型都派生自 `IViewModel` 类。
`LayoutManager.DocumentTemplates` 属性包含一个数据模板列表,用于文档,每个模板定义了视图模型与相应视图之间的映射。因此,`DocumentOneViewModel` 视图模型类提供了 `DocumentOneView` 视图类显示的数据。
`LayoutManager.ToolTemplates` 属性包含一个数据模板列表,用于工具,每个模板定义了视图模型与相应视图之间的映射。因此,`ToolOneViewModel` 视图模型类提供了 `ToolOneView` 视图类显示的数据。
工具视图与其工具视图模型之间存在直接的一对一对应关系,并且给定工具视图只能有一个实例。`Name` 属性至关重要,因为它在序列化视图到文件和从文件反序列化时标识视图模型。`Name` 属性的值必须是唯一的。
相比之下,文档视图模型可以有多个实例,因此视图也可以有多个实例。每个实例都通过一个唯一的 URI 来区分,该 URI 可以是数据的文件路径,例如 *C:\Data\Document1.txt*。
UI 的外观是通过一个主题来设置的,该主题包含一个定义样式的字典,以及一个派生自 `Theme` 类的类,该类提供字典的 URI。例如:
public class ModernTheme : Theme
{
public override Uri Uri
{
get
{
return new Uri
("/OpenControls.Wpf.DockManager.Themes.Modern;component/Dictionary.xaml",
UriKind.Relative);
}
}
}
主题类实现了一个名为 `Uri` 的方法,该方法返回 `Theme` 字典的 Uri。
按照以下示例,主题在 `LayoutManager.Theme` 属性中设置:
<dockManager:LayoutManager.Theme>
<themes:ModernTheme/>
</dockManager:LayoutManager.Theme>
应用程序运行时无法更改主题。
字典样式在很大程度上是强制性的,如果缺少必需的样式,应用程序将崩溃。
应用程序需要添加对主题程序集的引用,假设它是外部的。当然,主题也可以实现在应用程序程序集中。主应用程序窗口必须引用停靠管理器和主题的命名空间。例如:
xmlns:dockManager="clr-namespace:OpenControls.Wpf.DockManager;
assembly=OpenControls.Wpf.DockManager"
xmlns:themes="clr-namespace:OpenControls.Wpf.DockManager.Themes;
assembly=OpenControls.Wpf.DockManager.Themes.Modern"
任何熟悉 AvalonDock 的人都将意识到我使用了相同的 `Theme` 机制。
如上所述,应用程序为文档和工具提供了视图模型实例列表。每个视图模型类都必须实现 `IViewModel` 接口,该接口定义如下:
public interface IViewModel
{
// A user friendly title
string Title { get; }
string Tooltip { get; }
/*
* Not used by tools.
* Uniquely identifies a document instance.
* For example a file path for a text document.
*/
string URL { get; }
bool CanClose { get; }
/*
* Return true if there are edits that need to be saved
* Not used by Tool view model
*/
bool HasChanged { get; }
void Save();
}
`Title` 属性返回一个 `string`,该字符串显示在选项卡标题中。
`Tooltip` 属性返回一个 `string`,该字符串显示为选项卡标题的工具提示。
`URL` 属性仅由文档视图模型使用,不用于工具。它返回文档的 URL,在大多数情况下是文件路径。
`CanClose` 属性返回 `true` 如果文档/工具可以关闭,否则返回 `false`。
`HasChanged` 属性返回 `true` 如果文档有未保存的更改,否则返回 `false`。此属性仅对文档有效。
`Save` 方法由系统调用,用于保存与文档相关的数据。
架构
关键类及其关系由以下 UML 图说明:
布局管理逻辑被划分为少数关键组件,每个组件都有自己的职责,每个组件都通过明确定义的接口使用依赖注入与其他组件交互。这减少了耦合和依赖,使得代码更容易维护和扩展。
`LayoutManager` 类派生自 `Grid` 控件类,它是文档和工具的二叉树的顶层。树中的每个节点都可以是分隔器面板,或者文档面板组或工具面板组。分隔器面板由 `SplitterPane` 类实现,它代表由 `GridSplitter` 分隔的两个节点(例如,可以是工具面板组)。因此,它代表二叉树中的一个叶节点。网格分隔器可以是水平的或垂直的。
`DockPaneManager` 类管理停靠面板的布局。它通过 `IDockPaneHost` 和 `ILayoutFactory` 接口与 `LayoutManager` 实例进行交互。后一个接口提供了实例化文档/工具树中的节点的服务,包括文档面板组和工具面板组。
`DocumentPaneGroup` 和 `ToolPaneGroup` 类分别代表文档和工具面板组。每个类都包含一个派生自 `IViewContainer` 接口的类的实例,该类负责视图内容。因此,`ToolContainer` 类显示一个或多个工具视图,`DocumentContainer` 类显示一个或多个文档视图。
`FloatingPaneManager` 类管理浮动面板。它通过 `IFloatingPaneHost` 和 `ILayoutFactory` 接口与 `LayoutManager` 实例进行交互。
`FloatingDocumentPaneGroup` 和 `FloatingToolPaneGroup` 类分别代表浮动的文档和工具面板组。每个类都包含一个派生自 `IViewContainer` 接口的类的实例。
`UnpinnedToolManager` 类管理未固定的工具,即显示为停靠区域一侧标题栏的工具。
示例布局
示例代码在顶层文件夹中包含五个示例布局。
GitHub 仓库
代码也可从 GitHub 存储库获取。请注意,该代码现已成为免费开源的 OpenControls.Wpf 项目的一部分,该项目包含许多其他有用的 WPF 控件。存储库地址如下:
https://github.com/LeifUK/OpenControls.Wpf我建议您从存储库获取代码,因为它包含了最新的更改,特别是我已经将一些功能移到了单独的共享库中,以及其他有用的组件。
历史
- 2020 年 6 月 19 日:版本 1。
- 2020 年 6 月 24 日:版本 2,添加了说明如何运行演示应用程序的部分。
- 2020 年 12 月 8 日:版本 3,增加了对当前或活动文档的支持。修复了当未拖动浮动面板时,浮动面板下方的面板被高亮显示的错误。
- 2020 年 12 月 11 日:版本 4,修复了用户 SadE54 发现并修复的序列化代码中的一个错误。非常感谢。ZIP 文件和 GitHub 存储库已相应更新。
- 2021 年 1 月 2 日:版本 5:创建了一个新的 OpenControls.Wpf 项目,其中包含 DockManager。