WPF Diagram Designer: Part 1
在画布上拖动、调整大小和旋转元素

引言
在本文中,我将向您展示如何在一个画布上移动、调整大小和旋转任何类型的对象。为此,我将提供两种不同的解决方案——第一种不使用 WPF Adorners,然后一种使用 WPF Adorners。
关于代码
附带的 Visual Studio Express 2008 解决方案包含三个项目

MoveResize
:此版本展示了如何在不使用 WPF Adorners 的情况下移动和调整对象大小。
MoveResizeRotate
:此外,此项目还提供了对象旋转功能,同样不使用 WPF Adorners。旋转会带来一些需要考虑的细微副作用,这些副作用会在您将此项目与前一个项目进行比较时显现出来。
MoveResizeRotateWithAdorners
:第三个项目最终展示了如何借助 WPF Adorners 来移动、调整大小和旋转项目。它还提供了一个示例,说明 Adorners 如何用于在调整大小时提供视觉反馈,以指示对象的实际大小。

准备工作
我们从一个简单的图形开始
<Canvas>
<Ellipse Fill="Blue"
Width="100"
Height="100"
Canvas.Top="100"
Canvas.Left="100"/>
</Canvas>
您可能不会对这个图形印象深刻,但它是一个很好的起点。它易于理解,并且具有一个基本图形所需的一切:一个带有形状的绘图画布。但您是对的,这个图形并没有什么实际用处——它太静态了。
因此,让我们通过将椭圆包装到 ContentControl
中来开始一些准备工作
<Canvas>
<ContentControl Width="100"
Height="100"
Canvas.Top="100"
Canvas.Left="100">
<Ellipse Fill="Blue"/>
</ContentControl>
</Canvas>
您可能会说,这并没有好多少,我们仍然无法移动椭圆,那它有什么用呢?嗯,ContentControl
用作我们要放在画布上的对象的容器,而实际上是我们即将移动、调整大小和旋转的这个 ContentControl
!而且,由于 ContentControl
的内容可以是任何类型,因此我们将能够移动、调整大小和旋转画布上的任何类型的对象!
注意:由于其关键作用,此 ContentControl
也被称为 DesignerItem
。
我们通过为 DesignerItem
分配 ControlTemplate
来完成准备工作。这引入了更高级别的抽象,因此从现在开始,我们将只展开此模板,而完全不触及 DesignerItem
及其内容。
<Canvas>
<Canvas.Resources>
<ControlTemplate x:Key="DesignerItemTemplate" TargetType="ContentControl">
<ContentPresenter Content="{TemplateBinding ContentControl.Content}"/>
</ControlTemplate>
</Canvas.Resources>
<ContentControl Name="DesignerItem"
Width="100"
Height="100"
Canvas.Top="100"
Canvas.Left="100"
Template="{StaticResource DesignerItemTemplate}">
<Ellipse Fill="Blue"/>
</ContentControl>
</Canvas>
既然我们已经完成了准备工作,我们就可以开始在画布上进行一些活动了。
移动
WPF 中有一个控件,MSDN 文档将其描述为:“...代表一个允许用户拖动和调整控件大小的控件。” 这似乎是我们的完美选择。它是 Thumb
控件,我们将在下面展示如何使用它
public class MoveThumb : Thumb
{
public MoveThumb()
{
DragDelta += new DragDeltaEventHandler(this.MoveThumb_DragDelta);
}
private void MoveThumb_DragDelta(object sender, DragDeltaEventArgs e)
{
Control item = this.DataContext as Control;
if (item != null)
{
double left = Canvas.GetLeft(item);
double top = Canvas.GetTop(item);
Canvas.SetLeft(item, left + e.HorizontalChange);
Canvas.SetTop(item, top + e.VerticalChange);
}
}
}
MoveThumb
继承自 Thumb
,并提供了 DragDelta
事件处理程序的实现。在事件处理程序中,首先将 DataContext
转换为 ContentControl
,然后根据水平和垂直拖动变化更新其位置。您可能已经猜到,从 DataContext
中检索到的控件就是我们的 DesignerItem
,但它从何而来?如果您查看更新后的 DesignerItem
的模板,您就能找到答案。
<ControlTemplate x:Key="DesignerItemControlTemplate" TargetType="ContentControl">
<Grid>
<s:DragThumb DataContext="{Binding RelativeSource={RelativeSource TemplatedParent}}"
Cursor="SizeAll"/>
<ContentPresenter Content="{TemplateBinding ContentControl.Content}"/>
</Grid>
</ControlTemplate>
在这里,您可以看到 MoveThumb
的 DataContext
属性绑定到模板化的父级,即我们的 DesignerItem
。请注意,我们添加了一个 Grid
作为模板的布局面板,这使得 ContentPresenter
和 MoveThumb
都能占据 DesignerItem
的全部实际空间。现在我们可以编译并运行代码了。

