程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> .NET網頁編程 >> 關於.NET >> 從WPF Diagram Designer Part 2學習面板、縮略圖、框線選擇和工具箱

從WPF Diagram Designer Part 2學習面板、縮略圖、框線選擇和工具箱

編輯:關於.NET

在從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支持移動選擇區域

  

在從WPF Diagram Designer Part 1學習控件模板、移動、改變大小和旋轉中介紹了圖形設計器的移動、大小和旋轉等功能的實現,本篇繼續第二部分,學習設計面板、縮略圖、框線旋轉和工具箱等功能的實現。

WPF Diagram Designer - Part 2

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>

  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved