原文: 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