WPF画箭头

简介

参考Using WPF to Visualize a Graph with Circular Dependencies的基础上写了一个WPF画箭头的库。

效果图如下:
arrow

使用的XAML代码如下:

<Window x:Class="WPFArrows.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:arrow="clr-namespace:WPFArrows.Arrows"
        Title="MainWindow"
        Width="525"
        Height="350">
    <Canvas>
        <arrow:ArrowLine Stroke="Black"
                         StartPoint="10,10"
                         EndPoint="100,100" />
        <arrow:ArrowLineWithText ArrowEnds="Both"
                                 IsTextUp="True"
                                 Stroke="Blue"
                                 StrokeDashArray="5,3"
                                 Text="推导出"
                                 TextAlignment="Center"
                                 StartPoint="110,110"
                                 EndPoint="180,180" />
        <arrow:ArrowQuadraticBezier ControlPoint="200,100"
                                    Stroke="Yellow"
                                    StartPoint="250,180"
                                    EndPoint="500,20" />
        <arrow:AdjustableArrowBezierCurve ControlPoint1="230,200"
                                          ControlPoint2="300,300"
                                          ShowControl="True"
                                          Stroke="Black"
                                          StartPoint="200,200"
                                          EndPoint="500,300" />
    </Canvas>
</Window>

类关系

arrow2

形状绘制原理

我们常用的形状,如Rectangle、Ellipse、Line、Path等,都继承自Shape类,类关系如下:

3

(图像摘自<<WPF编程宝典>>)

而具体Shape类是如何绘制形状的呢?我们转到Shape的定义,发现其中有一个虚方法

// 摘要: 
        //     Gets a value that represents the System.Windows.Media.Geometry of the System.Windows.Shapes.Shape.
        //
        // 返回结果: 
        //     The System.Windows.Media.Geometry of the System.Windows.Shapes.Shape.
        protected abstract Geometry DefiningGeometry { get; }

使 用工具(我用的是ILSpy)反汇编Shape类所在的PresentationFramework.dll的源码,就会发现 DefiningGeometry是最重要的方法,在MeasureOverride、ArrangeOverride、OnRender都会间接调用该 方法。

在Line类中,重载后的方法内容如下:

Point startPoint = new Point(this.X1, this.Y1);
            Point endPoint = new Point(this.X2, this.Y2);
            this._lineGeometry = new LineGeometry(startPoint, endPoint);

即直接返回了一个LineGeometry的新实例。

在其余各类中,原理与Line类中一样。

各个类介绍

ArrowBase

ArrowBase是箭头的基类,继承自Shape类。

在ArrowBase中,重载了DefiningGeometry方法,如下:

protected override Geometry DefiningGeometry
        {
            get
            {
                _figureConcrete.StartPoint = StartPoint;

                //清空具体形状,避免重复添加
                _figureConcrete.Segments.Clear();
                var segements = FillFigure();
                if (segements != null)
                {
                    foreach (var segement in segements)
                    {
                        _figureConcrete.Segments.Add(segement);
                    }
                }

                //绘制开始处的箭头
                if ((ArrowEnds & ArrowEnds.Start) == ArrowEnds.Start)
                {
                    CalculateArrow(_figureStart, GetStartArrowEndPoint(), StartPoint);
                }

                // 绘制结束处的箭头
                if ((ArrowEnds & ArrowEnds.End) == ArrowEnds.End)
                {
                    CalculateArrow(_figureEnd, GetEndArrowStartPoint(), GetEndArrowEndPoint());
                }

                return _wholeGeometry;
            }
        }

在其中_figureConcrete是用来保存具体形状的PathFigure,其余几个受保护的方法定义如下:

/// <summary>
        /// 获取具体形状的各个组成部分
        /// </summary>
        protected abstract PathSegmentCollection FillFigure();

        /// <summary>
        /// 获取开始箭头处的结束点
        /// </summary>
        /// <returns>开始箭头处的结束点</returns>
        protected abstract Point GetStartArrowEndPoint();

        /// <summary>
        /// 获取结束箭头处的开始点
        /// </summary>
        /// <returns>结束箭头处的开始点</returns>
        protected abstract Point GetEndArrowStartPoint();

        /// <summary>
        /// 获取结束箭头处的结束点
        /// </summary>
        /// <returns>结束箭头处的结束点</returns>
        protected abstract Point GetEndArrowEndPoint();

在ArrowBase中,一个重要的方法是计算箭头的方法:

