WPF 图形设计器 - 第 4 部分
拼凑而成

引言
在本文中,我添加了以下命令
打开
,保存
剪切
,复制
,粘贴
,删除
打印
组合
,取消组合
对齐
(左对齐
,右对齐
,顶对齐
,底对齐
,水平居中
,垂直居中
)分布
(水平
,垂直
)排序
(前移一层
,移到顶层
,后移一层
,移到底层
)
注意:我将只支持 .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 日 -- 提交的原始版本