The Qt® 4 Resource Center

A Zoomable Picture Viewer

A common scenario is to show a document in a scrollable, zoomable area. This article shows a quick solution that provides a scrollable, zoomable viewer for pictures. It makes sure that the image is centered when smaller than the available are, and that scrollbars are shown when needed.

The Parts and Tools

First of all, figure 1 shows what we want to achieve. The zoomer shows an ugly bug that is caught in a drinking glass, but that is not what where focusing on in this article.


Figure 1

The parts that we will use is a QMainWindow to hold the widgets, a QScrollArea to provide the scrollbars and a custom picture viewer widget, PixmapWidget, to show the picture. These are all created and setup in our main function shown below as example 1.

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

  QMainWindow *mw = new QMainWindow();
  QScrollArea *sa = new QScrollArea( mw );
  PixmapWidget *pw = new PixmapWidget( QString("./test.png"), sa );

  sa->setWidgetResizable( true );
  sa->setWidget( pw );

  mw->setCentralWidget( sa );
  mw->show();

  a.connect( &a, SIGNAL(lastWindowClosed()), &a, SLOT(quit()) );

  return a.exec();
}
Example 1

The first line creates a QApplication instance. It is required to use Qt's features such as receiving user interface events, sending and receiving signals, etc. After that, the main window, scroll area and pixmap viewer are created. The scroll area is made a child of the main window, while the pixmap viewer is made a child of the scroll area. The viewer is also initialized to show the test.png picture.

In the next couple of lines the scroll area is setup to always resize the contained widget to the largest possible size and to use the pixmap viewer as the contained widget. After that, the scroll area is designated as the main widget of the main window and the main window is shown.

The last two code lines are just a matter of standard procedure. First, a connection to quit the application when all main windows are closed is created, then the application is started.

The Interface

There are two interfaces in this project, the user interface, and the application programmer interface, the API. Let us begin with the user interface. Figures 2-4 shows the different cases of scrollbars that we can run into, i.e. one horizontal, one vertical or both. Of course, the scenario with no scrollbars (as shown in figure 1) is possible too.


Figure 2


Figure 3


Figure 4

The API is a bit more complex, but nothing hard. Our pixmap widget is declared as shown in example 2, and provides a limited, but functional API. Some obvious things are missing, like the ability to change the shown pixmap.

class QPixmap;

class PixmapWidget : public QWidget
{
  Q_OBJECT

public:
  PixmapWidget( const QString &filename, QWidget *parent=0 );
  ~PixmapWidget();

public slots:
  void setZoomFactor( float );

signals:
  void zoomFactorChanged( float );

protected:
  void paintEvent( QPaintEvent* );
  void wheelEvent( QWheelEvent* );

private:
  QPixmap *m_pm;
  float zoomFactor;
};
Example 2

Let us study the class from the top down. First, the Q_OBJECT macro tells Qt that we are dealing with a Qt object capable of sending and receiving signals. After that follows the constructor and destructor. The constructor is of the standard Qt 4 type. For those of you who are used to earlier Qt versions, notice that the name has disappeared. The name is now available as a property and can be accessed through the setObjectName and objectName members.

The next pair of code lines declares a slot for changing the zoom factor, and the following pair of lines declares the corresponding signal. Just after that, event handlers for painting events and mouse wheel events are declared. Finally, the private member variables are declared. They are a pointer to the pixmap and the zoom factor.

This leaves us with a class definition to implement.

The Details

When entering the constructor, the pixmap must be taken care of and the zoom factor must be initialized. Also, we must not forget to set the minimum size of the widget, to make it possible for the scroll area to determine when scroll bars are needed. This is shown in example 3. The destructor is more of the trivial kind, it simply deletes the pixmap.

PixmapWidget::PixmapWidget( const QString &filename, QWidget *parent ) : QWidget( parent )
{
  m_pm = new QPixmap( filename );
  zoomFactor = 1.2;

  setMinimumSize( m_pm->width()*zoomFactor, m_pm->height()*zoomFactor );
}
Example 3

The slot for setting the zoom factor is shown in example 4. It emits the new zoom factor as a signal if it has changed (this check is important to avoid endless loops hanging the application). It then calculates the new minimum size of the widget and forces an update of the scrollbars (through calling resize) and a new paint event through the call of repaint. Remember that in Qt 4, painting outside a paint event is forbidden.

void PixmapWidget::setZoomFactor( float f )
{
  int w, h;

  if( f == zoomFactor )
    return;

  zoomFactor = f;
  emit( zoomFactorChanged( zoomFactor ) );

  w = m_pm->width()*zoomFactor;
  h = m_pm->height()*zoomFactor;
  setMinimumSize( w, h );

  QWidget *p = dynamic_cast<QWidget*>( parent() );
  if( p )
    resize( p->width(), p->height() );

  repaint();
}
Example 4

When dealing with the zoom factor it is time to mention that this is what the wheel event is used for. The wheel event changes the zoom factor, but also limits it, so that we avoid running into a factor of, for example, zero. The zoom factor is limited to making the pixmap smaller than 32 pixels (this is not checked in the constructor nor set zoom factor slot, so workarounds are possible). This size is shown in figure 5.


Figure 5

The most interesting part of the widget is the actual paint event handler. It centers the pixmap and determines if a border is needed as shown in example 5. Then it scales the painter, draws the pixmap, restores the painter and draws the border (example 6).

  int xoffset, yoffset;
  bool drawBorder = false;

  if( width() > m_pm->width()*zoomFactor )
  {
    xoffset = (width()-m_pm->width()*zoomFactor)/2;
    drawBorder = true;
  }
  else
  {
    xoffset = 0;
  }

  if( height() > m_pm->height()*zoomFactor )
  {
    yoffset = (height()-m_pm->height()*zoomFactor)/2;
    drawBorder = true;
  }
  else
  {
    yoffset = 0;
  }
Example 5

In the example below, it is important to know how the painter scaling and translation takes place. A so-called transformation matrix is used to scale, translate, rotate or shear any graphical operation. By calling save, the original matrix is stored. The subsequent calls to translate and scale must not change place, because then the translation would apply to the scaled painter, which would be wrong (download the source and try it). When the pixmap has been drawn, the restore call retrieves the save matrix and we can paint as if nothing has happened.

  QPainter p( this );
  p.save();
  p.translate( xoffset, yoffset );
  p.scale( zoomFactor, zoomFactor );
  p.drawPixmap( 0, 0, *m_pm );
  p.restore();
  if( drawBorder )
  {
    p.setPen( Qt::black );
    p.drawRect( xoffset-1, yoffset-1, m_pm->width()*zoomFactor+1, m_pm->height()*zoomFactor+1 );
  }
Example 6

The Result

The code used in this article can be downloaded here. Try it, change it, use it and give me your suggestions. You can always reach me at e8johan - a t - gmail - d o t - com.

By: Johan Thelin