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

posted on 2010-08-17 16:51  周 金根  阅读(9533)  评论(3编辑  收藏  举报

导航