Scroll Views
The QScrollView class provides a scrollable viewport, two scroll bars, and a "corner" widget (usually an empty QWidget). If we want to add scroll bars to a widget, it is much simpler to use a QScrollView than to instantiate our own QScrollBars and implement the scrolling functionality ourselves.
Figure 6.9. QScrollView's constituent widgets
The easiest way to use QScrollView is to call addChild() with the widget we want to add scroll bars to. QScrollView automatically reparents the widget to make it a child of the viewport (accessible through QScrollView::viewport()) if it isn't already. For example, if we want scroll bars around the IconEditor widget we developed in Chapter 5, we can write this:
#include #include #include "iconeditor.h" int main(int argc, char *argv[]) { QApplication app(argc, argv); QScrollView scrollView; scrollView.setCaption(QObject::tr("Icon Editor")); app.setMainWidget(&scrollView); IconEditor *iconEditor = new IconEditor; scrollView.addChild(iconEditor); scrollView.show(); return app.exec(); }
By default, the scroll bars are only displayed when the viewport is smaller than the child widget. We can force the scroll bars to always be shown by writing this code:
scrollView.setHScrollBarMode(QScrollView::AlwaysOn); scrollView.setVScrollBarMode(QScrollView::AlwaysOn);
When the child widget's size hint changes, QScrollView automatically adapts to the new size hint.
Figure 6.10. Resizing a QScrollView
An alternative way of using a QScrollView with a widget is to make the widget inherit QScrollView and to reimplement drawContents() to draw the contents. This is the approach used by Qt classes like QIconView, QListBox, QListView, QTable, and QTextEdit. If a widget is likely to require scroll bars, it's usually a good idea to implement it as a subclass of QScrollView.
To show how this works, we will implement a new version of the IconEditor class as a QScrollView subclass. We will call the new class ImageEditor, since its scroll bars make it capable of handling large images.
#ifndef IMAGEEDITOR_H #define IMAGEEDITOR_H #include #include class ImageEditor : public QScrollView { Q_OBJECT Q_PROPERTY(QColor penColor READ penColor WRITE setPenColor) Q_PROPERTY(QImage image READ image WRITE setImage) Q_PROPERTY(int zoomFactor READ zoomFactor WRITE setZoomFactor) public: ImageEditor(QWidget *parent = 0, const char *name = 0); void setPenColor(const QColor &newColor); QColor penColor() const { return curColor; } void setZoomFactor(int newZoom); int zoomFactor() const { return zoom; } void setImage(const QImage &newImage); const QImage &image() const { return curImage; } protected: void contentsMousePressEvent(QMouseEvent *event); void contentsMouseMoveEvent(QMouseEvent *event); void drawContents(QPainter *painter, int x, int y, int width, int height); private: void drawImagePixel(QPainter *painter, int i, int j); void setImagePixel(const QPoint &pos, bool opaque); void resizeContents(); QColor curColor; QImage curImage; int zoom; }; #endif
The header file is very similar to the original (p. 100). The main difference is that we inherit from QScrollView instead of QWidget. We will run into the other differences as we review the class's implementation.
ImageEditor::ImageEditor(QWidget *parent, const char *name) : QScrollView(parent, name, WStaticContents | WNoAutoErase) { curColor = black; zoom = 8; curImage.create(16, 16, 32); curImage.fill(qRgba(0, 0, 0, 0)); curImage.setAlphaBuffer(true); resizeContents(); }
The constructor passes the WStaticContents and WNoAutoErase flags to the QScrollView. These flags are actually set on the viewport. We don't set a size policy, because QScrollView's default of (Expanding, Expanding) is appropriate.
In the original version, we didn't call updateGeometry() in the constructor because we could depend on Qt's layout managers picking up the initial widget size by themselves. But here we must give the QScrollView base class an initial size to work with, and we do this with the resizeContents() call.
void ImageEditor::resizeContents() { QSize size = zoom * curImage.size(); if (zoom >= 3) size += QSize(1, 1); QScrollView::resizeContents(size.width(), size.height()); }
The resizeContents() private function calls QScrollView::resizeContents() with the size of the content part of the QScrollView. The QScrollView displays scroll bars depending on the content's size in relation to the viewport's size.
We don't need to reimplement sizeHint(); QScrollView's version uses the content's size to provide a reasonable size hint.
void ImageEditor::setImage(const QImage &newImage) { if (newImage != curImage) { curImage = newImage.convertDepth(32); curImage.detach(); resizeContents(); updateContents(); } }
In many of the original IconEditor functions, we called update() to schedule a repaint and updateGeometry() to propagate a size hint change. In the QScrollView versions, these calls are replaced by resizeContents() to inform the QScrollView about a change of the content's size and updateContents() to force a repaint.
void ImageEditor::drawContents(QPainter *painter, int, int, int, int) { if (zoom >= 3) { painter->setPen(colorGroup().foreground()); for (int i = 0; i <= curImage.width(); ++i) painter->drawLine(zoom * i, 0, zoom * i, zoom * curImage.height()); for (int j = 0; j <= curImage.height(); ++j) painter->drawLine(0, zoom * j, zoom * curImage.width(), zoom * j); } for (int i = 0; i < curImage.width(); ++i) { for (int j = 0; j < curImage.height(); ++j) drawImagePixel(painter, i, j); } }
The drawContents() function is called by QScrollView to repaint the content's area. The QPainter object is already initialized to account for the scrolling offset. We just need to perform the drawing as we normally do in a paintEvent().
The second, third, fourth, and fifth parameters specify the rectangle that must be redrawn. We could use this information to only draw the rectangle that needs repainting, but for the sake of simplicity we redraw everything.
The drawImagePixel() function that is called near the end of drawContents() is essentially the same as in the original IconEditor class (p. 106), so it is not reproduced here.
void ImageEditor::contentsMousePressEvent(QMouseEvent *event) { if (event->button() == LeftButton) setImagePixel(event->pos(), true); else if (event->button() == RightButton) setImagePixel(event->pos(), false); } void ImageEditor::contentsMouseMoveEvent(QMouseEvent *event) { if (event->state() & LeftButton) setImagePixel(event->pos(), true); else if (event->state() & RightButton) setImagePixel(event->pos(), false); }
Mouse events for the content part of the scroll view can be handled by reimplementing special event handlers in QScrollView, whose names all start with contents. Behind the scenes, QScrollView automatically converts the viewport coordinates to content coordinates, so we don't need to convert them ourselves.
void ImageEditor::setImagePixel(const QPoint &pos, bool opaque) { int i = pos.x() / zoom; int j = pos.y() / zoom; if (curImage.rect().contains(i, j)) { if (opaque) curImage.setPixel(i, j, penColor().rgb()); else curImage.setPixel(i, j, qRgba(0, 0, 0, 0)); QPainter painter(viewport()); painter.translate(-contentsX(), -contentsY()); drawImagePixel(&painter, i, j); } }
The setImagePixel() function is called from contentsMousePressEvent() and contentsMouseMoveEvent() to set or clear a pixel. The code is almost the same as the original version, except for the way the QPainter object is initialized. We pass viewport() as the parent because the painting is performed on the viewport, and we translate the QPainter's coordinate system to account for the scrolling offset.
We could replace the three lines that deal with the QPainter with this line:
updateContents(i * zoom, j * zoom, zoom, zoom);
This would tell QScrollView to update only the small rectangular area occupied by the (zoomed) image pixel. But since we didn't optimize drawContents() to draw only the necessary area, this would be inefficient, so it's better to construct a QPainter and do the painting ourselves.
If we use ImageEditor now, it is practically indistinguishable from the original, QWidget-based IconEditor used inside a QScrollView widget. However, for certain more sophisticated widgets, subclassing QScrollView is the more nattural approach. For example, a class such as QTextEdit that implements wordwrapping needs tight integration between the document that is shown and the QScrollView.
Also note that you should subclass QScrollView if the contents are likely to be very tall or wide, because some window systems don't support widgets that are larger than 32,767 pixels.
One thing that the ImageEditor example doesn't demonstrate is that we can put child widgets in the viewport area. The child widgets simply need to be added using addWidget(), and can be moved using moveWidget(). Whenever the user scrolls the content area, QScrollView automatically moves the child widgets on screen. (If the QScrollView contains many child widgets, this can slow down scrolling. We can call enableClipper(true) to optimize this case.) One example where this approach would make sense is for a web browser. Most of the contents would be drawn directly on the viewport, but buttons and other form-entry elements would be represented by child widgets.