Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Calculating view offset for zooming in at the position of the mouse cursor

Tags:

c++

qt

qml

qtquick2

I've got a "canvas" that the user can draw pixels, etc. onto. It works well, but my zoom functionality currently uses the same origin regardless of the position of the mouse. I'd like to implement functionality like that of Google Maps' zoom behaviour:

google maps zoom

That is, the zoom's origin should always be the position of the mouse cursor.

What I currently have is not exactly right...

My attempts have mostly been stabs in the dark, but I've also tried using the code from this answer without success.

main.cpp:

#include <QGuiApplication>
#include <QtQuick>

class Canvas : public QQuickPaintedItem
{
    Q_OBJECT

public:
    Canvas() :
        mTileWidth(25),
        mTileHeight(25),
        mTilesAcross(10),
        mTilesDown(10),
        mOffset(QPoint(400, 400)),
        mZoomLevel(1)
    {
    }

    void paint(QPainter *painter) override {
        painter->translate(mOffset);

        const int zoomedTileWidth =  mTilesAcross * mZoomLevel;
        const int zoomedTileHeight =  mTilesDown * mZoomLevel;
        const int zoomedMapWidth = qMin(mTilesAcross * zoomedTileWidth, qFloor(width()));
        const int zoomedMapHeight = qMin(mTilesDown * zoomedTileHeight, qFloor(height()));
        painter->fillRect(0, 0, zoomedMapWidth, zoomedMapHeight, QColor(Qt::gray));

        for (int y = 0; y < mTilesDown; ++y) {
            for (int x = 0; x < mTilesAcross; ++x) {
                const QRect rect(x * zoomedTileWidth, y * zoomedTileHeight, zoomedTileWidth, zoomedTileHeight);
                painter->drawText(rect, QString::fromLatin1("%1, %2").arg(x).arg(y));
            }
        }
    }

protected:
    void wheelEvent(QWheelEvent *event) override {
        const int oldZoomLevel = mZoomLevel;
        mZoomLevel = qMax(1, qMin(mZoomLevel + (event->angleDelta().y() > 0 ? 1 : -1), 30));

        const QPoint cursorPosRelativeToOffset = event->pos() - mOffset;

        if (mZoomLevel != oldZoomLevel) {
            mOffset.rx() -= cursorPosRelativeToOffset.x();
            mOffset.ry() -= cursorPosRelativeToOffset.y();

            // Attempts based on https://stackoverflow.com/a/14085161/904422
//            mOffset.setX((event->pos().x() * (mZoomLevel - oldZoomLevel)) + (mZoomLevel * -mOffset.x()));
//            mOffset.setY((event->pos().y() * (mZoomLevel - oldZoomLevel)) + (mZoomLevel * -mOffset.y()));

//            mOffset.setX((cursorPosRelativeToOffset.x() * (mZoomLevel - oldZoomLevel)) + (mZoomLevel * -mOffset.x()));
//            mOffset.setY((cursorPosRelativeToOffset.y() * (mZoomLevel - oldZoomLevel)) + (mZoomLevel * -mOffset.y()));

            update();
        }
    }

    void keyReleaseEvent(QKeyEvent *event) override {
        static const int panDistance = 50;
        switch (event->key()) {
        case Qt::Key_Left:
            mOffset.rx() -= panDistance;
            update();
            break;
        case Qt::Key_Right:
            mOffset.rx() += panDistance;
            update();
            break;
        case Qt::Key_Up:
            mOffset.ry() -= panDistance;
            update();
            break;
        case Qt::Key_Down:
            mOffset.ry() += panDistance;
            update();
            break;
        }
    }

private:
    const int mTileWidth;
    const int mTileHeight;
    const int mTilesAcross;
    const int mTilesDown;
    QPoint mOffset;
    int mZoomLevel;
};

int main(int argc, char *argv[])
{
    QGuiApplication app(argc, argv);

    qmlRegisterType<Canvas>("App", 1, 0, "Canvas");

    QQmlApplicationEngine engine;
    engine.load(QUrl(QStringLiteral("qrc:/main.qml")));

    return app.exec();
}

#include "main.moc"

main.qml:

import QtQuick 2.5
import QtQuick.Window 2.2

import App 1.0 as App

Window {
    visible: true
    width: 1200
    height: 900
    title: qsTr("Hello World")

    Shortcut {
        sequence: "Ctrl+Q"
        onActivated: Qt.quit()
    }

    App.Canvas {
        focus: true
        anchors.fill: parent
    }
}

What am I doing wrong in the wheelEvent() function?

like image 701
Mitch Avatar asked Jul 09 '16 12:07

Mitch


People also ask

How do I zoom in cursor position?

WIN and the PLUS (or MINUS) key. Default settings are it zooms in around the cursor. WIN ESC resets zoom to 100%.

What is offset in mouse?

The offsetX read-only property of the MouseEvent interface provides the offset in the X coordinate of the mouse pointer between that event and the padding edge of the target node.

How do I know the position of my cursor?

In Mouse Properties, on the Pointer Options tab, at the bottom, select Show location of pointer when I press the CTRL key, and then select OK. To see it in action, press CTRL.


1 Answers

You have a rectangle R = [x_0, x_0 + w] x [y_0, y_0 + h] with absolute coordinates. When you map it to a widget (another rectangle), you apply some transformation T to an area W of R. This transformation is linear with offset:

T(x, y) = (a_x x + b_x, a_y y + b_y).

Values of a_x, b_x, a_y, b_y are calculated to satisfy some simple conditions, you have already done it.

You also have a cursor (x_c, y_c) in R. It's coordinates in W are T(x_c, y_c). Now you want to apply another transformation T'\colon R \rightarrow W',

T'(x, y) = (a_x' x + b_x', a_y' y + b_y')

changing scale coefficients a_x, a_y to known a_x', a_y' with following condition: you want your cursor to point at the same coordinates (x_c, y_c) in R. I.e. T'(x_c, y_c) = T(x_c, y_c) — the same point in relative coordinates points to the same position in absolute coordinates. We derive a system for unknown offsets b_x', b_y' with known rest values. It gives

b_z' = z_c (a_z - a_z') + b_z, z = x,y.

Last work is to find (x_c, y_c) from widget cursor position (x_p, y_p) = T(x_c, y_c):

z_c = (z_p - b_z) / a_z, z = x,y

and to substitute it:

b_z' = z_p - a_z' / a_z (z_p - b_z), \quad z = x,y.

In your terms it is

mOffset = event->pos() - float(mZoomLevel) / float(oldZoomLevel) *
     (event->pos() - mOffset);
like image 95
ilotXXI Avatar answered Sep 30 '22 02:09

ilotXXI