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

VS.NET 可设计 PropertyTree

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.84/5 (41投票s)

2001 年 9 月 14 日

Apache

28分钟阅读

viewsIcon

349648

downloadIcon

8030

基于 TreeView 的 TabControl 风格选项对话框。

PropertyTree (inside VS.NET)

引言

自从我第一次从 1995 年开始使用 OWL 进行编程以来,我每次学习一种新的 GUI 编程语言时都会编写这种控件。我用 OWL(Borland 的类似 MFC 的库)、纯 C 和 Java 2 的 Swing 等语言编写过。我一直称它们为“PropertyTree”——主要是因为我第一次看到它们是在 Netscape 的 Edit | Properties 对话框中实现的。CodeProject 上已经有许多类似的控件(使用 MFC)—— Chris Losinger 的 SAPrefs 控件,以及 Sven Wiegand 的 CTreePropSheet

然而,这个 PropertyTree 是为了集成 Visual Studio .NET 的炫酷设计时环境而编写的。它用 C# 编写,但可以被任何 .NET 语言使用。然而,要利用 PropertyTree 的设计时功能,您需要使用 VS.NET。

如果您不想这样做,您永远都不需要编写一行代码来设置您的 PropertyTree——您只需添加 PropertyPane,然后像将控件拖放到 TabPage 上一样,将控件拖放到它们上面。

更新通知

本文档涵盖了 PropertyTree 较新的 2.0.1.0 (Alpha) 版本。自 1.0 版本以来,代码已进行了彻底重写。许多功能仍然相同(当然也增加了一些新功能),但实现代码的方式已发生了巨大变化。

PropertyTree 2.0 中的这些更改意味着它与 PropertyTree 0.9 和 1.0 代码完全不兼容。主要原因将在本文档中概述或解释。

术语

在开始之前,我们将介绍三个主要类:PropertyTreePropertyPanePaneNodePropertyTree 是包含 TreeView 的类,而 PropertyPane 是一个容器控件,它最终会与 PropertyTreeTreeView 中的某个节点相关联。PaneNode 是一个类,负责将特定的 PropertyPane 表示为 PropertyTree 中的一个节点。当在 PropertyTreeTreeView 中选择一个节点时,将使用其对应的 PaneNode 来获取一个 PropertyPane,然后将其显示在 TreeView 的右侧。

虽然这个描述听起来可能有点含糊——您只需将 PropertyTree 视为一个使用 TreeView 而不是一排标签页的 TabControl

使用 PropertyTree

PropertyTree 被构建为一个独立的第三方控件,并且在上面的下载链接中提供了一个签名的程序集 (WRM.PropertyTree.dll),因此您可以直接将其安装到您的系统中,将其添加到您的 VS.NET 工具箱中,并立即开始使用它。当然,也提供了源代码,以便您可以对其进行修改并按需构建自己的版本——但如果您不想麻烦这些,则不必这样做。

如果您想在 VS.NET WinForms 设计器中使用 PropertyTree(您很可能会这样做),您首先需要右键单击 VS.NET 工具箱并选择“自定义工具箱...”在出现的对话框中,请按照以下步骤操作

  1. 选择“.NET Framework 组件”选项卡
  2. 点击“浏览...”按钮
  3. 导航到并选择 WRM.PropertyTree.dll 程序集
  4. 确保选中“PropertyTree”和“PropertyPane”控件。
  5. 一直按确定,直到您回到 VS.NET

如果您根本不想使用 VS.NET WinForms 设计器,则无需执行这些步骤。您可以像创建任何其他控件一样简单地创建 PropertyTree。但是,您将仅限于“自定义 PropertyPane”和“共享 PropertyPane”设计方案。

设计场景 - 创建 PropertyPane

PropertyTree 支持三种主要的设计场景。所有这些都受 VS.NET 的 WinForms 设计器支持,其中三种中有两种无需它即可使用。

场景 可用性 描述
匿名 PropertyPane WinForms 设计器 PropertyPane 的实例在设计时创建并添加到 PropertyTree。控件从 VS.NET 工具箱中拖放,并像将控件拖放到 TabControlTabPage 一样,将它们拖放到这些 PropertyPane 实例上。
自定义 PropertyPane WinForms 设计器,常规代码 程序员从 UserPropertyPane 派生类,并以设计 UserControl 派生类的方式对其进行设计。然后,可以通过 WinForms 设计器(从工具箱的“PropertyPanes”选项卡组中拖放它们)或通过常规代码将这些“自定义窗格”添加到 PropertyTree
共享 PropertyPane WinForms 设计器(无 PropertyTree 交互),常规代码 程序员从 SharedPropertyPane 派生类,并以设计 UserControl 派生类的方式对其进行设计。然后,可以通过常规代码将这些“共享窗格”添加到 PropertyTree。(有关为什么它们不能在设计时添加到 PropertyTree 的更多详细信息,请参阅下面的共享窗格设计场景部分。)

匿名 PropertyPanes

此设计场景仅在使用 VS.NET WinForms 设计器中的 PropertyTree 时可用。这是因为此场景完全围绕 WinForms 设计器将控件拖放到设计图面上以设计 UI 的能力而构建。VS.NET 使用 PropertyTreePropertyPane 的自定义设计器组件,使程序员能够可视化地编辑和重新排列 PropertyTree 中的 PropertyPane,以及每个 PropertyPane 上的控件。