结果是,我们在灰色的 MoveThumb
上方看到一个蓝色的椭圆。如果您玩弄它,您会注意到实际上可以抓住并拖动对象,但仅限于灰色 MoveThumb
可见的地方。这是因为椭圆阻碍了鼠标事件到达 MoveThumb
。我们可以通过将椭圆的 IsHitTest
属性设置为 false
来轻松更改此行为。
<Ellipse Fill="Blue" IsHitTestVisible="False"/>
MoveThumb
从基类 Thumb
继承了样式,这在我们这里并不理想。为此,我们创建了一个仅包含透明矩形的新模板。一个更通用的解决方案是为 MoveThumb
类创建一个默认样式,但目前自定义模板就足够了。
现在 DesignerItem
的控件模板看起来像这样
<ControlTemplate x:Key="MoveThumbTemplate" TargetType="{x:Type s:MoveThumb}">
<Rectangle Fill="Transparent"/>
</ControlTemplate>
<ControlTemplate x:Key="DesignerItemTemplate" TargetType="Control">
<Grid>
<s:MoveThumb Template="{StaticResource MoveThumbTemplate}"
DataContext="{Binding RelativeSource={RelativeSource TemplatedParent}}"
Cursor="SizeAll"/>
<ContentPresenter Content="{TemplateBinding ContentControl.Content}"/>
</Grid>
</ControlTemplate>
这就是在画布上移动项目所需的一切,现在我将向您展示如何调整对象的大小。
调整大小
您还记得 MSDN 文档曾承诺 Thumb
控件允许用户拖动和调整控件大小吗?因此,我们坚持使用 Thumb
控件,并构建了一个名为 ResizeDecoratorTeamplate
的控件模板。
<ControlTemplate x:Key="ResizeDecoratorTemplate" TargetType="Control">
<Grid>
<Thumb Height="3" Cursor="SizeNS" Margin="0 -4 0 0"
VerticalAlignment="Top" HorizontalAlignment="Stretch"/>
<Thumb Width="3" Cursor="SizeWE" Margin="-4 0 0 0"
VerticalAlignment="Stretch" HorizontalAlignment="Left"/>
<Thumb Width="3" Cursor="SizeWE" Margin="0 0 -4 0"
VerticalAlignment="Stretch" HorizontalAlignment="Right"/>
<Thumb Height="3" Cursor="SizeNS" Margin="0 0 0 -4"
VerticalAlignment="Bottom" HorizontalAlignment="Stretch"/>
<Thumb Width="7" Height="7" Cursor="SizeNWSE" Margin="-6 -6 0 0"
VerticalAlignment="Top" HorizontalAlignment="Left"/>
<Thumb Width="7" Height="7" Cursor="SizeNESW" Margin="0 -6 -6 0"
VerticalAlignment="Top" HorizontalAlignment="Right"/>
<Thumb Width="7" Height="7" Cursor="SizeNESW" Margin="-6 0 0 -6"
VerticalAlignment="Bottom" HorizontalAlignment="Left"/>
<Thumb Width="7" Height="7" Cursor="SizeNWSE" Margin="0 0 -6 -6"
VerticalAlignment="Bottom" HorizontalAlignment="Right"/>
</Grid>
</ControlTemplate>
这里您看到一个控件模板,它由一个填充了 8 个 Thumb
控件的网格组成,这些控件应该作为调整大小的句柄。通过像上面那样设置 Thumb
属性,我们实现了看起来像真正的调整大小装饰器的布局。

