這篇文章介紹了通過WPF的控件模闆以及Thumb來實作圖形設計器的移動Drag、改變大小resize和旋轉rotate這三個幾本功能,示例代碼界面如下:

控件模闆由ControlTemplate類來表示,它派生自FrameworkTemplate抽象類,它的重要部分是它的VisualTree内容屬性,它包含了定義想要的外觀的可視樹。通過以下方式可以定義一個控件模闆類:
<Canvas>
<Canvas.Resources>
<ControlTemplate x:Key="DesignerItemTemplate" TargetType="ContentControl">
<ContentPresenter Content="{TemplateBinding ContentControl.Content}"/>
</ControlTemplate>
</Canvas.Resources>
<ContentControl Name="DesignerItem"
Width="100"
Height="100"
Canvas.Top="100"
Canvas.Left="100"
Template="{StaticResource DesignerItemTemplate}">
<Ellipse Fill="Blue"/>
</ContentControl>
</Canvas>
限制目标類型
ControlTemplate和Style一樣,也有一個TargetType屬性來限制模闆可以被應用到的類型上,如果沒有一個顯示的TargetType,則目标類型将被隐式的設定為Control。由于沒有預設的控件模闆,是以它與Style是不同的,當使用TargetType時不允許移除模闆的x:Key。
模闆綁定TemplateBinding
在控件模闆中,從目标元素插入屬性值的關鍵是資料綁定,我們可以通過一個簡單、輕量級的模闆綁定TemplateBinding來處理。TemplateBinding的資料源總是目标元素,而Path則是目标元素的任何一個依賴屬性。使用方式如上例的{TemplateBinding ContentControl.Content},如果我們設定了TargetType,可以更簡單的使用為{TemplateBinding Content}
TemplateBinding僅僅是一個便捷的設定模闆綁定的機制,對于有些可當機的屬性(如Brush的Color屬性)時綁定會失敗,這時候我們可以使用正常的Binding來達到同樣效果,通過使用一個RelativeSource,其值為{Relative Source TemplatedParent}以及一個Path。
ContentPresenter
在控件模闆中應該使用輕量級的内容顯示元素ContentPresenter,而不是ContentControl。ContentPresenter顯示的内容和ContentControl是一樣的,但是ContentControl是一個帶有控件模闆的成熟控件,其内部包含了ContentPresenter。
如果我們在使用ContentPresenter時忘記了将它的Content設定為{TemplateBinding Content}時,它将隐式的假設{TemplateBinding Content}就是我們需要的内容
與觸發器互動
在模闆内部可以使用觸發器,但是在進行綁定時需要注意隻能使用Binding,因為觸發器位于控件可視樹模闆外部
在WPF中有一個Thumb的控件,在MSDN文檔中是這麼寫的: " ...represents a control that lets the user drag and resize controls." 從字面上來看這個是一個用來處理拖放和設定大小的控件,正好應該在圖形設計器中來處理移動和改變大小等動作。在以下介紹的Move、Resize和Rotate這三個功能都是使用Thumb來做的。
MoveThumb 是從Thumb繼承下來,我們實作了DragDelta事件來處理移動操作,
代碼
public class MoveThumb : Thumb
{
public MoveThumb()
DragDelta += new DragDeltaEventHandler(this.MoveThumb_DragDelta);
}
private void MoveThumb_DragDelta(object sender, DragDeltaEventArgs e)
ContentControl designerItem = DataContext as ContentControl;
if (designerItem != null)
Point dragDelta = new Point(e.HorizontalChange, e.VerticalChange);
RotateTransform rotateTransform = designerItem.RenderTransform as RotateTransform;
if (rotateTransform != null)
dragDelta = rotateTransform.Transform(dragDelta);
Canvas.SetLeft(designerItem, Canvas.GetLeft(designerItem) + dragDelta.X);
Canvas.SetTop(designerItem, Canvas.GetTop(designerItem) + dragDelta.Y);
實作代碼中假定DataContext為我們需要操作的圖形控件,這個可以在控件模闆中看到:
<ControlTemplate x:Key="DesignerItemControlTemplate" TargetType="ContentControl">
<Grid>
<s:DragThumb DataContext="{Binding RelativeSource={RelativeSource TemplatedParent}}" Cursor="SizeAll"/>
</Grid>
RelativeSource
PreviousData 清單的前一個資料項
TemplatedParent 應用模闆的元素
Self 元素自身
FindAncestor 通過父元素鍊去找
命中測試 IsHitTestVisible
如果我們現在拖動一個圓形,那麼界面如下:
我們現在拖動時會發現,隻能在灰色部分才允許拖動,在圓形區域由于捕獲的不是MoveThumb而不能拖動。這時候我們隻需要簡單的設定<code>IsHitTest為false即可</code>
<Ellipse Fill="Blue" IsHitTestVisible="False"/>
更改大小仍舊使用的是Thumb,我們建立了一個控件模闆:
<ControlTemplate x:Key="ResizeDecoratorTemplate" TargetType="Control">
<Thumb Height="3" Cursor="SizeNS" Margin="0 -4 0 0"
VerticalAlignment="Top" HorizontalAlignment="Stretch"/>
<Thumb Width="3" Cursor="SizeWE" Margin="-4 0 0 0"
VerticalAlignment="Stretch" HorizontalAlignment="Left"/>
<Thumb Width="3" Cursor="SizeWE" Margin="0 0 -4 0"
VerticalAlignment="Stretch" HorizontalAlignment="Right"/>
<Thumb Height="3" Cursor="SizeNS" Margin="0 0 0 -4"
VerticalAlignment="Bottom" HorizontalAlignment="Stretch"/>
<Thumb Width="7" Height="7" Cursor="SizeNWSE" Margin="-6 -6 0 0"
VerticalAlignment="Top" HorizontalAlignment="Left"/>
<Thumb Width="7" Height="7" Cursor="SizeNESW" Margin="0 -6 -6 0"
VerticalAlignment="Top" HorizontalAlignment="Right"/>
<Thumb Width="7" Height="7" Cursor="SizeNESW" Margin="-6 0 0 -6"
VerticalAlignment="Bottom" HorizontalAlignment="Left"/>
<Thumb Width="7" Height="7" Cursor="SizeNWSE" Margin="0 0 -6 -6"
VerticalAlignment="Bottom" HorizontalAlignment="Right"/>
設定了這個樣式的Control界面如下圖所示:
對于改變大小,我們隻要按照MoveThumb一樣,從Thumb繼承一個ResizeThumb來處理改變大小的動作,對于控件模闆,我們隻要把上面的Thumb替換成ResizeThumb即可
public class ResizeThumb : Thumb
public ResizeThumb()
DragDelta += new DragDeltaEventHandler(this.ResizeThumb_DragDelta);
private void ResizeThumb_DragDelta(object sender, DragDeltaEventArgs e)
Control item = this.DataContext as Control;
if (item != null)
double deltaVertical, deltaHorizontal;
switch (VerticalAlignment)
case VerticalAlignment.Bottom:
deltaVertical = Math.Min(-e.VerticalChange,
item.ActualHeight - item.MinHeight);
item.Height -= deltaVertical;
break;
case VerticalAlignment.Top:
deltaVertical = Math.Min(e.VerticalChange,
Canvas.SetTop(item, Canvas.GetTop(item) + deltaVertical);
default:
switch (HorizontalAlignment)
case HorizontalAlignment.Left:
deltaHorizontal = Math.Min(e.HorizontalChange,
item.ActualWidth - item.MinWidth);
Canvas.SetLeft(item, Canvas.GetLeft(item) + deltaHorizontal);
item.Width -= deltaHorizontal;
case HorizontalAlignment.Right:
deltaHorizontal = Math.Min(-e.HorizontalChange,
e.Handled = true;
加入到DesignerItemTemplate控件模闆
<Grid DataContext="{Binding RelativeSource={RelativeSource TemplatedParent}}">
<s:MoveThumb Template="{StaticResource MoveThumbTemplate}" Cursor="SizeAll"/>
<Control Template="{StaticResource ResizeDecoratorTemplate}"/>
我們實作旋轉功能,仍舊是通過從Thumb繼承下來一個RotateThumb,具體實作代碼如下:
public class RotateThumb : Thumb
private double initialAngle;
private RotateTransform rotateTransform;
private Vector startVector;
private Point centerPoint;
private ContentControl designerItem;
private Canvas canvas;
public RotateThumb()
DragDelta += new DragDeltaEventHandler(this.RotateThumb_DragDelta);
DragStarted += new DragStartedEventHandler(this.RotateThumb_DragStarted);
private void RotateThumb_DragStarted(object sender, DragStartedEventArgs e)
this.designerItem = DataContext as ContentControl;
if (this.designerItem != null)
this.canvas = VisualTreeHelper.GetParent(this.designerItem) as Canvas;
if (this.canvas != null)
this.centerPoint = this.designerItem.TranslatePoint(
new Point(this.designerItem.Width * this.designerItem.RenderTransformOrigin.X,
this.designerItem.Height * this.designerItem.RenderTransformOrigin.Y),
this.canvas);
Point startPoint = Mouse.GetPosition(this.canvas);
this.startVector = Point.Subtract(startPoint, this.centerPoint);
this.rotateTransform = this.designerItem.RenderTransform as RotateTransform;
if (this.rotateTransform == null)
this.designerItem.RenderTransform = new RotateTransform(0);
this.initialAngle = 0;
else
this.initialAngle = this.rotateTransform.Angle;
private void RotateThumb_DragDelta(object sender, DragDeltaEventArgs e)
if (this.designerItem != null && this.canvas != null)
Point currentPoint = Mouse.GetPosition(this.canvas);
Vector deltaVector = Point.Subtract(currentPoint, this.centerPoint);
double angle = Vector.AngleBetween(this.startVector, deltaVector);
RotateTransform rotateTransform = this.designerItem.RenderTransform as RotateTransform;
rotateTransform.Angle = this.initialAngle + Math.Round(angle, 0);
this.designerItem.InvalidateMeasure();
樣式如下:
<!-- RotateThumb Style -->
<Style TargetType="{x:Type s:RotateThumb}">
<Setter Property="RenderTransformOrigin" Value="0.5,0.5"/>
<Setter Property="Cursor" Value="Hand"/>
<Setter Property="Control.Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type s:RotateThumb}">
<Grid Width="30" Height="30">
<Path Fill="#AAD0D0DD"
Stretch="Fill"
Data="M 50,100 A 50,50 0 1 1 100,50 H 50 V 100"/>
</Setter.Value>
</Setter>
</Style>
<!-- RotateDecorator Template -->
<ControlTemplate x:Key="RotateDecoratorTemplate" TargetType="{x:Type Control}">
<s:RotateThumb Margin="-18,-18,0,0" VerticalAlignment="Top" HorizontalAlignment="Left"/>
<s:RotateThumb Margin="0,-18,-18,0" VerticalAlignment="Top" HorizontalAlignment="Right">
<s:RotateThumb.RenderTransform>
<RotateTransform Angle="90" />
</s:RotateThumb.RenderTransform>
</s:RotateThumb>
<s:RotateThumb Margin="0,0,-18,-18" VerticalAlignment="Bottom" HorizontalAlignment="Right">
<RotateTransform Angle="180" />
<s:RotateThumb Margin="-18,0,0,-18" VerticalAlignment="Bottom" HorizontalAlignment="Left">
<RotateTransform Angle="270" />
<Style x:Key="DesignerItemStyle" TargetType="ContentControl">
<Setter Property="MinHeight" Value="50"/>
<Setter Property="MinWidth" Value="50"/>
<Setter Property="Template">
<ControlTemplate TargetType="ContentControl">
<Control x:Name="RotateDecorator"
Template="{StaticResource RotateDecoratorTemplate}"
Visibility="Collapsed"/>
<s:MoveThumb Template="{StaticResource MoveThumbTemplate}"
Cursor="SizeAll"/>
<Control x:Name="ResizeDecorator"
Template="{StaticResource ResizeDecoratorTemplate}"
<ControlTemplate.Triggers>
<Trigger Property="Selector.IsSelected" Value="True">
<Setter TargetName="ResizeDecorator"
Property="Visibility" Value="Visible"/>
<Setter TargetName="RotateDecorator"
</Trigger>
</ControlTemplate.Triggers>
WPF支援Adorner來修飾WPF控件,在改變大小等情況下我們可以根據需要來顯示,有些模組化工具支援選中控件後顯示快捷工具條,這個就可以通過使用Adorner來實作。
本篇通過建立一個裝飾類DesignerItemDecorator控件,加入DesignerItemTemplate中,由DesignerItemDecorator控件來控制是否顯示以及如何顯示裝飾部分。
控件模闆
<s:DesignerItemDecorator x:Name="decorator" ShowDecorator="true"/>
<Setter TargetName="decorator" Property="ShowDecorator" Value="true"/>
實作 class DesignerItemDecorator : Control
public class DesignerItemDecorator : Control
private Adorner adorner;
public bool ShowDecorator
get { return (bool)GetValue(ShowDecoratorProperty); }
set { SetValue(ShowDecoratorProperty, value); }
public static readonly DependencyProperty ShowDecoratorProperty =
DependencyProperty.Register
("ShowDecorator", typeof(bool), typeof(DesignerItemDecorator),
new FrameworkPropertyMetadata
(false, new PropertyChangedCallback(ShowDecoratorProperty_Changed)));
private void HideAdorner()
...
private void ShowAdorner()
private static void ShowDecoratorProperty_Changed
(DependencyObject d, DependencyPropertyChangedEventArgs e)
DesignerItemDecorator decorator = (DesignerItemDecorator)d;
bool showDecorator = (bool)e.NewValue;
if (showDecorator)
decorator.ShowAdorner();
decorator.HideAdorner();
實作 class DesignerItemAdorner : Adorner
public class DesignerItemAdorner : Adorner
private VisualCollection visuals;
private DesignerItemAdornerChrome chrome;
protected override int VisualChildrenCount
get
return this.visuals.Count;
public DesignerItemAdorner(ContentControl designerItem)
: base(designerItem)
this.chrome = new DesignerItemAdornerChrome();
this.chrome.DataContext = designerItem;
this.visuals = new VisualCollection(this);
protected override Size ArrangeOverride(Size arrangeBounds)
this.chrome.Arrange(new Rect(arrangeBounds));
return arrangeBounds;
protected override Visual GetVisualChild(int index)
return this.visuals[index];