起因
項目上需要對Canvas中的控件添加調整大小功能,即能在控件的四個角和四條邊上可進行相應的拖動,類似Windows窗口那種。於是在參考以前同事寫的代碼基礎上,完成了該功能。
代碼實現
Adorner
我們是給現有的控件添加功能,屬於裝飾功能。當然首先想到的就是Adorner。在MSDN中Adorner的介紹如下:
裝飾器是一個綁定到 UIElement 的自定義 FrameworkElement。 裝飾器呈現在裝飾器層中,它是一個呈現圖面,始終位於裝飾元素或裝飾元素集合的頂部;呈現裝飾器獨立於呈現該裝飾器綁定到的 UIElement。 裝飾器通常相對於其綁定到的元素進行定位,且使用位於裝飾元素的左上部的標准 2-D 坐標原點進行定位。
關於Adorner更詳細的信息,可參考WPF - Adorner - loveis715 - 博客園。Adorner是一個抽象類,我們可以繼承自該類來實現自己的裝飾功能。
Thumb
WPF中存在支持拖動的Thumb控件,而且Thumb控件繼承自Control,可以定義控件模板。Thumb最重要的三個事件如下:
Thumb 提供 DragStarted, DragCompleted 和 DragDelta 事件來管理與鼠標指針相關的拖動操作。 當用戶按下鼠標左鍵時,Thumb 控件接收邏輯焦點和鼠標捕獲,並引發 DragStarted 事件。 在 Thumb 控件具有焦點和鼠標捕獲的同時,可以無限制地多次引發 DragDelta 事件。 當用戶釋放鼠標左鍵時,Thumb 控件失去鼠標捕獲,並引發 DragCompleted 事件。
實現原理
思路很明確,就是自定義一個Adorner,在四條邊和四個角上添加相應的Thumb,處理相應的事件實現改變大小。值得注意的是,在左上角、右上角、左下角、上邊、左邊這些地方實際上不僅是改變大小,同時也會改變控件在宿主中的位置,所以我更願意稱之為調整布局。
主要類及其關系如下:
添加CanvasArrangementAdorner之后控件效果如下(淺藍色為控件):
因為將Thumb設為透明了,看不出來是由8個Thumb組成的,如果改下顏色,會更容易理解些。
可以很明顯的看出,在四個角和四條邊上各有4個Thumb,我重新定義了Thumb的控件模板,控件模板內部是一個Rectangle。
主要類
各個主要類如下,因代碼較簡單,就不多解釋了。
ArrangementDirection
using System; /// <summary> /// 布局方向 /// </summary> [Flags] public enum ArrangementDirection { None = 0, LeftTop = 1, Top = 2, RightTop = 4, Right = 8, RightBottom = 16, Bottom = 32, LeftBottom = 64, Left = 128, All = LeftTop | Top | RightTop | Right | RightBottom | Bottom | LeftBottom | Left, }
ArrangementChangedEventArgs
using System; using System.Windows; /// <summary> /// 布局變化的事件 /// </summary> public class ArrangementChangedEventArgs : EventArgs { public ArrangementChangedEventArgs(Rect oldArrangement, Rect newArrangement) { this.OldArrangement = oldArrangement; this.NewArrangement = newArrangement; } /// <summary> /// 舊布局信息 /// </summary> public Rect OldArrangement { get; private set; } /// <summary> /// 新布局信息 /// </summary> public Rect NewArrangement { get; private set; } }
ArrangementDirection
using System; /// <summary> /// 布局方向 /// </summary> [Flags] public enum ArrangementDirection { None = 0, LeftTop = 1, Top = 2, RightTop = 4, Right = 8, RightBottom = 16, Bottom = 32, LeftBottom = 64, Left = 128, All = LeftTop | Top | RightTop | Right | RightBottom | Bottom | LeftBottom | Left, }
ArrangementAdorner
using System; using System.Diagnostics.Contracts; using System.Windows; using System.Windows.Controls; using System.Windows.Controls.Primitives; using System.Windows.Documents; using System.Windows.Input; using System.Windows.Media; using System.Windows.Shapes; /// <summary> /// 布局裝飾器 /// </summary> public abstract class ArrangementAdorner : Adorner { #region Fields /// <summary> /// 拖動方塊的邊長 /// </summary> private const double ThumbSideLength = 6; /// <summary> /// 可視化對象集合 /// </summary> private readonly VisualCollection visualCollection; /// <summary> /// 對齊方向 /// </summary> private readonly ArrangementDirection direction; /// <summary> /// 各個方向的拖動方塊 /// </summary> private readonly Thumb topThumb, leftTopthumb, rightTopThumb, righThumb, rightBottomThumb, bottomThumb, leftBottomThumb, leftThumb; /// <summary> /// 當前位置 /// </summary> private Point currentLocation; /// <summary> /// 拖動前的大小 /// </summary> private Size oldSize; /// <summary> /// 拖動前左邊緣的值 /// </summary> private double oldLeft; /// <summary> /// 拖動前上邊緣的值 /// </summary> private double oldTop; #endregion Fields #region Constructors /// <summary> /// 構造函數 /// </summary> /// <param name="adornedElement">裝飾器所要綁定到的元素。</param> /// <param name="arrangementDirection">布局方向</param> protected ArrangementAdorner(FrameworkElement adornedElement, ArrangementDirection arrangementDirection = ArrangementDirection.All) : base(adornedElement) { this.direction = arrangementDirection; this.visualCollection = new VisualCollection(this); this.AddThumbIfNeeded( ref this.leftTopthumb, ArrangementDirection.LeftTop, HorizontalAlignment.Left, VerticalAlignment.Top, Cursors.SizeNWSE); this.AddThumbIfNeeded( ref this.topThumb, ArrangementDirection.Top, HorizontalAlignment.Stretch, VerticalAlignment.Top, Cursors.SizeNS); this.AddThumbIfNeeded( ref this.rightTopThumb, ArrangementDirection.RightTop, HorizontalAlignment.Right, VerticalAlignment.Top, Cursors.SizeNESW); this.AddThumbIfNeeded( ref this.righThumb, ArrangementDirection.Right, HorizontalAlignment.Right, VerticalAlignment.Stretch, Cursors.SizeWE); this.AddThumbIfNeeded( ref this.rightBottomThumb, ArrangementDirection.RightBottom, HorizontalAlignment.Right, VerticalAlignment.Bottom, Cursors.SizeNWSE); this.AddThumbIfNeeded( ref this.bottomThumb, ArrangementDirection.Bottom, HorizontalAlignment.Stretch, VerticalAlignment.Bottom, Cursors.SizeNS); this.AddThumbIfNeeded( ref this.leftBottomThumb, ArrangementDirection.LeftBottom, HorizontalAlignment.Left, VerticalAlignment.Bottom, Cursors.SizeNESW); this.AddThumbIfNeeded( ref this.leftThumb, ArrangementDirection.Left, HorizontalAlignment.Left, VerticalAlignment.Stretch, Cursors.SizeWE); } #endregion Constructors public event EventHandler<ArrangementChangedEventArgs> ArrangementChanged; #region Protected Methods #region Overrides /// <summary> /// 獲取此元素內的可視化子元素的數目。 /// </summary> /// <returns> /// 此元素內的可視化子元素的數目。 /// </returns> protected override int VisualChildrenCount { get { return this.visualCollection.Count; } } /// <summary> /// 定位子元素並確定大小。 /// </summary> /// <returns> /// 所用的實際大小。 /// </returns> /// <param name="finalSize">排列自身及其子元素的最終區域。</param> protected override Size ArrangeOverride(Size finalSize) { this.ArrangeThumbIfNeeded( this.leftTopthumb, new Point(-ThumbSideLength, -ThumbSideLength), new Size(ThumbSideLength, ThumbSideLength)); this.ArrangeThumbIfNeeded( this.topThumb, new Point(0, -ThumbSideLength), new Size(finalSize.Width, ThumbSideLength)); this.ArrangeThumbIfNeeded( this.rightTopThumb, new Point(finalSize.Width, -ThumbSideLength), new Size(ThumbSideLength, ThumbSideLength)); this.ArrangeThumbIfNeeded( this.righThumb, new Point(finalSize.Width, 0), new Size(ThumbSideLength, finalSize.Height)); this.ArrangeThumbIfNeeded( this.rightBottomThumb, new Point(finalSize.Width, finalSize.Height), new Size(ThumbSideLength, ThumbSideLength)); this.ArrangeThumbIfNeeded( this.bottomThumb, new Point(0, finalSize.Height), new Size(finalSize.Width, ThumbSideLength)); this.ArrangeThumbIfNeeded( this.leftBottomThumb, new Point(-ThumbSideLength, finalSize.Height), new Size(ThumbSideLength, ThumbSideLength)); this.ArrangeThumbIfNeeded( this.leftThumb, new Point(-ThumbSideLength, 0), new Size(ThumbSideLength, finalSize.Height)); return base.ArrangeOverride(finalSize); } /// <summary> /// 從子元素集合返回指定索引處的子級。 /// </summary> /// <returns> /// 所請求的子元素。它不應返回 null;如果提供的索引超出范圍,將引發異常。 /// </returns> /// <param name="index">集合中所請求子元素從零開始的索引。</param> protected override Visual GetVisualChild(int index) { return this.visualCollection[index]; } #endregion Overrides #region Virtuals /// <summary> /// 創建布局方塊 /// </summary> /// <param name="horizontalAlignment">方塊的水平對齊方向</param> /// <param name="verticalAlignment">方塊的垂直對齊方向</param> /// <param name="cursor">方塊的光標</param> /// <returns>創建好的方塊</returns> protected virtual Thumb CreateResizeThumb( HorizontalAlignment horizontalAlignment, VerticalAlignment verticalAlignment, Cursor cursor) { var thumb = new Thumb { HorizontalAlignment = horizontalAlignment, VerticalAlignment = verticalAlignment, Cursor = cursor, Template = this.GetResizeThumbControlTemplate() }; return thumb; } /// <summary> /// 獲取框架元素的位置 /// </summary> /// <param name="element">框架元素</param> /// <returns>框架元素所在的位置</returns> protected abstract Point GetLocation(FrameworkElement element); /// <summary> /// 判斷框架元素的位置偏移是否合法 /// </summary> /// <param name="element">框架元素</param> /// <param name="offset">偏移向量</param> /// <returns>合法返回true,否則返回false</returns> protected virtual bool IsLocationOffsetLegal(FrameworkElement element, Vector offset) { var targetLocation = this.currentLocation + offset; if (targetLocation.X < 0) { return false; } return true; } /// <summary> /// 設置框架元素的位置 /// </summary> /// <param name="element">框架元素</param> /// <param name="location">新位置</param> protected abstract void SetLocation(FrameworkElement element, Point location); /// <summary> /// 獲取框架元素的寬度 /// </summary> /// <param name="element">框架元素</param> /// <returns>寬度</returns> protected virtual double GetWidth(FrameworkElement element) { return element.Width; } /// <summary> /// 獲取框架元素的高度 /// </summary> /// <param name="element">框架元素</param> /// <returns>高度</returns> protected virtual double GetHeight(FrameworkElement element) { return element.Height; } /// <summary> /// 判斷框架元素的寬度變化是否合法 /// </summary> /// <param name="element">框架元素</param> /// <param name="widthDelta">寬度變化</param> /// <returns>變化是否合法</returns> protected virtual bool IsWidthDeltaLegal(FrameworkElement element, double widthDelta) { double newWidth = this.GetWidth(element) + widthDelta; return this.IsInRange(element.MaxWidth, element.MinWidth, newWidth); } /// <summary> /// 判斷框架元素的高度變化是否合法 /// </summary> /// <param name="element">框架元素</param> /// <param name="heightDelta">高度變化</param> /// <returns>變化是否合法</returns> protected virtual bool IsHeightDeltaLegal(FrameworkElement element, double heightDelta) { double newHeight = this.GetHeight(element) + heightDelta; return this.IsInRange(element.MaxHeight, element.MinHeight, newHeight); } /// <summary> /// 設置框架元素的寬度變化 /// </summary> /// <param name="element">框架元素</param> /// <param name="widthDelta">寬度變化</param> protected virtual void SetWidthDelta(FrameworkElement element, double widthDelta) { element.Width += widthDelta; } /// <summary> /// 設置框架元素的高度變化 /// </summary> /// <param name="element">框架元素</param> /// <param name="heightDelta">高度變化</param> protected virtual void SetHeightDelta(FrameworkElement element, double heightDelta) { element.Height += heightDelta; } #endregion Virtuals #endregion Protected Methods #region Private Methods /// <summary> /// 在需要時添加拖動方塊 /// </summary> /// <param name="thumb">類中對應的方塊</param> /// <param name="arrangementDirection">方塊對應的布局方向</param> /// <param name="horizontalAlignment">方塊的水平對齊方向</param> /// <param name="verticalAlignment">方塊的垂直對齊方向</param> /// <param name="cursor">方塊的光標</param> private void AddThumbIfNeeded( ref Thumb thumb, ArrangementDirection arrangementDirection, HorizontalAlignment horizontalAlignment, VerticalAlignment verticalAlignment, Cursor cursor) { if (this.HasDirectionFlagSet(arrangementDirection)) { thumb = this.CreateResizeThumb(horizontalAlignment, verticalAlignment, cursor); thumb.DragStarted += this.ThumbDragStarted; thumb.DragDelta += this.ThumbDragDelta; thumb.DragCompleted += this.ThumbDragCompleted; this.visualCollection.Add(thumb); } } /// <summary> /// 判斷布局方向是否被設置 /// </summary> /// <param name="arrangementDirection">布局方向</param> /// <returns>被設置返回true,否則返回false</returns> private bool HasDirectionFlagSet(ArrangementDirection arrangementDirection) { return (this.direction & arrangementDirection) == arrangementDirection; } /// <summary> /// 獲取布局方塊的控件模板 /// </summary> /// <returns>控件模板</returns> private ControlTemplate GetResizeThumbControlTemplate() { var factory = new FrameworkElementFactory(typeof(Rectangle)); factory.SetValue(Shape.FillProperty, Brushes.Transparent); factory.SetValue(Shape.StrokeProperty, Brushes.Transparent); var controlTemplate = new ControlTemplate { TargetType = typeof(Thumb), VisualTree = factory }; return controlTemplate; } /// <summary> /// 在需要時定位並確定方塊大小 /// </summary> /// <param name="thumb">方塊</param> /// <param name="location">方塊位置</param> /// <param name="size">方塊大小</param> private void ArrangeThumbIfNeeded(Thumb thumb, Point location, Size size) { if (thumb != null) { if (thumb.HorizontalAlignment != HorizontalAlignment.Stretch) { thumb.Width = size.Width; } if (thumb.VerticalAlignment != VerticalAlignment.Stretch) { thumb.Height = size.Height; } thumb.Arrange(new Rect(location, size)); } } /// <summary> /// 判斷一個值是否在范圍中 /// </summary> /// <param name="maximum">最大值,可取</param> /// <param name="minimum">最小值,可取</param> /// <param name="value">值</param> /// <returns>值在范圍中返回true,否則返回false</returns> private bool IsInRange(double maximum, double minimum, double value) { return (value >= minimum) && (value <= maximum); } #endregion Private Methods #region Events Handler /// <summary> /// 拖動開始的響應 /// </summary> /// <param name="sender"></param> /// <param name="e"></param> private void ThumbDragStarted(object sender, DragStartedEventArgs e) { var frameworkElement = this.AdornedElement as FrameworkElement; this.currentLocation = this.GetLocation(frameworkElement); this.oldLeft = this.currentLocation.X; this.oldTop = this.currentLocation.Y; var width = this.GetWidth(frameworkElement); var height = this.GetHeight(frameworkElement); this.oldSize = new Size(width, height); } /// <summary> /// 拖動變化的響應 /// </summary> /// <param name="sender"></param> /// <param name="e"></param> private void ThumbDragDelta(object sender, DragDeltaEventArgs e) { var frameworkElement = this.AdornedElement as FrameworkElement; var thumb = sender as Thumb; Contract.Assert(thumb != null); switch (thumb.HorizontalAlignment) { case HorizontalAlignment.Left: { var offset = new Vector(e.HorizontalChange, 0); if (this.IsLocationOffsetLegal(frameworkElement, offset)) { this.currentLocation.Offset(e.HorizontalChange, 0); if (this.IsWidthDeltaLegal(frameworkElement, -e.HorizontalChange)) { this.SetWidthDelta(frameworkElement, -e.HorizontalChange); } } break; } case HorizontalAlignment.Right: { if (this.IsWidthDeltaLegal(frameworkElement, e.HorizontalChange)) { this.SetWidthDelta(frameworkElement, e.HorizontalChange); } break; } } switch (thumb.VerticalAlignment) { case VerticalAlignment.Top: { var offset = new Vector(0, e.VerticalChange); if (this.IsLocationOffsetLegal(frameworkElement, offset)) { this.currentLocation.Offset(0, e.VerticalChange); if (this.IsHeightDeltaLegal(frameworkElement, -e.VerticalChange)) { this.SetHeightDelta(frameworkElement, -e.VerticalChange); } } break; } case VerticalAlignment.Bottom: { if (this.IsHeightDeltaLegal(frameworkElement, e.VerticalChange)) { this.SetHeightDelta(frameworkElement, e.VerticalChange); } break; } } this.SetLocation(frameworkElement, this.currentLocation); } /// <summary> /// 拖動結束的響應 /// </summary> /// <param name="sender"></param> /// <param name="e"></param> private void ThumbDragCompleted(object sender, DragCompletedEventArgs e) { if (this.ArrangementChanged != null) { var frameworkElement = this.AdornedElement as FrameworkElement; var oldArrangement = new Rect(new Point(this.oldLeft, this.oldTop), this.oldSize); var newArrangement = new Rect( this.GetLocation(frameworkElement), new Size(this.GetWidth(frameworkElement), this.GetHeight(frameworkElement))); this.ArrangementChanged(this, new ArrangementChangedEventArgs(oldArrangement, newArrangement)); } } #endregion Events Handler }
CanvasArrangementAdorner
using System.Windows; using System.Windows.Controls; /// <summary> /// 畫布布局裝飾器 /// </summary> public class CanvasArrangementAdorner : ArrangementAdorner { /// <summary> /// 構造函數 /// </summary> /// <param name="adornedElement">裝飾器所要綁定到的元素。</param> /// <param name="arrangementDirection">布局方向</param> public CanvasArrangementAdorner(FrameworkElement adornedElement, ArrangementDirection arrangementDirection = ArrangementDirection.All) : base(adornedElement, arrangementDirection) { } #region Overrides of ArrangementAdorner /// <summary> /// 獲取框架元素的位置 /// </summary> /// <param name="element">框架元素</param> /// <returns>框架元素所在的位置</returns> protected override Point GetLocation(FrameworkElement element) { return new Point(Canvas.GetLeft(element), Canvas.GetTop(element)); } /// <summary> /// 設置框架元素的位置 /// </summary> /// <param name="element">框架元素</param> /// <param name="location">新位置</param> protected override void SetLocation(FrameworkElement element, Point location) { Canvas.SetLeft(element, location.X); Canvas.SetTop(element, location.Y); } #endregion }
代碼下載
博客園:ControlResize