你的位置:首页 > ASP.net教程

[ASP.net教程]WPF标注装饰器


标注

在许多地方我们都会用到标注,比如在画图中:

1

 

在Office中:

1

在Foxit Reader中:

1

在Blend中:

1

等等。

简介

以前,因项目上需要做标注,简单找了一下,没发现适合要求的控件(包括Blend中的标注,标注的两个点距离是固定的)。所以自己简单的写了一个。后来又私下修改了几次,基本完成了圆角矩形的标注。

效果图如下:

1

对应的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)。

设置各项参数时需要注意,不能让与锚点相对应的两个点的坐标都边框以内,否则会产生奇怪的效果。

1

但是好在我们一般情况下都不会将边框设的过大,而将两个点设置的较小。

代码

代码中重载了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);        }      }    }  }}