在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,處理相應的事件實現改變大小。值得注意的是,在左上角、右上角、左下角、上邊、左邊這些地方實際上不僅是改變大小,同時也會改變控件在宿主中的位置,所以我更願意稱之為調整布局。

主要類及其關系如下:

1

添加CanvasArrangementAdorner之后控件效果如下(淺藍色為控件):

2

因為將Thumb設為透明了,看不出來是由8個Thumb組成的,如果改下顏色,會更容易理解些。

2

可以很明顯的看出,在四個角和四條邊上各有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


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM