起因

项目上需要对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

在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#如何在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://stackoverflow.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(右键菜单)使用Command在部分控件上默认为灰色的处理方法

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

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

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

随机推荐

  1. Hadoop的Map侧join

    写了关于Hadoop下载地址的Map侧join 和Reduce的join,今天我们就来在看另外一种比较中立的Join. SemiJoin,一般称为半链接,其原理是在Map侧过滤掉了一些不需要join的 ...

  2. Dynamics AX for Retail POS Development blogs

    Dynamics AX for Retail POS Development Dynamics AX for Retail POS Development - Code Samples AX for ...

  3. android ButterKnife 解决重复findViewById

    简介: 程序员都是懒惰的,不想写一大堆像下面这样的代码 class ExampleActivity extends Activity { TextView title; TextView subtit ...

  4. Android自动更新安装后显示‘完成’‘打开’按钮

    /** * 安装apk * * @param url */ private void installApk() { File apkfile = new File(apkFilePath); if ( ...

  5. C语言内存对齐详解

    一.字节对齐基本概念 现代计算机中内存空间都是按照byte划分的,从理论上讲似乎对任何类型的变量的访问可以从任何地址开始,但实际情况是在访问特定类型变量的时候经常在特定的内存地址访问,这就需要各种类型 ...

  6. Web应用程序系统的多用户权限控制设计及实现-栏目模块【8】

    前五章均是从整体上讲述了Web应用程序的多用户权限控制实现流程,本章讲述Web权限管理系统的基本模块-栏目模块.栏目模块涉及到的数据表为目录表. 1.1栏目域 为了更规范和方便后期系统的二次开发和维护 ...

  7. 多选按钮(CheckBox)

    今天我们介绍的是Checkbox多选框: 1.Activity //复选框,[基础控件]---状态切换控件CompoundButton及其子类CheckBox.RadioButton.ToggleBu ...

  8. android studio annotation 配置过程

    参考了好些配置,发现总有这样,那样的问题. 环境:androidstudio 1.5 preview 2 sdk 6.0 1.首先新建一个android项目. 过程略 2.配置project的buil ...

  9. 转 Android Dalvik虚拟机初识

    首先,让我们来思考下面几个问题: 什么是Dalvik虚拟机? Dalvik VM与JVM有什么区别? Dalvik VM有什么新的特点? Dalvik VM的架构是怎么样的? 首先,我得承认第一个问题 ...

  10. Effective Java 30 Use Enums instead of int constants

    Enumerated type is a type whose legal values consist of a fixed set of constants, such as the season ...