JAN
22
2011

Loading and Scaling Images in a Thread

In a previous blog post, I showed a simple example of using threads to perform complex calculations (SHA hashes) in a worker thread. I used them there because generating the hash of a DVD ISO can take a while, and GUIs that block make everyone cry. In this post, I'll use the same technique to load and scale an image whilst still letting my GUI startup instantly.

To begin with, lets look at the declaration of my Resizer class - this is the class that does most of the work. As you can see it's just a normal QObject, and provides slots to let us specify the input, target size etc. It also has a couple of signals one to provide the result of the work, and another to indicate that something went wrong. One thing to note is that the API is based around QImage not QPixmap, that's because it will be running outside the main thread, meaning GUI classes like QPixmap cannot be used safely.

class Resizer : public QObject
{
    Q_OBJECT
public:
    Resizer( QObject *parent=0 );
    ~Resizer();

public slots:
    void setSize( const QSize &size );
    void setAspectRatioMode( const Qt::AspectRatioMode mode );
   
    void setInput( const QImage &input );
    void setInput( const QString &filename );

    void start();

signals:
    void error();
    void finished( const QImage &output );

private:
    struct ResizerPrivate *d;
};

The implementation is pretty simple, I'll skip over the setters etc., since they simple do what you'd expect. In fact the only interesting method is the start() slot since that's where all the work is done.

struct ResizerPrivate
{
    QSize size;
    Qt::AspectRatioMode aspectMode;
    QImage input;
    QString inputFilename;
};

void Resizer::start()
{
    if  ( !d->inputFilename.isEmpty() ) {
        d->input.load( d->inputFilename );
    }

    if ( d->input.isNull() ) {
        emit error();
        return;       
    }

    QImage output = d->input.scaled( d->size, d->aspectMode, Qt::SmoothTransformation );
    emit finished( output );
}

The start() method first looks to see if a filename has been provided, if it has then it tries to load it. If the load failed (or the user has provided no input at all) then the error signal is emitted and we return. Finally, if we have an image to work with, we perform a smooth scaling operation then emit the finished() slot with the result. As you can see, we haven't had to do anything special to deal with threads, since we're using signals to return our scaled image.

The main function of this example is where all the threading is dealt with. The Viewer class is simply a UI file that wraps a QLabel and provides a slot that will set the label's pixmap based on a QImage.

int main( int argc, char **argv )
{
    QApplication app( argc, argv );

    if ( argc != 2 ) {
        printf( "Usage: %s \n", argv[0] );
        return 1;
    }

    Viewer *view = new Viewer();

    QThread *thread = new QThread();
    Resizer *resizer = new Resizer();
    resizer->moveToThread( thread );

    QObject::connect( thread, SIGNAL(started()), resizer, SLOT(start()) );
    QObject::connect( resizer, SIGNAL(finished(const QImage &)), view, SLOT(setImage(const QImage &)) );

    view->show();

    resizer->setInput( QString::fromLocal8Bit(argv[1]) );
    resizer->setSize( QSize(400,400) );

    thread->start();

    return app.exec();
}

The first interesting thing we do is create the QThread, note that we are using it directly here rather than subclassing it. We then create our Resizer object. When we've done that, we move it into our QThread, this will ensure that it runs there. Next, we connect the thread's started() signal to our start() method so that we'll begin loading and scaling as soon as the thread is run. We also connect our finished() signal (which will be in our thread) to the view (which will be in the main thread). Qt will automatically ensure that the image is passed safely from one thread to the other.

Since we haven't started our thread yet, we can safely set the input and size directly rather than calling these methods as slots. If we wanted to change these while the thread was running (which our resizer doesn't really support in this case) then we'd want to use signals or QMetaObject's invokeMethod() in order to do so safely.

Finally, we start() our thread then start the event loop. When the thread starts, the started() signal is emitted, which will trigger our resizer to begin processing. Because this is happenning asynchronously, our GUI will display immediately even if we're loading and scaling very large images. The same technique can be used in many scenarios, for example if you have expensive startup operations that need to be performed by your application, then you might want to consider using this technique.

As usual, the code is in my gitorious repository.