如何在QT中绘制一条经过多个点的平滑曲线?

4
有没有办法在QT中通过一组点绘制平滑的曲线?这些点的数量和位置在运行时设置。
目前,我绘制了一个QPainterPath,其中包含从一个点到另一个点的lineTo,创建了一条路径。 我确实使用了渲染提示抗锯齿,但路径仍然很粗糙。
我看到QSplineSeries似乎可以提供这种曲线路径,但它在我使用的Qt4.8中不可用。
经常被建议的另一个选项是使用贝塞尔曲线,但那需要一个起点和终点以及两个控制点,所以我需要为每个线段(每个lineTo)计算,并以某种方式计算这些控制点,但现在我没有它们。
3个回答

7
最终,我实现了一种解决方法,基本上是将两条连接的线条取出,删除它们之间的连接点,并用曲线替换它。由于我有很多小线条,这种改变并不容易察觉,所以我删除所有非常短的线条,并重新连接开放端点。该函数主要由Bojan Kverh提供,请查看他的教程:https://www.toptal.com/c-plus-plus/rounded-corners-bezier-curves-qpainter 以下是函数列表:
namespace
{
    float distance(const QPointF& pt1, const QPointF& pt2)
    {
        float hd = (pt1.x() - pt2.x()) * (pt1.x() - pt2.x());
        float vd = (pt1.y() - pt2.y()) * (pt1.y() - pt2.y());
        return std::sqrt(hd + vd);
    }

    QPointF getLineStart(const QPointF& pt1, const QPointF& pt2)
    {
        QPointF pt;
        float rat = 10.0 / distance(pt1, pt2);
        if (rat > 0.5) {
            rat = 0.5;
        }
        pt.setX((1.0 - rat) * pt1.x() + rat * pt2.x());
        pt.setY((1.0 - rat) * pt1.y() + rat * pt2.y());
        return pt;
    }

    QPointF getLineEnd(const QPointF& pt1, const QPointF& pt2)
    {
        QPointF pt;
        float rat = 10.0 / distance(pt1, pt2);
        if (rat > 0.5) {
            rat = 0.5;
        }
        pt.setX(rat * pt1.x() + (1.0 - rat)*pt2.x());
        pt.setY(rat * pt1.y() + (1.0 - rat)*pt2.y());
        return pt;
    }

}

void PainterPath::smoothOut(const float& factor)
{
    QList<QPointF> points;
    QPointF p;
    for (int i = 0; i < mPath->elementCount() - 1; i++) {
        p = QPointF(mPath->elementAt(i).x, mPath->elementAt(i).y);

        // Except for first and last points, check what the distance between two
        // points is and if its less then min, don't add them to the list.
        if (points.count() > 1 && (i < mPath->elementCount() - 2) && (distance(points.last(), p) < factor)) {
            continue;
        }
        points.append(p);
    }

    // Don't proceed if we only have 3 or less points.
    if (points.count() < 3) {
        return;
    }

    QPointF pt1;
    QPointF pt2;
    QPainterPath* path = new QPainterPath();
    for (int i = 0; i < points.count() - 1; i++) {
        pt1 = getLineStart(points[i], points[i + 1]);
        if (i == 0) {
            path->moveTo(pt1);
        } else {
            path->quadTo(points[i], pt1);
        }
        pt2 = getLineEnd(points[i], points[i + 1]);
        path->lineTo(pt2);
    }

    delete mPath;
    mPath = path;
    prepareGeometryChange();
}

1
我认为在Qt 4.8中没有现成的解决方案(正如你所注意到的,QSplineSeries是Qt 5.x的功能)。另外,QSplineSeriesQtCharts模块的一部分,而该模块是商业模块(像QtDataVisualization),因此除非您拥有商业许可证或您的项目是GPL,否则您无法使用它。
您必须手动完成这个过程,即通过需要的数学知识并自己实现它(或找到一个漂亮的实现(不必是C ++,更不用说Qt兼容了))。
既然您提到了贝塞尔曲线,我建议尝试一下复合贝塞尔曲线。我记得在我工作的一个项目中实现了这个东西。它需要一些......工作。:D 本文可能会帮助您入门。
贝塞尔曲线实际上是B样条(如果我没记错的话)。特别是如果你可以接受一定程度的不平滑,你可以相当快地生成复合贝塞尔曲线。由于它们的健壮性和流行度,我100%确定你可以在网上找到一个不错的实现。可能不适用于Qt,但如果代码编写正确,你应该能够很快地适应它。
这个链接看起来非常有前途(它是用ActionScript编写的,但是嘛)。或者你也可以尝试使用QPainterPath::cubicTo(),只要你还提供了计算曲线所需的两个控制点,就可以为你创建贝塞尔曲线。

是的,我担心这没有简单的解决方案,我会再看一下贝塞尔曲线和B样条。 - Damir Porobic
抱歉带来不好的消息。 :D 我已经更新了我的答案,并附上了一个非常有用的链接(使用ActionScript进行实现)。 - rbaleksandar
谢谢,看起来有点凌乱但很有趣,我会试一下的 :D - Damir Porobic
没有什么是完美的。 :P - rbaleksandar
如果您使用贝塞尔曲线,曲线将不会穿过点。 - m. c.
显示剩余3条评论

1
几乎每个人都使用立方插值来完成此任务,你的选择是贝塞尔曲线或Catmull-Rom样条。如果必须命中每个点,则需要保持Beziers的控制点之间的“手柄”或线条直线。然后使用最小二乘法进行拟合,正如你已经发现的那样,这有点复杂。
Catmull Rom样条的优点在于它们只需要两个额外的控制点(起点和终点,只需镜像点即可创建它们)。只要点比较平滑,线条就会表现良好。QT图形不太可能直接绘制CatMull Rom样条,因此将其转换为Beziers,这是一种标准的公布方法,你可以很容易地从Catmull Rom转换为Bezier,但反之则不行——不是每个Bezier都可以用少量点表示为Catmull Rom。
如果立方体无法给出所需曲线,则可以使用其他插值方法,例如五次插值。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接