很神奇,不是吗?但到目前为止,它只是一个假象,因为没有任何事件处理程序会处理 Thumb
的 DragDelta
事件。为此,我们将 Thumb
对象替换为 ResizeThumb
。
public class ResizeThumb : Thumb
{
public ResizeThumb()
{
DragDelta += new DragDeltaEventHandler(this.ResizeThumb_DragDelta);
}
private void ResizeThumb_DragDelta(object sender, DragDeltaEventArgs e)
{
Control item = this.DataContext as Control;
if (item != null)
{
double deltaVertical, deltaHorizontal;
switch (VerticalAlignment)
{
case VerticalAlignment.Bottom:
deltaVertical = Math.Min(-e.VerticalChange,
item.ActualHeight - item.MinHeight);
item.Height -= deltaVertical;
break;
case VerticalAlignment.Top:
deltaVertical = Math.Min(e.VerticalChange,
item.ActualHeight - item.MinHeight);
Canvas.SetTop(item, Canvas.GetTop(item) + deltaVertical);
item.Height -= deltaVertical;
break;
default:
break;
}
switch (HorizontalAlignment)
{
case HorizontalAlignment.Left:
deltaHorizontal = Math.Min(e.HorizontalChange,
item.ActualWidth - item.MinWidth);
Canvas.SetLeft(item, Canvas.GetLeft(item) + deltaHorizontal);
item.Width -= deltaHorizontal;
break;
case HorizontalAlignment.Right:
deltaHorizontal = Math.Min(-e.HorizontalChange,
item.ActualWidth - item.MinWidth);
item.Width -= deltaHorizontal;
break;
default:
break;
}
}
e.Handled = true;
}
}
ResizeThumb
仅根据 ResizeThumb
的垂直和水平对齐方式更新 DesignerItem
的宽度、高度和/或位置。现在,通过添加一个具有 ResizeDecoratorTemplate
的 Control
对象,我们将调整大小装饰器集成到 DesignerItem
的控件模板中。
<ControlTemplate x:Key="DesignerItemTemplate" TargetType="ContentControl">
<Grid DataContext="{Binding RelativeSource={RelativeSource TemplatedParent}}">
<s:MoveThumb Template="{StaticResource MoveThumbTemplate}" Cursor="SizeAll"/>
<Control Template="{StaticResource ResizeDecoratorTemplate}"/>
<ContentPresenter Content="{TemplateBinding ContentControl.Content}"/>
</Grid>
</ControlTemplate>
太棒了,现在我们可以移动和调整对象大小了。接下来是对象的旋转。
旋转
为了实现对象的旋转,我们遵循与前一章相同的解决方案路径,但这次我们创建一个 RotateThumb
并将其四个实例排列在一个名为 RotateDecoratorTemplate
的控件模板中。与调整大小装饰器一起,它看起来像这样

RotateThumb
和 RotateDecoratorTemplate
的代码结构与我们在前一章看到的内容非常相似,因此我将不再在此列出代码。
注意:我最初尝试拖动、调整大小和旋转项目的方法是使用 WPF 的 TranslateTransform
、ScaleTransform
和 RotateTransform
。但事实证明这是错误的做法,因为 WPF 中的 Transforms 实际上并没有改变对象的属性,例如宽度或高度,WPF Transforms 仅仅是渲染问题。所以,我没有使用 TranslateTransform
和 ScaleTransform
来拖动和调整项目大小,但由于没有替代方案,我不得不使用 RotateTransform
。
DesignerItem 样式
为了方便起见,我们将 DesignerItem
的控件模板包装到一个样式中,我们在其中还设置了各种属性,例如 MinWidth
、MinHeight
和 RenderTransformOrigin
。触发器允许我们在项目被选中时才显示调整大小和旋转装饰器,这由附加属性 Selector.IsSelected
指示。
注意:WPF 提供了一个名为 Selector
的类,它是一个允许用户从其子元素中选择项目的控件。我在这篇文章中没有使用这个控件,但我使用了附加的 Selector.IsSelected
属性来模拟选择。
<Style x:Key="DesignerItemStyle" TargetType="ContentControl">
<Setter Property="MinHeight" Value="50"/>
<Setter Property="MinWidth" Value="50"/>
<Setter Property="RenderTransformOrigin" Value="0.5,0.5"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="ContentControl">
<Grid DataContext="{Binding RelativeSource={RelativeSource TemplatedParent}}">
<Control x:Name="RotateDecorator"
Template="{StaticResource RotateDecoratorTemplate}"
Visibility="Collapsed"/>
<s:MoveThumb Template="{StaticResource MoveThumbTemplate}"
Cursor="SizeAll"/>
<Control x:Name="ResizeDecorator"
Template="{StaticResource ResizeDecoratorTemplate}"
Visibility="Collapsed"/>
<ContentPresenter Content="{TemplateBinding ContentControl.Content}"/>
</Grid>
<ControlTemplate.Triggers>
<Trigger Property="Selector.IsSelected" Value="True">
<Setter TargetName="ResizeDecorator"
Property="Visibility" Value="Visible"/>
<Setter TargetName="RotateDecorator"
Property="Visibility" Value="Visible"/>
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
就是这样。现在我们可以移动、调整对象大小和旋转对象了。我们必须认识到,只需几行 XAML 代码和三个类就可以提供我们所需的一切!最棒的是,我们无需触及对象本身:所有行为都完全封装在 ControlTemplate
中。
基于 Adorner 的解决方案
在本章中,我将介绍一种将调整大小和旋转装饰器提升到 AdornerLayer
,从而使它们渲染在所有其他项目之上的解决方案。

