WPF中的图表设计器 – 1

[原文地址] http://www.codeproject.com/KB/WPF/WPFDiagramDesigner_Part1.aspx
[原文作者] sukram

[译者]WizRay

介绍

在这篇文章中,我将介绍在Canvas中移动、缩放或者旋转任何类型的对象。为此,我将提供两个不同的解决方案——一个使用了WPF中Adoner的方案和一个不使用的。

关于代码

附件中的Visual Studio 2008 工程由三个项目组成:

ScreenShot02

MoveResize: 这个版本解释了如何在不使用WPF中Adorner的情况下移动、缩放对象;

MoveResizeRotate: 另外,这个项目解释了如何在不使用WPF中Adorner的情况下旋转对象。当我们移动、缩放对象时,旋转可能会出现一些小的副作用。我们能够通过比较这个项目和前一个项目来简单的跟踪这些副作用;

MoveResizeRotateWithAdorners: 第三个项目最终解释了如何使用WPF中的Adorner来对WPF中的对象进行移动、缩放、旋转。而且提供了一个示例,解释了如何在缩放操作时使用Adorner来提供对象真实尺寸的可视化回馈。

ScreenShot03

 

准备工作

我们从一个简单的图表开始:

<Canvas>
    <Ellipse Fill="Blue" Width="100" Height="100" Canvas.Top="100" Canvas.Left="100" />
</Canvas>

你可能觉得这个图表很不起眼,但不论怎样,这就是我们的开始。它很好理解,而且具有一个图表所需的所有要素:一个用于绘制图形的Canvas。不过你也许是对的,这个图表没什么大用——他只是静态的。

所以让我们做一些准备工作,把这个圆形保存到ContentControl中:

<Canvas>
    <ContentControl Width="100" Height="100" Canvas.Top="100" Canvas.Left="100">
        <Ellipse Fill="Blue" />
    </ContentControl>
</Canvas>

你或许说这也没比刚才好多少,我们仍然不能移动这个圆形,那它比好在哪儿呢?OK,这个ContentControl提供了一个我们放置在Canvas中的对象的容器,事实上,这个ContentControl就是我们要移动、缩放和旋转的对象。由于ContentControl的Content可以使任何类型的对象,所以我们将能够移动、缩放和旋转在Canvas中的任何对象!

注意:由于ContentControl的关键作用,我们把他称作DesignerItem。

我们现在为DesignerItem声明一个Control Template。这实际上提供了进一步的抽象,这样,从这儿开始,我们在完全不顾及ContentControl的内容情况下对此进行扩展。

<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元素是这样解释的:“…represents a control that lets the user drag and resize controls.”。这看上去正好是我们需要的元素来做移动,下面我们将使用Thumb来完成我们的功能:

public class MoveThumb : Thumb
{
    public MoveThumb()
    {
        DragDelta += MoveThumb_DragDelta;
    }

