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

How to draw a smooth curved line that goes through several points in QT?

有没有办法在QT中通过一组点绘制一条平滑的线? 在 运行 时间内设置点的数量和位置。

目前,我绘制了一个 QPainterPath,其中包含从一点到另一点的 lineTo,创建一条路径。我确实使用渲染提示抗锯齿,但路径仍然是锯齿状的。

我见过 QSplineSeries,它似乎提供了这种弯曲的路径,但它在 Qt4.8 中不可用,这是我正在使用的 QT 版本。

另一个经常被建议的选项是使用贝塞尔曲线,但那些使用一个起点和终点以及两个控制点,所以我需要为每个段(每个 lineTo)计算它并以某种方式计算那些控制点我暂时没有。

我认为 Qt 4.8 中没有开箱即用的解决方案(您已经注意到 QSplineSeries 是 Qt 5.x 的一项功能)。另外 QSplineSeriesQtCharts 模块的一部分,它是一个商业模块(如 QtDataVisualization),所以除非你有商业许可证或者你的项目是 GPL,否则你不能使用它。

你必须手动完成它,即通过它所需的数学运算并自己实现它(或者找到一个很好的实现(甚至不需要在 C++ 中,更不用说与 Qt 兼容了))。

既然你提到了贝塞尔曲线,我建议给出 composite Bezier curve a shot. I remember implementing that thing for a project I worked on. It required some...work. :D This article 可能会帮助你开始。

贝塞尔曲线实际上是 B 样条曲线(如果我没记错的话)。尤其是如果您可以解决一定程度的平滑度不足问题,您可以非常快速地生成复合贝塞尔曲线。考虑到它们的稳健性和受欢迎程度,我 100% 确定您可以在网上找到一个不错的实现。可能对 Qt 不友好,但如果编写得当,您应该能够立即调整代码。

This looks quite promising (it's in ActionScript but meh). Or you can given the QPainterPath::cubicTo()一个可以为您创建贝塞尔曲线的镜头,您也可以提供计算曲线所需的两个控制点。

几乎每个人都使用三次插值来完成这项任务,您的选择是贝塞尔曲线或 Catmull-Rom 样条。如果您必须击中每个点,那么您需要保持 "handles" 或贝塞尔曲线控制点之间的直线。然后您使用最小二乘法进行拟合,您发现这有点复杂。

Catmull Rom 样条的优点是它们只需要两个额外的控制点(开始和结束,只需镜像点即可创建它们)。只要点相当平滑,线就会表现得很好。 QT 图形不太可能直接绘制 CatMull Rom 样条曲线,因此转换为 Beziers,这是一种标准的已发布方法,您可以很容易地从 Catmull Rom 转到 Bezier,但不是相反 - 并非每个 Bezier 都可以用 Catmull Rom 表示只有几点。

您可以使用其他插值方法,eq quintic,如果三次不能给您想要的曲线。

最后我实施了某种变通方法,基本上采用两条连接线,删除它们之间的连接点并用曲线替换它。由于我有很多细线,这样的变化是不可见的,所以我删除了所有非常短的线并重新连接了开口端。该功能主要由 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();
}