65.9K
CodeProject 正在变化。 阅读更多。
Home

WPF 箭头和自定义形状

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.42/5 (14投票s)

2008年1月22日

CPOL

2分钟阅读

viewsIcon

167131

downloadIcon

3831

本文演示了如何在 WPF 中创建箭头形状,并提供了创建自定义形状的指南

引言

WPF 是有史以来最好的 UI 框架。它为我们提供了大量矢量图形类型,例如 Line, Ellipse, Path 等。 有时我们需要 WPF 中未提供的形状(例如 Arrow),并且考虑到 Path 形状可以用来创建任何类型的 2D 形状,我们不想每次都重新计算每个点。 这就是创建自定义形状的一个好理由和机会。

背景

WPF 提供了两种矢量类型:Shapes 和 Geometries。

Shape 是任何派生自 Shape 基类的类型。 它提供 FillStroke 和其他用于着色的属性,并且实际上是一个 FrameworkElement。 因此,我们可以将形状放置在 Panel 中,我们可以注册形状路由事件并执行与 FrameworkElement 相关的任何操作。 (MSDN)

Geometry 是任何派生自 Geometry 基类型的类型。 它提供了用于描述任何类型的 2D 几何体的属性。 几何体实际上是一种 Freezable 类型,因此可以被冻结。 冻结的对象通过不通知更改来提供更好的性能,并且可以被其他线程安全地访问。 Geometry 不是 Visual,因此应由其他类型(例如 Path)绘制。 (MSDN)

Using the Code

现在我们有了一些背景知识,并且知道了 GeometryShape 之间的区别,我们就可以基于这两种类型之一创建我们的形状了。 对吗?

好吧,令人惊讶的是,我们不能基于 Geometry 类型创建自定义形状,因为它的唯一默认构造函数被标记为 internal。 微软真令人失望。

别担心! 我们仍然可以选择将自定义形状基于 Shape 基类。

现在,假设我们要创建一个 Arrow 形状。 箭头实际上是一种线,所以让我们从具有 X1Y1X2Y2 属性的 WPF Line 类型派生我们的自定义类型。

哎呀... Line 是 sealed 的! (再次令人失望)。

没关系,让我们直接从 Shape 基类派生,并添加 X1Y1X2Y2 和两个额外的属性来定义箭头的头部 widthheight

Arrow.jpg

我们的代码应该最终会变成这样

    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 形状的更多信息。

© . All rights reserved.