WPF進階教程 - 使用Decorator自定義帶三角形的邊框


寫下來,備忘。      

  Decorator,有裝飾器、裝飾品的意思,很容易讓人聯想到設計模式里面的裝飾器模式。Decorator類負責包裝某個UI元素,用來提供額外的行為。它有一個類型為UIElement的Child屬性,其中含有待包裝的內容。Decorator可以被用於添加簡單的視覺裝飾,比如Border邊框,或者更為復雜的行為,比如ViewBox、AdornerDecorator。

       當我們繼承Decorator時,也可以自定義添加一些依賴屬性,比如Border就定義了BorderBrush,BorderThinckness等用來設置Border的樣式。
 
在想到自定義帶三角形的Border之前,我們會想到這么幾個問題
1、邊框如何根據里面的內容大小變化而變化
2、如何才能使三角形和矩形的連接處無縫對接
 
       在之前,我都是使用Grid布局,上面使用BorderThiness為0的Border來包裹文字或者其他空間,下面使用一個三角形Path,這樣可以粗略的實現類似效果,但是這個有很大的一個問題就是不能設置BorderThiness,否則三角形Path和Border的連接處會有一根線,無法去除。之外這樣寫一點都不通用,很傻,但怎么辦呢。冥冥中自有天意,無意中看到了Decorator,真是柳暗花明(其實還是自己基礎知識不扎實,否則怎么會不知道Decorator)。
 
好了,在開始自定義控件之前,需要先了解Decorator的一個工作原理。要繪制邊框,首先這個邊框得先知道我里面包裹的Child元素到底有多大,這就涉及到容器的計算問題。
 
容器的計算規則
        計算容器永遠都是先測量( MeasureOverride ),然后通知父元素分配控件,計算好控件后就需要設置子元素的大小與位置(ArrangeOverride),最后准備工作都做好了之后,就要開始繪制了(OnRender)。
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Media;

namespace WpfDemo
{
    public sealed class AngleBorder : Decorator
    {
        public enum EnumPlacement
        {
            /// <summary>
            /// 左上
            /// </summary>
            LeftTop,
            /// <summary>
            /// 左中
            /// </summary>
            LeftBottom,
            /// <summary>
            /// 左下
            /// </summary>
            LeftCenter,
            /// <summary>
            /// 右上
            /// </summary>
            RightTop,
            /// <summary>
            /// 右下
            /// </summary>
            RightBottom,
            /// <summary>
            /// 右中
            /// </summary>
            RightCenter,
            /// <summary>
            /// 上左
            /// </summary>
            TopLeft,
            /// <summary>
            /// 上中
            /// </summary>
            TopCenter,
            /// <summary>
            /// 上右
            /// </summary>
            TopRight,
            /// <summary>
            /// 下左
            /// </summary>
            BottomLeft,
            /// <summary>
            /// 下中
            /// </summary>
            BottomCenter,
            /// <summary>
            /// 下右
            /// </summary>
            BottomRight,
        }

        #region 依賴屬性
        public static readonly DependencyProperty PlacementProperty =
            DependencyProperty.Register("Placement", typeof(EnumPlacement), typeof(AngleBorder),
            new FrameworkPropertyMetadata(EnumPlacement.RightCenter, FrameworkPropertyMetadataOptions.AffectsRender, OnDirectionPropertyChangedCallback));

        public EnumPlacement Placement
        {
            get { return (EnumPlacement)GetValue(PlacementProperty); }
            set { SetValue(PlacementProperty, value); }
        }

        public static readonly DependencyProperty TailWidthProperty =
            DependencyProperty.Register("TailWidth", typeof(double), typeof(AngleBorder), new PropertyMetadata(10d));
        /// <summary>
        /// 尾巴的寬度,默認值為7
        /// </summary>
        public double TailWidth
        {
            get { return (double)GetValue(TailWidthProperty); }
            set { SetValue(TailWidthProperty, value); }
        }
        public static readonly DependencyProperty TailHeightProperty =
            DependencyProperty.Register("TailHeight", typeof(double), typeof(AngleBorder), new PropertyMetadata(10d));
        /// <summary>
        /// 尾巴的高度,默認值為10
        /// </summary>
        public double TailHeight
        {
            get { return (double)GetValue(TailHeightProperty); }
            set { SetValue(TailHeightProperty, value); }
        }

