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

WPF 图形设计器 - 第 4 部分

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.97/5 (238投票s)

2008年3月26日

CPOL

6分钟阅读

viewsIcon

1263134

downloadIcon

39875

拼凑而成

WPF FlowChart Designer

引言

在本文中,我添加了以下命令

  • 打开, 保存
  • 剪切, 复制, 粘贴, 删除
  • 打印
  • 组合, 取消组合
  • 对齐 (左对齐, 右对齐, 顶对齐, 底对齐, 水平居中, 垂直居中)
  • 分布 (水平, 垂直)
  • 排序 (前移一层, 移到顶层, 后移一层, 移到底层)

注意:我将只支持 .NET 3.5 上的 Visual Studio 8.0!

Commands

我使用 WPF 命令的方式非常直接,正如 WPF SDK 文档中所述,无需额外基础结构。

分组

我组合项目的第一个方法是使用一个 `DesignerItem` 对象,它应该作为组容器。为此,我创建了 `DesignerItem` 类的一个新实例,其内容为一个 `Canvas` 对象。我计划在该画布上定位要组合的设计师项目。但在将项目放入组画布之前,我必须将它们从设计师画布中移除,因为在 WPF 中,一个元素不能是两个元素的子元素。如果您尝试这样做,将会收到一个 `InvalidOperationException` 异常,其消息如下:

"Specified element is already the logical child of another element. 
Disconnect it first."

因此,我将项目从设计师画布中移除,并将它们添加到组画布。现在,理解 WPF 在后台做了什么很有趣:一旦我将一个项目从设计师画布中移除,它的模板就会被卸载;当我将其添加到组画布时,一个新的模板就会被加载。您还记得上一篇文章我向您展示了如何连接设计师项目吗?在那里,我通过连接器连接项目,这些连接器是设计师项目模板的一部分,一旦我将项目从设计师画布中移除,该模板就会丢失。您看到了问题所在吗?我通过项目的模板连接了设计师项目,因此设计师项目本身对已有的连接一无所知。所有与连接相关的信息都隔离在设计师项目的模板中。

想象一个数据库图,其中设计师项目的内容是一个数据库表。该表永远不会识别与其他表的任何关系。一种解决方案是将信息从模板传递到设计师项目,再到表。更好的解决方案是重新设计应用程序,并将整个大块分解成单独的部分,例如:

  • 模板(视图)
  • 设计师项目(视图模型)
  • 数据库表(模型)

我不会在一篇文章的中间开始重新设计代码,而是会坚持使用这种“仅视图方法”,直到本文结束。这次旅程越痛苦,更好的解决方案就越受欢迎。(我将在未来的文章中介绍一个模型驱动的设计器。)

那么,让我们继续。组合设计师项目的另一种方法使用了以下接口

 public interface IGroupable 
 { 
     Guid ID { get; } 
     Guid ParentID { get; set; } 
     bool IsGroup { get; set; } 
 }

其思想是 `DesignerItem` 类必须实现此接口才能成为组合基础结构的一部分,该基础结构的工作方式如下:

  • 创建一个新的 `DesignerItem` 对象,该对象具有唯一的 `ID` 并且其 `IsGroup` 属性设置为 `true`
  • 对于每个组内成员,将 `ParentID` 设置为组父级的 `ID`。

这很简单,但真正的工作发生在修改项目时(选择、移动、调整大小、复制……);每次执行这些操作时,我都要考虑项目的组状态。这听起来像是一项繁重的工作,但如果没有 LINQ,情况会更糟。为此,我将大部分工作封装在 `SelectionService` 类中。

注意: `Connection` 类没有实现 `IGroupable` 接口,因此不能直接成为组的一部分,但可以间接实现——因为连接始终连接到一个项目。这使我能够灵活地重新连接/连接项目,无论它们是否属于组。

保存

为了保存图表,我选择使用 XML 和 XAML 的组合。对于 `DesignerItem` 相关的数据,我使用 XML,而内容则序列化为 XAML。在此,请再次注意,将设计师项目的内容序列化为 XAML 只会保留视觉方面,因此仅用作短期解决方案。为了创建 XML 文件,我使用了 LINQ。由于这是我第一次尝试使用 LINQ,所以不要期望这一定是“正确”的使用方式。

以下是我序列化设计师项目的一个示例

 XElement serializedItems = new XElement("DesignerItems",
                          from item in designerItems
                          let contentXaml = XamlWriter.Save(((DesignerItem)item).Content)
                          select new XElement("DesignerItem",
                                      new XElement("Left", Canvas.GetLeft(item)),
                                      new XElement("Top", Canvas.GetTop(item)),
                                      new XElement("Width", item.Width),
                                      new XElement("Height", item.Height),
                                      new XElement("ID", item.ID),
                                      new XElement("zIndex", Canvas.GetZIndex(item)),
                                      new XElement("IsGroup", item.IsGroup),
                                      new XElement("ParentID", item.ParentID),
                                      new XElement("Content", contentXaml)
                                      )
                           );

`let` 关键字允许您将子表达式的结果存储在一个变量中,该变量可以在后续表达式中使用。我在这里使用此功能将序列化内容存储在 `contentXaml` 变量中,该变量在几行后被使用。最后,我使用 `XElement` 类的 `Save` 方法来存储元素的底层 XML 树。

 XElement.Save(fileName)

打开

从 XML 文件加载图表时,我们必须先加载设计师项目,因为我们需要它们的连接器来创建连接。我们已经了解到连接器是项目模板的一部分,因此设计师项目必须在我们可以继续之前加载其模板。幸运的是,`Control` 类提供了 `ApplyTemplate()` 方法,该方法强制 WPF 布局系统加载控件模板,以便可以引用其部分。

在上一篇文章中,我提供了一种自定义 `ConnectorDecorator` 模板的机制,该模板允许您自由地将连接器定位在设计师项目的周围。该解决方案是在设计师项目的 `Loaded` 事件触发后应用自定义模板的,而该事件在项目出现在屏幕上之前不会触发。现在屏幕在命令结束之前无法重绘。因此,唯一的方法是在 `Open` 命令中显式设置自定义的 `ConnectorDecorator` 模板,请参阅 `SetConnectorDecoratorTemplate(item)` 方法。

注意:定义自定义连接器时,必须设置 `x:Name` 属性。连接使用此名称来标识其源连接器和目标连接器。

 <s:Connector x:Name="Left" Orientation="Left" 
    VerticalAlignment="Center" HorizontalAlignment="Left"/>

复制、粘贴、删除、剪切

`Copy` 和 `Paste` 命令在功能上类似于 `Open` 和 `Save` 命令,不同之处在于它们仅应用于选定的项目,并且它们将序列化内容读写到 `Clipboard`。`Delete` 命令只是从设计师画布的 `Children` 集合中移除所有选定的项目,而 `Cut` 命令最终是 `Copy` 和 `Delete` 命令的组合。

对齐、分布

关于这些命令没有什么可说的,除了对齐的参考项目是第一个被选中的项目(也称为主选定项)。这仅在您使用鼠标左键 + Ctrl 或鼠标左键 + Shift 选择项目时才有效,但如果您使用橡皮筋选择则无效。

Order

`Panel` 类(`Canvas` 由其派生而来)提供了一个名为 `ZIndex` 的附加属性,该属性定义了子元素在 Z 轴上的出现顺序,因此我们只需要更改该属性即可将项目前移或后移。

历史

  • 2008 年 3 月 25 日 -- 提交的原始版本
© . All rights reserved.