简介

参考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[] as PolyLineSegment;
if (polyseg != null)
{
var matx = new Matrix();
Vector vect = startPoint - endPoint;
//获取单位向量
vect.Normalize();
vect *= ArrowLength;
//旋转夹角的一半
matx.Rotate(ArrowAngle / );
//计算上半段箭头的点
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 < )
{
//怎么也得有一行
textLineCount = ;
}
//计算朝上的偏移
offsetY = -formattedText.Height*textLineCount -StrokeThickness;
}
startPoint = startPoint +new Vector(,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

WPF画箭头的更多相关文章

  1. 菱形实现气泡Bubble,菱形画箭头,菱形画三角形

    菱形实现气泡Bubble,菱形画箭头,菱形画三角形 >>>>>>>>>>>>>>>>>>&g ...

  2. android 使用Canvas画箭头

    public class MyCanvas extends View{        private Canvas myCanvas;    private Paint myPaint=new Pai ...

  3. Swift实时画箭头的实现

    iOS上实现画箭头,如果是指定了坐标点,那是很简单的,但如果需要做到实时绘制,就需要计算一下了 需求: 在白板上,根据手势落下点和移动点,实时绘制一条箭头直线(如下图) 实现代码: /// 获取箭头的 ...

  4. SVG 使用marker画箭头(一)

    一.使用Marker画箭头 1.定义一个箭头的marker引用 <defs> <marker id='markerArrow' markerWidth='13' markerHeig ...

  5. WPF 画线动画效果实现

    原文:WPF 画线动画效果实现 弄了将近三天才搞定的,真是艰辛的实现. 看了很多博客,都太高深了,而且想要实现的功能都太强大了,结果基础部分一直实现不了,郁闷啊~ 千辛万苦终于找到了一个Demo,打开 ...

  6. canvas画箭头demo

    效果图: 代码: <!DOCTYPE html> <html> <title>canvas画箭头demo</title> <body> &l ...

  7. D2D画箭头的例子

    原文:D2D画箭头的例子 版权声明:本文为博主原创文章,未经博主允许不得转载. https://blog.csdn.net/sunnyloves/article/details/50830102 用处 ...

  8. 如何用CorelDRAW画箭头?

    CorelDRAW,简称为cdr,是一款专业的矢量绘图软件,在设计界也是常用的专业设计之一,在日常的设计工作中,我们常常需要绘制一些特殊的图形,比如箭头.很多对cdr不是特别熟练的小伙伴不知道如何用c ...

  9. 在matlab 画箭头

    [转载]在matlab 画箭头 原文地址:在matlab 画箭头作者:纯情小郎君 完整见链接http://www.mathworks.com/matlabcentral/fx_files/14056/ ...

随机推荐

  1. Ubuntu16.04实用python脚本 - 启动nautilus(Gnome的文件管理器)!

    nautilus是Gnome的图形的文件管理器,可以很方便管理各种文件,但是通常我们不是在root用户下,如果想在root下使用,必须在shell里面输入命令: sudo nautilus 这样做固然 ...

  2. UML图基础

    UML(Unified Model Language)统一建模语言,是对象管理组织(OMG)制定的一个通用的.可视化的建模标准语言,可以用来可视化.描述.构造和文档化软件密集型系统的各种工作.在学习设 ...

  3. [ActionScript 3.0] 利用InteractivePNG.as类精确选择识别png图片有像素的区域

    用法:如果是把png直接导入flash转换成影片剪辑,只需在影片剪辑属性中勾选为ActionScript导出(x),并把基类里的flash.display.MovieClip替换成Interactiv ...

  4. Oracle中对多行查询结果进行拼接

    to_char(wmsys.wm_concat(to_char( st.col_name))) as new_name to_char: 将当前值转换成字符串类型; wmsys.wm_concat:拼 ...

  5. luogu P1518 两只塔姆沃斯牛 The Tamworth Two

    luogu P1518 两只塔姆沃斯牛 The Tamworth Two 题目描述 两只牛逃跑到了森林里.农夫John开始用他的专家技术追捕这两头牛.你的任务是模拟他们的行为(牛和John). 追击在 ...

  6. redis存储的数据类型

    key-velue数据结构存储 key   只能是字符串 value 有5种数据leixing. 字符串 string 哈希 hash 列表 list 集合 set 有序集合 zset

  7. js常见报错解决方法

    1.获得类名document.getElementsClassName(常出现一个问题): getClassName("gn","pt")[0].appendC ...

  8. iOS 音频/视频 学习目录

    参考 iOS原生API  音/视频录制 编辑 https://www.cnblogs.com/kenshincui/p/4186022.html#summary iOS视频编解码常用库比较 http: ...

  9. SpringBoot入门(IDEA篇)(二)

    一.SpringBoot启动的3种方式 第一种:借助IDE工具直接启动 run as 第二种:mvn命令启动 1:打开命令行,进入到项目目录中(我这里还是用上次建立的dog项目来操作)cd E:\Wo ...

  10. appium桌面版和命令行版的安装

    一.appium桌面版: 启动很慢,一般用于元素定位 Appium-Desktop官方下载地址:https://github.com/appium/appium-desktop/releases/ ( ...