/// <summary>
        /// 计算两个点之间的有向箭头
        /// </summary>
        /// <param name="pathfig">箭头所在的形状</param>
        /// <param name="startPoint">开始点</param>
        /// <param name="endPoint">结束点</param>
        /// <returns>计算好的形状</returns>
        private void CalculateArrow(PathFigure pathfig, Point startPoint, Point endPoint)
        {
            var polyseg = pathfig.Segments[0] as PolyLineSegment;
            if (polyseg != null)
            {
                var matx = new Matrix();
                Vector vect = startPoint - endPoint;
                //获取单位向量
                vect.Normalize();
                vect *= ArrowLength;
                //旋转夹角的一半
                matx.Rotate(ArrowAngle / 2);
                //计算上半段箭头的点
                pathfig.StartPoint = endPoint + vect * matx;

                polyseg.Points.Clear();
                polyseg.Points.Add(endPoint);

                matx.Rotate(-ArrowAngle);
                //计算下半段箭头的点
                polyseg.Points.Add(endPoint + vect * matx);
            }

            pathfig.IsClosed = IsArrowClosed;
        }

ArrowLine

ArrowLine是带箭头的直线,该类非常简单,重载了ArrowBase中定义的相关方法

/// <summary>
    /// 两点之间带箭头的直线
    /// </summary>
    public class ArrowLine:ArrowBase
    {
        #region Fields

        /// <summary>
        /// 线段
        /// </summary>
        private readonly LineSegment _lineSegment=new LineSegment();

        #endregion Fields

        #region Properties

        /// <summary>
        /// 结束点
        /// </summary>
        public static readonly DependencyProperty EndPointProperty = DependencyProperty.Register(
            "EndPoint", typeof(Point), typeof(ArrowLine), 
            new FrameworkPropertyMetadata(default(Point), FrameworkPropertyMetadataOptions.AffectsMeasure));

        /// <summary>
        /// 结束点
        /// </summary>
        public Point EndPoint
        {
            get { return (Point) GetValue(EndPointProperty); }
            set { SetValue(EndPointProperty, value); }
        }

        #endregion Properties

        #region Protected Methods

        /// <summary>
        /// 填充Figure
        /// </summary>
        protected override PathSegmentCollection FillFigure()
        {
            _lineSegment.Point = EndPoint;
            return new PathSegmentCollection
            {
                _lineSegment
            };
        }

        /// <summary>
        /// 获取开始箭头处的结束点
        /// </summary>
        /// <returns>开始箭头处的结束点</returns>
        protected override Point GetStartArrowEndPoint()
        {
            return EndPoint;
        }

        /// <summary>
        /// 获取结束箭头处的开始点
        /// </summary>
        /// <returns>结束箭头处的开始点</returns>
        protected override Point GetEndArrowStartPoint()
        {
            return StartPoint;
        }

        /// <summary>
        /// 获取结束箭头处的结束点
        /// </summary>
        /// <returns>结束箭头处的结束点</returns>
        protected override Point GetEndArrowEndPoint()
        {
            return EndPoint;
        }

        #endregion  Protected Methods

    }
}

ArrowLineWithText

ArrowLineWithText,可在直线上方或下方显示文字,继承自ArrowLine。所做的主要工作就是重载渲染事件,使其绘制文字

