在WPF控件上添加Windows窗口式调整大小行为
起因
项目上需要对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