        public static readonly DependencyProperty TailVerticalOffsetProperty =
            DependencyProperty.Register("TailVerticalOffset", typeof(double), typeof(AngleBorder), new PropertyMetadata(13d));
        /// <summary>
        /// 尾巴距離頂部的距離,默認值為10
        /// </summary>
        public double TailVerticalOffset
        {
            get { return (double)GetValue(TailVerticalOffsetProperty); }
            set { SetValue(TailVerticalOffsetProperty, value); }
        }
        public static readonly DependencyProperty TailHorizontalOffsetProperty =
            DependencyProperty.Register("TailHorizontalOffset", typeof(double), typeof(AngleBorder), 
                new PropertyMetadata(12d));
        /// <summary>
        /// 尾巴距離頂部的距離,默認值為10
        /// </summary>
        public double TailHorizontalOffset
        {
            get { return (double)GetValue(TailHorizontalOffsetProperty); }
            set { SetValue(TailHorizontalOffsetProperty, value); }
        }
        public static readonly DependencyProperty BackgroundProperty =
            DependencyProperty.Register("Background", typeof(Brush), typeof(AngleBorder)
                , new PropertyMetadata(new SolidColorBrush(Color.FromRgb(255, 255, 255))));
        /// <summary>
        /// 背景色,默認值為#FFFFFF,白色
        /// </summary>
        public Brush Background
        {
            get { return (Brush)GetValue(BackgroundProperty); }
            set { SetValue(BackgroundProperty, value); }
        }

        public static readonly DependencyProperty PaddingProperty =
            DependencyProperty.Register("Padding", typeof(Thickness), typeof(AngleBorder)
                , new PropertyMetadata(new Thickness(10, 5, 10, 5)));
        /// <summary>
        /// 內邊距
        /// </summary>
        public Thickness Padding
        {
            get { return (Thickness)GetValue(PaddingProperty); }
            set { SetValue(PaddingProperty, value); }
        }

        public static readonly DependencyProperty BorderBrushProperty =
            DependencyProperty.Register("BorderBrush", typeof(Brush), typeof(AngleBorder)
                , new PropertyMetadata(default(Brush)));
        /// <summary>
        /// 邊框顏色
        /// </summary>
        public Brush BorderBrush
        {
            get { return (Brush)GetValue(BorderBrushProperty); }
            set { SetValue(BorderBrushProperty, value); }
        }

        public static readonly DependencyProperty BorderThicknessProperty =
            DependencyProperty.Register("BorderThickness", typeof(Thickness), typeof(AngleBorder), new PropertyMetadata(new Thickness(1d)));
        /// <summary>
        /// 邊框大小
        /// </summary>
        public Thickness BorderThickness
        {
            get { return (Thickness)GetValue(BorderThicknessProperty); }
            set { SetValue(BorderThicknessProperty, value); }
        }

        public static readonly DependencyProperty CornerRadiusProperty =
            DependencyProperty.Register("CornerRadius", typeof(System.Windows.CornerRadius)
                , typeof(AngleBorder), new PropertyMetadata(new CornerRadius(0)));
        /// <summary>
        /// 邊框大小
        /// </summary>
        public System.Windows.CornerRadius CornerRadius
        {
            get { return (System.Windows.CornerRadius)GetValue(CornerRadiusProperty); }
            set { SetValue(CornerRadiusProperty, value); }
        }
        #endregion

        #region 方法重寫
        /// <summary>
        /// 該方法用於測量整個控件的大小
        /// </summary>
        /// <param name="constraint"></param>
        /// <returns>控件的大小</returns>
        protected override Size MeasureOverride(Size constraint)
        {
            Thickness padding = this.Padding;

            Size result = new Size();
            if (Child != null)
            {
                //測量子控件的大小
                Child.Measure(constraint);

                //三角形在左邊與右邊的,整個容器的寬度則為:里面子控件的寬度 + 設置的padding + 三角形的寬度
                //三角形在上面與下面的,整個容器的高度則為:里面子控件的高度 + 設置的padding + 三角形的高度
                switch (Placement)
                {
                    case EnumPlacement.LeftTop:
                    case EnumPlacement.LeftBottom:
                    case EnumPlacement.LeftCenter:
                    case EnumPlacement.RightTop:
                    case EnumPlacement.RightBottom:
                    case EnumPlacement.RightCenter:
                        result.Width = Child.DesiredSize.Width + padding.Left + padding.Right + this.TailWidth;
                        result.Height = Child.DesiredSize.Height + padding.Top + padding.Bottom;
                        break;
                    case EnumPlacement.TopLeft:
                    case EnumPlacement.TopCenter:
                    case EnumPlacement.TopRight:
                    case EnumPlacement.BottomLeft:
                    case EnumPlacement.BottomCenter:
                    case EnumPlacement.BottomRight:
                        result.Width = Child.DesiredSize.Width + padding.Left + padding.Right;
                        result.Height = Child.DesiredSize.Height + padding.Top + padding.Bottom + this.TailHeight;
                        break;
                    default:
                        break;
                }
            }
            return result;
        }

