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);
                }
            }
        }
    }
}
posted @ 2015-10-30 22:03  赵御辩  阅读(2286)  评论(0编辑  收藏  举报