要在 PropertyTree 中添加匿名 PropertyPane,只需右键单击 PropertyTreeTreeView 区域以打开上下文菜单。选择“添加 PropertyPane...”将在当前在 PropertyTree 中选定的 PropertyPane 的同级位置添加一个新的匿名 PropertyPane。选择“添加 PropertyPane 作为子项”将在当前在 PropertyTree 中选定的 PropertyPane 的子项位置添加一个新的匿名 PropertyPane。选择“删除 PropertyPane”将从 PropertyTree 中删除当前选定的 PropertyPane

要向匿名 PropertyPane 添加控件,请在 PropertyTree 中选择其节点。请注意,PropertyTree 右侧的 PropertyPane 区域具有与 Form 相同的网格背景。这是因为您可以像对待任何其他容器控件一样,通过拖放将控件添加到选定的 PropertyPane 中。

当每个窗格都是唯一的,并且涉及的逻辑相对简单时,您可以使用匿名 PropertyPane。第二点值得强调:托管在匿名 PropertyPane 上的控件所引发的事件都由包含这些匿名 PropertyPanePropertyTree 所承载控件的代码处理。如果您在匿名 PropertyPane 上有大量的控件,或者它们涉及大量的逻辑,那么您的窗体代码可能会迅速变得庞大而混乱。当出现这种情况时,您应该使用自定义 PropertyPane 设计场景来更好地封装和隔离每个窗格的行为。

自定义 PropertyPanes

此设计场景是否使用 VS.NET WinForms 设计器均可用。当然,使用 WinForms 设计器可以简化它,但并非完全必要。在此设计场景中,程序员通过从 UserPropertyPane 派生来创建自定义 PropertyPane,然后(通过使用 WinForms 设计器或在运行时)将这些新的自定义 PropertyPane 类的实例添加到 PropertyTree

由于 PropertyPane 只是一个派生自 UserControl 的类,因此您可以从它派生自己的类,并在任何可以使用 PropertyPane 的地方使用它。在 VS.NET 中,此自定义 PropertyPane 派生类的设计视图与 UserControl 的设计视图相同。因此,您所要做的就是像设计任何常规 UserControl 类一样设计您的自定义 PropertyPane 派生类。完成后,构建项目。

请注意,VS.NET 工具箱中现在有一个名为“PropertyPanes”的新选项卡组。该项目中的每个派生自 PropertyPane 的类(但不是派生自 SharedPropertyPane 的类,见下文)都将添加到此选项卡组中。然后,您可以通过将这些工具箱项拖放到要添加到的 PropertyTreeTreeView 上,来将自定义 PropertyPane 派生类的实例添加到 PropertyTree 中。

顺便提一下:PropertyPaneRootDesigner 在实例化时会更新“PropertyPanes”选项卡组中的 ToolboxItem。因此,在打开了该自定义 PropertyPane 派生类的设计视图一次之前,在“PropertyPanes”选项卡组中将不会出现任何 ToolboxItem。所以,如果您在该选项卡组中看不到自定义 PropertyPane,请尝试重新打开其设计视图并再次检查。

当您认为某个窗格将涉及复杂、可本地化的逻辑,或者需要同时使用不同实例的窗格时,您应该使用自定义 PropertyPane 设计场景。但是,当看起来将使用大量一个窗格的实例时,您应该考虑共享 PropertyPane 设计场景。

共享 PropertyPanes

此设计场景仅部分受 VS.NET WinForms 设计器支持。具体来说,共享 PropertyPane 可以像自定义 PropertyPane 一样创建和设计,但不能在设计时添加到 PropertyTree。这是因为共享 PropertyPane——它们派生自 SharedPropertyPane——有点特殊。在此不赘述,一个 SharedPropertyPane 实例与 PropertyTree 中的节点可能存在一对多关系。这种多对一关系通过使用 PaneNode 实用程序类来解决,该类将在下面讨论。因此,将共享 PropertyPane 添加到 PropertyTree 的唯一方法是在运行时使用常规代码。

当您认为需要许多“实例”的特定 PropertyPane 时,您将使用共享 PropertyPane 设计场景。在此场景中,您将构建一个自定义对象,该对象包含您的共享 PropertyPane 所需的所有信息,然后 PropertyTree 负责将这些数据对象的实例打包到一个共享 PropertyPane 的实例中。这样,只会创建一个共享 PropertyPane 的实际实例,但它可以表现得好像每个数据对象都有一个单独的实例。

一个很好的例子是类似 Explorer 的应用程序。您将创建一个 SharedPropertyPane 的派生类 FileInfoPane。在这种情况下,自定义数据对象可能只是一个 FileInfo 对象。当用户在 PropertyTree 中的节点之间单击时,PropertyTree 将仅仅更改 FileInfoPane 实例正在处理的 FileInfo 对象。

通用的设计时行为