        /// <summary>
        /// 設置子控件的大小與位置
        /// </summary>
        /// <param name="arrangeSize"></param>
        /// <returns></returns>
        protected override Size ArrangeOverride(Size arrangeSize)
        {
            Thickness padding = this.Padding;
            if (Child != null)
            {
                switch (Placement)
                {
                    case EnumPlacement.LeftTop:
                    case EnumPlacement.LeftBottom:
                    case EnumPlacement.LeftCenter:
                        Child.Arrange(new Rect(new Point(padding.Left + this.TailWidth, padding.Top), Child.DesiredSize));
                        //ArrangeChildLeft();
                        break;
                    case EnumPlacement.RightTop:
                    case EnumPlacement.RightBottom:
                    case EnumPlacement.RightCenter:
                        ArrangeChildRight(padding);
                        break;
                    case EnumPlacement.TopLeft:
                    case EnumPlacement.TopRight:
                    case EnumPlacement.TopCenter:
                        Child.Arrange(new Rect(new Point(padding.Left, this.TailHeight + padding.Top), Child.DesiredSize));
                        break;
                    case EnumPlacement.BottomLeft:
                    case EnumPlacement.BottomRight:
                    case EnumPlacement.BottomCenter:
                        Child.Arrange(new Rect(new Point(padding.Left, padding.Top), Child.DesiredSize));
                        break;
                    default:
                        break;
                }
            }
            return arrangeSize;
        }

        private void ArrangeChildRight(Thickness padding)
        {
            double x = padding.Left;
            double y = padding.Top;

            if (!Double.IsNaN(this.Height) && this.Height != 0)
            {
                y = (this.Height - (Child.DesiredSize.Height)) / 2;
            }

            Child.Arrange(new Rect(new Point(x, y), Child.DesiredSize));
        }

        /// <summary>
        /// 繪制控件
        /// </summary>
        /// <param name="drawingContext"></param>
        protected override void OnRender(DrawingContext drawingContext)
        {
            if (Child != null)
            {
                Geometry cg = null;
                Brush brush = null;
                //DpiScale dpi = base.getd();
                Pen pen = new Pen();

                pen.Brush = this.BorderBrush;
                //pen.Thickness = BorderThickness * 0.5;
                pen.Thickness = AngleBorder.RoundLayoutValue(BorderThickness.Left, DoubleUtil.DpiScaleX);
                
                switch (Placement)
                {
                    case EnumPlacement.LeftTop:
                    case EnumPlacement.LeftBottom:
                    case EnumPlacement.LeftCenter:
                        //生成小尾巴在左側的圖形和底色
                        cg = CreateGeometryTailAtLeft();
                        brush = CreateFillBrush();
                        break;
                    case EnumPlacement.RightTop:
                    case EnumPlacement.RightCenter:
                    case EnumPlacement.RightBottom:
                        //生成小尾巴在右側的圖形和底色
                        cg = CreateGeometryTailAtRight();
                        brush = CreateFillBrush();
                        break;
                    case EnumPlacement.TopLeft:
                    case EnumPlacement.TopCenter:
                    case EnumPlacement.TopRight:
                        //生成小尾巴在右側的圖形和底色
                        cg = CreateGeometryTailAtTop();
                        brush = CreateFillBrush();
                        break;
                    case EnumPlacement.BottomLeft:
                    case EnumPlacement.BottomCenter:
                    case EnumPlacement.BottomRight:
                        //生成小尾巴在右側的圖形和底色
                        cg = CreateGeometryTailAtBottom();
                        brush = CreateFillBrush();
                        break;
                    default:
                        break;
                }
                GuidelineSet guideLines = new GuidelineSet();
                drawingContext.PushGuidelineSet(guideLines);
                drawingContext.DrawGeometry(brush, pen, cg);
            }
        }
        #endregion

        private static double RoundLayoutValue(double value, double dpiScale)
        {
            double num;
            if (!AngleBorder.AreClose(dpiScale, 1.0))
            {
                num = Math.Round(value * dpiScale) / dpiScale;
                if (double.IsInfinity(num) || AngleBorder.AreClose(num, 1.7976931348623157E+308))
                {
                    num = value;
                }
            }
            else
            {
                num = Math.Round(value);
            }
            return num;
        }