    private void MoveThumb_DragDelta(object sender, DragDeltaEventArgs e)
    {
        Control item = 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的事件响应。通过相应DragDelta事件,将DataContext转换为ContentControl类型,而后根据Thumb在水平和垂直方向上拖动的距离更新DataContext的位置。你可能已经猜到了,DataContext中的元素就是刚才的DesignerItem,但是他是从哪儿来的呢?我们通过更新DesignerItme的模板来处理:

<ControlTemplate x:Key="DesignerItemControlTemplate" TargetType="ContentControl">
    <Grid>
        <s:MoveThumbDataContext="{Binding RelativeSource={RelativeSource TemplatedParent}}"
                Cursor="SizeAll" />
        <ContentPresenter Content="{TemplateBinding ContentControl.Content}" />
    </Grid>
</ControlTemplate>

从这儿我们看到,MoveThumb的DataContext属性绑定到了父级模板对象,也就是DesignerItem。注意我们在这儿加入了一个Grid作为面板来布局,它能够将ContentPresenter和MoveThumb显示在同样的位置,具有同样的尺寸。现在,我们编译、运行这个程序:

ScreenShot04

运行结果中,我们得到了一个灰色的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>

到此,我们已经能够在Canvas中移动对象了,下面,我们来让它支持缩放。

缩放

我们记得,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填满的Grid元素组成,这就是我们用来支持缩放的控件。通过设定Thumb的各种属性,我们实现了一个类似于真正的缩放修饰框的布局。

ScreenShot05

这很令人兴奋,但是现在它只不过是个表象,因为还没有在上面添加任何关于DragDelta事件的处理方法。于是,我们使用ResizeThumb来代替Thumb元素。

public class ResizeThumb : Thumb
{
    public ResizeThumb()
    {
        DragDelta += ResizeThumb_DragDelta;
    }

    private void ResizeThumb_DragDelta(object sender, DragDeltaEventArgs e)
    {
        Control item = 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模板的控件,我们把刚刚完成的拖动和缩放两部分集成到一起。

<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>

很好,现在,我们能够对对象进行拖动和缩放了。下面我们让他转起来。

旋转

为了能够让Canvas中的元素能够旋转,我们可以遵循前两张阐述的方法完成,但是这次,我们创建一个名为RotateThumb的继承自Thumb的类,并且在名为RotateDecoratorTemplate的模板中加入了4个RotateThumb来实现。这些和缩放的修饰框集成到一起时,就象这样:

ScreenShot06

RotateThumb和RotateDecoratorTemplate的代码与之前我们使用的代码非常相像,所以在此,我不列出详细代码。

注意:我最早的解决方案中,使用了WPF中的TranslateTransform,ScaleTransform和RotateTransform。而后发现这是错误的,因为在WPF中,Transform没有改变对象的真实属性(宽度、高度、位置等),Transform只是一种渲染时的修改。所以我在实现拖动、放缩时,没有使用TranslateTransform和ScaleTransform,但是使用RotateTransform实现了旋转功能,因为在WPF中没有其他手段来实现对所有元素的旋转。

DesignerItem的样式

为了方便起见,我们将DesignerItem的模板包装到一个Style中,同时我们也能设定很多其他属性诸如MinWidth、MaxHeight和RenderTransformOrigin等。一个Trigger使得再选中状态时显示缩放和旋转的修饰框,这其中使用了Attached Property,也就是Selector.IsSelected。

注意:在WPF中提供了名为Selector的类,它允许用户在他的子级元素中选择出一个。本文中,我没有使用任何Selector元素,但是我是用了Selector.IsSelected的Attached Property来模拟了选中操作。

<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>

OK,我们完成了!现在,我们可以拖动、缩放、旋转一个对象。仅仅三个类和数行的XAML代码就使我们完成了这些功能!最妙的是,我们不需要知道需要拖动、缩放、旋转的元素本身的任何信息,所有的行为都被包装到一个模板中!

使用Adorner的方案

在本章,我将演示如何将缩放和旋转的修饰框提取到AdornerLayer中,以便能够在其他元素表面加以渲染。

ScreenShot07

通过DesignerItem的模板,我们能够解释基于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继承,没有默认Style,替换为在ShowAdorner属性为true时显示的类。

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就被显示出来。

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,这是实际上在缩放和旋转项目上提供了拖动的事件处理。这个元素具有默认的Style,使得其具有我们前面几章提到的ResizeThumb和RotateThumb的样式,所以我再次不再重复这些代码。

自定义Adorner

显然我们可以将自定义的Adorner加入到DesignerItem中。下面的示例中,我将一个能够显示被缩放元素实际宽高的Adorner加入到其中。详细的代码请参见附件,如果你有任何问题,请与我联系。

ScreenShot08

 



(全文完)


以下为广告部分

您部署的HTTPS网站安全吗?

如果您想看下您的网站HTTPS部署的是否安全,花1分钟时间来 myssl.com 检测以下吧。让您的HTTPS网站变得更安全!

SSL检测评估

快速了解HTTPS网站安全情况。

安全评级(A+、A、A-...)、行业合规检测、证书信息查看、证书链信息以及补完、服务器套件信息、证书兼容性检测等。

SSL证书工具

安装部署SSL证书变得更方便。

SSL证书内容查看、SSL证书格式转换、CSR在线生成、SSL私钥加解密、CAA检测等。

SSL漏洞检测

让服务器远离SSL证书漏洞侵扰

TLS ROBOT漏洞检测、心血漏洞检测、FREAK Attack漏洞检测、SSL Poodle漏洞检测、CCS注入漏洞检测。

posted @ 2009-12-20 15:03  麒麟  阅读(6373)  评论(2编辑  收藏  举报