WPF中的图表设计器 – 2

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

[译者]WizRay

ScreenShot08

介绍

在上一篇文章中,我们展示了如何移动、缩放和旋转一个Canvas中的对象。这次,我们要加入一些典型的图表编辑器所必不可少的更深一层的特性:

  • Designer Canvas(可变大小、可缩放)
  • Zoombox
  • 框选
  • 键盘支持(Ctrl + 鼠标左键)
  • Toolbox(可拖动)
  • 可旋转组件

Designer Canvas

在上一篇文章中,你大概已经注意到了,当你把一个元素拖动到DesignerCanvas边框以外时,它就无法再访问到了。通常,用户期望设计器能够提供一个滚动条,使得用户可以将工作区移动到画布可视范围以外的任何区域。为此,我不得不吧DesignerCanvas移动到一个ScrollViewer里面,不过这没有用。很快,我知道了失效的原因,先让我来解释下面这些代码段:

<Canvas Width="200" Height="200" Background="WhiteSmoke">
    <Rectangle Fill="Blue" Width="100" Height="100" Canvas.Left="300" Canvas.Top="300" />
</Canvas>

 

我把一个Rectangle放置在一个Canvas中,但是把它放在Canvas的边界以外。这显然不会改变Canvas的尺寸,不管我们把那个Rectangle放在哪儿。

这一位置对于DesignerCanvas,不论我们把一个对象移到离它的边界多么远的地方,他都不会改变尺寸。这样我们就理解了为什么ScrollView在这儿不管用:DesignerCanvas永远不会通知ScrollViewer它的尺寸发生变化,因为它根本就没有变化。

解决方案是我们必须强制设定DesignerCanvas的尺寸随时与移动或缩放的元素保持适应。幸运的是,Canvas提供了名为MeasureOverride的方法,这个方法能够允许DesignerCanvas计算它所需的尺寸,并将结果返回WPF的布局系统。这种计算很简单,就像下面这样:

protected override Size MeasureOverride(Size constraint)
{
    Size size = new Size();
    foreach (UIElement element in base.Children)
    {
        double left = Canvas.GetLeft(element);
        double top = Canvas.GetTop(element);
        left = double.IsNaN(left) ? 0 : left;
        top = double.IsNaN(top) ? 0 : top;

        //measure desired size for each child
        element.Measure(constraint);

        Size desiredSize = element.DesiredSize;
        if (!double.IsNaN(desiredSize.Width) && !double.IsNaN(desiredSize.Height))
        {
            size.Width = Math.Max(size.Width, left + desiredSize.Width);
            size.Height = Math.Max(size.Height, top + desiredSize.Height);
        }
    }
    //for aesthetic reasons add extra points
    size.Width += 10;
    size.Height += 10;
    return size;
}

DesignerItem

DesignerItem是从ContentControl继承下来的,所以它能够重用上一篇文章中的ControlTemplate。DesignerItem提供了IsSelected属性来表示他是否被选中:

public class DesignerItem : ContentControl
{
    public bool IsSelected
    {
        get { return (bool)GetValue(IsSelectedProperty); }
        set { SetValue(IsSelectedProperty, value); }
    }
    public static readonly DependencyProperty IsSelectedProperty =
       DependencyProperty.Register("IsSelected", typeof(bool),
                                    typeof(DesignerItem),
                                    new FrameworkPropertyMetadata(false));

 

而后我们实现了MouseDown的事件处理来支持多选:

protected override void OnPreviewMouseDown(MouseButtonEventArgs e)
{
    base.OnPreviewMouseDown(e);
    DesignerCanvas designer = VisualTreeHelper.GetParent(this) as DesignerCanvas;

    if (designer != null)
    {
        if ((Keyboard.Modifiers &
        (ModifierKeys.Shift | ModifierKeys.Control)) != ModifierKeys.None)
        {
            this.IsSelected = !this.IsSelected;
        }
        else
        {
            if (!this.IsSelected)
            {
                designer.DeselectAll();
                this.IsSelected = true;
            }
        }
    }

    e.Handled = false;
}

 

注意到我们对PreviewMouseDown事件进行监听,并且我们设定该事件没有被处理过。这是因为我们希望即使MouseDown指向了DesignerItem里的一个子级元素,这个DesignerItem也能被选中;就像在Visual Studio的类设计器中,如果我们点击了Expander的ToggleButton,这个项目会被选中,而且Expander会被打开,两个是同时发生的。

ScreenShot003_new

最后,我们更新一下DesignerItem的模板,添加一个简单的Trigger,使得缩放的修饰框只在被选择是才可见。

<Style TargetType="{x:Type s:DesignerItem}">
    <Setter Property="MinHeight" Value="50" />
    <Setter Property="MinWidth" Value="50" />
    <Setter Property="SnapsToDevicePixels" Value="true" />
    <Setter Property="Template">
        <Setter.Value>
            <ControlTemplate TargetType="{x:Type s:DesignerItem}">
                <Grid DataContext="{Binding RelativeSource={RelativeSource TemplatedParent},
      Path=.}">
                    <s:MoveThumb x:Name="PART_MoveThumb" Cursor="SizeAll"
                            Template="{StaticResource MoveThumbTemplate}" />
                    <ContentPresenter x:Name="PART_ContentPresenter"
                            Content="{TemplateBinding ContentControl.Content}"
                            Margin="{TemplateBinding Padding}" />
                    <s:ResizeDecorator x:Name="PART_DesignerItemDecorator" />
                </Grid>
                <ControlTemplate.Triggers>
                    <Trigger Property="IsSelected" Value="True">
                        <Setter TargetName="PART_DesignerItemDecorator"
                                Property="ShowDecorator" Value="True" />
                    </Trigger>
                </ControlTemplate.Triggers>
            </ControlTemplate>
        </Setter.Value>
    </Setter>
