• 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注入漏洞检测。

  • 相关阅读:
    BZOJ 4716 假摔
    【UER #4】量子态的棋盘
    [Lydsy2017省队十连测]最长路径
    [Lydsy2017省队十连测]航海舰队
    [Lydsy2017省队十连测]公路建设
    [Lydsy2017省队十连测]商店购物
    湖南省队集训题【2018】(我也不知道是第几场)
    CXMS 胡策2
    [TJOI2018]异或
    TJOI 2018 数学计算
  • 原文地址:https://www.cnblogs.com/zhuqil/p/WPF.html
Copyright © 2020-2023  润新知