寫下來,備忘。
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