</Style>

 

Toolbox

Toolbox是一种使用ToolboxItem作为默认容器来显示控件的ItemsControl。为此,我们必须重写GetContainerForItemOverride方法和IsItemItsWenContainerOverride方法:

public class Toolbox : ItemsControl
{
    private Size defaultItemSize = new Size(65, 65);
    public Size DefaultItemSize
    {
        get { return this.defaultItemSize; }
        set { this.defaultItemSize = value; }
    }

    protected override DependencyObject GetContainerForItemOverride()
    {
        return new ToolboxItem();
    }

    protected override bool IsItemItsOwnContainerOverride(object item)
    {
        return (item is ToolboxItem);
    }
}

 

而且,我们希望Toolbox能够使用WrapPanel作为布局面板。

<Setter Property="ItemsPanel">
    <Setter.Value>
        <ItemsPanelTemplate>
            <WrapPanel Margin="0,5,0,5" ItemHeight="{Binding Path=DefaultItemSize.Height,
                RelativeSource={RelativeSource AncestorType=s:Toolbox}}" ItemWidth="{Binding Path=DefaultItemSize.Width,
                RelativeSource={RelativeSource AncestorType=s:Toolbox}}" />
        </ItemsPanelTemplate>
    </Setter.Value>
</Setter>

 

请注意,WrapPanel的ItemHeight和ItemWidth属性是与Toolbox的DefaultItemSize绑定到一起的。

ToolboxItem

如果你希望从Toolbox中拖动一个元素到Canvas中放开的话,ToolboxItem是拖动操作真正开始的地方。拖动和释放本身没有什么问题,但是你需要注意怎么把一个元素从他拖动的起点(Toolbox)复制到释放的位置(DesignerCanvas)。我们使用XamlWriter.Save方法来把ToolboxItem中的元素串行化成XAML,这种串行化有一些已知的限制,在下一节中,我们将使用二进制串行化来代替它。

public class ToolboxItem : ContentControl
    {
        private Point? dragStartPoint = null;

        static ToolboxItem()
        {
            FrameworkElement.DefaultStyleKeyProperty.OverrideMetadata(typeof(ToolboxItem),
                   new FrameworkPropertyMetadata(typeof(ToolboxItem)));
        }

        protected override void OnPreviewMouseDown(MouseButtonEventArgs e)
        {
            base.OnPreviewMouseDown(e);
            this.dragStartPoint = new Point?(e.GetPosition(this));
        }

        protected override void OnMouseMove(MouseEventArgs e)
        {
            base.OnMouseMove(e);
            if (e.LeftButton != MouseButtonState.Pressed)
            {
                this.dragStartPoint = null;
            }
            if (this.dragStartPoint.HasValue)
            {
                Point position = e.GetPosition(this);
                if ((SystemParameters.MinimumHorizontalDragDistance <=
                     Math.Abs((double)(position.X - this.dragStartPoint.Value.X))) ||
                     (SystemParameters.MinimumVerticalDragDistance <=
                     Math.Abs((double)(position.Y - this.dragStartPoint.Value.Y))))
                {
                    string xamlString = XamlWriter.Save(this.Content);
                    DataObject dataObject = new DataObject("DESIGNER_ITEM", xamlString);

                    if (dataObject != null)
                    {
                        DragDrop.DoDragDrop(this, dataObject, DragDropEffects.Copy);
                    }
                }
                e.Handled = true;
            }
        }
    }

 

框选

当用户直接从DesignerCanvas上开始一个拖动动作时,一个RubberbandAdorner对象会被创建:

