Search code examples
c++qtrotationqgraphicsviewqgraphicsitem

How to correctly rotate a QGraphicsItem around different anchors in Qt C++


I am working on a custom QGraphicsItem that has two anchor points, and I want to be able to rotate the item around these anchors when the user interacts with them. I have implemented a mousePressEvent and mouseMoveEvent to detect which anchor the user clicked on, set the rotation anchor point, and compute the angle of rotation.

Here is a simplified version of my code:

MyView.h

static constexpr float ANCHOR_RADIUS = 10;

class MyView : public QGraphicsItem {
public:
    MyView(float xPos, float yPos, float width, float height, QGraphicsItem *parent = nullptr)
        : _width(width), _height(height), _viewState(VIEW) {
        setPos(xPos, yPos);

        setFlag(ItemIsMovable);

        auto diameter = 2 * ANCHOR_RADIUS;
        _anchor1.setRect(0, 0, diameter, diameter);
        _anchor2.setRect(width - diameter, 0, diameter, diameter);
    }

    void paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget) override {
        auto diameter = 2 * ANCHOR_RADIUS;
        _anchor1.setRect(0, 0, diameter, diameter);
        _anchor2.setRect(_width - diameter, 0, diameter, diameter);

        // Pin 1 and 2 coordinate
        auto c1 = _anchor1.center();
        auto c2 = _anchor2.center();

        painter->drawLine(static_cast<int> (c1.x()), static_cast<int>(c1.y()),
                          static_cast<int>(c2.x()), static_cast<int>(c2.y()));

        painter->drawRect(boundingRect());

        painter->drawEllipse(_anchor1);
        painter->drawEllipse(_anchor2);
    }

    [[nodiscard]] QRectF boundingRect() const override {
        return {0, 0, static_cast<qreal>(_width), static_cast<qreal>(_height)};
    }

    enum ViewState {
        ANCHOR1, ANCHOR2, VIEW
    };

protected:
    void mousePressEvent(QGraphicsSceneMouseEvent *event) override {
        _tapPoint = event->pos();

        auto cp1 = _anchor1.center(); // get center point of anchor 1
        auto cp2 = _anchor2.center(); // get center point of anchor 2

        // Anchor 1 clicked
        if (_anchor1.contains(_tapPoint)) {
            setTransformOriginPoint(cp2.x(), cp2.y()); // set rotation anchor to anchor 2
            _viewState = ANCHOR1;
        }
            // Anchor 2 clicked
        else if (_anchor2.contains(_tapPoint)) {
            setTransformOriginPoint(cp1.x(), cp1.y()); // set rotation anchor to anchor 1
            _viewState = ANCHOR2;
        }
            // View clicked
        else {
            QGraphicsItem::mousePressEvent(event);
            _viewState = VIEW;
        }
    }
    void mouseMoveEvent(QGraphicsSceneMouseEvent *event) override {
        auto p = event->pos();

        switch (_viewState) {
            case ANCHOR1: {

                // calculate the angle of the rotation based on the mouse touch
                auto angle = qRadiansToDegrees(qAtan2(p.y() - _anchor2.y(), _width));
                setRotation(rotation() - angle); // rotate the item around anchor 2

                break;
            }
            case ANCHOR2: {
                // calculate the angle of the rotation based on the mouse touch
                auto angle = qRadiansToDegrees(qAtan2(p.y() - _anchor1.y(), _width));
                setRotation(rotation() + angle); // rotate the item around anchor 1
                break;
            }
            default:
                QGraphicsItem::mouseMoveEvent(event); // move the item normally
        }
    }

private:
    float _width, _height;
    QRectF _anchor1, _anchor2;
    QPointF _tapPoint;
    ViewState _viewState;
};

main.cpp

static constexpr int WIDTH = 500;
static constexpr int HEIGHT = 500;

int main(int argc, char *argv[]) {
    QApplication a(argc, argv);

    QGraphicsScene scene;
    scene.setSceneRect(QRectF(0, 0, WIDTH, HEIGHT));

    QGraphicsView view(&scene);
    view.setRenderHint(QPainter::Antialiasing);

    QVBoxLayout layout;
    layout.addWidget(&view);

    QWidget widget;
    widget.setLayout(&layout);

    MyView myView(100, 100, 200, 20);
    scene.addItem(&myView);

    widget.show();

    return QApplication::exec();
}

However, when I try to rotate the item from one anchor point (around the other) and then rotate it again from the other anchor point, it jumps back to the initial position! I am not sure why this is happening.

As you can see in this video, when I first rotate the view it works, but when I try to rotate it from the other anchor, its position jumps to another position!

enter image description here

This is what I am trying to achieve (created with the GeoGebra tool): enter image description here

The solution needs to be applicable to any shape drawn within the MyView::paint() function, rather than being limited to just a line. Although there is an online solution available here, it only works for a line, and similarly, @kenash0625's solution also only works for a line.

Question: What could be causing this issue, and how can I modify my code to achieve the desired behavior of smoothly rotating around different anchor points?


