WPF 箭头和自定义形状






4.42/5 (14投票s)
本文演示了如何在 WPF 中创建箭头形状,并提供了创建自定义形状的指南
引言
WPF 是有史以来最好的 UI 框架。它为我们提供了大量矢量图形类型,例如 Line
, Ellipse
, Path
等。 有时我们需要 WPF 中未提供的形状(例如 Arrow
),并且考虑到 Path
形状可以用来创建任何类型的 2D 形状,我们不想每次都重新计算每个点。 这就是创建自定义形状的一个好理由和机会。
背景
WPF 提供了两种矢量类型:Shapes 和 Geometries。
Shape
是任何派生自 Shape
基类的类型。 它提供 Fill
、Stroke
和其他用于着色的属性,并且实际上是一个 FrameworkElement
。 因此,我们可以将形状放置在 Panel
中,我们可以注册形状路由事件并执行与 FrameworkElement
相关的任何操作。 (MSDN)
Geometry
是任何派生自 Geometry
基类型的类型。 它提供了用于描述任何类型的 2D 几何体的属性。 几何体实际上是一种 Freezable
类型,因此可以被冻结。 冻结的对象通过不通知更改来提供更好的性能,并且可以被其他线程安全地访问。 Geometry
不是 Visual
,因此应由其他类型(例如 Path
)绘制。 (MSDN)
Using the Code
现在我们有了一些背景知识,并且知道了 Geometry
和 Shape
之间的区别,我们就可以基于这两种类型之一创建我们的形状了。 对吗?
好吧,令人惊讶的是,我们不能基于 Geometry
类型创建自定义形状,因为它的唯一默认构造函数被标记为 internal。 微软真令人失望。
别担心! 我们仍然可以选择将自定义形状基于 Shape
基类。
现在,假设我们要创建一个 Arrow
形状。 箭头实际上是一种线,所以让我们从具有 X1
、Y1
、X2
和 Y2
属性的 WPF Line
类型派生我们的自定义类型。
哎呀... Line
是 sealed 的! (再次令人失望)。
没关系,让我们直接从 Shape
基类派生,并添加 X1
、Y1
、X2
、Y2
和两个额外的属性来定义箭头的头部 width
和 height
。

我们的代码应该最终会变成这样
public sealed class Arrow : Shape
{
public static readonly DependencyProperty X1Property = ...;
public static readonly DependencyProperty Y1Property = ...;
public static readonly DependencyProperty HeadHeightProperty = ...;
...
[TypeConverter(typeof(LengthConverter))]
public double X1
{
get { return (double)base.GetValue(X1Property); }
set { base.SetValue(X1Property, value); }
}
[TypeConverter(typeof(LengthConverter))]
public double Y1
{
get { return (double)base.GetValue(Y1Property); }
set { base.SetValue(Y1Property, value); }
}
[TypeConverter(typeof(LengthConverter))]
public double HeadHeight
{
get { return (double)base.GetValue(HeadHeightProperty); }
set { base.SetValue(HeadHeightProperty, value); }
}
...
protected override Geometry DefiningGeometry
{
get
{
// Create a StreamGeometry for describing the shape
StreamGeometry geometry = new StreamGeometry();
geometry.FillRule = FillRule.EvenOdd;
using (StreamGeometryContext context = geometry.Open())
{
InternalDrawArrowGeometry(context);
}
// Freeze the geometry for performance benefits
geometry.Freeze();
return geometry;
}
}
/// <summary>
/// Draws an Arrow
/// </summary>
private void InternalDrawArrowGeometry(StreamGeometryContext context)
{
double theta = Math.Atan2(Y1 - Y2, X1 - X2);
double sint = Math.Sin(theta);
double cost = Math.Cos(theta);
Point pt1 = new Point(X1, this.Y1);
Point pt2 = new Point(X2, this.Y2);
Point pt3 = new Point(
X2 + (HeadWidth * cost - HeadHeight * sint),
Y2 + (HeadWidth * sint + HeadHeight * cost));
Point pt4 = new Point(
X2 + (HeadWidth * cost + HeadHeight * sint),
Y2 - (HeadHeight * cost - HeadWidth * sint));
context.BeginFigure(pt1, true, false);
context.LineTo(pt2, true, true);
context.LineTo(pt3, true, true);
context.LineTo(pt2, true, true);
context.LineTo(pt4, true, true);
}
}
正如您所看到的,由于 Shape
基类中的出色工作,实现自定义形状非常容易。 我们所要做的就是从 Shape
派生我们的自定义形状类型,并覆盖 DefiningGeometry
属性。 此属性应返回任何类型的 Geometry
。
关注点
我的解决方案在每次调用时创建并返回一个新的冻结几何体。 或者,您可以通过将非冻结几何体作为字段保存在自定义 Shape
类中来缓存它。
结束语
尽管自定义形状的实现非常简单,但您可以打开 Reflector
或使用最新的 Microsoft .NET Framework 代码发布,以了解有关 WPF 团队如何实现 WPF 形状的更多信息。