        static bool AreClose(double value1, double value2)
        {
            if (value1 == value2)
            {
                return true;
            }
            double num = (Math.Abs(value1) + Math.Abs(value2) + 10.0) * 2.2204460492503131E-16;
            double num2 = value1 - value2;
            return -num < num2 && num > num2;
        }

        #region 私有方法
        private Geometry CreateGeometryTailAtRight()
        {
            CombinedGeometry result = new CombinedGeometry();

            //三角形默認居中
            this.TailVerticalOffset = (this.ActualHeight - this.TailHeight) / 2;

            #region 繪制三角形
            Point arcPoint1 = new Point(this.ActualWidth - TailWidth, TailVerticalOffset);
            Point arcPoint2 = new Point(this.ActualWidth, TailVerticalOffset + TailHeight / 2);
            Point arcPoint3 = new Point(this.ActualWidth - TailWidth, TailVerticalOffset + TailHeight);

            LineSegment as1_2 = new LineSegment(arcPoint2, false);
            LineSegment as2_3 = new LineSegment(arcPoint3, false);

            PathFigure pf1 = new PathFigure();
            pf1.IsClosed = false;
            pf1.StartPoint = arcPoint1;
            pf1.Segments.Add(as1_2);
            pf1.Segments.Add(as2_3);

            PathGeometry pg1 = new PathGeometry();
            pg1.Figures.Add(pf1);
            #endregion

            #region 繪制矩形邊框
            RectangleGeometry rg2 = new RectangleGeometry(new Rect(0, 0, this.ActualWidth - TailWidth, this.ActualHeight)
                , CornerRadius.TopLeft, CornerRadius.BottomRight, new TranslateTransform(0.5, 0.5));
            #endregion

            #region 合並兩個圖形
            result.Geometry1 = pg1;
            result.Geometry2 = rg2;
            result.GeometryCombineMode = GeometryCombineMode.Union;
            #endregion

            return result;
        }

        private Geometry CreateGeometryTailAtLeft()
        {
            CombinedGeometry result = new CombinedGeometry();

            switch (this.Placement)
            {
                case EnumPlacement.LeftTop:
                    //不做任何處理
                    break;
                case EnumPlacement.LeftBottom:
                    this.TailVerticalOffset = this.ActualHeight - this.TailHeight - this.TailVerticalOffset;
                    break;
                case EnumPlacement.LeftCenter:
                    this.TailVerticalOffset = (this.ActualHeight - this.TailHeight) / 2;
                    break;
            }

            #region 繪制三角形
            Point arcPoint1 = new Point(TailWidth, TailVerticalOffset);
            Point arcPoint2 = new Point(0, TailVerticalOffset + TailHeight / 2);
            Point arcPoint3 = new Point(TailWidth, TailVerticalOffset + TailHeight);

            LineSegment as1_2 = new LineSegment(arcPoint2, false);
            LineSegment as2_3 = new LineSegment(arcPoint3, false);

            PathFigure pf = new PathFigure();
            pf.IsClosed = false;
            pf.StartPoint = arcPoint1;
            pf.Segments.Add(as1_2);
            pf.Segments.Add(as2_3);

            PathGeometry g1 = new PathGeometry();
            g1.Figures.Add(pf);
            #endregion

            #region 繪制矩形邊框
            RectangleGeometry g2 = new RectangleGeometry(new Rect(TailWidth, 0, this.ActualWidth - this.TailWidth, this.ActualHeight)
                , CornerRadius.TopLeft, CornerRadius.BottomRight);
            #endregion

            #region 合並兩個圖形
            result.Geometry1 = g1;
            result.Geometry2 = g2;
            result.GeometryCombineMode = GeometryCombineMode.Union;
            #endregion

            return result;
        }