Solution

  • this one should be applicable to any shape drawn within the MyView::paint() function

    I made 2 change to your code

    1. add call to QGraphicsItem::setTransformations(const QList<QGraphicsTransform *> &transformations)

    2. change from

    auto angle = qRadiansToDegrees(qAtan2(p.y()- _anchor2.y() , _width));

    to

    auto angle = qRadiansToDegrees(qAtan2( _anchor2.y() -p.y(), _width));

    here is edited code:

    
    #include<QGraphicsItem>
    #include<QPainter>
    #include<QGraphicsSceneMouseEvent>
    #include<QGraphicsScene>
    #include<QGraphicsView>
    #include <QGraphicsRotation>
    #include<qmath.h>
    static constexpr float ANCHOR_RADIUS = 10;
    
    class MyView : public QGraphicsItem {
    public:
        MyView(float xPos, float yPos, float width, float height, QGraphicsItem* parent = nullptr)
            : _width(width), _height(height), _viewState(VIEW) {
            setPos(xPos, yPos);
    
            setFlag(ItemIsMovable);
    
            auto diameter = 2 * ANCHOR_RADIUS;
            _anchor1.setRect(0, 0, diameter, diameter);
            _anchor2.setRect(width - diameter, 0, diameter, diameter);
        }
    
        void paint(QPainter* painter, const QStyleOptionGraphicsItem* option, QWidget* widget) override {
            auto diameter = 2 * ANCHOR_RADIUS;
            _anchor1.setRect(0, 0, diameter, diameter);
            _anchor2.setRect(_width - diameter, 0, diameter, diameter);
    
            // Pin 1 and 2 coordinate
            auto c1 = _anchor1.center();
            auto c2 = _anchor2.center();
    
            painter->drawLine(static_cast<int> (c1.x()), static_cast<int>(c1.y()),
                static_cast<int>(c2.x()), static_cast<int>(c2.y()));
    
            painter->drawRect(boundingRect());
    
            painter->drawEllipse(_anchor1);
            painter->drawEllipse(_anchor2);
        }
    
        [[nodiscard]] QRectF boundingRect() const override {
            return { 0, 0, static_cast<qreal>(_width), static_cast<qreal>(_height) };
        }
    
        enum ViewState {
            ANCHOR1, ANCHOR2, VIEW
        };
    
    protected:
        void mousePressEvent(QGraphicsSceneMouseEvent* event) override {
            _tapPoint = event->pos();
    
            // Anchor 1 clicked
            if (_anchor1.contains(_tapPoint)) {
                _viewState = ANCHOR1;
            }
            // Anchor 2 clicked
            else if (_anchor2.contains(_tapPoint)) {
                _viewState = ANCHOR2;
            }
            // View clicked
            else {
                QGraphicsItem::mousePressEvent(event);
                _viewState = VIEW;
            }
        }
        void mouseMoveEvent(QGraphicsSceneMouseEvent* event) override {
            auto p = event->pos();
    
            auto cp1 = _anchor1.center(); // get center point of anchor 1
            auto cp2 = _anchor2.center(); // get center point of anchor 2
    
            switch (_viewState) {
            case ANCHOR1: {
                // calculate the angle of the rotation based on the mouse touch
                auto angle = qRadiansToDegrees(qAtan2( _anchor2.y() -p.y(), _width));
                QGraphicsRotation* rot = new QGraphicsRotation;
                rot->setOrigin(QVector3D(cp2.x(), cp2.y(), 0));
                rot->setAxis(Qt::ZAxis);
                rot->setAngle(angle);
                _trans.push_back(rot);
                setTransformations(_trans);
                break;
            }
            case ANCHOR2: {
                // calculate the angle of the rotation based on the mouse touch
                auto angle = qRadiansToDegrees(qAtan2(p.y() - _anchor1.y(), _width));
                QGraphicsRotation* rot = new QGraphicsRotation;
                rot->setOrigin(QVector3D(cp1.x(), cp1.y(), 0));
                rot->setAxis(Qt::ZAxis);
                rot->setAngle(angle);
                _trans.push_back(rot);
                setTransformations(_trans);
                break;
            }
            default:
                QGraphicsItem::mouseMoveEvent(event); // move the item normally
            }
        }
    
    private:
        float _width, _height;
        QRectF _anchor1, _anchor2;
        QPointF _tapPoint;
        ViewState _viewState;
        QList<QGraphicsTransform*> _trans;
    };
    
    static constexpr int WIDTH = 500;
    static constexpr int HEIGHT = 500;
    
    int main7(int argc, char* argv[]) {
        QApplication a(argc, argv);
    
        QGraphicsScene scene;
        scene.setSceneRect(QRectF(0, 0, WIDTH, HEIGHT));
    
        QGraphicsView view(&scene);
        view.setRenderHint(QPainter::Antialiasing);
    
        QVBoxLayout layout;
        layout.addWidget(&view);
    
        QWidget widget;
        widget.setLayout(&layout);
    
        MyView myView(100, 100, 200, 20);
        scene.addItem(&myView);
    
        widget.show();
    
        return QApplication::exec();
    }
    #include"FileName.moc"