简介

参考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题库升级,iZ0-052新加的考题及答案整理-18

    18.You want to Install Oracle 11g database software and create a database on ASM Immediately after t ...

  2. 原生态js回顶部

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

  3. “全栈2019”Java第四十三章:封装

    难度 初级 学习时间 10分钟 适合人群 零基础 开发语言 Java 开发环境 JDK v11 IntelliJ IDEA v2018.3 文章原文链接 "全栈2019"Java第 ...

  4. [BZOJ4530][Bjoi2014]大融合(LCT)

    传送门 大佬们似乎都是用树剖+并查集优雅地A了此题 然后我太弱了,只能打打LCT的板子 虽然的确可以挺无脑的A掉…… 不过至少这题教了我该怎么维护LCT上虚子树的信息,具体看这里 首先,答案很明显是断 ...

  5. 外网域名映射内网Ip 内网使用外网域名的https证书

    用nslookup www.baidu.com nslookup www.qq.com 15.64.64.53 测试了内网的dns可以解析外网域名 那么我买个外网域名,再对这个外网域名买个证书,然后外 ...

  6. 小记 Linux 之 Vim

    小记 Linux 之 Vim 使用vim用来进行文本流查询,是非常重要的部分. 技巧一:使用 '#' 系统将列出文档相同字符,在代码时很重要. 技巧二:使用 ']I' 具体操作是先使用 ? 或 \ 进 ...

  7. Levenshtein字符串距离算法介绍

    Levenshtein字符串距离算法介绍 文/开发部 Dimmacro KMP完全匹配算法和 Levenshtein相似度匹配算法是模糊查找匹配字符串中最经典的算法,配合近期技术栏目关于算法的探讨,上 ...

  8. sql 简单语法

    1.数据库操作 create database student_info -- 创建数据库 drop database student_info -- 删除数据库 2.表操作 -- 创建表 creat ...

  9. Django项目使用七牛云存储图片

    Django项目使用七牛云存储图片 最近,写了一个django项目,想在项目中使用七牛云存储上传图片,在网上搜索到django-qiniu-storage,查看文档,按步骤居然设置成功了. 安装 1 ...

  10. idea validation code

    K71U8DBPNE-eyJsaWNlbnNlSWQiOiJLNzFVOERCUE5FIiwibGljZW5zZWVOYW1lIjoibGFuIHl1IiwiYXNzaWduZWVOYW1lIjoiI ...