在WPF中的Canvas上實現控制元件的拖動、縮放
如題,專案中需要實現使用滑鼠拖動、縮放一個矩形框,WPF中沒有現成的,那就自己造一個輪子:)
造輪子前先看看Windows自帶的畫圖工具中是怎樣做的,如下圖:
在被拖動的矩形框四周有9個小框,可以從不同方向拖動來放大縮小矩形框,另外需要注意的是,還有一個框,就是圖中虛線的矩形框,這個框,是用來拖動目標控制元件的;我們要做的,就是模仿畫圖中的做法,在自定義控制元件中顯示10個框,然後根據滑鼠所在的框來處理滑鼠輸入,實現拖動與放大。
參考這篇博文繼續聊WPF——Thumb控制元件得知,WPF中有現成的拖動控制元件,可以提供對應的事件(DragDelta & DragCompleted), 就用它了。
還有一個需要考慮的是,我們的這個自定義控制元件中有10個不同作用的Thumb控制元件,如何區分事件從哪個Thumb發出來的呢?這樣我們才能知道使用者希望的操作是拖動,還是縮放,而且縮放也要知道朝哪個方向縮放。可以使用Tag屬性,但是它是Object型別的,會涉及到拆箱,所以還是自定義一個CustomThumb。
首先,定義說明拖動方向的列舉:
public enum DragDirection { TopLeft = 1, TopCenter = 2, TopRight = 4, MiddleLeft = 16, MiddleCenter = 32, MiddleRight = 64, BottomLeft = 256, BottomCenter = 512, BottomRight = 1024, }
好了,有了這個列舉,就可以知道使用者操作的意圖了,現在自定義一個CustomThumb。
public class CustomThumb : Thumb
{
public DragDirection DragDirection { get; set; }
}
這些都弄好了,現在來寫自定義控制元件的模板:
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:local="clr-namespace:UICommon.Controls" xmlns:Core="clr-namespace:System;assembly=mscorlib" mc:Ignorable="d"> <ControlTemplate TargetType="{x:Type local:DragHelperBase}" x:Key="DrapControlHelperTemplate"> <ControlTemplate.Resources> <Style TargetType="{x:Type Thumb}" x:Key="CornerThumbStyle"> <Setter Property="Width" Value="{Binding CornerWidth, RelativeSource={RelativeSource TemplatedParent}, Mode=OneWay}"/> <Setter Property="Height" Value="{Binding CornerWidth, RelativeSource={RelativeSource TemplatedParent}, Mode=OneWay}"/> <Setter Property="BorderBrush" Value="{Binding BorderBrush, RelativeSource={RelativeSource TemplatedParent}, Mode=OneWay}"/> <Setter Property="BorderThickness" Value="3"/> <Setter Property="Background" Value="Transparent"/> <Setter Property="Template"> <Setter.Value> <ControlTemplate TargetType="{x:Type Thumb}"> <Border SnapsToDevicePixels="True" Width="{TemplateBinding Width}" Height="{TemplateBinding Height}" Background="{TemplateBinding Background}" BorderBrush="{TemplateBinding BorderBrush}" BorderThickness="{TemplateBinding BorderThickness}"/> </ControlTemplate> </Setter.Value> </Setter> </Style> <Style TargetType="{x:Type Thumb}" x:Key="AreaThumbStyle"> <Setter Property="BorderBrush" Value="{Binding BorderBrush, RelativeSource={RelativeSource TemplatedParent}}"/> <Setter Property="Background" Value="Transparent"/> <Setter Property="Padding" Value="0"/> <Setter Property="Margin" Value="0"/> <Setter Property="Template"> <Setter.Value> <ControlTemplate TargetType="{x:Type Thumb}"> <Rectangle Margin="0" Fill="{TemplateBinding Background}" SnapsToDevicePixels="True" Stroke="{TemplateBinding BorderBrush}" StrokeDashArray="2.0 2.0" Stretch="Fill" StrokeThickness="{Binding RelativeSource={RelativeSource TemplatedParent}, Path=BorderThickness.Top, Mode=OneWay}"/> </ControlTemplate> </Setter.Value> </Setter> </Style> </ControlTemplate.Resources> <Grid x:Name="PART_MainGrid"> <Grid.ColumnDefinitions> <ColumnDefinition Width="*"/> <ColumnDefinition Width="*"/> <ColumnDefinition Width="*"/> </Grid.ColumnDefinitions> <Grid.RowDefinitions> <RowDefinition Height="*"/> <RowDefinition Height="*"/> <RowDefinition Height="*"/> </Grid.RowDefinitions> <local:CustomThumb DragDirection="MiddleCenter" Grid.RowSpan="3" Grid.ColumnSpan="3" Cursor="SizeAll" Style="{StaticResource AreaThumbStyle}"/> <local:CustomThumb DragDirection="TopLeft" Style="{StaticResource CornerThumbStyle}" Grid.Row="0" Grid.Column="0" HorizontalAlignment="Left" VerticalAlignment="Top" Cursor="SizeNWSE"/> <local:CustomThumb DragDirection="TopCenter" Style="{StaticResource CornerThumbStyle}" Grid.Row="0" Grid.Column="1" HorizontalAlignment="Center" VerticalAlignment="Top" Cursor="SizeNS"/> <local:CustomThumb DragDirection="TopRight" Style="{StaticResource CornerThumbStyle}" Grid.Row="0" Grid.Column="2" HorizontalAlignment="Right" VerticalAlignment="Top" Cursor="SizeNESW"/> <local:CustomThumb DragDirection="MiddleLeft" Style="{StaticResource CornerThumbStyle}" Grid.Row="1" Grid.Column="0" HorizontalAlignment="Left" VerticalAlignment="Center" Cursor="SizeWE"/> <local:CustomThumb DragDirection="MiddleRight" Style="{StaticResource CornerThumbStyle}" Grid.Row="1" Grid.Column="2" HorizontalAlignment="Right" VerticalAlignment="Center" Cursor="SizeWE"/> <local:CustomThumb DragDirection="BottomLeft" Style="{StaticResource CornerThumbStyle}" Grid.Row="2" Grid.Column="0" HorizontalAlignment="Left" VerticalAlignment="Bottom" Cursor="SizeNESW"/> <local:CustomThumb DragDirection="BottomCenter" Style="{StaticResource CornerThumbStyle}" Grid.Row="2" Grid.Column="1" HorizontalAlignment="Center" VerticalAlignment="Bottom" Cursor="SizeNS"/> <local:CustomThumb DragDirection="BottomRight" Style="{StaticResource CornerThumbStyle}" Grid.Row="2" Grid.Column="2" HorizontalAlignment="Right" VerticalAlignment="Bottom" Cursor="SizeNWSE"/> </Grid> </ControlTemplate> <Style TargetType="{x:Type local:DragHelperBase}" BasedOn="{StaticResource {x:Type ContentControl}}"> <Setter Property="BorderBrush" Value="Green"/> <Setter Property="BorderThickness" Value="1"/> <Setter Property="Padding" Value="0"/> <Setter Property="Margin" Value="0"/> <Setter Property="MinHeight" Value="5"/> <Setter Property="MinWidth" Value="5"/> <Setter Property="Template" Value="{StaticResource DrapControlHelperTemplate}"/> </Style> </ResourceDictionary>
下面編寫控制元件的建構函式,設定DefaultStyleKeyProperty,否則控制元件載入時將會找不到控制元件模板
static DragHelperBase()
{
DefaultStyleKeyProperty.OverrideMetadata(typeof(DragHelperBase),
new FrameworkPropertyMetadata(typeof(DragHelperBase)));
}
public DragHelperBase()
{
SetResourceReference(StyleProperty, typeof(DragHelperBase));
}
從控制元件模板可以看出,10個CustomThumb都在自定義控制元件的視覺樹中,所以我們可以使用Thumb的路由事件,接收滑鼠操作的事件並進行處理:
在重寫的方法OnApplyTemplate中新增路由事件訂閱:
public sealed override void OnApplyTemplate()
{
base.OnApplyTemplate();
MainGrid = GetPartFormTemplate<Grid>("PART_MainGrid");
AddLogicalChild(MainGrid);
AddHandler(Thumb.DragDeltaEvent, new DragDeltaEventHandler(OnDragDelta));
AddHandler(Thumb.DragCompletedEvent, new RoutedEventHandler(OnDragCompleted));
Visibility = Visibility.Collapsed;
}
可以看到在最後一句的程式碼中將自定義控制元件的Visibility屬性設為Collapsed,有2個原因,1.使用者沒選中目標控制元件前,我們的拖動控制元件是不應該顯示出來的,第二,如果在建構函式中設定這個屬性,OnApplyTemplate將不會被呼叫。
由於這是個抽象類,所以我們需要派生類提供幾個必須的方法:
protected abstract bool GetTargetIsEditable();
protected abstract Rect GetTargetActualBound();
protected abstract void SetTargetActualBound(Rect NewBound);
protected abstract void RaisenDragChangingEvent(Rect NewBound);
protected abstract void RaisenDragCompletedEvent(Rect NewBound);
另外,還要註冊2個路由事件,方便其他物件獲取目標控制元件的ActualBound:
#region Drag Event
public static readonly RoutedEvent DragChangingEvent
= EventManager.RegisterRoutedEvent("DragChangingEvent", RoutingStrategy.Bubble, typeof(DragChangedEventHandler), typeof(DragHelperBase));
public event DragChangedEventHandler DragChanging
{
add
{
AddHandler(DragChangingEvent, value);
}
remove
{
RemoveHandler(DragChangingEvent, value);
}
}
public static readonly RoutedEvent DragCompletedEvent
= EventManager.RegisterRoutedEvent("DragCompletedEvent", RoutingStrategy.Bubble, typeof(DragChangedEventHandler), typeof(DragHelperBase));
public event DragChangedEventHandler DragCompleted
{
add
{
AddHandler(DragCompletedEvent, value);
}
remove
{
RemoveHandler(DragCompletedEvent, value);
}
}
#endregion
public class DragChangedEventArgs : RoutedEventArgs
{
public DragChangedEventArgs(RoutedEvent Event, Rect NewBound, object Target = null) : base(Event)
{
this.NewBound = NewBound;
DragTargetElement = Target;
}
public Rect NewBound { get; private set; }
public object DragTargetElement { get; private set; }
}
public delegate void DragChangedEventHandler(object Sender, DragChangedEventArgs e);
當用戶點選目標控制元件時,我們的拖動控制元件應該顯示出來,而且,拖動控制元件的大小、位置應該跟目標控制元件一致:
#region SetupVisualPropertes
protected void SetupVisualPropertes(double TargetThickness, bool IsEditable)
{
Visibility IsCornerVisibe = IsEditable ? Visibility.Visible : Visibility.Collapsed;
double ActualMargin = (CornerWidth - TargetThickness) / 2.0;
//讓9個小框排布在目標邊框的中線上
MainGrid.Margin = new Thickness(0 - ActualMargin);
foreach (CustomThumb item in MainGrid.Children)
{
if (item != null)
{
item.BorderThickness = new Thickness(TargetThickness);
if (item.DragDirection == DragDirection.MiddleCenter)
{
item.Margin = new Thickness(ActualMargin);
}
else
{
item.Visibility = IsCornerVisibe;
}
}
}
}
#endregion
如果目標控制元件當前不允許編輯,則不要顯示四周的9個小框,只顯本體區域(虛線框),指示目標控制元件已經選中但不可以編輯。當用戶拖動滑鼠時,處理拖動事件:
private void OnDragDelta(object sender, DragDeltaEventArgs e)
{
if(!GetTargetIsEditable())
{
e.Handled = true;
return;
}
CustomThumb thumb = e.OriginalSource as CustomThumb;
if (thumb == null)
{
return;
}
double VerticalChange = e.VerticalChange;
double HorizontalChange = e.HorizontalChange;
Rect NewBound = Rect.Empty;
if (thumb.DragDirection == DragDirection.MiddleCenter)
{
NewBound = DragElement(HorizontalChange, VerticalChange);
}
else
{
NewBound = ResizeElement(thumb, HorizontalChange, VerticalChange);
}
RaisenDragChangingEvent(NewBound);
SetTargetActualBound(NewBound);
e.Handled = true;
}
private void OnDragCompleted(object sender, RoutedEventArgs e)
{
Rect NewBound = new Rect
{
Y = Canvas.GetTop(this),
X = Canvas.GetLeft(this),
Width = this.ActualWidth,
Height = this.ActualHeight
};
RaisenDragCompletedEvent(NewBound);
e.Handled = true;
}
下面是處理目標控制元件的拖動:修改目標控制元件的XY座標即可
private Rect DragElement(double HorizontalChange, double VerticalChange)
{
Rect TargetActualBound = GetTargetActualBound();
double TopOld = CorrectDoubleValue(TargetActualBound.Y);
double LeftOld = CorrectDoubleValue(TargetActualBound.X);
double TopNew = CorrectDoubleValue(TopOld + VerticalChange);
double LeftNew = CorrectDoubleValue(LeftOld + HorizontalChange);
TopNew = CorrectNewTop(DragHelperParent, TopNew, TargetActualBound.Height);
LeftNew = CorrectNewLeft(DragHelperParent, LeftNew, TargetActualBound.Width);
Canvas.SetTop(this, TopNew);
Canvas.SetLeft(this, LeftNew);
return new Rect
{
Y = TopNew,
X = LeftNew,
Width = TargetActualBound.Width,
Height = TargetActualBound.Height
};
}
下面是處理縮放目標控制元件,思考一下,其實原理就是從不同的方向放大或縮小目標控制元件的 Width & Height 屬性,或者XY座標,並且加上限制,不讓目標控制元件超過父控制元件(在這裡是Canvas)的邊界:
private Rect ResizeElement(CustomThumb HitedThumb, double HorizontalChange, double VerticalChange)
{
#region Get Old Value
if (HitedThumb == null) return Rect.Empty;
Rect TargetActualBound = GetTargetActualBound();
double TopOld = CorrectDoubleValue(TargetActualBound.Y);
double LeftOld = CorrectDoubleValue(TargetActualBound.X);
double WidthOld = CorrectDoubleValue(TargetActualBound.Width);
double HeightOld = CorrectDoubleValue(TargetActualBound.Height);
double TopNew = TopOld;
double LeftNew = LeftOld;
double WidthNew = WidthOld;
double HeightNew = HeightOld;
#endregion
if (HitedThumb.DragDirection == DragDirection.TopLeft
|| HitedThumb.DragDirection == DragDirection.MiddleLeft
|| HitedThumb.DragDirection == DragDirection.BottomLeft)
{
ResizeFromLeft(DragHelperParent, LeftOld, WidthOld, HorizontalChange, out LeftNew, out WidthNew);
}
if (HitedThumb.DragDirection == DragDirection.TopLeft
|| HitedThumb.DragDirection == DragDirection.TopCenter
|| HitedThumb.DragDirection == DragDirection.TopRight)
{
ResizeFromTop(DragHelperParent, TopOld, HeightOld, VerticalChange, out TopNew, out HeightNew);
}
if (HitedThumb.DragDirection == DragDirection.TopRight
|| HitedThumb.DragDirection == DragDirection.MiddleRight
|| HitedThumb.DragDirection == DragDirection.BottomRight)
{
ResizeFromRight(DragHelperParent, LeftOld, WidthOld, HorizontalChange, out WidthNew);
}
if (HitedThumb.DragDirection == DragDirection.BottomLeft
|| HitedThumb.DragDirection == DragDirection.BottomCenter
|| HitedThumb.DragDirection == DragDirection.BottomRight)
{
ResizeFromBottom(DragHelperParent, TopOld, HeightOld, VerticalChange, out HeightNew);
}
this.Width = WidthNew;
this.Height = HeightNew;
Canvas.SetTop(this, TopNew);
Canvas.SetLeft(this, LeftNew);
return new Rect
{
X = LeftNew,
Y = TopNew,
Width = WidthNew,
Height = HeightNew
};
}
下面是從不同的方向修改目標控制元件的XY座標或者Width & Height 屬性:
#region Resize Base Methods
#region ResizeFromTop
private static void ResizeFromTop(FrameworkElement Parent, double TopOld, double HeightOld, double VerticalChange, out double TopNew, out double HeightNew)
{
double MiniHeight = 10;
double top = TopOld + VerticalChange;
TopNew = ((top + MiniHeight) > (HeightOld + TopOld)) ? HeightOld + TopOld - MiniHeight : top;
TopNew = TopNew < 0 ? 0 : TopNew;
HeightNew = HeightOld + TopOld - TopNew;
HeightNew = CorrectNewHeight(Parent, TopNew, HeightNew);
}
#endregion
#region ResizeFromLeft
private static void ResizeFromLeft(FrameworkElement Parent, double LeftOld, double WidthOld, double HorizontalChange, out double LeftNew, out double WidthNew)
{
double MiniWidth = 10;
double left = LeftOld + HorizontalChange;
LeftNew = ((left + MiniWidth) > (WidthOld + LeftOld)) ? WidthOld + LeftOld - MiniWidth : left;
LeftNew = LeftNew < 0 ? 0 : LeftNew;
WidthNew = WidthOld + LeftOld - LeftNew;
WidthNew = CorrectNewWidth(Parent, LeftNew, WidthNew);
}
#endregion
#region ResizeFromRight
private static void ResizeFromRight(FrameworkElement Parent, double LeftOld, double WidthOld, double HorizontalChange, out double WidthNew)
{
if (LeftOld + WidthOld + HorizontalChange < Parent.ActualWidth)
{
WidthNew = WidthOld + HorizontalChange;
}
else
{
WidthNew = Parent.ActualWidth - LeftOld;
}
WidthNew = WidthNew < 0 ? 0 : WidthNew;
}
#endregion
#region ResizeFromBottom
private static void ResizeFromBottom(FrameworkElement Parent, double TopOld, double HeightOld, double VerticalChange, out double HeightNew)
{
if (TopOld + HeightOld + VerticalChange < Parent.ActualWidth)
{
HeightNew = HeightOld + VerticalChange;
}
else
{
HeightNew = Parent.ActualWidth - TopOld;
}
HeightNew = HeightNew < 0 ? 0 : HeightNew;
}
#endregion
#region CorrectNewTop
private static double CorrectNewTop(FrameworkElement Parent, double Top, double Height)
{
double NewHeight = ((Top + Height) > Parent.ActualHeight) ? (Parent.ActualHeight - Height) : Top;
return NewHeight < 0 ? 0 : NewHeight;
}
#endregion
#region CorrectNewLeft
private static double CorrectNewLeft(FrameworkElement Parent, double Left, double Width)
{
double NewLeft = ((Left + Width) > Parent.ActualWidth) ? (Parent.ActualWidth - Width) : Left;
return NewLeft < 0 ? 0 : NewLeft;
}
#endregion
#region CorrectNewWidth
private static double CorrectNewWidth(FrameworkElement Parent, double Left, double WidthNewToCheck)
{
double Width = ((Left + WidthNewToCheck) > Parent.ActualWidth) ? (Parent.ActualWidth - Left) : WidthNewToCheck;
return Width < 0 ? 0 : Width;
}
#endregion
#region CorrectNewHeight
private static double CorrectNewHeight(FrameworkElement Parent, double Top, double HeightNewToCheck)
{
double Height = ((Top + HeightNewToCheck) > Parent.ActualHeight) ? (Parent.ActualHeight - Top) : HeightNewToCheck;
return Height < 0 ? 0 : Height;
}
#endregion
#region CorrectDoubleValue
protected static double CorrectDoubleValue(double Value)
{
return (double.IsNaN(Value) || (Value < 0.0)) ? 0 : Value;
}
#endregion
#endregion
下面是測試效果:
完整的測試程式碼可以到我的github下載