zoukankan      html  css  js  c++  java
  • 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);
                    }
                }
            }
        }
    }
  • 相关阅读:
    UVA 10617 Again Palindrome
    UVA 10154 Weights and Measures
    UVA 10201 Adventures in Moving Part IV
    UVA 10313 Pay the Price
    UVA 10271 Chopsticks
    Restore DB後設置指引 for maximo
    每行SQL語句加go換行
    种服务器角色所拥有的权限
    Framework X support IPV6?
    模擬DeadLock
  • 原文地址:https://www.cnblogs.com/yiyan127/p/WPF-CalloutDecorator.html
Copyright © 2011-2022 走看看