Kevin Meyer
Kevin Meyer

Reputation: 121

QPainting QPixmap using clipping to gain performance

I'm trying to create a simple Image Viewer in Qt with zooming supported.

To display an image file I load it into a QImage and create a QPixmap.

class NN: public QWidget{
    Q_OBJECT
    Q_DISABLE_COPY(NN)
public:
    NN(QWidget* parent = nullptr) : QWidget(parent){
    }
    const QPixmap& pixmap() const 
    {
        return m_pixmap;
    }
    void setPixmap(const QPixmap& px) 
    {
        m_pixmap = px;
        update();
    }
protected:
    void paintEvent(QPaintEvent*)
    {
        QPainter painter(this);
        painter.setRenderHint(QPainter::Antialiasing, false);
        style()->drawItemPixmap(&painter, rect(), Qt::AlignCenter, m_pixmap.scaled(rect().size()));
    }
private:
    QPixmap m_pixmap;
};

(This Widget is part of a ScrollArea)

This works fine, but when I try to load large images and zoom in, the performance starts to decrease (lag).

I thought of applying a clip to the drawItemPixmap() method, but I am not quite sure how and whether it would help increasing the performance.

My question is whether the clipping idea would work, and if so how. If not, maybe there is another way to gain performance?

Upvotes: 1

Views: 328

Answers (2)

Jeremy Friesner
Jeremy Friesner

Reputation: 73304

When m_pixmap and/or rect() are very large, the bulk of your slowdown is likely coming from here:

m_pixmap.scaled(rect().size())

Here you are you are asking Qt to create a new QPixmap object the same size as rect(), which is a potentially very expensive operation; and passing that QPixmap object into the call to drawItemPixmap() which will draw just a small portion of the pixmap, after which the QPixmap object will get discarded, and the whole procedure will have to be done again the next time you want to redraw your object.

Needless to say, that can be very inefficient.

A more efficient approach would be to call QPainter::drawPixmap(const QRect & target, const Pixmap & pixmap, const QRect & source), like this:

painter.drawPixmap(rect(), m_pixmap, srcRect);

... and drawPixmap() will draw a scaled pixmap of size rect() (i.e. just the size of your widget) by rescaling the content of m_pixmap that is inside srcRect; much more efficient than rescaling the entire m_pixmap image.

You'll need to calculate the correct left/top/width/height values for srcRect, of course, but that should be straightforward with a little bit of algebra. (Basically just figure out what portion of the pixmap should currently be visible based on your widget's current zoom/pan state)

Upvotes: 2

eyllanesc
eyllanesc

Reputation: 244301

As I pointed out in this answer, it is better to use QGraphicsView for image scaling so I will translate the code to C++:

#include <QtWidgets>

class ImageViewer: public QGraphicsView{
public:
    ImageViewer(QWidget *parent=nullptr):QGraphicsView(parent){
        setRenderHints(QPainter::Antialiasing | QPainter::SmoothPixmapTransform);
        // setAlignment(Qt::AlignLeft | Qt::AlignTop);
        setAlignment(Qt::AlignCenter);
        setBackgroundRole(QPalette::Dark);
        QGraphicsScene *scene = new QGraphicsScene(this);
        setScene(scene);
        pixmapItem = new QGraphicsPixmapItem;
        scene->addItem(pixmapItem);
    }
    bool setPixmap(const QPixmap & pixmap){
        if(pixmap.isNull())
            return false;
        pixmapItem->setPixmap(pixmap);
        return true;
    }
    void zoom(qreal f){
        scale(f, f);
    }
    void zoomIn(){
        zoom(factor);
    }
    void zoomOut(){
        zoom(1.0 / factor);
    }
    void resetZoom(){
        resetTransform();
    }
    void fitToWindow(){
        fitInView(sceneRect(), Qt::KeepAspectRatio);
    }
private:
    qreal factor = 2.0;
    QGraphicsPixmapItem * pixmapItem;
};

