简介

参考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. OCP 12c最新考试原题及答案(071-7)

    7.(5-1) choose two:View the Exhibit and examine the structure of the PRODUCTS table.Which two tasks ...

  2. [ActionScript 3.0] 十进制与二进制,十六进制等数据之间的相互转换

    将十进制转换为二进制,方法是:将数字除以2,根据余数来从右往左排列二进制的位数,如下以十进制数10为例 10除以2得5,余数为0,故第一个位置为0: 5除以2得2,余数为1,故第二个位置为1: 2除以 ...

  3. 正则表达式 python

    下面这种方式 从结果上看 匹配的是关键字, 但是不是 每一次都可以 100% 准确 search_words_dict = { "肠炎宁": 0, "维生素AD" ...

  4. js控制输入框只能输入数字不能输入其他字符

    <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8&quo ...

  5. 免费观看vip/要劵的电影

    免费观看vip/要劵的电影 1.在爱奇艺/腾讯视频中复制电影的连接 2.复制连接到这个网站中(http://www.qmaile.com/) 3.粘贴路径到这个网站相应的位置 4.点击go ,等待解析 ...

  6. SimpleITK学习(一)基本概念

    断断续续使用simpleitk处理CT和X光图片有些时间了,但是学的知识都比较零散,没有形成系统的概念,于是对着SimpleITK的英文文档https://simpleitk.readthedocs. ...

  7. 【医学影像】《Dermatologist-level classification of skin cancer with deep neural networks》论文笔记

    这是一篇关于皮肤癌分类的文章,核心就是分类器,由斯坦福大学团队发表,居然发到了nature上,让我惊讶又佩服,虽然在方法上没什么大的创新,但是论文本身的工作却意义重大,并且这篇17年见刊的文章,引用量 ...

  8. Python开发转盘小游戏

    Python开发转盘小游戏 Python  一 原理分析 Python开发一个图形界面 有12个选项和2个功能键 确定每个按钮的位置 每个按钮的间隔相同 点击开始时转动,当前选项的背景颜色为红色,其他 ...

  9. 基础篇:6.9)GD&T较线性尺寸公差的优缺点

    本章目的:理解GD&T标注对比线性/传统/坐标尺寸公差的优势,但也不要忘记其使用限制. 1.线性尺寸公差   1.1 定义 线性尺寸公差=传统尺寸公差=坐标尺寸公差. 传统尺寸公差(Tradi ...

  10. post调试postman

    载地址:https://www.getpostman.com/ 教程地址:http://www.cnblogs.com/s380774061/p/4624326.html