在从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
类型。
本文转自 jingen_zhou 51CTO博客,原文链接:http://blog.51cto.com/zhoujg/517451,如需转载请自行联系原作者