• WPF:从WPF Diagram Designer Part 2学习面板、缩略图、框线选择和工具箱


      在从WPF Diagram Designer Part 1学习控件模板、移动、改变大小和旋转中介绍了图形设计器的移动、大小和旋转等功能的实现,本篇继续第二部分,学习设计面板、缩略图、框线旋转和工具箱等功能的实现。 

    WPF Diagram Designer - Part 2

    设计面板(Designer Canvas :variable size, scrollable)

      在从WPF Diagram Designer Part 1学习控件模板、移动、改变大小和旋转中的示例出来的设计器,当把设计对象拖动到DesignerCanvas边界外时,因为DesignerCanvas没有滚动条,我们会发现再也找不到这个对象了。想到解决最简单的办法就是给DesignerCanvas添加一个ScrollViewer,但是这个办法解决不了这个问题,因为当拖动到Canvas之外时,并不会出发Canvas的大小发生变化,所以仍旧没有滚动条,为了解决这个问题,我们则必须在设计对象移动和改变大小时去调整Canvas的大小。

      WPF控件提供一个MeassureOverride允许控件计算希望的大小,再返回WPF框架来进行布局。我们可以在DesignerCanvas中重载这个方法来解决上面所说的问题,重载方法如下:

    代码
    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;
    }

      注:当设计对象很多时,我猜测可能会有性能问题。在ZoomableApplication2: A Million Items介绍了一个可以显示百万级对象的示例,不知道能否解决这个性能问题,先把这个在这里留个足迹,以便以后可以找到

    缩略图(Zoombox)

    缩略图如上图所示,使用ZoomBox时需要传入一个  ScrollViewer="{Binding ElementName=DesignerScrollViewer}",以便可以通过移动缩略图上的选择框来移动DesignerCanvas

    代码文件【ZoomBox.cs】如下:

    代码
    public class ZoomBox : Control
    {
    private Thumb zoomThumb;
    private Canvas zoomCanvas;
    private Slider zoomSlider;
    private ScaleTransform scaleTransform;
    private DesignerCanvas designerCanvas;

    public ScrollViewer ScrollViewer
    {
    get { return (ScrollViewer)GetValue(ScrollViewerProperty); }
    set { SetValue(ScrollViewerProperty, value); }
    }

    public static readonly DependencyProperty ScrollViewerProperty =
    DependencyProperty.Register(
    "ScrollViewer", typeof(ScrollViewer), typeof(ZoomBox));

    public override void OnApplyTemplate()
    {
    base.OnApplyTemplate();

    if (this.ScrollViewer == null)
    return;

    this.designerCanvas = this.ScrollViewer.Content as DesignerCanvas;
    if (this.designerCanvas == null)
    throw new Exception("DesignerCanvas must not be null!");

    this.zoomThumb = Template.FindName("PART_ZoomThumb", this) as Thumb;
    if (this.zoomThumb == null)
    throw new Exception("PART_ZoomThumb template is missing!");

    this.zoomCanvas = Template.FindName("PART_ZoomCanvas", this) as Canvas;
    if (this.zoomCanvas == null)
    throw new Exception("PART_ZoomCanvas template is missing!");

    this.zoomSlider = Template.FindName("PART_ZoomSlider", this) as Slider;
    if (this.zoomSlider == null)
    throw new Exception("PART_ZoomSlider template is missing!");

    this.designerCanvas.LayoutUpdated += new EventHandler(this.DesignerCanvas_LayoutUpdated);

    this.zoomThumb.DragDelta += new DragDeltaEventHandler(this.Thumb_DragDelta);

    this.zoomSlider.ValueChanged += new RoutedPropertyChangedEventHandler<double>(this.ZoomSlider_ValueChanged);

    this.scaleTransform = new ScaleTransform();
    this.designerCanvas.LayoutTransform = this.scaleTransform;
    }

    private void ZoomSlider_ValueChanged(object sender, RoutedPropertyChangedEventArgs<double> e)
    {
    double scale = e.NewValue / e.OldValue;

    double halfViewportHeight = this.ScrollViewer.ViewportHeight / 2;
    double newVerticalOffset = ((this.ScrollViewer.VerticalOffset + halfViewportHeight) * scale - halfViewportHeight);

    double halfViewportWidth = this.ScrollViewer.ViewportWidth / 2;
    double newHorizontalOffset = ((this.ScrollViewer.HorizontalOffset + halfViewportWidth) * scale - halfViewportWidth);

    this.scaleTransform.ScaleX *= scale;
    this.scaleTransform.ScaleY *= scale;

    this.ScrollViewer.ScrollToHorizontalOffset(newHorizontalOffset);
    this.ScrollViewer.ScrollToVerticalOffset(newVerticalOffset);
    }

    private void Thumb_DragDelta(object sender, DragDeltaEventArgs e)
    {
    double scale, xOffset, yOffset;
    this.InvalidateScale(out scale, out xOffset, out yOffset);

    this.ScrollViewer.ScrollToHorizontalOffset(this.ScrollViewer.HorizontalOffset + e.HorizontalChange / scale);
    this.ScrollViewer.ScrollToVerticalOffset(this.ScrollViewer.VerticalOffset + e.VerticalChange / scale);
    }

    private void DesignerCanvas_LayoutUpdated(object sender, EventArgs e)
    {
    double scale, xOffset, yOffset;
    this.InvalidateScale(out scale, out xOffset, out yOffset);

    this.zoomThumb.Width = this.ScrollViewer.ViewportWidth * scale;
    this.zoomThumb.Height = this.ScrollViewer.ViewportHeight * scale;

    Canvas.SetLeft(
    this.zoomThumb, xOffset + this.ScrollViewer.HorizontalOffset * scale);
    Canvas.SetTop(
    this.zoomThumb, yOffset + this.ScrollViewer.VerticalOffset * scale);
    }

    private void InvalidateScale(out double scale, out double xOffset, out double yOffset)
    {
    // designer canvas size
    double w = this.designerCanvas.ActualWidth * this.scaleTransform.ScaleX;
    double h = this.designerCanvas.ActualHeight * this.scaleTransform.ScaleY;

    // zoom canvas size
    double x = this.zoomCanvas.ActualWidth;
    double y = this.zoomCanvas.ActualHeight;

    double scaleX = x / w;
    double scaleY = y / h;

    scale
    = (scaleX < scaleY) ? scaleX : scaleY;

    xOffset
    = (x - scale * w) / 2;
    yOffset
    = (y - scale * h) / 2;
    }

    样式文件【ZoomBox.xaml】 如下:

    代码
    <Setter Property="Template">
    <Setter.Value>
    <ControlTemplate TargetType="{x:Type s:ZoomBox}">
    <Border CornerRadius="1"
    BorderThickness
    ="1"
    Background
    ="#EEE"
    BorderBrush
    ="DimGray">
    <Expander IsExpanded="True"
    Background
    ="Transparent">
    <Border BorderBrush="DimGray"
    BorderThickness
    ="0,1,0,0"
    Padding
    ="0"
    Height
    ="180">
    <Grid>
    <Canvas Margin="5"
    Name
    ="PART_ZoomCanvas">
    <Canvas.Background>
    <VisualBrush Stretch="Uniform"
    Visual
    ="{Binding RelativeSource={RelativeSource TemplatedParent}, Path=ScrollViewer.Content}" />
    </Canvas.Background>
    <Thumb Name="PART_ZoomThumb"
    Cursor
    ="SizeAll">
    <Thumb.Style>
    <Style TargetType="Thumb">
    <Setter Property="Template">
    <Setter.Value>
    <ControlTemplate TargetType="Thumb">
    <Rectangle StrokeThickness="1"
    Stroke
    ="Black"
    Fill
    ="Transparent" />
    </ControlTemplate>
    </Setter.Value>
    </Setter>
    </Style>
    </Thumb.Style>
    </Thumb>
    </Canvas>
    </Grid>
    </Border>
    <Expander.Header>
    <Grid>
    <Grid.ColumnDefinitions>
    <ColumnDefinition Width="Auto" />
    <ColumnDefinition Width="*" />
    </Grid.ColumnDefinitions>
    <Slider Name="PART_ZoomSlider"
    VerticalAlignment
    ="Center"
    HorizontalAlignment
    ="Center"
    Margin
    ="0"
    Ticks
    ="25,50,75,100,125,150,200,300,400,500"
    Minimum
    ="25"
    Maximum
    ="500"
    Value
    ="100"
    IsSnapToTickEnabled
    ="True"
    IsMoveToPointEnabled
    ="False" />

    <TextBlock Text="{Binding ElementName=PART_ZoomSlider, Path=Value}"
    Grid.Column
    ="1"
    VerticalAlignment
    ="Center"
    HorizontalAlignment
    ="Right"
    Margin
    ="0,0,14,0" />
    <TextBlock Text="%"
    Grid.Column
    ="1"
    VerticalAlignment
    ="Center"
    HorizontalAlignment
    ="Right"
    Margin
    ="1,0,2,0" />
    </Grid>
    </Expander.Header>
    </Expander>
    </Border>
    </ControlTemplate>
    </Setter.Value>
    </Setter>
    </Style>

    框线选择(Rubberband selection)

    • Adorner、Adorner Layer

    框线是通过第一篇说过的Adorner来做的,其实在WPF中很多地方都用到了这个功能,如光标、高亮等。这些Adorner都是放在一个Adorner Layer上,MSDN解释说Adorner Layer是置于一个窗口内所有其它控件之上的。AdornerLayer类只能通过 AdornerLayer.GetAdornerLayer(this) 获取。还可以参考:Defining WPF Adorners in XAML   Group Sort Adorner ListView

    • DesignerCanvas生成RubberbandAdorner
      当按住鼠标左键点击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 : Adorner
      代码
      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;
      }
      }

      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);
      }

      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;
      }
      }
      }
      ...
      }

    工具箱Toolbox (drag & drop)

    • Toolbox

    工具箱Toolbox是一个ItemsControl控件,它的子是ToolboxItem类型。

    代码Toolbox.cs如下:

    代码
    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显示ToolboxItem,样式文件Toolbox.xaml如下:

    代码
    <Style TargetType="{x:Type s:ToolboxItem}">
    <Setter Property="Control.Padding"
    Value
    ="5" />
    <Setter Property="ContentControl.HorizontalContentAlignment"
    Value
    ="Stretch" />
    <Setter Property="ContentControl.VerticalContentAlignment"
    Value
    ="Stretch" />
    <Setter Property="ToolTip"
    Value
    ="{Binding ToolTip}" />
    <Setter Property="Template">
    <Setter.Value>
    <ControlTemplate TargetType="{x:Type s:ToolboxItem}">
    <Grid>
    <Rectangle Name="Border"
    StrokeThickness
    ="1"
    StrokeDashArray
    ="2"
    Fill
    ="Transparent"
    SnapsToDevicePixels
    ="true" />
    <ContentPresenter Content="{TemplateBinding ContentControl.Content}"
    Margin
    ="{TemplateBinding Padding}"
    SnapsToDevicePixels
    ="{TemplateBinding UIElement.SnapsToDevicePixels}" />
    </Grid>
    <ControlTemplate.Triggers>
    <Trigger Property="IsMouseOver"
    Value
    ="true">
    <Setter TargetName="Border"
    Property
    ="Stroke"
    Value
    ="Gray" />
    </Trigger>
    </ControlTemplate.Triggers>
    </ControlTemplate>
    </Setter.Value>
    </Setter>
    </Style>

    <Style TargetType="{x:Type s:Toolbox}">
    <Setter Property="SnapsToDevicePixels"
    Value
    ="true" />
    <Setter Property="Focusable"
    Value
    ="False" />
    <Setter Property="Template">
    <Setter.Value>
    <ControlTemplate>
    <Border BorderThickness="{TemplateBinding Border.BorderThickness}"
    Padding
    ="{TemplateBinding Control.Padding}"
    BorderBrush
    ="{TemplateBinding Border.BorderBrush}"
    Background
    ="{TemplateBinding Panel.Background}"
    SnapsToDevicePixels
    ="True">
    <ScrollViewer VerticalScrollBarVisibility="Auto">
    <ItemsPresenter SnapsToDevicePixels="{TemplateBinding UIElement.SnapsToDevicePixels}" />
    </ScrollViewer>
    </Border>
    </ControlTemplate>
    </Setter.Value>
    </Setter>
    <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>
    </Style>
    • ToolboxItem

    ToolboxItem是显示在工具箱中的对象,我们可以通过鼠标点击它进行选择,然后拖拽到DesignerCanvas来生成一个设计对象,示例中是通过XamlWriter.Save保存到DataObject,然后在DesignerCanvas接收这个对象,这部分在进行自己的设计器开发时会进行更改
    ToolboxItem的代码如下:

    代码
    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;
    }
    }
    }

    DesignerItem增加IsSelected属性

    DesignerItem增加是否选择属性,代码如下:

    代码
    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事件时会去设置IsSelected属性:

    代码
    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;
    }

    IsSelected属性触发ResizeDecorator是否显示:

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

    DesignerItem支持移动选择区域

      

    DesignerItem默认允许移动的是一个透明的矩形区域,如上图左边这个。我们一般希望点击这个形状内部才允许移动和选择,这时候我们可以通过DesignerItem.MoveThumbTemplate来更改这个支持Move的区域,代码如下:

    <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.MoveThumbTemplate>
    <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.MoveThumbTemplate >
    </Path>

    欢迎转载,转载请注明:转载自周金根 [ http://zhoujg.cnblogs.com/ ]

  • 相关阅读:
    running Android Studio on Windows 7 fails, no Android SDK found
    Dalvik虚拟机简要介绍和学习计划
    免费HTTP上传文件控件WebUploadFile.ocx发布,让实现Word在线编辑器变得容易
    ASP.NET生成缩略图的代码
    C++ MFC 关于SelectObject,请解释下,谢谢
    Visual C++中MFC消息的分类
    深入思考全局静态存储区、堆区和栈区
    coredump简介与coredump原因总结
    c++函数中的 指针参数跟地址参数区别
    解决SWFUpload在Chrome、Firefox等浏览器下的问题
  • 原文地址:https://www.cnblogs.com/zhoujg/p/1801427.html
Copyright © 2020-2023  润新知