【翻译】WPF Diagram Designer: Part 1
原文地址
WPF Diagram Designer: Part 1
sukram于2008年8月23日

介绍
文中,我将想你展示如何在canvas中移动、调整大小和旋转任意类型对象的方法。这里提供两种不同的方案,分别是不使用WPF Adorners技术和使用该技术。
关于代码
下图为VS Express2008工程中的三个项目:

MoveResize: 该版本展示不使用WPF Adorners进行移动和调整大小的技术。
MoveResizeRotate: 这一版本展示不使用WPF Adorners进行对象旋转的技术。被旋转的对象在移动和调整大小的时候,要考虑一些的边际效应 。对比该项目与上一个项目,你会发现边际效应的差别在哪里。
MoveResizeRotateWithAdorners: 第三个项目最终向读者们展示一个,在采用WPF Adorners技术帮助时,如何做到一个移动、调整大小、旋转对象的方案。该项目也展示了在调整大小操作时,如何通过Adorners,提供一个对象实际大小的视觉反馈。

准备工作
从一个简单的diagram(图形)开始:
<Canvas> <Ellipse Fill="Blue" Width="100" Height="100" Canvas.Top="100" Canvas.Left="100"/> </Canvas>
如果你对于diagram还没什么印象,那么,这是一个好的开始。其实很好理解,最基本的diagram是一个绘有图形的画布。到目前为止,这个diagram还是没什么用,因为它是静态的。
那么,我们接着做。先用一个ContentControl把这个椭圆包装一下:
<Canvas> <ContentControl Width="100" Height="100" Canvas.Top="100" Canvas.Left="100"> <Ellipse Fill="Blue"/> </ContentControl> </Canvas>
看起来没啥进展,我们还不能移动这个椭圆。其实,ContentControl作为放置在Canvas中其它对象的容器,我们可以对它进行移动、调整大小以及旋转!因为ContentControl中的内容可以是任意类型,正基于此,我们就可以操纵canvas中的任意类型的对象。
注意:因为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>
到此,我们已经完成了我们的准备工作,接下来我们会让canvas中的对象活动起来。
移动
MSDN上介绍了一种控件,称其“代表一种可以让用拖拽和调整大小的控件”,这种控件就是Thumb控件。从MSDN描述来看,我们正好可以用这种控件来实现我们的目的:
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类继承自Thrumb,它仅仅包含一个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,就可以在整个MoveThumb区域拖拽它了。
<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>
现在我们完成了canvas上移动一个对象的所有工作,接下来将介绍如何调整对象的大小。
调整大小
还记得MSDN上说,Thumb可以用来拖拽和调整控件的大小码?我们坚持用Thumb控件,并构建一个新的叫ResizeDecoratorTemplate模板来完成调整大小的功能:
<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的Grid构成。我们可以用它来作为调整大小的手柄。按上面的代码对Thumb的个属性赋值,我们可以取得一个如图所示的调整大小装饰框:

很神奇吧。但是到目前为止,它还是个假的手柄。因为这些Thumb并没添加事件处理方法。下面,我们用下面定义的ResizeThumb来代替上面xaml中的的那些Thumb:
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仅仅依靠它的水平和竖直对其方式来更新DesignerItem的宽度、高度和位置。现在,把ResizeDecoratorTemplate调整大小装饰框集成到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中防止四个这样的Thumb。集成到一起的装饰框开起来如下图所示:

RotateThumb以及RotateDecoratorTemplate的代码结构与前几张介绍非常相似,这里就不在详细列出了。唯一需要注意的,旋转时我们采用的RotateTransform会影响上面提到的平移和调整大小方案的代码,所以会连带的修改他们的时机计算代码,大家可以从项目的实际源代码中看到差别。
注意: 在我的第一个拖拽、调整大小和旋转的方案中,我采用WPF的TranslateTransform、ScaleTransform和RotateTransform。实际上这是不对的,因为WPF中,Transform并不真的改变对象的宽和高,他只是在渲染的时候起作用。所以,在上述方案中的平移和调整大小功能,并没有采用TranslateTransform和ScaleTransform。但是旋转功能,则不得不采用RotateTransform,因为没有别的方法。
DesignerItem的式样
为了方便,我们把DesignerItem的控件模板封装在式样当中。这样式样中,我们就可以设置控件的各种属性,如最小宽高、旋转变换原点等。式样中,我们还添加了一个触发器,以便在对象被选中时,装饰框才出现。这个触发器用到了附加属性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代码,就可以把三个所需的功能集成到一起。最重要的是,我们并不需要改动对象本身,就可以完成所有这些功能。所有的这些对象行为完全被封装在一个控件模板中。
基于Adorner技术的方案
在这一小节中,将讲述在Adorner层中提供调整宽高旋转装饰框的一个方案。这一层是渲染在所有其它对象之上的。

基于Adorner的方案用下述的控件模板很容易解释:
<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类,并且不附带默认式样。这个类就会提供一个adorner对象,当该类的依赖属性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对象。原理跟前几章讲的十分接近,这里不再重复。
自定义Adorner
你也可以添加一些其它的自定义adorner对象。作为例子,这里额外提供一个adorner对象,在调整大小的时候,用于显示对象的宽和高,如下图所示。具体细节请参看附件中的源代码。如果有任何问题,请直接提问。

版本历史
- 2008年1月10日 -- 原始版本
- 2008年1月18日 -- 更新:引入ContentControl代替designer item(设计器对象)
- 2008年2月5日 -- 更新: 添加旋转项目
- 2008年8月22日 -- 更新: 添加基于adorner的解决方案
许可
本文以及任何附加的代码和文件,都基于CPOL(The Code Project Open License)许可。


浙公网安备 33010602011771号