public class DesignerCanvas : Canvas
{
...

protected override void OnMouseMove(MouseEventArgs e)
{
    base.OnMouseMove(e);

    if (e.LeftButton != MouseButtonState.Pressed)
        this.dragStartPoint = null;

    if (this.dragStartPoint.HasValue)
    {
        AdornerLayer adornerLayer = AdornerLayer.GetAdornerLayer(this);
        if (adornerLayer != null)
        {
            RubberbandAdorner adorner = new RubberbandAdorner(this, dragStartPoint);
            if (adorner != null)
            {
                adornerLayer.Add(adorner);
            }
        }

        e.Handled = true;
    }
}

...
}

 

一旦RubberbandAdorner被创建,他就开始接管拖动事件、绘制框选的外框、绘制被选中的元素的外框。这些更新在UpdateRubberband()和UpdateSelection()方法内部进行:

public class RubberbandAdorner : Adorner
{
....

private Point? startPoint, endPoint;

protected override void OnMouseMove(MouseEventArgs e)
{
    if (e.LeftButton == MouseButtonState.Pressed)
    {
        if (!this.IsMouseCaptured)
        {
            this.CaptureMouse();
        }

        this.endPoint = e.GetPosition(this);
        this.UpdateRubberband();
        this.UpdateSelection();
        e.Handled = true;
    }
}

...
}

 

框选的外框实际上是一个Rectangle元素,所以UpdateRubberband()方法只需要更新他的尺寸和位置即可。

private void UpdateRubberband()
{
    double left = Math.Min(this.startPoint.Value.X, this.endPoint.Value.X);
    double top = Math.Min(this.startPoint.Value.Y, this.endPoint.Value.Y);

    double width = Math.Abs(this.startPoint.Value.X - this.endPoint.Value.X);
    double height = Math.Abs(this.startPoint.Value.Y - this.endPoint.Value.Y);

    this.rubberband.Width = width;
    this.rubberband.Height = height;
    Canvas.SetLeft(this.rubberband, left);
    Canvas.SetTop(this.rubberband, top);
}

 

在UpdateSelection()方法中还有一些地方需要处理。我们需要在这儿检查每一个DesignerItem来确定它是否在框选的范围之内。为此,VisualTreeHelper.GetDescendantBounds(item)方法提供了我们每个子级对象的外框范围。我们通过rubberband.Containes(itemBounds)来确定这些元素是否需要被选中。

private void UpdateSelection()
{
    Rect rubberBand = new Rect(this.startPoint.Value, this.endPoint.Value);
    foreach (DesignerItem item in this.designerCanvas.Children)
    {
        Rect itemRect = VisualTreeHelper.GetDescendantBounds(item);
        Rect itemBounds = item.TransformToAncestor
            (designerCanvas).TransformBounds(itemRect);

        if (rubberBand.Contains(itemBounds))
        {
            item.IsSelected = true;
        }
        else
        {
            item.IsSelected = false;
        }
    }
}

 

需要注意的是,在拖动中,无论何时,如果MouseMove事件被触发都会导致上面所说的界面更新方法。它被触发的及其频繁,你也可以换一种方法:在拖动结束时(即MouseUp事件触发是)再判断这些。

自定义DragThumb

DragThumb的默认样式是一个透明的Rectangle,但是如果我们希望调整这个样式,我们可是使用一个叫做DesignerItem.DragThumbTemplate的Attached Property。下面这个示例将解释这种操作,如果DesignerItem的Content是下面这个五角星的话:

<Path Stroke="Red" StrokeThickness="5" Stretch="Fill" IsHitTestVisible="false"
        Data="M 9,2 11,7 17,7 12,10 14,15 9,12 4,15 6,10 1,7 7,7 Z" />

 

为了更好地说明,我使用了彩色的DragThumb的模板:

ScreenShot004_new

现在添加下面的代码:

<Path Stroke="Red" StrokeThickness="5" Stretch="Fill" IsHitTestVisible="false"
            Data="M 9,2 11,7 17,7 12,10 14,15 9,12 4,15 6,10 1,7 7,7 Z">
        <s:DesignerItem.DragThumbTemplate>
            <ControlTemplate>
                <Path Data="M 9,2 11,7 17,7 12,10 14,15 9,12 4,15 6,10 1,7 7,7 Z"
                        Fill="Transparent" Stretch="Fill" />
            </ControlTemplate>
        </s:DesignerItem.DragThumbTemplate>
    </Path>

 

现在,DragThumb的样子比刚才合适多了。



(全文完)


以下为广告部分

您部署的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:05  麒麟  阅读(2497)  评论(0编辑  收藏  举报