WPF 图形设计器 - 第 3 部分
连接项

引言
在典型的图形设计器中,存在不同的连接项的技术。一种方法是在工具箱中提供连接元素,用户可以将它们拖放到设计器画布上,然后将端点拖动到源项和目标项。另一种方法是项本身提供连接点,用户可以从这些连接点拖动连接到其他项。第二种策略是我将在本文中解释的。
用例:如何连接项
我相信您知道如何在设计器应用程序中连接项,但我仍将详细说明,以便更容易识别哪个类涉及哪个活动。
当您将鼠标悬停在设计器项上时,每个侧面会出现四个 Connector 类型的视觉元素。此默认布局定义在 ConnectorDecoratorTemplate 中,并且是默认 DesignerItem 模板的一部分。现在将鼠标悬停在其中一个连接器上,光标会变成一个十字。 |
![]() |
如果您单击鼠标左键并开始拖动,连接器将创建一个 ConnectorAdorner 类型的装饰器。此装饰器负责绘制源连接器和当前鼠标位置之间的路径。在拖动过程中,装饰器会持续对 DesignerCanvas 进行命中测试,以检查鼠标是否悬停在潜在的目标连接器上。 |
![]() |
如果您在 Connector 元素上释放鼠标按钮,ConnectorAdorner 会创建一个新的 Connection 实例并将其添加到设计器画布的子元素中。如果鼠标按钮在其他地方释放,则不会创建 Connection 实例。 |
![]() |
与 DesignerItem 类似,Connection 实现了 ISelectable 接口。如果一个 Connection 实例被选中,您将在连接路径的每个末端看到两个矩形。它们属于一个 ConnectionAdorner 类型的装饰器,当 Connection 实例被选中时,该装饰器会自动显示。注意: ConnectorAdorner 属于 Connector ,而 ConnectionAdorner 属于 Connection 。 |
![]() |
这两个矩形中的每一个都代表一个 Thumb 控件,它们是 ConnectionAdorner 实例的一部分,允许您修改现有的连接。 |
![]() |
例如,如果您将连接的目标 thumb 拖动到另一个连接器并将其释放到那里,您可以重新连接现有连接。 注意: ConnectorAdorner 和 ConnectionAdorner 在功能上是相似的,但在如何使用 Adorner 类方面有所不同。 |
![]() |
连接如何“粘合”到项上?
连接器的默认布局定义在 ConnectorDecoratorTemplate
中,这是 DesignerItem
模板的一部分
<ControlTemplate x:Key="ConnectorDecoratorTemplate" TargetType="{x:Type Control}">
<Grid Margin="-5">
<s:Connector Orientation="Left" VerticalAlignment="Center"
HorizontalAlignment="Left"/>
<s:Connector Orientation="Top" VerticalAlignment="Top"
HorizontalAlignment="Center"/>
<s:Connector Orientation="Right" VerticalAlignment="Center"
HorizontalAlignment="Right"/>
<s:Connector Orientation="Bottom" VerticalAlignment="Bottom"
HorizontalAlignment="Center"/>
</Grid>
</ControlTemplate>
Connector
类有一个 Position
属性,指定连接器中心点相对于设计器画布的相对位置。由于 Connector
类实现了 INotifyPropertyChanged
接口,因此它可以通知客户端属性值已更改。现在,当设计器项更改其位置或大小后,连接器的 LayoutUpdated
事件将作为 WPF 布局过程的一部分自动触发。这时 Position
属性会更新,并自身触发一个事件来通知客户端。
public class Connector : Control, INotifyPropertyChanged
{
private Point position;
public Point Position
{
get { return position; }
set
{
if (position != value)
{
position = value;
OnPropertyChanged("Position");
}
}
}
public Connector()
{
// fired when layout changes
base.LayoutUpdated += new EventHandler(Connector_LayoutUpdated);
}
void Connector_LayoutUpdated(object sender, EventArgs e)
{
DesignerCanvas designer = GetDesignerCanvas(this);
if (designer != null)
{
//get center position of this Connector relative to the DesignerCanvas
this.Position = this.TransformToAncestor(designer).Transform
(new Point(this.Width / 2, this.Height / 2));
}
}
...
}
现在我们切换到 Connection
类。Connection
类有一个 Source
和一个 Sink
属性,两者都是 Connector
类型。当源连接器或目标连接器被设置时,我们立即注册一个事件处理程序来监听连接器的 PropertyChanged
事件。
public class Connection : Control, ISelectable, INotifyPropertyChanged
{
private Connector source;
public Connector Source
{
get
{
return source;
}
set
{
if (source != value)
{
if (source != null)
{
source.PropertyChanged -=
new PropertyChangedEventHandler(OnConnectorPositionChanged);
source.Connections.Remove(this);
}
source = value;
if (source != null)
{
source.Connections.Add(this);
source.PropertyChanged +=
new PropertyChangedEventHandler(OnConnectorPositionChanged);
}
UpdatePathGeometry();
}
}
}
void OnConnectorPositionChanged(object sender, PropertyChangedEventArgs e)
{
if (e.PropertyName.Equals("Position"))
{
UpdatePathGeometry();
}
}
....
}
此代码片段仅显示源连接器,但目标连接器的工作方式类似。事件处理程序最终会更新连接路径的几何形状,就是这样。
自定义连接器布局
默认布局和连接器数量可能并不总是满足您的需求。以一个三角形路径的例子为例,它具有自定义的 DragThumbTemplate
(请参阅上一篇文章,了解如何自定义 DragThumbTemplate
)。
<Path IsHitTestVisible="False"
Fill="Orange"
Stretch="Fill"
Data="M 0,10 5,0 10,10 Z">
<s:DesignerItem.DragThumbTemplate>
<ControlTemplate>
<Path Fill="Transparent" Stretch="Fill"
Data="M 0,10 5,0 10,10 Z"/>
</ControlTemplate>
</s:DesignerItem.DragThumbTemplate>
</Path>

