标注
在许多地方我们都会用到标注,比如在画图中:
在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);
}
}
}
}
}