Zooming and panning an image in a QScrollArea
Asked Answered
B

1

6

I have created a preview that shows a rendered image. I used the Image Viewer Example for zooming functionality - so I have a class inheriting QScrollArea, capable of showing an image, in a QLabel, and zooming in/out/fit with specific limits. I was showing scrollbars "as needed".

As a new requirement, I must be able to do panning, and not show scrollbars.

I have been looking for ways to do it - and found examples of people using mouse press, move and release events to relate a point on the image to scrollbars.
The problems:
1) the direction of move, if scrollbars are invisible, is unexpected - in panning he object moves in the direction of the mouse (stays under mouse), while scrollbars move in the opposite direction
2) I think the move is limited to scrollbar size so... if I calculate a reverse move, I would hit a wall while still have room to move in one direction
3) This would not work with zooming, which is exactly when the panning is needed; more complex calculations would be needed.

I could alternately use a QGraphicsView, and

setDragMode(ScrollHandDrag);

It would work nice with zooming as well, and I would not have to implement it myself.
The reason I have not done this yet is, I would need to add a QGraphicsScene as well, and a QGraphicsPixmapItem containing the image I want - then find how to disable all mouse events other than panning - and still use a QScrollArea to hold the QGraphicsView;
It seems it is too much overhead (and this is meant to be extremely light-weight, for an embedded device with little speed and memory).

What is the best option ? is there some way I can pan a zoomed image in a viewer, as light weight as possible ?

Boondocks answered 18/11, 2016 at 18:31 Comment(0)
P
10

Given that the paintEvent implementation for a custom zoomable and pannable pixmap viewer is 5 lines long, one might as well implement it from scratch:

// https://github.com/KubaO/stackoverflown/tree/master/questions/image-panzoom-40683840
#include <QtWidgets>
#include <QtNetwork>

class ImageViewer : public QWidget {
    QPixmap m_pixmap;
    QRectF m_rect;
    QPointF m_reference;
    QPointF m_delta;
    qreal m_scale = 1.0;
    void paintEvent(QPaintEvent *) override {
        QPainter p{this};
        p.translate(rect().center());
        p.scale(m_scale, m_scale);
        p.translate(m_delta);
        p.drawPixmap(m_rect.topLeft(), m_pixmap);
    }
    void mousePressEvent(QMouseEvent *event) override {
        m_reference = event->pos();
        qApp->setOverrideCursor(Qt::ClosedHandCursor);
        setMouseTracking(true);
    }
    void mouseMoveEvent(QMouseEvent *event) override {
        m_delta += (event->pos() - m_reference) * 1.0/m_scale;
        m_reference = event->pos();
        update();
    }
    void mouseReleaseEvent(QMouseEvent *) override {
        qApp->restoreOverrideCursor();
        setMouseTracking(false);
    }
public:
    void setPixmap(const QPixmap &pix) {
        m_pixmap = pix;
        m_rect = m_pixmap.rect();
        m_rect.translate(-m_rect.center());
        update();
    }
    void scale(qreal s) {
        m_scale *= s;
        update();
    }
    QSize sizeHint() const override { return {400, 400}; }
};

A comparable QGraphicsView-based widget would be only slightly shorter, and would have a bit more overhead if the pixmap was very small. For large pixmaps, the time spent rendering the pixmap vastly overshadows any overhead due to the QGraphicsScene/QGraphicsView machinery. After all, the scene itself is static, and that is the ideal operating point for performance of QGraphicsView.

class SceneImageViewer : public QGraphicsView {
    QGraphicsScene m_scene;
    QGraphicsPixmapItem m_item;
public:
    SceneImageViewer() {
        setScene(&m_scene);
        m_scene.addItem(&m_item);
        setDragMode(QGraphicsView::ScrollHandDrag);
        setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
        setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
        setResizeAnchor(QGraphicsView::AnchorViewCenter);
    }
    void setPixmap(const QPixmap &pixmap) {
        m_item.setPixmap(pixmap);
        auto offset = -QRectF(pixmap.rect()).center();
        m_item.setOffset(offset);
        setSceneRect(offset.x()*4, offset.y()*4, -offset.x()*8, -offset.y()*8);
        translate(1, 1);
    }
    void scale(qreal s) { QGraphicsView::scale(s, s); }
    QSize sizeHint() const override { return {400, 400}; }
};

And a test harness:

int main(int argc, char *argv[])
{
    QApplication a{argc, argv};
    QWidget ui;
    QGridLayout layout{&ui};
    ImageViewer viewer1;
    SceneImageViewer viewer2;
    QPushButton zoomOut{"Zoom Out"}, zoomIn{"Zoom In"};
    layout.addWidget(&viewer1, 0, 0);
    layout.addWidget(&viewer2, 0, 1);
    layout.addWidget(&zoomOut, 1, 0, 1, 1, Qt::AlignLeft);
    layout.addWidget(&zoomIn, 1, 1, 1, 1, Qt::AlignRight);

    QNetworkAccessManager mgr;
    QScopedPointer<QNetworkReply> rsp(
                mgr.get(QNetworkRequest({"http://i.imgur.com/ikwUmUV.jpg"})));
    QObject::connect(rsp.data(), &QNetworkReply::finished, [&]{
        if (rsp->error() == QNetworkReply::NoError) {
            QPixmap pixmap;
            pixmap.loadFromData(rsp->readAll());
            viewer1.setPixmap(pixmap);
            viewer2.setPixmap(pixmap);
        }
        rsp.reset();
    });
    QObject::connect(&zoomIn, &QPushButton::clicked, [&]{
        viewer1.scale(1.1); viewer2.scale(1.1);
    });
    QObject::connect(&zoomOut, &QPushButton::clicked, [&]{
        viewer1.scale(1.0/1.1); viewer2.scale(1.0/1.1);
    });
    ui.show();
    return a.exec();
}
Platonic answered 18/11, 2016 at 22:52 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.