在WPF控件上添加Windows窗口式调整大小行为

时间:2021-09-29 05:12:44

起因

项目上需要对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,处理相应的事件实现改变大小。值得注意的是,在左上角、右上角、左下角、上边、左边这些地方实际上不仅是改变大小,同时也会改变控件在宿主中的位置,所以我更愿意称之为调整布局。

主要类及其关系如下:

在WPF控件上添加Windows窗口式调整大小行为

添加CanvasArrangementAdorner之后控件效果如下(浅蓝色为控件):

在WPF控件上添加Windows窗口式调整大小行为

因为将Thumb设为透明了,看不出来是由8个Thumb组成的,如果改下颜色,会更容易理解些。

在WPF控件上添加Windows窗口式调整大小行为

可以很明显的看出,在四个角和四条边上各有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

在WPF控件上添加Windows窗口式调整大小行为的更多相关文章

  1. Android控件上添加图片

    项目中有一个点赞功能,点赞的小图标添加在点赞列表旁边,在xml里可以进行设置,也可以在代码中进行绘图. 下面是两种方法的设置: 1.xml里:一些控件:button.textView等等里面有个属性是 ...

  2. 如何在WPF控件上应用简单的褪色透明效果?

    原文 https://dailydotnettips.com/how-to-create-simple-faded-transparent-controls-in-wpf/ 使用OpacityMask ...

  3. C&num;如何在panl控件上添加Form窗体

    . if (treeView1.SelectedNode.Text == "个人信息") { Form1 f4 = new Form1(); f4.TopLevel = false ...

  4. 张奎师弟参与devexpress chartControl绘图--解决了devexpress的chartControl控件不能添加系列的问题

    using DevExpress.XtraCharts; using System; using System.Collections.Generic; using System.ComponentM ...

  5. 使用触发器定义 WPF 控件的行为

    Expression Studio 4.0   其他版本 Expression Studio 3.0 Expression Studio 2.0   此主题尚未评级 - 评价此主题   在应用程序的生 ...

  6. 在 WPF 中如何在控件上屏蔽系统默认的触摸长按事件

    来源:https://*.com/questions/5962108/disable-a-right-click-press-and-hold-in-wpf-applicati ...

  7. WPF如何将数据库中的二进制图片数据显示在Image控件上

    首先在xaml文件里定义一个Image控件,取名为img MemoryStream stream = new MemoryStream(获得的数据库对象): BitMapImage bmp = new ...

  8. WPF中ContextMenu&lpar;右键菜单&rpar;使用Command在部分控件上默认为灰色的处理方法

    原文:WPF中ContextMenu(右键菜单)使用Command在部分控件上默认为灰色的处理方法 问题描述 今天发现如果我想在一个TextBlock弄一个右键菜单,并且使用Command绑定,结果发 ...

  9. 如何获得 Qt窗口部件在主窗口中的位置--确定鼠标是否在某一控件上与在控件上的位置

    用Qt Creator 设计程序时,最方便的就是ui设计器,可以很容易的得到想要的布局. 但是这样自动布局带来的后果是很难知道窗口中某一部件在主窗口中的相对位置. 在处理子窗口鼠标事件时变的很麻烦.主 ...

随机推荐

  1. pycharm2016 激活

    pycharm 2016 专业版 激活方式选第二种 code 43B4A73YYJ-eyJsaWNlbnNlSWQiOiI0M0I0QTczWVlKIiwibGljZW5zZWVOYW1lIjoibG ...

  2. &lbrack;转&rsqb; 添加新的系统调用 &lowbar;syscall0&lpar;int&comma; mysyscall&rpar;

    实验目的阅读 Linux 内核源代码,通过添加一个简单的系统调用实验,进一步理解Linux操作系统处理系统调用的统一流程.通过用kernel module的方法来实现一个系统调用实验,进一步理解Lin ...

  3. storysnail的Linux串口编程笔记

    storysnail的Linux串口编程笔记 作者 He YiJun – storysnail<at>gmail.com 团队 ls 版权 转载请保留本声明! 本文档包含的原创代码根据Ge ...

  4. 使用 &period;gitignore来忽略某些文件【转】

    转自:http://www.cnblogs.com/shangdawei/archive/2012/09/08/2676493.htmlhttp://blog.csdn.net/richardyste ...

  5. log4net 日志文件占用,不能及时释放

    在appender 下面加 <lockingModel type="log4net.Appender.FileAppender+MinimalLock" />

  6. 不可不知的socket和TCP连接过程

    html { font-family: sans-serif } body { margin: 0 } article,aside,details,figcaption,figure,footer,h ...

  7. Spark2 文件处理和jar包执行

    上传数据文件 mkdir -p data/ml/ hadoop fs -mkdir -p /datafile/wangxiao/ hadoop fs -ls / hadoop fs -put /hom ...

  8. python list 去掉重复元素

    貌似用遍历最方便. http://www.cnblogs.com/tudas/p/python-delete-duplicate-element-from-list.html

  9. SQL Server 日期格式和日期操作

    SQL Server发展至今,关于日期的格式的控制方法,有传统的方法,比如CONVERT(),也有比较便利的新方法,比如FORMAT():同样,关于日期的操作函数,也分为传统方法:DATEADD()等 ...

  10. Hibernate 一对一 (one-to-one)

    一对一(one-to-one)实例(Person-IdCard) 一对一的关系在数据库中表示为主外关系.例如.人和身份证的关系.每个人都对应一个身份证号.我们应该两个表.一个是关于人信息的表(Pers ...