Search code examples
c++qtsplineqgraphicssceneqgraphicsitem

What's the best way to implement interactive spline-like curve on QGraphicsView?


I'm developing application which would allow user to upload an image of a rock crack and apply spline approximation to that crack.

For that purpose i have QGraphicsView, that displays an uploaded image. Once image is uploaded, user has an option to draw points on a scene, which i want to be consistenly connected with lines. Those connected points would make a curve, which i want to be interactive. By interactive i mean that i want user to be able to drag points without breaking the curve, making lines, that connect points, move with the point. I also want to make user able to delete selected points so that adjacent points stay connected. Those are not all the features i want, but i think you get the idea.

What i've done is i've created a class MeasurePoint that inherits from QGraphicsItem and contains all the information about one individual point. It has the following fields:

int xPos;
int yPos;
int index;
bool movable;
bool selected;

That class also has several methods for managing those points (getter and setter functions for the fields, etc). But i don't implement connection feature in that class because it only contains information about individual point, and not further. I like the way points behave if i simply add them to the scene. But now i need to connect them the way i described earlier and i can't really figure out how to do it.

What's the best way to store the points? How exactly should i implement connections between points? Any possible help is appreciated.

P.s. if my implementation of class MeasurePoint is needed in this question, i'll edit it.


Solution

  • Use a QGraphicsPathItem to draw the spline. Then, use the QPainterPath::cubicTo method to create the connections between the points.

    You need to compute the control points to draw a smooth bezier curve between two points.

    You can find a bunch of better libraries based on Quadratic Bezier curves, but a quick example below that uses Qt types and a naive approach to understand how to get the control points:

    QPair<QPointF, QPointF> controlPoints(QPointF const& p0, QPointF const& p1, QPointF const& p2, qreal t=0.25)
    {
        QPair<QPointF, QPointF> pair;
        qreal d01 = qSqrt( ( p1.x() - p0.x() ) * ( p1.x() - p0.x() ) + ( p1.y() - p0.y() ) * ( p1.y() - p0.y() ) );
        qreal d12 = qSqrt( ( p2.x() - p1.x() ) * ( p2.x() - p1.x() ) + ( p2.y() - p1.y() ) * ( p2.y() - p1.y() ) );
    
        qreal fa = t * d01 / ( d01 + d12 );
        qreal fb = t * d12 / ( d01 + d12 );
    
        qreal c1x = p1.x() - fa * ( p2.x() - p0.x() );
        qreal c1y = p1.y() - fa * ( p2.y() - p0.y() );
        qreal c2x = p1.x() + fb * ( p2.x() - p0.x() );
        qreal c2y = p1.y() + fb * ( p2.y() - p0.y() );
    
        pair.first = QPointF( c1x, c1y );
        pair.second = QPointF( c2x, c2y );
    
        return pair;
    }
    

    Then, browse your points list to create a QPainterPath:

    QPainterPath BackgroundBuilder::buildPath(QList<QPointF> const& points)
    {
        QPainterPath pth;
    
        QPair<QPointF, QPointF> pair = controlPoints(points.at(0), points.at(1), points.at(2));
        QPointF p0 = pair.second;
        pth.moveTo(0, 0);
        pth.lineTo(p0);
        for (int i = 2; i != points.count() - 1; ++i)
        {
            QPair<QPointF, QPointF> pair = controlPoints( points.at(i - 1), points.at(i), points.at(i + 1));
            pth.cubicTo( p0, pair.first, points.at( i ) );
            p0 = pair.second;
        }
        return pth;
    }
    

    You may need to convert your MeasurePoint to QPointF to keep this code generic:

    QList<QPointF> points;
    QList<MeasurePoint*> measures = ...;
    for (MeasurePoint* measure: measures)
    {
        points << QPointF(measure.xPos, measure.yPos);
    }