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);
                    }
                }
            }
        }
    }
  • 相关阅读:
    验证数字范围的小插件
    解决EJB懒加载问题
    JS获取按键的代码,Js如何屏蔽用户的按键,Js获取用户按键对应的ASII码(兼容所有浏览器)
    struts2标签之<s:select>
    c#(winform)中自定义ListItem类方便ComboBox和ListBox添加项完全解决
    辞职前须慎重考虑
    怎样把PDF文件在WinForm窗口中显示出来
    加载报表失败
    经典正则表达式 Javascript
    无法生成项目输出组“内容文件来自...
  • 原文地址:https://www.cnblogs.com/yiyan127/p/WPF-CalloutDecorator.html
Copyright © 2011-2022 走看看