class MainWindow: public QMainWindow{
    Q_OBJECT
public:
    MainWindow(QWidget *parent=nullptr):QMainWindow(parent),
        view(new ImageViewer)
    {
        setCentralWidget(view);
        createActions();
        createMenus();
        resize(640, 480);
    }
private Q_SLOTS:
    void open(){
        QStringList l;
        for(const QByteArray & ba: QImageReader::supportedImageFormats()){
            l << ("*." + QString::fromUtf8(ba));
        }
        QString filter = QString("Image Files(%1)").arg(l.join(" "));
        QString fileName = QFileDialog::getOpenFileName(
            this,
            tr("Open Image"),
            QDir::currentPath(),
            filter
        );
        if(!fileMenu->isEmpty()){
            bool loaded = view->setPixmap(QPixmap(fileName));
            fitToWindowAct->setEnabled(loaded);
            updateActions();
        }
    }
    void fitToWindow(){
        if(fitToWindowAct->isChecked())
           view->fitToWindow();
        else
            view->resetZoom();
        updateActions();
    }
    void about(){
        QMessageBox::about(this, "ImageViewer", "ImageViewer");
    }
private:
    void createActions(){
        openAct = new QAction("&Open...", this);
        openAct->setShortcut(QKeySequence("Ctrl+O"));
        connect(openAct, &QAction::triggered, this, &MainWindow::open);

        exitAct = new QAction("E&xit", this);
        exitAct->setShortcut(QKeySequence("Ctrl+Q"));
        connect(exitAct, &QAction::triggered, this, &MainWindow::close);

        zoomInAct = new QAction(tr("Zoom &In (25%)"), this);
        zoomInAct->setShortcut(QKeySequence("Ctrl++"));
        zoomInAct->setEnabled(false);
        connect(zoomInAct, &QAction::triggered, view, &ImageViewer::zoomIn);

        zoomOutAct = new QAction(tr("Zoom &Out (25%)"), this);
        zoomOutAct->setShortcut(QKeySequence("Ctrl+-"));
        zoomOutAct->setEnabled(false);
        connect(zoomOutAct, &QAction::triggered, view, &ImageViewer::zoomOut);

        normalSizeAct = new QAction(tr("&Normal Size"), this);
        normalSizeAct->setShortcut(QKeySequence("Ctrl+S"));
        normalSizeAct->setEnabled(false);
        connect(normalSizeAct, &QAction::triggered, view, &ImageViewer::resetZoom);

        fitToWindowAct = new QAction(tr("&Fit to Window"), this);
        fitToWindowAct->setShortcut(QKeySequence("Ctrl+F"));
        fitToWindowAct->setEnabled(false);
        fitToWindowAct->setCheckable(true);
        connect(fitToWindowAct, &QAction::triggered, this, &MainWindow::fitToWindow);

        aboutAct = new QAction(tr("&About"), this);
        connect(aboutAct, &QAction::triggered, this, &MainWindow::about);

        aboutQtAct = new QAction(tr("About &Qt"), this);
        connect(aboutQtAct, &QAction::triggered, qApp, &QApplication::aboutQt);
    }

    void createMenus(){
        fileMenu = new QMenu(tr("&File"), this);
        fileMenu->addAction(openAct);
        fileMenu->addSeparator();
        fileMenu->addAction(exitAct);

        viewMenu = new QMenu(tr("&View"), this);
        viewMenu->addAction(zoomInAct);
        viewMenu->addAction(zoomOutAct);
        viewMenu->addAction(normalSizeAct);
        viewMenu->addSeparator();
        viewMenu->addAction(fitToWindowAct);

        helpMenu = new QMenu(tr("&Help"), this);
        helpMenu->addAction(aboutAct);
        helpMenu->addAction(aboutQtAct);

        menuBar()->addMenu(fileMenu);
        menuBar()->addMenu(viewMenu);
        menuBar()->addMenu(helpMenu);
    }
    void updateActions(){
        zoomInAct->setEnabled(not fitToWindowAct->isChecked());
        zoomOutAct->setEnabled(not fitToWindowAct->isChecked());
        normalSizeAct->setEnabled(not fitToWindowAct->isChecked());
    }
    ImageViewer *view;
    QAction *openAct;
    QAction *exitAct;
    QAction *zoomInAct;
    QAction *zoomOutAct;
    QAction *normalSizeAct;
    QAction *fitToWindowAct;
    QAction *aboutAct;
    QAction *aboutQtAct;
    QMenu *fileMenu;
    QMenu *viewMenu;
    QMenu *helpMenu;
};

int main(int argc, char *argv[])
{
    QApplication a(argc, argv);
    MainWindow w;
    w.show();
    return a.exec();
}

#include "main.moc"

Upvotes: 1

Related Questions