前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >如何在WPF绘图中(通过贝塞尔曲线)绘制平滑曲线

如何在WPF绘图中(通过贝塞尔曲线)绘制平滑曲线

作者头像
程序你好
发布2020-11-19 14:17:00
2.8K0
发布2020-11-19 14:17:00
举报
文章被收录于专栏:程序你好程序你好

GDI图形系统已经形成了很多年。它提供了2D图形和文本功能,以及受限的图像处理功能,在传统的Windows Form 编程中,我们经常使用Graphics图形对象的DrawCurve方法绘制平滑的曲线。

该方法定义如下:

public void DrawCurve(Pen pen, Point[] points, float tension)

其中tension参数是弯曲强度(张力),用来确定样条的形状及平滑点直接的连线,该值范围为0.0f ~1.0f,默认0.5,超出此范围会产生异常,当弯曲强度为零时,两点直接的连线就成了直线。

WPF绘图编程与传统GDI编程有显著不同,WPF中已经提供很多更强大灵活的方法进行绘制,可以方便绘制任意的矢量图形。

DrawingContext比较类似WinForm中的Graphics 类,是基础的绘图对象,用于绘制各种图形,它主要API有如下几种:

常用的基础的绘图API有:

  • DrawEllipse
  • DrawGeometry
  • DrawGlyphRun
  • DrawImage
  • DrawRectangle
  • DrawRoundedRectangle

基本绘图API跟GDI中的类似,大家发现了没有?WPF并没有DrawCurve的方法,虽然z有DrawGeometry方法可以绘制图形,但是找不到没有“张力”的参数。由于没有提供与DrawCurve方法等价的方法,WPF中没有提供方法调用来绘制光滑曲线,我们可以通过一系列贝塞尔曲线绘制一个平滑的曲线。

贝塞尔曲线(Bézier curve),又称贝兹曲线或贝济埃曲线,是应用于二维图形应用程序的数学曲线。贝塞尔曲线是计算机图形图像造型的基本工具,是图形造型运用得最多的基本线条之一。它通过控制曲线上的四个点(起始点、终止点以及两个相互分离的中间点)来创造、编辑图形。其中起重要作用的是位于曲线中央的控制线。这条线是虚拟的,中间与贝塞尔曲线交叉,两端是控制端点。移动两端的端点时贝塞尔曲线改变曲线的曲率(弯曲的程度);移动中间点(也就是移动虚拟的控制线)时,贝塞尔曲线在起始点和终止点锁定的情况下做均匀移动。

上图显示了这四个点是如何决定曲线形状的。曲线从起始点(A)开始,向第一个控制点(B)的方向移动。它在终点(D)结束,从第二个控制点(C)的方向来。图中的蓝色线显示了端点和控制点之间的方向。

从起点和终点到控制点的距离决定了曲线与蓝色线的距离。如果控制点较远,则曲线沿蓝色线较长。

要绘制一条连接一系列点的平滑曲线,可以构建多个从这些点开始和结束的贝塞尔曲线。为了使曲线平滑,你需要在相邻的曲线上对齐控制点,使它们的上图蓝色指向相同的方向。下图显示两条贝塞尔曲线平滑地连接在一起。第一条曲线的第二个控制点(标记为“control 1b”)和第二条曲线的第一个控制点(标记为“control 2a”)与连接两条Bezier曲线的点共线。

根据需要我们可以移动控制点控制1b和控制2a离它们控制的点更近或更远,只要这三个点是共线的。例如,您可以将控件2a移动到更靠近点的位置,使第二条贝塞尔曲线在开始时变得更紧。就像GDI绘图中DrawCurve方法提供了一个参数tension(它允许您调整控制点与曲线上的点的距离)一样。当你构建一系列贝塞尔曲线时,你可以单独放置每个控制点。

WPF提供了一个类PolyBezierSegment,你可以用来保存一组连接的Bezier曲线:PolyBezierSegment。该对象包含一个起始点和一组点,这些点包括控制点和Bezier曲线的曲线点。这将非常有用(需要一些工作),但是不能简单地显示一个PolyBezierSegment。

首先,使用您想要连接的点来找到适当的控制点。然后使用它们来构建一个包含PolyBezierSegment对象和所有其他必要的中间对象的路径。这样就可以使用WPF构建平滑的曲线。

寻找控制点

那么如何定义控制点呢?看看右边的图片,它显示了三条连接点A、B、C和D的贝塞尔曲线。现在关注蓝色曲线。它需要两个控制点,一个在B点之后,一个在C点之前。

为了找到数据点B附近的控制点,我们查看由点B的两个相邻点A和C定义的线段。红色虚线段将这些点连接起来。现在我们从点B沿着线段的方向移动。绿色虚线段表示平移后的红色线段,它与点B相交。我们沿着这段线段移动来放置控制点的距离取决于曲线的张力。当您查看代码时,您将看到它是如何工作的。

请注意,您使用同一段来定义特定数据点两侧的控制点。在图中,你使用相同的绿色虚线段来定义点B之前和之后的控制点。因为这些控制点在与点B相交的一条线上,点B两边的两条Bezier曲线将会平滑地相交。

