在從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>