这里的问题是连接器仅在鼠标悬停在项上时可见。如果您尝试触及左侧或右侧的连接器,可能会遇到一些问题。但解决方案以名为 DesignerItem.ConnectorDecoratorTemplate
的附加属性的形式出现,它允许您为连接器装饰器定义自定义模板。用法最好通过示例来解释
<Path IsHitTestVisible="False"
Fill="Orange"
Stretch="Fill"
Data="M 0,10 5,0 10,10 Z">
<!-- Custom DragThumb Template -->
<s:DesignerItem.DragThumbTemplate>
<ControlTemplate>
<Path Fill="Transparent" Stretch="Fill"
Data="M 0,10 5,0 10,10 Z"/>
</ControlTemplate>
<s:DesignerItem.DragThumbTemplate>
<!-- Custom ConnectorDecorator Template -->
<s:DesignerItem.ConnectorDecoratorTemplate>
<ControlTemplate>
<Grid Margin="0">
<s:Connector Orientation="Top" HorizontalAlignment="Center"
VerticalAlignment="Top" />
<s:Connector Orientation="Bottom" HorizontalAlignment="Center"
VerticalAlignment="Bottom" />
<UniformGrid Columns="2">
<s:Connector Grid.Column="0" Orientation="Left" />
<s:Connector Grid.Column="1" Orientation="Right"/>
</UniformGrid>
</Grid>
</ControlTemplate>
</s:DesignerItem.ConnectorDecoratorTemplate>
</Path>

此解决方案提供了更好的结果,但仍然需要一些棘手的布局,这可能不总是可行的。为此,我提供了一个 RelativePositionPanel
,它允许您相对于面板边界定位项。下面的示例通过设置 RelativePosition
属性(一个附加属性)来定位 RelativePositionPanel
上的三个按钮。
<c:RelativePositionPanel>
<Button Content="TopLeft" c:RelativePositionPanel.RelativePosition="0,0"/>
<Button Content="Center" c:RelativePositionPanel.RelativePosition="0.5,0.5"/>
<Button Content="BottomRight" c:RelativePositionPanel.RelativePosition="1,1"/>
</ControlTemplate>
当需要排列连接器时,这个面板会非常有用
<Path IsHitTestVisible="False"
Fill="Orange"
Stretch="Fill"
Data="M 9,2 11,7 17,7 12,10 14,15 9,12 4,15 6,10 1,7 7,7 Z">
<!-- Custom DragThumb Template -->
<s:DesignerItem.DragThumbTemplate>
<ControlTemplate>
<Path Fill="Transparent" Stretch="Fill"
Data="M 9,2 11,7 17,7 12,10 14,15 9,12 4,15 6,10 1,7 7,7 Z"/>
</ControlTemplate>
</s:DesignerItem.DragThumbTemplate>
<!-- Custom ConnectorDecorator Template -->
<s:DesignerItem.ConnectorDecoratorTemplate>
<ControlTemplate>
<c:RelativePositionPanel Margin="-4">
<s:Connector Orientation="Top"
c:RelativePositionPanel.RelativePosition="0.5,0"/>
<s:Connector Orientation="Left"
c:RelativePositionPanel.RelativePosition="0,0.385"/>
<s:Connector Orientation="Right"
c:RelativePositionPanel.RelativePosition="1,0.385"/>
<s:Connector Orientation="Bottom"
c:RelativePositionPanel.RelativePosition="0.185,1"/>
<s:Connector Orientation="Bottom"
c:RelativePositionPanel.RelativePosition="0.815,1"/>
</c:RelativePositionPanel>
</ControlTemplate>
</s:DesignerItem.ConnectorDecoratorTemplate>
</Path>

Outlook(展望)
在下一篇文章中,我将重点介绍命令
- 剪切、复制、粘贴
- 组合项
- 对齐项
- Z 顺序(置于顶层、置于底层等)
历史
- 2008 年 2 月 24 日 -- 提交原始版本