65.9K
CodeProject 正在变化。 阅读更多。
Home

WPF Diagram Designer: Part 1

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.97/5 (296投票s)

2008年1月16日

CPOL

7分钟阅读

viewsIcon

997481

downloadIcon

62872

在画布上拖动、调整大小和旋转元素

引言

在本文中,我将向您展示如何在一个画布上移动、调整大小和旋转任何类型的对象。为此,我将提供两种不同的解决方案——第一种不使用 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> 

在这里,您可以看到 MoveThumbDataContext 属性绑定到模板化的父级,即我们的 DesignerItem。请注意,我们添加了一个 Grid 作为模板的布局面板,这使得 ContentPresenterMoveThumb 都能占据 DesignerItem 的全部实际空间。现在我们可以编译并运行代码了。

Default visual representation of a Thumb control in WPF

结果是,我们在灰色的 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 属性,我们实现了看起来像真正的调整大小装饰器的布局。

A resize decorator build with 8 Thumbs

很神奇,不是吗?但到目前为止,它只是一个假象,因为没有任何事件处理程序会处理 ThumbDragDelta 事件。为此,我们将 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 的宽度、高度和/或位置。现在,通过添加一个具有 ResizeDecoratorTemplateControl 对象,我们将调整大小装饰器集成到 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 的控件模板中。与调整大小装饰器一起,它看起来像这样

A rotate decorator build with 4 Thumbs

RotateThumbRotateDecoratorTemplate 的代码结构与我们在前一章看到的内容非常相似,因此我将不再在此列出代码。

注意:我最初尝试拖动、调整大小和旋转项目的方法是使用 WPF 的 TranslateTransformScaleTransformRotateTransform。但事实证明这是错误的做法,因为 WPF 中的 Transforms 实际上并没有改变对象的属性,例如宽度或高度,WPF Transforms 仅仅是渲染问题。所以,我没有使用 TranslateTransformScaleTransform 来拖动和调整项目大小,但由于没有替代方案,我不得不使用 RotateTransform

DesignerItem 样式

为了方便起见,我们将 DesignerItem 的控件模板包装到一个样式中,我们在其中还设置了各种属性,例如 MinWidthMinHeightRenderTransformOrigin。触发器允许我们在项目被选中时才显示调整大小和旋转装饰器,这由附加属性 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 控件有一个默认样式,该样式排列 ResizeThumbRotateThumb 对象,方式与我们在上一章中看到的相似,因此我将不再在此重复该代码。

自定义 Adorners

当然,您可以向 DesignerItem 添加自己的自定义 adorners。例如,我添加了一个 adorner,可以在调整对象大小时显示宽度和高度。有关更多详细信息,请参阅附带的代码。如果您有任何疑问,请随时提问。

历史

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