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

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

WPF Diagram Designer - Part 2

WPF:从WPF Diagram Designer Part 2学习面板、缩略图、框线选择和工具箱_第1张图片

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

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

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

ContractedBlock.gif ExpandedBlockStart.gif 代码
 
     
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)

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

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

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

ContractedBlock.gif ExpandedBlockStart.gif 代码
 
     
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】 如下:

ContractedBlock.gif ExpandedBlockStart.gif 代码
 
     
< 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)

WPF:从WPF Diagram Designer Part 2学习面板、缩略图、框线选择和工具箱_第3张图片

  • Adorner、Adorner Layer

WPF:从WPF Diagram Designer Part 2学习面板、缩略图、框线选择和工具箱_第4张图片WPF:从WPF Diagram Designer Part 2学习面板、缩略图、框线选择和工具箱_第5张图片

框线是通过第一篇说过的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,代码如下:
    ContractedBlock.gif ExpandedBlockStart.gif 代码
     
           
    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
    ContractedBlock.gif ExpandedBlockStart.gif 代码
     
           
    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)

WPF:从WPF Diagram Designer Part 2学习面板、缩略图、框线选择和工具箱_第6张图片

  • Toolbox

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

代码Toolbox.cs如下:

 

ContractedBlock.gif ExpandedBlockStart.gif 代码
 
     
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如下:

 

 

ContractedBlock.gif ExpandedBlockStart.gif 代码
 
     
< 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的代码如下:

ContractedBlock.gif ExpandedBlockStart.gif 代码
 
     
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增加是否选择属性,代码如下:

ContractedBlock.gif ExpandedBlockStart.gif 代码
 
     
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属性:

 

 

ContractedBlock.gif ExpandedBlockStart.gif 代码
 
     
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是否显示:

ContractedBlock.gif ExpandedBlockStart.gif 代码
 
     
< 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:从WPF Diagram Designer Part 2学习面板、缩略图、框线选择和工具箱_第7张图片   WPF:从WPF Diagram Designer Part 2学习面板、缩略图、框线选择和工具箱_第8张图片

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

你可能感兴趣的:(WPF:从WPF Diagram Designer Part 2学习面板、缩略图、框线选择和工具箱)