        private Geometry CreateGeometryTailAtTop()
        {
            CombinedGeometry result = new CombinedGeometry();

            switch (this.Placement)
            {
                case EnumPlacement.TopLeft:
                    break;
                case EnumPlacement.TopCenter:
                    this.TailHorizontalOffset = (this.ActualWidth - this.TailWidth) / 2;
                    break;
                case EnumPlacement.TopRight:
                    this.TailHorizontalOffset = this.ActualWidth - this.TailWidth - this.TailHorizontalOffset;
                    break;
            }

            #region 繪制三角形
            Point anglePoint1 = new Point(this.TailHorizontalOffset, this.TailHeight);
            Point anglePoint2 = new Point(this.TailHorizontalOffset + (this.TailWidth / 2), 0);
            Point anglePoint3 = new Point(this.TailHorizontalOffset + this.TailWidth, this.TailHeight);

            LineSegment as1_2 = new LineSegment(anglePoint2, true);
            LineSegment as2_3 = new LineSegment(anglePoint3, true);

            PathFigure pf = new PathFigure();
            pf.IsClosed = false;
            pf.StartPoint = anglePoint1;
            pf.Segments.Add(as1_2);
            pf.Segments.Add(as2_3);

            PathGeometry g1 = new PathGeometry();
            g1.Figures.Add(pf);
            #endregion

            #region 繪制矩形邊框
            RectangleGeometry g2 = new RectangleGeometry(new Rect(0, this.TailHeight, this.ActualWidth, this.ActualHeight - this.TailHeight)
                , CornerRadius.TopLeft, CornerRadius.BottomRight);
            #endregion

            #region 合並
            result.Geometry1 = g1;
            result.Geometry2 = g2;
            result.GeometryCombineMode = GeometryCombineMode.Union;
            #endregion

            return result;
        }

        private Geometry CreateGeometryTailAtBottom()
        {
            CombinedGeometry result = new CombinedGeometry();

            switch (this.Placement)
            {
                case EnumPlacement.BottomLeft:
                    break;
                case EnumPlacement.BottomCenter:
                    this.TailHorizontalOffset = (this.ActualWidth - this.TailWidth) / 2;
                    break;
                case EnumPlacement.BottomRight:
                    this.TailHorizontalOffset = this.ActualWidth - this.TailWidth - this.TailHorizontalOffset;
                    break;
            }
            

            #region 繪制三角形
            Point anglePoint1 = new Point(this.TailHorizontalOffset, this.ActualHeight - this.TailHeight);
            Point anglePoint2 = new Point(this.TailHorizontalOffset + this.TailWidth / 2, this.ActualHeight);
            Point anglePoint3 = new Point(this.TailHorizontalOffset + this.TailWidth, this.ActualHeight - this.TailHeight);

            LineSegment as1_2 = new LineSegment(anglePoint2, true);
            LineSegment as2_3 = new LineSegment(anglePoint3, true);

            PathFigure pf = new PathFigure();
            pf.IsClosed = false;
            pf.StartPoint = anglePoint1;
            pf.Segments.Add(as1_2);
            pf.Segments.Add(as2_3);

            PathGeometry g1 = new PathGeometry();
            g1.Figures.Add(pf);
            #endregion

            #region 繪制矩形邊框
            RectangleGeometry g2 = new RectangleGeometry(new Rect(0, 0, this.ActualWidth, this.ActualHeight - this.TailHeight)
                , CornerRadius.TopLeft, CornerRadius.BottomRight);
            #endregion

            #region 合並
            result.Geometry1 = g1;
            result.Geometry2 = g2;
            result.GeometryCombineMode = GeometryCombineMode.Union;
            #endregion

            return result;
        }

        private Brush CreateFillBrush()
        {
            Brush result = null;

            GradientStopCollection gsc = new GradientStopCollection();
            gsc.Add(new GradientStop(((SolidColorBrush)this.Background).Color, 0));
            LinearGradientBrush backGroundBrush = new LinearGradientBrush(gsc, new Point(0, 0), new Point(0, 1));
            result = backGroundBrush;

            return result;
        }

        /// <summary>
        /// 根據三角形方向設置消息框的水平位置,偏左還是偏右
        /// </summary>
        /// <param name="d"></param>
        /// <param name="e"></param>
        public static void OnDirectionPropertyChangedCallback(DependencyObject d, DependencyPropertyChangedEventArgs e)
        {
            var self = d as AngleBorder;
            self.HorizontalAlignment = ((EnumPlacement)e.NewValue == EnumPlacement.RightCenter) ?
                HorizontalAlignment.Right : HorizontalAlignment.Left;
        }
        #endregion
    }
}

  

這里面使用了RectangleGeometry與LineSegment,其中使用 RectangleGeometry是為了利用其Radius屬性可以用來設置圓角, LineSegment則用來繪制三角形的幾條直線邊,最后利用CombinedGeometry的GeometryCombineMode屬性將兩個圖形進行合並,這樣它們連接處就不會有邊框存在了,看起來就是一個整體
result.GeometryCombineMode=GeometryCombineMode.Union;

效果圖:

源碼下載:

鏈接: https://pan.baidu.com/s/1gfcHLp5 密碼: 5b4e
 


免責聲明!

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



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