6

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?

Community
  • 1
  • 1
Mitch
  • 23,716
  • 9
  • 83
  • 122
  • What do you expect and what do you see? And why not Qt Graphics framework? – ilotXXI Jul 09 '16 at 21:03
  • @ilotXXI All of the information is there in the question if you take the time to read it. – Mitch Jul 10 '16 at 05:18
  • 2
    Oh, found a GIF. Try this: `mOffset = event->pos() - float(mZoomLevel) / float(oldZoomLevel) * (event->pos() - mOffset);`. – ilotXXI Jul 10 '16 at 14:03
  • @ilotXXI thanks a lot! That works great! :) Please answer so I can accept it. It would be great if you could explain what the code is doing as well, if possible. – Mitch Jul 11 '16 at 06:23
  • Done. Please let me know if it is difficult to understand or has mistakes. – ilotXXI Jul 11 '16 at 08:11

2 Answers2

7

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);
Community
  • 1
  • 1
ilotXXI
  • 1,075
  • 7
  • 9
  • 2
    I'm pretty horrible at maths, but this looks very well-explained for someone who isn't. :D – Mitch Jul 11 '16 at 08:17
0

If you want to zoom like Google maps then your origin must be at top-left corner of the image(lets say (x,y) = (0,0) and (width, height) = (100,100)) with initial zoomLevel 100. If you want to zoom at point(40,20) with zoom Factor of 5% then, the displacement can be calculated as-

newX = 40 - 40*(100.0/105)
newY = 20 - 20*(100.0/105)
newWidth = width - (100.0/105)
newHeight = height - (100.0/105)

then set newX, newY as your origin and change width, height to newWidth and newHeight. By this implementation you'll be able to zoom at a particular point where the cursor is. But this implementation will not work when you move the cursor at some other positions. I am also looking for that implementation.