/// <summary>
        /// 重载渲染事件
        /// </summary>
        /// <param name="drawingContext">绘图上下文</param>
        protected override void OnRender(DrawingContext drawingContext)
        {
            base.OnRender(drawingContext);

            if (ShowText&&(Text != null))
            {
                var txt = Text.Trim();
                var startPoint = StartPoint;
                if (!string.IsNullOrEmpty(txt))
                {
                    var vec = EndPoint - StartPoint;
                    var angle = GetAngle(StartPoint, EndPoint);

                    //使用旋转变换,使其与线平行
                    var transform = new RotateTransform(angle) { CenterX = StartPoint.X, CenterY = StartPoint.Y };
                    drawingContext.PushTransform(transform);

                    var defaultTypeface = new Typeface(SystemFonts.StatusFontFamily, SystemFonts.StatusFontStyle,
                        SystemFonts.StatusFontWeight, new FontStretch());
                    var formattedText = new FormattedText(txt, CultureInfo.CurrentCulture,
                        FlowDirection.LeftToRight,
                        defaultTypeface, SystemFonts.StatusFontSize, Brushes.Black)
                    {
                        //文本最大宽度为线的宽度
                        MaxTextWidth = vec.Length,
                        //设置文本对齐方式
                        TextAlignment = TextAlignment
                    };

                    var offsetY = StrokeThickness;
                    if (IsTextUp)
                    {
                        //计算文本的行数
                        double textLineCount = formattedText.Width/formattedText.MaxTextWidth;
                        if (textLineCount < 1)
                        {
                            //怎么也得有一行
                            textLineCount = 1;
                        }
                        //计算朝上的偏移
                        offsetY = -formattedText.Height*textLineCount -StrokeThickness;
                    }
                    startPoint = startPoint +new Vector(0,offsetY);
                    drawingContext.DrawText(formattedText, startPoint);
                    drawingContext.Pop();
                }
            }

ArrowBezierCurve和ArrowQuadraticBezier

ArrowBezierCurve和ArrowQuadraticBezier代码与ArrowLine基本相似,只是添加了控制点的依赖属性。分别表示贝塞尔曲线和二次贝塞尔曲线,代码从略。

AdjustableArrowQuadraticBezier

AdjustableArrowQuadraticBezier表示可调整的二次贝塞尔曲线。根据鼠标按住控制点(通过重载渲染绘制)的移动来更新控制点,从而起到调整的作用。主要重载了鼠标按下、鼠标移动、鼠标释放、渲染等方法。

/// <summary>
        /// 当未处理的 <see cref="E:System.Windows.Input.Mouse.MouseDown"/> 附加事件在其路由中到达派生自此类的元素时,调用该方法。实现此方法可为此事件添加类处理。
        /// </summary>
        /// <param name="e">包含事件数据的 <see cref="T:System.Windows.Input.MouseButtonEventArgs"/>。此事件数据报告有关按下的鼠标按钮和已处理状态的详细信息。
        ///                 </param>
        protected override void OnMouseDown(MouseButtonEventArgs e)
        {
            base.OnMouseDown(e);

            if (ShowControl&&(e.LeftButton == MouseButtonState.Pressed))
            {
                CaptureMouse();
                Point pt = e.GetPosition(this);
                Vector slide = pt - ControlPoint;
                //在控制点的圆圈之内
                if (slide.Length < EllipseRadius)
                {
                    _isPressedControlPoint = true;
                }
            }
        }

        /// <summary>
        /// 当未处理的 <see cref="E:System.Windows.Input.Mouse.MouseUp"/> 路由事件在其路由中到达派生自此类的元素时,调用该方法。实现此方法可为此事件添加类处理。
        /// </summary>
        /// <param name="e">包含事件数据的 <see cref="T:System.Windows.Input.MouseButtonEventArgs"/>。事件数据将报告已释放了鼠标按钮。
        ///                 </param>
        protected override void OnMouseUp(MouseButtonEventArgs e)
        {
            base.OnMouseUp(e);
            ReleaseMouseCapture();
            _isPressedControlPoint = false;
        }

        /// <summary>
        /// 当未处理的 <see cref="E:System.Windows.Input.Mouse.MouseMove"/> 附加事件在其路由中到达派生自此类的元素时,调用该方法。实现此方法可为此事件添加类处理。
        /// </summary>
        /// <param name="e">包含事件数据的 <see cref="T:System.Windows.Input.MouseEventArgs"/>///                 </param>
        protected override void OnMouseMove(MouseEventArgs e)
        {
            base.OnMouseMove(e);
            if ((ShowControl)&&(e.LeftButton == MouseButtonState.Pressed) && (_isPressedControlPoint))
            {
                //更新控制点
                ControlPoint = e.GetPosition(this);
            }
        }

        /// <summary>
        /// 在派生类中重写时,会参与由布局系统控制的呈现操作。调用此方法时,不直接使用此元素的呈现指令,而是将其保留供布局和绘制在以后异步使用。
        /// </summary>
        /// <param name="drawingContext">特定元素的绘制指令。此上下文是为布局系统提供的。
        ///                 </param>
        protected override void OnRender(DrawingContext drawingContext)
        {
            base.OnRender(drawingContext);

            if (ShowControl)
            {
                drawingContext.DrawLine(_linePen, StartPoint, ControlPoint);
                drawingContext.DrawEllipse(_ellipseBrush, _ellipsePen, ControlPoint, EllipseRadius, EllipseRadius);
            }
        }

AdjustableArrowBezierCurve

AdjustableArrowBezierCurve为可调整的贝塞尔曲线,代码与AdjustableArrowQuadraticBezier相似,只是从一个控制点变成两个控制点。代码从略。

代码

博客园:WPFArrows

GitHub:WPFArrows

posted @ 2015-03-07 23:48  赵御辩  阅读(10684)  评论(4编辑  收藏  举报