简介
参考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。