要找到蓝色曲线在点C附近的控制点,您可以类似地查看点B和D之间的部分。

建立这一系列曲线有两种特殊情况。起始点和结束点两边都没有邻居,所以它们被用来代替它们缺少的邻居。例如,在前面的图片中,红色虚线将从点A指向点b,这意味着曲线开始指向第二个点。

类似地,点D的红色虚线段从点D点指向点C,所以曲线结束时远离倒数第二个点。

定义寻找控制点的方法:

参数points:是绘制平滑曲线的一组点数据。

参数tension:张力参数决定控制点与数据点的距离。

返回值Point[]:并返回一个数组,该数组包含这些点和它们之间的控制点。

代码语言:javascript
复制

private Point[] MakeCurvePoints(Point[] points, double tension)
{
    if (points.Length < 2) return null;
    double control_scale = tension / 0.5 * 0.175;

    List<Point> result_points = new List<Point>();
    result_points.Add(points[0]);

    for (int i = 0; i < points.Length - 1; i++)
    {
        // Get the point and its neighbors.
        Point pt_before = points[Math.Max(i - 1, 0)];
        Point pt = points[i];
        Point pt_after = points[i + 1];
        Point pt_after2 = points[Math.Min(i + 2, points.Length - 1)];

        double dx1 = pt_after.X - pt_before.X;
        double dy1 = pt_after.Y - pt_before.Y;

        Point p1 = points[i];
        Point p4 = pt_after;

        double dx = pt_after.X - pt_before.X;
        double dy = pt_after.Y - pt_before.Y;
        Point p2 = new Point(
            pt.X + control_scale * dx,
            pt.Y + control_scale * dy);

        dx = pt_after2.X - pt.X;
        dy = pt_after2.Y - pt.Y;
        Point p3 = new Point(
            pt_after.X - control_scale * dx,
            pt_after.Y - control_scale * dy);

        // Save points p2, p3, and p4.
        result_points.Add(p2);
        result_points.Add(p3);
        result_points.Add(p4);
    }

    // Return the points.
    return result_points.ToArray();
}

需要设置一个control_scale,用来设置控制点与数据点的距离。

接下来,代码创建一个result_points列表来保存数据点和控制点。它将曲线的“第一个点”添加到列表中。

然后,该方法循环遍历数据点,在到达最后一个数据点之前停止。对于每个数据点,代码必须找到从该数据点开始的贝塞尔曲线的控制点。

程序找到这个点之前的点,这个点之后的点,以及这个点之后的两个位置。如果数据点是第一个或最后一个点,那么这个位置之前或这个位置之后的两个点将不存在。在这种情况下,代码将使用数据点来获取前一节中描述的红色虚线,用于那些特殊情况。

然后,代码计算在这个点之前和之后的点之间X和Y坐标的变化。它将这些值乘以缩放因子control_scale,并将结果添加到当前点的坐标中,以获得控制点p2的位置。

然后,该方法执行类似的计算,以找到曲线的第二个控制点p3。

构建包含一系列Bezier曲线的Path对象

下面的方法接受一个包含数据和控制点的数组作为输入,并构建一个包含适当的PolyBezierSegment的Path对象。

创建保存PolyBezierSegment所需的WPF对象

代码语言:javascript
复制
private Path MakeBezierPath(Point[] points)
{
    // Create a Path to hold the geometry.
    Path path = new Path();

    // Add a PathGeometry.
    PathGeometry path_geometry = new PathGeometry();
    path.Data = path_geometry;

    // Create a PathFigure.
    PathFigure path_figure = new PathFigure();
    path_geometry.Figures.Add(path_figure);

    // Start at the first point.
    path_figure.StartPoint = points[0];

    // Create a PathSegmentCollection.
    PathSegmentCollection path_segment_collection =
        new PathSegmentCollection();
    path_figure.Segments = path_segment_collection;

    // Add the rest of the points to a PointCollection.
    PointCollection point_collection =
        new PointCollection(points.Length - 1);
    for (int i = 1; i < points.Length; i++)
        point_collection.Add(points[i]);

    // Make a PolyBezierSegment from the points.
    PolyBezierSegment bezier_segment = new PolyBezierSegment();
    bezier_segment.Points = point_collection;

    // Add the PolyBezierSegment to othe segment collection.
    path_segment_collection.Add(bezier_segment);

    return path;
}

绘制曲线

最后,参考 Graphics对象的DrawCurve(Pen pen, Point[] points, float tension)方法,

定义一个MakeCurve方法从一组点建立一系列贝塞尔曲线,该方法将连接点数组和张力值作为参数。它调用MakeCurvePoints来创建控制点,然后调用MakeBezierPath来构建Bezier曲线。

代码语言:javascript
复制
private Path MakeCurve(Point[] points, double tension)
{
    if (points.Length < 2) return null;
    Point[] result_points = MakeCurvePoints(points, tension);

    // Use the points to create the path.
    return MakeBezierPath(result_points.ToArray());
}
本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2020-11-12,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 程序你好 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 寻找控制点
  • 构建包含一系列Bezier曲线的Path对象
  • 绘制曲线
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档