WPF标注装饰器
标注
在许多地方我们都会用到标注,比如在画图中:
在Office中:
在Foxit Reader中:
在Blend中:
等等。
简介
以前,因项目上需要做标注,简单找了一下,没发现适合要求的控件(包括Blend中的标注,标注的两个点距离是固定的)。所以自己简单的写了一个。后来又私下修改了几次,基本完成了圆角矩形的标注。
效果图如下:
对应的XAML代码如下:
<local:CalloutDecorator Margin="5" AnchorOffsetX="150" AnchorOffsetY="50" Background="Purple" BorderBrush="Red" BorderThickness="10,20,30,40" CornerRadius="10,20,30,40" Dock="Left" FirstOffset="110" Padding="40" SecondOffset="130"> <Border Background="Yellow" /> </local:CalloutDecorator>
支持设置锚点(AnchorOffsetX和AnchorOffsetY)、与锚点相对应的两个点的坐标(FirstOffset
和SecondOffset)、朝向(Dock)、圆角信息(CornerRadius)、边框信息(BorderThickness、BorderBrush)、保留空间(Padding)、背景(Background)。
设置各项参数时需要注意,不能让与锚点相对应的两个点的坐标都边框以内,否则会产生奇怪的效果。
但是好在我们一般情况下都不会将边框设的过大,而将两个点设置的较小。
代码
代码中重载了WPF三个重要过程,测量(MeasureOverride)、布局(ArrangeOverride)、绘制(OnRender)。为了提高绘制效率,使用了缓存。代码较简单,也有注释,就不再多说了。
namespace YiYan127.WPF.Decorator { using System; using System.Windows; using System.Windows.Controls; using System.Windows.Media; /// <summary> /// 标注式装饰器 /// </summary> public class CalloutDecorator : Border { #region Fields #region DependencyProperty public static readonly DependencyProperty DockProperty = DependencyProperty.Register( "Dock", typeof(Dock), typeof(CalloutDecorator), new FrameworkPropertyMetadata(Dock.Bottom, Refresh)); public static readonly DependencyProperty AnchorOffsetXProperty = DependencyProperty.Register( "AnchorOffsetX", typeof(double), typeof(CalloutDecorator), new FrameworkPropertyMetadata(20.0, Refresh), DoubleGreatterThanZero); public static readonly DependencyProperty AnchorOffsetYProperty = DependencyProperty.Register( "AnchorOffsetY", typeof(double), typeof(CalloutDecorator), new FrameworkPropertyMetadata(20.0, Refresh), DoubleGreatterThanZero); public static readonly DependencyProperty FirstOffsetProperty = DependencyProperty.Register( "FirstOffset", typeof(double), typeof(CalloutDecorator), new FrameworkPropertyMetadata(10.0, FrameworkPropertyMetadataOptions.AffectsArrange), DoubleGreatterThanZero); public static readonly DependencyProperty SecondOffsetProperty = DependencyProperty.Register( "SecondOffset", typeof(double), typeof(CalloutDecorator), new FrameworkPropertyMetadata(20.0, FrameworkPropertyMetadataOptions.AffectsArrange), DoubleGreatterThanZero); #endregion DependencyProperty /// <summary> /// 刷新选项 /// </summary> private const FrameworkPropertyMetadataOptions Refresh = FrameworkPropertyMetadataOptions.AffectsMeasure | FrameworkPropertyMetadataOptions.AffectsRender | FrameworkPropertyMetadataOptions.AffectsArrange; /// <summary> /// 是否为Callout模式,为false的话,表示border模式 /// </summary> private bool isCalloutMode; /// <summary> /// 背景的缓存 /// </summary> private StreamGeometry backgroundGeometryCache; /// <summary> /// 标注的缓存 /// </summary> private StreamGeometry calloutGeometryCache; #endregion Fields #region Properties /// <summary> /// 引线朝向(左、上、右、下) /// </summary> public Dock Dock { get { return (Dock)GetValue(DockProperty); } set { this.SetValue(DockProperty, value); } } /// <summary> /// X方向的锚点偏移(针对子控件) /// </summary> public double AnchorOffsetX { get { return (double)GetValue(AnchorOffsetXProperty); } set { this.SetValue(AnchorOffsetXProperty, value); } } /// <summary> /// Y方向的锚点偏移(针对子控件) /// </summary> public double AnchorOffsetY { get { return (double)GetValue(AnchorOffsetYProperty); } set { this.SetValue(AnchorOffsetYProperty, value); } } /// <summary> /// 在对应的轴上第一个偏移位置 /// </summary> public double FirstOffset { get { return (double)GetValue(FirstOffsetProperty); } set { this.SetValue(FirstOffsetProperty, value); } } /// <summary> /// 在对应的轴上的第二个偏移位置 /// </summary> public double SecondOffset { get { return (double)GetValue(SecondOffsetProperty); } set { this.SetValue(SecondOffsetProperty, value); } } #endregion Properties #region Overrides /// <summary> /// 重载测量过程 /// </summary> /// <param name="constraint">约束</param> /// <returns>需要的大小</returns> protected override Size MeasureOverride(Size constraint) { this.isCalloutMode = (this.Child != null) && (!IsZero(this.AnchorOffsetX) && (!IsZero(this.AnchorOffsetY))); if (!this.isCalloutMode) { return base.MeasureOverride(constraint); } Size borderSize = GetDesiredSize(this.BorderThickness); Size paddingSize = GetDesiredSize(this.Padding); // 最少需要的大小 var basicSize = new Size(borderSize.Width + paddingSize.Width, borderSize.Height + paddingSize.Height); // 计算需要的实际大小 switch (Dock) { case Dock.Left: case Dock.Right: { // 宽度不能小于0 double availableWidth = Math.Max(0, constraint.Width - basicSize.Width - this.AnchorOffsetX); var availableSize = new Size(availableWidth, Math.Max(0.0, constraint.Height - basicSize.Height)); this.Child.Measure(availableSize); Size desiredSize = this.Child.DesiredSize; return new Size( desiredSize.Width + basicSize.Width + this.AnchorOffsetX, desiredSize.Height + basicSize.Height); } case Dock.Top: case Dock.Bottom: { double availableHeight = Math.Max(0, constraint.Height - basicSize.Height - this.AnchorOffsetY); var availableSize = new Size(Math.Max(0.0, constraint.Width - basicSize.Width), availableHeight); this.Child.Measure(availableSize); Size desiredSize = this.Child.DesiredSize; return new Size( desiredSize.Width + basicSize.Width, desiredSize.Height + basicSize.Height + this.AnchorOffsetY); } } return basicSize; } /// <summary> /// 重载布局过程 /// </summary> /// <param name="finalSize">可用的布局大小</param> /// <returns>布局大小</returns> protected override Size ArrangeOverride(Size finalSize) { if (!this.isCalloutMode) { return base.ArrangeOverride(finalSize); } var boundaryRect = new Rect(finalSize); var outterRect = new Rect(); switch (Dock) { #region 根据不同的Dock进行处理 case Dock.Left: { outterRect = DeflateRect(boundaryRect, new Thickness(this.AnchorOffsetX, 0, 0, 0)); break; } case Dock.Right: { outterRect = DeflateRect(boundaryRect, new Thickness(0, 0, this.AnchorOffsetX, 0)); break; } case Dock.Top: { outterRect = DeflateRect(boundaryRect, new Thickness(0, this.AnchorOffsetY, 0, 0)); break; } case Dock.Bottom: { outterRect = DeflateRect(boundaryRect, new Thickness(0, 0, 0, this.AnchorOffsetY)); break; } #endregion 根据不同的Dock进行处理 } Rect innerRect = DeflateRect(outterRect, this.BorderThickness); Rect finalRect = DeflateRect(innerRect, this.Padding); this.Child.Arrange(finalRect); var innerPoints = new BorderPoints(this.CornerRadius, this.BorderThickness, false); if (!IsZero(innerRect.Width) && !IsZero(innerRect.Height)) { var streamGeometry = new StreamGeometry(); using (StreamGeometryContext streamGeometryContext = streamGeometry.Open()) { this.GenerateGeometry(streamGeometryContext, innerRect, innerPoints, boundaryRect); } streamGeometry.Freeze(); this.backgroundGeometryCache = streamGeometry; } else { this.backgroundGeometryCache = null; } if (!IsZero(outterRect.Width) && !IsZero(outterRect.Height)) { var outterPoints = new BorderPoints(this.CornerRadius, this.BorderThickness, true); var streamGeometry = new StreamGeometry(); using (StreamGeometryContext streamGeometryContext = streamGeometry.Open()) { this.GenerateGeometry(streamGeometryContext, outterRect, outterPoints, boundaryRect); if (this.backgroundGeometryCache != null) { this.GenerateGeometry(streamGeometryContext, innerRect, innerPoints, boundaryRect); } } streamGeometry.Freeze(); this.calloutGeometryCache = streamGeometry; } else { this.calloutGeometryCache = null; } return finalSize; } /// <summary> /// 重载绘制 /// </summary> /// <param name="dc"></param> protected override void OnRender(DrawingContext dc) { if (!this.isCalloutMode) { base.OnRender(dc); return; } if (this.calloutGeometryCache != null && this.BorderBrush != null) { dc.DrawGeometry(this.BorderBrush, null, this.calloutGeometryCache); } if (this.backgroundGeometryCache != null && this.Background != null) { dc.DrawGeometry(this.Background, null, this.backgroundGeometryCache); } } #endregion Overrides #region Private Methods /// <summary> /// 验证类型为double且大于0 /// </summary> /// <param name="value">值</param> /// <returns>数据为double类型且大于0</returns> private static bool DoubleGreatterThanZero(object value) { return (value is double) && ((double)value) > 0; } /// <summary> /// 获取期望的大小 /// </summary> /// <param name="thickness">边框信息</param> /// <returns>期望的大小</returns> private static Size GetDesiredSize(Thickness thickness) { return new Size(thickness.Left + thickness.Right, thickness.Top + thickness.Bottom); } /// <summary> /// 返回在矩形中留出边框后的矩形 /// </summary> /// <param name="rt">矩形</param> /// <param name="thick">边框</param> /// <returns>留出边框后的矩形</returns> private static Rect DeflateRect(Rect rt, Thickness thick) { return new Rect(rt.Left + thick.Left, rt.Top + thick.Top, Math.Max(0.0, rt.Width - thick.Left - thick.Right), Math.Max(0.0, rt.Height - thick.Top - thick.Bottom)); } /// <summary> /// 判断一个数是否为0 /// </summary> /// <param name="value">数</param> /// <returns>为0返回true,否则返回false</returns> private static bool IsZero(double value) { return Math.Abs(value) < 2.22044604925031E-15; } /// <summary> /// 返回过两点的直线在Y坐标上的X坐标 /// </summary> /// <param name="point1">第一个点</param> /// <param name="point2">第二个点</param> /// <param name="y">Y坐标</param> /// <returns>对应的X坐标</returns> private static double CalculateLineX(Point point1, Point point2, double y) { return point1.X - ((point1.X - point2.X) * (point1.Y - y) / (point1.Y - point2.Y)); } /// <summary> /// 返回过两点的直线在X坐标上的Y坐标 /// </summary> /// <param name="point1">第一个点</param> /// <param name="point2">第二个点</param> /// <param name="x">X坐标</param> /// <returns>对应的Y坐标</returns> private static double CalculateLineY(Point point1, Point point2, double x) { return point1.Y - ((point1.X - x) * (point1.Y - point2.Y) / (point1.X - point2.X)); } /// <summary> /// 生成形状 /// </summary> /// <param name="ctx">绘制上下文</param> /// <param name="rect">绘制所在的矩形</param> /// <param name="points">边框绘制点</param> /// <param name="boundaryRect">绘制的外边界</param> private void GenerateGeometry(StreamGeometryContext ctx, Rect rect, BorderPoints points, Rect boundaryRect) { var leftTopPt = new Point(points.LeftTop, 0.0); var rightTopPt = new Point(rect.Width - points.RightTop, 0.0); var topRightPt = new Point(rect.Width, points.TopRight); var bottomRightPt = new Point(rect.Width, rect.Height - points.BottomRight); var rightBottomPt = new Point(rect.Width - points.RightBottom, rect.Height); var leftBottomPt = new Point(points.LeftBottom, rect.Height); var bottomLeftPt = new Point(0.0, rect.Height - points.BottomLeft); var topLeftPt = new Point(0.0, points.TopLeft); if (leftTopPt.X > rightTopPt.X) { double x = points.LeftTop / (points.LeftTop + points.RightTop) * rect.Width; leftTopPt.X = x; rightTopPt.X = x; } if (topRightPt.Y > bottomRightPt.Y) { double y = points.TopRight / (points.TopRight + points.BottomRight) * rect.Height; topRightPt.Y = y; bottomRightPt.Y = y; } if (rightBottomPt.X < leftBottomPt.X) { double x2 = points.LeftBottom / (points.LeftBottom + points.RightBottom) * rect.Width; rightBottomPt.X = x2; leftBottomPt.X = x2; } if (bottomLeftPt.Y < topLeftPt.Y) { double y2 = points.TopLeft / (points.TopLeft + points.BottomLeft) * rect.Height; bottomLeftPt.Y = y2; topLeftPt.Y = y2; } var vector = new Vector(rect.TopLeft.X, rect.TopLeft.Y); leftTopPt += vector; rightTopPt += vector; topRightPt += vector; bottomRightPt += vector; rightBottomPt += vector; leftBottomPt += vector; bottomLeftPt += vector; topLeftPt += vector; ctx.BeginFigure(leftTopPt, true, true); if (this.Dock == Dock.Top) { var secondOutPoint = new Point(this.SecondOffset, this.AnchorOffsetY); var firstOutPoint = new Point(this.FirstOffset, this.AnchorOffsetY); var calloutPoint = new Point(this.AnchorOffsetX, 0); ctx.LineTo(new Point(CalculateLineX(calloutPoint, firstOutPoint, rect.Top), rect.Top), true, false); ctx.LineTo(calloutPoint, true, false); ctx.LineTo(new Point(CalculateLineX(calloutPoint, secondOutPoint, rect.Top), rect.Top), true, false); } ctx.LineTo(rightTopPt, true, false); double sizeX = rect.TopRight.X - rightTopPt.X; double sizeY = topRightPt.Y - rect.TopRight.Y; if (!IsZero(sizeX) || !IsZero(sizeY)) { ctx.ArcTo(topRightPt, new Size(sizeX, sizeY), 0.0, false, SweepDirection.Clockwise, true, false); } if (this.Dock == Dock.Right) { var secondOutPoint = new Point(boundaryRect.Width - this.AnchorOffsetX, this.SecondOffset); var firstOutPoint = new Point(boundaryRect.Width - this.AnchorOffsetX, this.FirstOffset); var calloutPoint = new Point(boundaryRect.Width, this.AnchorOffsetY); ctx.LineTo(new Point(rect.Right, CalculateLineY(calloutPoint, firstOutPoint, rect.Right)), true, false); ctx.LineTo(calloutPoint, true, false); ctx.LineTo(new Point(rect.Right, CalculateLineY(calloutPoint, secondOutPoint, rect.Right)), true, false); } ctx.LineTo(bottomRightPt, true, false); sizeX = rect.BottomRight.X - rightBottomPt.X; sizeY = rect.BottomRight.Y - bottomRightPt.Y; if (!IsZero(sizeX) || !IsZero(sizeY)) { ctx.ArcTo(rightBottomPt, new Size(sizeX, sizeY), 0.0, false, SweepDirection.Clockwise, true, false); } if (this.Dock == Dock.Bottom) { var secondOutPoint = new Point(this.SecondOffset, boundaryRect.Height - this.AnchorOffsetY); var firstOutPoint = new Point(this.FirstOffset, boundaryRect.Height - this.AnchorOffsetY); var calloutPoint = new Point(this.AnchorOffsetX, boundaryRect.Height); ctx.LineTo(new Point(CalculateLineX(calloutPoint, secondOutPoint, rect.Bottom), rect.Bottom), true, false); ctx.LineTo(calloutPoint, true, false); ctx.LineTo(new Point(CalculateLineX(calloutPoint, firstOutPoint, rect.Bottom), rect.Bottom), true, false); } ctx.LineTo(leftBottomPt, true, false); sizeX = leftBottomPt.X - rect.BottomLeft.X; sizeY = rect.BottomLeft.Y - bottomLeftPt.Y; if (!IsZero(sizeX) || !IsZero(sizeY)) { ctx.ArcTo(bottomLeftPt, new Size(sizeX, sizeY), 0.0, false, SweepDirection.Clockwise, true, false); } if (this.Dock == Dock.Left) { var secondOutPoint = new Point(this.AnchorOffsetX, this.SecondOffset); var firstOutPoint = new Point(this.AnchorOffsetX, this.FirstOffset); var calloutPoint = new Point(0, this.AnchorOffsetY); ctx.LineTo(new Point(rect.Left, CalculateLineY(calloutPoint, firstOutPoint, rect.Left)), true, false); ctx.LineTo(calloutPoint, true, false); ctx.LineTo(new Point(rect.Left, CalculateLineY(calloutPoint, secondOutPoint, rect.Left)), true, false); } ctx.LineTo(topLeftPt, true, false); sizeX = leftTopPt.X - rect.TopLeft.X; sizeY = topLeftPt.Y - rect.TopLeft.Y; if (!IsZero(sizeX) || !IsZero(sizeY)) { ctx.ArcTo(leftTopPt, new Size(sizeX, sizeY), 0.0, false, SweepDirection.Clockwise, true, false); } } #endregion Private Methods /// <summary> /// 边框绘制点 /// </summary> private struct BorderPoints { internal readonly double LeftTop; internal readonly double TopLeft; internal readonly double TopRight; internal readonly double RightTop; internal readonly double RightBottom; internal readonly double BottomRight; internal readonly double BottomLeft; internal readonly double LeftBottom; /// <summary> /// 构造函数 /// </summary> /// <param name="borderCornerRadius">圆角信息</param> /// <param name="boderThickness">边框信息</param> /// <param name="outer">是否为外部</param> internal BorderPoints(CornerRadius borderCornerRadius, Thickness boderThickness, bool outer) { double halfLeft = 0.5 * boderThickness.Left; double halfTop = 0.5 * boderThickness.Top; double halfRight = 0.5 * boderThickness.Right; double halfBottom = 0.5 * boderThickness.Bottom; if (outer) { if (IsZero(borderCornerRadius.TopLeft)) { this.LeftTop = this.TopLeft = 0.0; } else { this.LeftTop = borderCornerRadius.TopLeft + halfLeft; this.TopLeft = borderCornerRadius.TopLeft + halfTop; } if (IsZero(borderCornerRadius.TopRight)) { this.TopRight = this.RightTop = 0.0; } else { this.TopRight = borderCornerRadius.TopRight + halfTop; this.RightTop = borderCornerRadius.TopRight + halfRight; } if (IsZero(borderCornerRadius.BottomRight)) { this.RightBottom = this.BottomRight = 0.0; } else { this.RightBottom = borderCornerRadius.BottomRight + halfRight; this.BottomRight = borderCornerRadius.BottomRight + halfBottom; } if (IsZero(borderCornerRadius.BottomLeft)) { this.BottomLeft = this.LeftBottom = 0.0; } else { this.BottomLeft = borderCornerRadius.BottomLeft + halfBottom; this.LeftBottom = borderCornerRadius.BottomLeft + halfLeft; } } else { this.LeftTop = Math.Max(0.0, borderCornerRadius.TopLeft - halfLeft); this.TopLeft = Math.Max(0.0, borderCornerRadius.TopLeft - halfTop); this.TopRight = Math.Max(0.0, borderCornerRadius.TopRight - halfTop); this.RightTop = Math.Max(0.0, borderCornerRadius.TopRight - halfRight); this.RightBottom = Math.Max(0.0, borderCornerRadius.BottomRight - halfRight); this.BottomRight = Math.Max(0.0, borderCornerRadius.BottomRight - halfBottom); this.BottomLeft = Math.Max(0.0, borderCornerRadius.BottomLeft - halfBottom); this.LeftBottom = Math.Max(0.0, borderCornerRadius.BottomLeft - halfLeft); } } } } }