虽然上述三种不同场景提供了不同的功能,但在 WinForms 设计器中始终可用一些功能。

  • 要选择一个 PropertyPane,请单击 PropertyTree 中的其节点。PropertyPane 将显示在 TreeView 右侧的区域。
  • 要更改 PropertyPane 的属性,请在 PropertyTree 中选择其节点,然后单击 PropertyPane 区域。这将会在“属性”窗口中选择 PropertyPane
  • 要重新排列 PropertyTreePropertyPane 的层次结构,只需单击要移动的 PropertyPane 的节点,然后将其拖动到所需位置。放置 PropertyPane 将使其成为放置在其上的节点的同级。但是,在放置时按住 Control 键会将拖动的 PropertyPane 变成放置的 PropertyPane 的子项。
  • 要将 PropertyPanePropertyTree 中删除,请右键单击 PropertyTree 中的其节点,然后从上下文菜单中选择“删除 PropertyPane”。

通用的运行时行为

所有 PropertyTree 功能在运行时都可用。匿名 PropertyPane 场景没有设计器就没什么意义,但如果您确实想在代码中直接使用它,也是可以的。但最有可能的是,您将使用运行时 PropertyTree 功能,将自定义或共享 PropertyPane 添加、重新排列或删除到 PropertyTree 中。

使用 PropertyPanes

无论您如何设计 PropertyPane(匿名、自定义、共享),您总可以在运行时调整其属性和位置。此版本的 PropertyTree(2.0)与早期版本之间最大的区别是,此版本不使用类似文件系统的路径字符串来指示 PropertyPanePropertyTree 中的位置。PropertyTree 2.0 使用更自然的 TreeNode 方法,并结合一个 PaneNode 类,该类在功能上非常类似于 TreeNode。

PaneNode

PaneNode 类的主要工作是表示特定 PropertyPanePropertyTree 中的位置。PropertyPane 类本身的一个实例无法完成这项工作,因为一个 SharedPropertyPane 实例可以被 PropertyTree 中的多个“节点”引用。PaneNode 类配备了存储有关常规 PropertyPaneSharedPropertyPane 的信息。

代表 PropertyPane

PaneNode 包含许多属性,这些属性代表 PropertyPane 上的内在属性。例如,像 .Title、.ImageIndex 和 .SelectedImageIndex 这样的属性。当 PaneNode 表示一个常规 PropertyPane 派生类的实例时(即,非派生自 SharedPropertyPane),这些属性会直接映射到由 .PropertyPane 属性引用的 PropertyPane 派生类的相应属性。当 PaneNode 表示一个 SharedPropertyPane 类的实例时,这些属性会由 PaneNode 实例本地存储,因为许多 PaneNode 对象可能引用该一个 SharedPropertyPane 派生类的实例。

PaneNode 还包含其他非聚合属性。当 PaneNode 表示一个 SharedPropertyPane 的实例时,它的 .IsShared 属性为 true,并且它的 .Data 属性包含一个指向自定义数据对象的引用,该对象将在选择此 PaneNode 时传递给由 .PropertyPane 属性引用的 SharedPropertyPane 实例。当 .PropertyPane 属性引用的 PropertyPane 类未派生自 SharedPropertyPane 时,.IsShared 属性为 false。在这种情况下,.Data 值为空,应完全忽略。

添加和删除 PropertyPanes

PaneNode 类的另一个重要工作是表示 PropertyTreePropertyPane 的层次结构关系。PaneNode 的子 PaneNode 存在于其 .PaneNodes 集合中。向此集合添加或从中删除 PaneNode 会影响它们在 PropertyTree 中的位置。这与早期版本的 PropertyTree 使用的“路径”字符串形成鲜明对比。

例如:以下代码将一些 PropertyPane 添加为 PaneNode 的子项,然后将它们删除并使它们成为另一个 PaneNode 的子项。

// Create two root nodes in the PropertyTree
// 
// [Root]
//   - rootNode1
//   - rootNode2
//
PaneNode rootNode1 = propertyTree.PaneNodes.Add(new MyCustomPropertyPane());
PaneNode rootNode2 = propertyTree.PaneNodes.Add(new MyCustomPropertyPane());

// Add two child nodes to the first root node
//
// [Root]
//   - rootNode1
//     - child1
//     - child2
//   - rootNode2
PaneNode child1 = rootNode1.PaneNodes.Add(new MyOtherCustomPane());
PaneNode child2 = rootNode1.PaneNodes.Add(new MyOtherCustomPane());

// Now, remove the two child nodes from the first root node and add them
// to the second root node
//
// [Root]
//   - rootNode1
//   - rootNode2
//     - child1
//     - child2
rootNode1.PaneNodes.Remove(child1);
rootNode1.PaneNodes.Remove(child2);
rootNode2.PaneNodes.Add(child1);
rootNode2.PaneNodes.Add(child2);
    

重要的是要注意,无论它们当前是否在 PropertyTree 中,您都可以像上面的代码那样使用 PaneNode

PaneNodeCollection

PaneNode.PaneNodes 属性始终引用一个 PaneNodeCollection 对象。该对象是一个专门构建的容器,用于处理添加和删除 PropertyPane 相关项。 .Add 方法重载了以处理添加 PropertyPane 实例、PaneNode 和 SharedPropertyPane 实例。

Add(PropertyPane pane)

Add(PropertyPane pane, int index, int imageIndex, int selectedImageIndex)

