【翻译】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的实际内容。现在,可以编译并运行我们的代码了。

Default visual representation of a Thumb control in WPF

运行结果中,蓝色椭圆放置在灰色的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的个属性赋值,我们可以取得一个如图所示的调整大小装饰框:

A resize decorator build with 8 Thumbs

很神奇吧。但是到目前为止,它还是个假的手柄。因为这些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。集成到一起的装饰框开起来如下图所示:

A rotate decorator build with 4 Thumbs

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)许可。

posted @ 2014-01-29 21:31  Sangplus  阅读(1193)  评论(0)    收藏  举报