How to draw a smooth curved line that goes through

2020-06-23 07:17发布

问题:

Is there a way to draw a smooth line through a set of points in QT? The number and position of the points is set during run time.

Currently, I draw a QPainterPath which contains lineTo's going from point to point, creating a path. I do use render-hints antialiasing but the path is still jagged.

I've seen QSplineSeries which seems to give this kind of curved path but it is not available in Qt4.8, which is the QT Version I'm using.

Another option that is being suggested often is using Bezier Curves but those use one start and end point and two control point, so I would need to calculate it for every segment (every lineTo) and somehow calculate those control points which I don't have at the moment.

回答1:

In the end I've implemented some kind of workaround which basically takes two connected lines, removes the connection point between them and replaces it with a curve. As I have a lot of small lines where such a change would not be visible I remove all lines that are very short and reconnect the open ends. The function was mostly provided by Bojan Kverh, check out his tutorial: https://www.toptal.com/c-plus-plus/rounded-corners-bezier-curves-qpainter

And here the functions:

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();
}


回答2:

I don't think that there is no out-of-the-box solution in Qt 4.8 (as you have noticed QSplineSeries is a Qt 5.x feature). Also QSplineSeries is part of QtCharts module which is a commercial one (like QtDataVisualization) so unless you have a commercial license or your project is GPL you can't use it.

You have to do it manually that is go through the math required for it and implement it yourself (or find a nice implementation (not necessary to be even in C++ let alone Qt-compatible)).

Since you have mentioned Bezier curves I would suggest giving the composite Bezier curve a shot. I remember implementing that thing for a project I worked on. It required some...work. :D This article might help you get started.

Bezier curves are in fact B-splines (if I remember correctly). Especially if you can settle with a certain lack of smoothness you can generate composite Bezier curves pretty fast. To to their robustness and popularity I'm 100% sure that you can find a decent implementation online. Probably not Qt-friendly but if written properly you should be able to adapt the code in no time.

This looks quite promising (it's in ActionScript but meh). Or you can given the QPainterPath::cubicTo() a shot which can create Bezier curves for you given you can also provide the two control points required for the calculation of the curve.



回答3:

Pretty much everyone uses cubic interpolation for this task, and your choice is a Bezier Curve or a Catmull-Rom spline. If you must hit every point, then you need to keep the "handles" or the line between the control points of the Beziers straight. You then fit using least squares, which as you have found out is a bit involved.

Catmull Rom splines have the advantage that they only need two extra control points (start and end, simply mirror points to create them). As long as the points are reasonably smooth, the line will be well-behaved. It's unlikely that QT graphics will draw CatMull Rom splines directly, so convert to Beziers, that's a standard published method, you can go from Catmull Rom to Bezier quite easily, though not the reverse - not every Bezier can be represented by a Catmull Rom with only a few points.

You can use other interpolation methods, eq quintic, if cubics won't give you the curve you desire.