WPF画箭头
简介
参考Using WPF to Visualize a Graph with Circular Dependencies的基础上写了一个WPF画箭头的库。
使用的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>
类关系
形状绘制原理
我们常用的形状,如Rectangle、Ellipse、Line、Path等,都继承自Shape类,类关系如下:
(图像摘自<<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。