您将使用 Add 的这些重载来添加新创建的 PropertyPane 实例。这将返回一个 PaneNode 对象,该对象表示该 PropertyPane 实例在 PropertyTree 中。index 参数标识插入 pane 的零基索引。将其设置为 -1 会将 pane 插入到列表末尾。

Add(PaneNode paneNode)

您将使用此重载来添加现有 PaneNode。现有的 paneNode 不能已存在于此 PropertyTree 或任何其他 PropertyTree 中。

Add(Type sharedPaneType, string title, object data)

Add(Type sharedPaneType, 
    string title, 
    int index, 
    int imageIndex, 
    int selectedImageIndex, 
    object data)

您将使用这两个重载之一来添加表示 sharedPaneType 类型的 SharedPropertyPanePaneNode。在后台,PropertyTree 会创建一个 sharedPaneType 的实例,并将其保留,只要有 PaneNode 引用它。index 参数标识插入表示 sharedPaneTypePaneNode 的零基索引。将其设置为 -1 会将 PaneNode 插入到列表末尾。

PaneNodeCollection 的其他集合式方法是不言自明的。它们的功能和用途与任何 IList 相同——唯一的区别是它们是强类型化的,只能处理 PaneNode 实例。

PropertyPanes 和选择事件

PropertyTree 具有一个相当复杂的选择/取消选择过程。它允许当前 PropertyPane 和新选择的 PropertyPane 在更改实际发生之前确认选择更改。如果其中任何一个 PropertyPane 否决了选择更改,则不会进行任何选择更改。如果两者都同意,则在选择更改发生后,每个都会再次收到通知。选择事件的顺序是

  1. PaneDeactivating(可否决 - 涉及当前选定的 PropertyPane
  2. PaneActivating(可否决 - 涉及新选定的 PropertyPane
  3. PaneDeactivated(涉及当前选定的 PropertyPane
  4. PaneActivated(涉及新选定的 PropertyPane

PropertyTree 在选择过程中会触发其四个事件。这使得承载 PropertyTree 的窗体在选择过程中也能发挥作用。当您使用匿名 PropertyPane 时,必须在这些 PropertyTree 事件中处理选择更改过程。

当您使用自定义或共享 PropertyPane 时,PropertyTree 会调用参与选择更改过程的这些类型的 PropetyPane 实例的 On[SelChangeEventName]() 方法。这允许在派生类内部处理选择更改过程。尽管 PropertyTree 仍会触发其四个事件,但覆盖自定义 PropertyPane 派生类中的 On[SelChangeEventName]() 方法是处理选择更改过程的首选方法。

在 PaneDeactivating 或 PaneActivating 事件期间,将 PaneSelectionEventArgs.Cancel 属性设置为 true 将否决选择更改。.Cancel 属性是所有被设置的值的逻辑 OR。这样,可能存在的多个事件监听器中的任何一个都可以否决选择更改。

PropertyTree 的选择更改过程是“选择退出”过程。默认情况下,所有选择更改都已批准。只有通过显式处理选择更改事件,您才能否决它。如果您从不需要否决选择更改,则可以安全地忽略整个过程。

PropertyTree 功能,锦上添花

在解释了创建和排列 PropertyPane 的主要任务后,还有一些 PropertyTree 的额外功能需要讨论。

根 PaneNodes

PropertyTree 定义了自己的 .PaneNodes 属性。该属性引用的 PaneNodeCollection 包含此 PropertyTree 的所有根级别 PaneNodes。

编程选择更改

PropertyTree 定义了读/写属性 SelectedPaneNode 来引用当前在 PropertyTree 中选定的 PaneNode。值为 null 表示没有选择。

在任何时候,您都可以通过将此值设置为 PropertyTree 中存在的有效 PaneNode 对象或 null 来操作当前在 PropertyTree 中选定的 PaneNode。设置此值会启动窗格选择过程(上面已讨论),该过程可能会被否决。如果窗格选择过程被否决,则 SelectedPaneNode 属性的值将不会更改。

PaneNode 图像

PaneNode 具有与它们关联的 ImageIndex 和 SelectedImageIndex 属性,这些属性从 PropertyTree 的 ImageList 中选择图像。这些属性遮蔽了底层 TreeNode 具有相同名称的属性。

AutoNavigate

AutoNavigate image

我非常喜欢的一项功能是 PropertyTree 对 VS.NET 中的选项浏览器(选择 Tools | Options...)的模拟。在此操作模式下,除选定的 PaneNode 及其直接祖节点外,所有节点都会自动折叠。SysTreeView32 有一个自动执行此操作的窗口样式 (TVS_SINGLEEXPAND),但我选择手动模拟此行为,以避免担心系统中使用的通用控件版本。

AutoNavigate 设置为 true 可使 PropertyTree 表现出此行为。请注意,这将更改 PropertyTree 使用的 ImageList,更新所有 PaneNode 的 ImageIndex 和 SelectedImageIndex 值,并关闭 TreeView 中的所有线条绘制和加号/减号框绘制。

实现细节 - 代码工作原理

代码本身约有 6000 行源代码和注释,因此我将只介绍其设计中最有趣的部分。如果您对 PropertyTree 的内部工作原理感兴趣,请深入研究源代码——它有详细的注释。我编写 PropertyTree 是为了给自己一个学习练习,我希望其他人也能以同样的方式使用它。

TreeNode -> PaneNode -> PropertyPane

这类控件的基本概念是,当用户单击 TreeView 中的节点时,会向用户显示一组特定的子控件。从这一点可以看出,我们有两个端点——TreeView 中的节点和子控件的容器。对于 PropertyTree,这两个端点是 TreeNode 对象和 PropertyPane 控件。

在简单场景中,从 TreeNode 直接映射到 PropertyPane 实例是可行的。然而,这个想法最大的问题在于它需要每个 PropertyPane 的单独实例。这对于“选项设置”风格的对话框来说不是问题,其中每个 PropertyPane 表面上都包含一组不同的控件。但是,对于构建类似 Explorer 的应用程序来说,这是不可接受的。树中可能有多达 100 个节点,但只有两种类型的 PropertyPane 派生类。在这种情况下,拥有每种类型 PropertyPane 派生类的单个实例,并在选择新节点时仅为其提供新数据,将更加高效。

共享 PropertyPane 的概念(如上所述)违反了之前每个 TreeNode 直接映射到 PropertyPane 实例的约束。因此,需要引入一个中间对象来提供 TreeNode 和 PropertyPane 之间的间接层。这个名为 PaneNode 的类将与 TreeView 中的节点有一对一的关系(就像在简单场景中 PropertyPane 一样),并且将允许(但不强制)与 PropertyPane 实例的多对一关系。此外,如果 PaneNode 表示一个共享 PropertyPane,则 PaneNode 类将包含该节点的“数据对象”。

遮蔽 .Controls - 协方差的简易实现

对于容器类控件,Controls 属性包含一个(类型为 Control.ControlCollection)的所有子控件的集合。这给任何打算仅托管特定类型控件子类的用户定义控件子类带来了一些问题。问题在于 Controls 属性的类型是 Control.ControlCollection——它被设计为与任何类型的 Control 派生类一起使用。然而,用户定义的控件子类只希望托管 Control 的特定子类。

在这种情况下,有两个主要的障碍

  • C# 不支持协变返回类型
  • 即使支持,由于 Control.Controls 属性不是虚拟的,所以也无关紧要。

如果这两个问题都不存在,代码就可以这样写

// Collection built for PropertyTree specifically
//
public class PaneNodeCollection : Control.ControlCollection
{
...
}

public class PropertyTree : UserControl 
{
    ...
    // Override of Control.Controls
    //
    public override PaneNodeCollection Controls
    {
        get
        {
            return mPaneNodes;
        }
    }
    ...
}
    

不幸的是,必须采取一种更丑陋的方式来解决这个问题。TabControl 通过从 Control.ControlCollection 派生自己的容器类,并通过其 TabPage 属性将其暴露出来。这对 TabControl 来说效果很好,因为它的子控件集合是同质的——它们都是 TabPageTabControl 所做的只是确保 WinForms 设计器不会尝试序列化其 TabPages 集合的内容,因为它与 Controls 集合的内容相同。

然而,PropertyTree 具有异质的 Controls 集合:它不仅包含 PropertyPanes,还包含一个 TreeView、一些 Labels 和一些其他控件。在代码序列化期间,WinForms 设计器会检查 Controls 集合的内容,并尝试将其中找到的对象实例与它知道在设计会话中已创建的对象实例进行匹配。但是,当它获取 PropertyTree.Controls 集合中的 TreeView 时,它不识别该对象实例(因为 PropertyTree 在从未告知 WinForms 设计器它的存在的情况下创建了它)。一旦发生这种情况,WinForms 设计器就会放弃,不会序列化 Controls 属性。最终结果当然是 Controls 集合永远不会被序列化。

PropertyTree 0.9 和 1.0 通过重新定义 PropertyTree.Controls 集合来返回实际充当 PropertyPane 父级的内部 Panel 的 Controls 集合来解决此问题。这效果很好,但并非最佳解决方案,因为 Controls 控件集合仍然仅类型化为与 Control 派生对象一起使用。如果 PropertyTree 拥有自己的专门用于处理 PropertyPanes 和相关对象的集合,那会更好。

这个自定义集合(在上面PaneNodeCollection 部分描述)就是为此目的而设计的。它所有的.Add 方法都强类型化为与 PaneNode 对象一起使用,除了 .Add 方法外,该方法有多个重载。这个自定义 PaneNodeCollection——由 PropertyTree.PaneNodes 属性公开——完全取代了 PropertyTree.Controls 集合。

但是 WinForms 设计器仍然会尝试序列化 PropertyTree.Controls 集合的内容!为了阻止这种情况,PropertyTreeDesigner 类会覆盖 OnPostFilterProperties() 函数,以便设计时环境甚至不知道 PropertyTree 具有“Controls”属性。

protected override void PostFilterProperties(
                                   System.Collections.IDictionary properties)
{
    string[] propertiesToExclude = {"Controls"};
    foreach(string prop in propertiesToExclude)
        if(properties.Contains(prop))
            properties.Remove(prop);

    base.PostFilterProperties(properties);
}
    

设计时集成

在开始之前,我想引用一些我所知道的相当不错的 VS.NET 设计时文章和教程。

在 VS.NET 中,每个放置在窗体上的控件都会在窗体设计器中关联一个“Designer”对象。对于大多数控件,这个 Designer 对象在设计时为控件提供功能。例如,PropertyTreeDesigner 对象在选中 PropertyTree 时会在上下文菜单中添加三个 DesignerVerbs,并处理鼠标点击以选择和重新排列节点。

控件类与“Designer”对象类型通过使用 Designer 属性相关联。

    [Designer(typeof(WRM.Windows.Forms.Design.PropertyTreeDesigner))]
    ...
    public class PropertyTree : UserControl
    {
        ...

由于这个属性,WinForms 设计器将为每个放置在设计图面上的 PropertyTree 使用一个新的 PropertyTreeDesigner 实例。

PropertyPane 也做了同样的处理。

[Designer(typeof(WRM.Windows.Forms.Design.PropertyPaneDesigner))]
[Designer(typeof(WRM.Windows.Forms.Design.PropertyPaneRootDesigner),
          typeof(IRootDesigner))]

请注意,PropertyPane 类有两个关联的 Designer 属性。只有一个参数的属性提供了当 PropertyPane 放置在设计图面上时要使用的 Designer 对象的 Type。另一个包含第二个参数的属性提供了当此 PropertyPane **本身**是设计图面时要使用的 Designer 对象的 Type。这种类型的 Designer 必须实现 IRootDesigner 接口。WinForms 设计器本身以及 UserControl 使用的 DocumentDesigner 是充当设计图面的这些 Designer 对象的示例。

因此,当 PropertyPane 存在于某个其他设计图面上时(例如,当它存在于某个窗体或设计时位于其他控件上的 PropertyTree 中时),PropertyPaneDesigner 对象用于驱动 PropertyPane 的设计时体验。PropertyPaneRootDesigner,派生自 DocumentDesigner,本身充当了设计图面,用于设计自定义或共享 PropertyPane

PropertyTreeDesigner - 有趣的部分

向上下文菜单添加 Verbs

每个 Designer 对象都有一个 Verbs 属性,该属性是一个菜单命令集合,当选中该控件时,VS.NET 会将这些菜单命令添加到上下文菜单中。PropertyTreeDesigner 添加了三个 DesignerVerbs

  • 添加 PropertyPane
  • 添加 PropertyPane 作为子项
  • 删除 PropertyPane

Verbs 属性在 PropertyTreeDesigner 中使用以下代码填充和实现。

public PropertyTreeDesigner()
{
    mVerbs = new DesignerVerbCollection();
    mVerbs.Add(new DesignerVerb("Add PropertyPane",
        new EventHandler(OnAddPane)));
    mVerbs.Add(new DesignerVerb("Add PropertyPane as child",
        new EventHandler(OnAddPaneAsChild)));
    mVerbs.Add(new DesignerVerb("Remove PropertyPane",
        new EventHandler(OnRemovePane)));
    mVerbs[2].Enabled = false;
    ...
}
...
public override DesignerVerbCollection Verbs
{
    get
    {
        return mVerbs;
    }
}

鼠标处理

PropertyTree 的设计时功能很大一部分来自于处理用户鼠标点击。TreeView 在设计模式下不允许用户通过点击来选择节点。为了允许用户选择 TreeView 中的节点(从而显示其对应的 PropertyPane),PropertyPaneDesigner 必须拦截鼠标点击并手动选择节点。此外,它还需要编写代码来实现拖放,以便在树中重新排列节点,并且 PropertyPane 工具箱项可以从工具箱拖放到 PropertyTree 上。

PropertyTreeDesigner 通过重写 ControlDesigner 的 WndProc 方法来实现这一点。通过拦截鼠标事件,它可以强制底层的 TreeView 在设计时按照我们期望的方式响应用户输入。

protected override void WndProc(ref Message m) 
{
    // Make sure that this message is for the TreeView
    if(mPropertyTree.TreeView.Created  && 
        (m.HWnd == mPropertyTree.TreeView.Handle ||
        m.HWnd == mPropertyTree.Handle) )
    {    
        switch(m.Msg)
        {
            // If the user has pressed the left mouse button, select a node
            // in the TreeView if necessary
            // 
            case WM_LBUTTONDOWN:
                ...
                break;
  
            case WM_MOUSEMOVE:
                ...
                break;

            case WM_LBUTTONUP:
                ...
                break;

            case WM_RBUTTONDOWN:
                ...
                break;
  
            case WM_RBUTTONUP:
                ...
                break;
  
            default:
                base.WndProc(ref m);
                break;
        }
    }
    else
    {
        base.WndProc(ref m);
    }
}

通过 IDesignerHost 创建新控件实例

当用户选择两个“添加 PropertyPane”动词之一,或将自定义 PropertyPane 从工具箱拖放到 PropertyTree 上时,PropertyTreeDesigner 需要向 PropertyTree 添加一个新的 PropertyPane 实例。但是,它还必须确保 WinForms 设计器知道新的 PropertyPane 实例。这是为了将其与唯一的名称(即引用它的变量名)关联,然后生成创建该控件并设置其属性的代码。

为了让 WinForms 设计器了解这一点,PropertyTreeDesigner 使用 IDesignerHost.CreateComponent 方法来创建新的 PropertyPane 实例。(注意 IDesignerHost 是 WinForms 设计器向 Designer 对象提供的服务接口)。当使用 CreateComponent 创建 PropertyPane 时,WinForms 设计器会意识到它的存在,并负责生成代码来创建它并在 InitializeComponent 中设置其属性。

public void OnAddPane(object sender, EventArgs e)
{
    string name = GenerateNewPaneName();
    ...
    PropertyPane pp = 
        mDesignerHost.CreateComponent(typeof(PropertyPane),name) 
                                                             as PropertyPane;

    //Add the pane to the PropertyTree
    pp.Text= name;
    PaneNode paneNode = null;

    if(parentNode == null)
        paneNode = mPropertyTree.PaneNodes.Add(pp);
    else
        paneNode = parentNode.PaneNodes.Add(pp);

    //Programatically select the pane we just added
    mPropertyTree.SelectedPaneNode = paneNode;
}

响应工具箱拖放

自定义 PropertyPane 场景的核心是通过将自定义 PropertyPane 派生类从工具箱拖放到 PropertyTree 来在设计时构建 PropertyTreePropertyPaneRootDesigner见下文)负责确保工具箱项被放入工具箱。PropertyTreeDesigner 只需响应普通的 OLE 拖放事件,留意工具箱项。它通过以下代码来实现这一点。

protected override void OnDragEnter(System.Windows.Forms.DragEventArgs de)
{
    base.OnDragEnter(de);

    IDataObject data = de.Data;

    if(!mToolboxService.IsToolboxItem(data))
    {
        de.Effect = DragDropEffects.None;
        return;
    }

    ToolboxItem ti = (ToolboxItem)mToolboxService.DeserializeToolboxItem(data);
    Type t = Type.GetType(ti.TypeName);
    if(t == null)
    {
        de.Effect = DragDropEffects.None;
        return;
    }

    if(typeof(PropertyPane).IsAssignableFrom(t) && 
       !typeof(SharedPropertyPane).IsAssignableFrom(t))
        de.Effect = DragDropEffects.Copy;
    else
        de.Effect = DragDropEffects.None;
}

PropertyPaneDesigner

PropertyPaneDesigner 本身并不是很有趣。它派生自 ParentControlDesigner,因此它可以承载在设计时被拖放到其上的其他控件。由于它派生自 ParentControlDesigner,因此在设计时其背景是一个网格——就像 Form 的背景一样。这使得用户难以区分 PropertyPane 和 Form 本身。因此,PropertyPaneDesigner 重写了 ControlDesignerOnPaintAdornments 方法,以在 PropertyPane 区域周围绘制虚线边框。

protected override void OnPaintAdornments(System.Windows.Forms.PaintEventArgs pe)
{
    base.OnPaintAdornments(pe);

    ...
    pe.Graphics.DrawRectangle(mBorderPen,
        1,1,mPropertyPane.Width-2,mPropertyPane.Height-2);
}

PropertyPaneRootDesigner

PropertyPaneRootDesigner 有点意思。它的主要目的是确保有一个代表该自定义 PropertyPane 的工具箱项。这个工具箱项可以从 VS.NET 工具箱拖放,然后放置到 PropertyTree 上,从而在该 PropertyTree 中创建该 PropertyPane 的一个实例。这就是自定义 PropertyPane 设计场景。

PropertyPaneRootDesigner 有一个相当直接的算法来添加工具箱项:

  1. 确定设计对象 TypePropertyPane
  2. 如果 Type 派生自 SharedPropertyPane,则不添加工具箱项。
  3. 否则,确定要用于工具箱项的显示名称和位图。
    1. 显示名称是类名,不带命名空间。
    2. 确定要使用的位图。
      1. 在资源中搜索 [显示名称].bmp。
      2. 使用默认的灰色箭头位图。
  4. 删除任何匹配的现有工具箱项。
  5. 添加此工具箱项。

执行此操作的代码如下(注意:文档注释已删除以节省空间)。

private void OnLoadComplete(object sender, EventArgs e) 
{
    IDesignerHost host = (IDesignerHost)sender;
    ...
    IToolboxService tbx = (IToolboxService)GetService(typeof(IToolboxService));

    Type paneType = host.RootComponent.GetType();

    if (tbx != null && 
        !paneType.Equals(typeof(SharedPropertyPane)) &&
        !paneType.IsSubclassOf(typeof(SharedPropertyPane)))
    {
        string fullClassName = host.RootComponentClassName;
        ToolboxItem item = new ToolboxItem();
        item.TypeName = fullClassName;
        int idx = fullClassName.LastIndexOf('.');
        if (idx != -1) 
        {
            item.DisplayName = fullClassName.Substring(idx + 1);
        }
        else 
        {
            item.DisplayName = fullClassName;
        }

        item.Bitmap = GetToolboxBitmap(Type.GetType(fullClassName),
                                       item.DisplayName);
        item.Lock();

        if(tbx.GetToolboxItems().Contains(item))
        {
            tbx.RemoveToolboxItem(item,"PropertyPanes");
        }
        tbx.AddLinkedToolboxItem(item, "PropertyPanes", host);
    }
}

...
private Bitmap GetToolboxBitmap(Type t, string className)
{
    Bitmap b;

    try
    {
        b = new Bitmap(t, className + ".bmp");
    }
    catch(Exception /*ex*/)
    {
        b = new Bitmap(typeof(PropertyPane),"PropertyPane.bmp");
    }

    b.MakeTransparent(Color.FromArgb(0,255,0));
    return b;
}

基本上,这段代码通过使用 IToolboxService(由 WinForms 设计器提供的众多 Designer 支持接口之一)来检查拖动的对象是否是工具箱项。如果是,它会确保它所代表的拖动组件派生自 PropertyPane,但不是派生自 SharedPropertyPane。如果这两个测试都通过,则允许拖放操作继续。

一旦发生拖放,将执行相当多的相当直接的代码来确定它被放置在哪个节点上,拖放的 PropertyPaneType 是什么等等。最终,将创建一个 PropertyPane 派生类的新实例并将其添加到 PropertyTree 中。

PaneNodeCollectionSerializer

WinForms 代码序列化器仅对设计时可见的对象序列化代码。这给 PropertyTreePropertyPane 带来了一些问题——它们需要以代码形式序列化所有 PropertyPane,但它们的 PaneNodes 集合只包含对 PaneNode 对象的引用,而这些对象不应在代码中序列化。

解决此问题的一种方法是使 PaneNode 类可序列化。但我没有选择这条路,因为我真的不认为 PaneNode 是一个真正值得在代码中持久化的类。我认为它更像是一个运行时副产品。

另一种解决方案使用 VS.NET 设计时环境的一些更酷的功能:自定义代码序列化器。代码序列化器对象派生自 CodeDomSerializer,知道如何将某个对象实例转换为该对象实例的 CodeDom 表示。尽管这种情况很少见,但此类旨在让 Control 作者对其 Control 如何被 WinForms 设计器持久化到源代码中以及如何从源代码中读取拥有完全控制权。

自定义 CodeDomSerializer 通过使用 CodeDomSerializer 属性与 Control 相关联。

[DesignerSerializer(typeof(WRM.Windows.Forms.Design.PaneNodeCollectionSerializer),
      typeof(CodeDomSerializer))]

当需要重新生成 InitializeComponent 中的代码时,WinForms 设计器将创建一个 PaneNodeCollectionSerializer 实例,并允许它生成 PropertyTreePropertyPane 的 CodeDom 表示。

PaneNodeCollectionSerializer 本身非常简单。它的行为与默认的 CodeDomSerializer 没有区别,除了它为 PaneNodes 属性生成的代码(PropertyTreePropertyPane 都有一个 PaneNodes 属性,这就是为什么这个自定义序列化器与这两个类相关联)。与其尝试(并失败)序列化 PaneNodes 中的 PaneNode 实例,不如生成代码将引用的 PropertyPane 添加到该 PaneNodes 集合中。对于 PaneNodes 属性以外的所有属性,序列化留给默认的 CodeDomSerializer 处理。

public override object Serialize(
  IDesignerSerializationManager manager, 
  object value)
{
  object codeObject = GetBaseSerializer(manager).Serialize(manager, value);
  ArrayList topLevelPanes = new ArrayList();
  
  CodeStatementCollection csc = (CodeStatementCollection)codeObject;

  PropertyDescriptor paneNodesProp
                          = TypeDescriptor.GetProperties(value)["PaneNodes"];
  PaneNodeCollection nodes = (PaneNodeCollection)paneNodesProp.GetValue(value);
  string compName = manager.GetName(value);

  // Create the call to PaneNodes.Add(...) for each PaneNode
  foreach(PaneNode child in nodes)
  {
    CodeThisReferenceExpression thisRef = 
      new CodeThisReferenceExpression();
    CodeFieldReferenceExpression fieldRef = 
      new CodeFieldReferenceExpression(thisRef,compName);
    CodeFieldReferenceExpression childRef = 
      new CodeFieldReferenceExpression(
                               thisRef,manager.GetName(child.PropertyPane));
    CodePropertyReferenceExpression paneNodesRef = 
      new CodePropertyReferenceExpression(fieldRef,"PaneNodes");
    CodeMethodInvokeExpression invokeExpr = new CodeMethodInvokeExpression(
      paneNodesRef,
      "Add",
      childRef,
      new CodePrimitiveExpression(child.Index),
      new CodePrimitiveExpression(child.ImageIndex),
      new CodePrimitiveExpression(child.SelectedImageIndex));

    csc.Add(invokeExpr);
  }

  return codeObject;
}

历史

  • 2001 年 1 月 - 8 月:PropertyTree 在 VS.NET Beta 1 和 2 中的初始实现
  • 2001 年 11 月:发布 PropertyTree 0.9,CodeProject 文章
  • 2002 年 3 月:发布 PropertyTree 1.0。代码更新以适应 VS.NET Final。
  • 2002 年 5 月 - 2003 年 3 月:2.0 版本工作(业余时间不多...)
  • 2003 年 2 月:在 SourceForge 上设置 PropertyTree 项目
  • 2003 年 3 月:CodeProject 文章更新以适应 2.0 版本。
© . All rights reserved.