基于 Adorner 的解决方案最好通过展示结果的 DesignerItem
控件模板来解释。
<ControlTemplate x:Key="DesignerItemTemplate" TargetType="ContentControl">
<Grid DataContext="{Binding RelativeSource={RelativeSource TemplatedParent}}">
<s:MoveThumb Template="{StaticResource MoveThumbTemplate}" Cursor="SizeAll"/>
<ContentPresenter Content="{TemplateBinding ContentControl.Content}"/>
<s:DesignerItemDecorator x:Name="decorator" ShowDecorator="true"/>
</Grid>
<ControlTemplate.Triggers>
<Trigger Property="Selector.IsSelected" Value="True">
<Setter TargetName="decorator" Property="ShowDecorator" Value="true"/>
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
此模板与上一章的内容相似,只是将调整大小和旋转装饰器替换为一个名为 DesignerItemDecorator
的新类的实例。此类派生自 Control
,没有自己的默认样式,而是提供一个在布尔值 ShowAdorner
属性为 true
时可见的 adorner。
public class DesignerItemDecorator : Control
{
private Adorner adorner;
public bool ShowDecorator
{
get { return (bool)GetValue(ShowDecoratorProperty); }
set { SetValue(ShowDecoratorProperty, value); }
}
public static readonly DependencyProperty ShowDecoratorProperty =
DependencyProperty.Register
("ShowDecorator", typeof(bool), typeof(DesignerItemDecorator),
new FrameworkPropertyMetadata
(false, new PropertyChangedCallback(ShowDecoratorProperty_Changed)));
private void HideAdorner()
{
...
}
private void ShowAdorner()
{
...
}
private static void ShowDecoratorProperty_Changed
(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
DesignerItemDecorator decorator = (DesignerItemDecorator)d;
bool showDecorator = (bool)e.NewValue;
if (showDecorator)
{
decorator.ShowAdorner();
}
else
{
decorator.HideAdorner();
}
}
}
当 DesignerItem
被选中时可见的 adorner 是 DesignerItemAdorner
类型,并派生自 Adorner
。
public class DesignerItemAdorner : Adorner
{
private VisualCollection visuals;
private DesignerItemAdornerChrome chrome;
protected override int VisualChildrenCount
{
get
{
return this.visuals.Count;
}
}
public DesignerItemAdorner(ContentControl designerItem)
: base(designerItem)
{
this.chrome = new DesignerItemAdornerChrome();
this.chrome.DataContext = designerItem;
this.visuals = new VisualCollection(this);
}
protected override Size ArrangeOverride(Size arrangeBounds)
{
this.chrome.Arrange(new Rect(arrangeBounds));
return arrangeBounds;
}
protected override Visual GetVisualChild(int index)
{
return this.visuals[index];
}
}
您可以看到,此 adorner 有一个类型为 DesignerItemAdornerChrome
的单一视觉子项,该子项实际上是提供用于调整大小和旋转项目的拖动句柄的控件。此 chrome 控件有一个默认样式,该样式排列 ResizeThumb
和 RotateThumb
对象,方式与我们在上一章中看到的相似,因此我将不再在此重复该代码。
自定义 Adorners
当然,您可以向 DesignerItem
添加自己的自定义 adorners。例如,我添加了一个 adorner,可以在调整对象大小时显示宽度和高度。有关更多详细信息,请参阅附带的代码。如果您有任何疑问,请随时提问。

历史
- 2008 年 1 月 10 日 -- 原始版本
- 2008 年 1 月 18 日 -- 更新:引入
ContentControl
作为设计器项 - 2008 年 2 月 5 日 -- 更新:添加了项目旋转功能
- 2008 年 8 月 22 日 -- 更新:添加了基于 Adorner 的解决方案