Advanced Qt Programming: Model/View Views
- QAbstractItemView Subclasses
- Model-Specific Visualizing Views
This chapter covers model/view views, and is the last chapter covering Qt's model/view architecture. Just like the previous two chapters, this chapter assumes a basic familiarity with the model/view architecture, as described at the beginning of Chapter 3 (88 ).
Qt's standard model views, QListView, QTableView, QColumnView, and QTreeView, are sufficient for most purposes, most of the time. And like other Qt classes, they can be subclassed—or we can use custom delegates—to affect how they display the model's items. However, there are two situations where we need to create a custom view. One is where we want to present the data in a radically different way from how Qt's standard views present data, and the other is where we want to visualize two or more data items combined in some way.
Broadly speaking there are two approaches we can take to the creation of custom views. One approach is used when we want to create a view component—that is, a view that is potentially reusable with a number of different models and that must fit in with Qt's model/view architecture. In such cases we would normally subclass QAbstractItemView, and provide the standard view API so that any model could make use of our view. The other approach is useful when we want to visualize the data in a particular model in such a unique way that the visualization has little or no potential for reuse. In these cases we can simply create a custom model viewer that has exactly—and only—the functionality required. This usually involves subclassing QWidget and providing our own API, but including a setModel() method.
In this chapter we will look at two examples of custom views. The first is a generic QAbstractItemView subclass that provides the same API as Qt's built-in views, and that can be used with any model, although it is designed in particular for the presentation and editing of list models. The second is a visualizer view that is specific to a particular model and that provides its own API.
QAbstractItemView Subclasses
In this section we will show how to create a QAbstractItemView subclass that can be used as a drop-in replacement for Qt's standard views. In practice, of course, just as there are list, table, and tree models, there are corresponding views, and so here we will develop a custom list view, although the principles are the same for all QAbstractItemView subclasses.
Figure 6.1 shows the central area of the Tiled List View application (tiledlistview). The area has two views that are using the same model: on the left a standard QListView, and on the right a TiledListView. Notice that although the widgets are the same size and use the same font, the TiledListView shows much more data. Also, as the figure illustrates, the TiledListView does not use multiple columns; rather, it shows as many items as it can fit in each row—for example, if it were resized to be a bit wider, it would fit four or more items on some rows.
Figure 6.1 A QListView and a TiledListView
One usability difference that makes keyboard navigation faster and easier—and more logical—in the TiledListView is that using the arrow keys does not simply go forward or backward in the list of items. When the user navigates through the items using the up (or down) arrow keys, the selected item is changed to the item visually above (or below) the current item. Similarly, when the user navigates using the left (or right) arrow keys, the selected item is changed to the item to the left (or right) as expected, unless the current item is at the left (or right) edge. For the edge cases, the selected item is changed to the item that is logically before (or after) the current item.
The QAbstractItemView API is large, and at the time of this writing, the Qt documentation does not explicitly specify which parts of the API must be reimplemented by subclasses and which base class implementations are sufficient. However, some of the methods are pure virtual and so must be reimplemented. Also, Qt comes with the examples/itemviews/chart example which serves as a useful guide for custom view implementations.
The API we have implemented for the TiledListView, and the one that we consider to be the minimum necessary for a custom QAbstractItemView subclass, is shown in Table 6.1. Qt's chart example reimplements all the methods listed in the table, and also the mouseReleaseEvent() and mouseMoveEvent() event handlers (to provide rubber band support—something not needed for the TiledListView). The chart example also implements the edit() method to initiate editing—again, something we don't need to do for the TiledListView even though it is editable, because the inherited base class's behavior is sufficient.
Table 6.1. The QAbstractItemView API
Method |
Description |
dataChanged(topLeft, bottomRight) |
This slot is called when the items with model indexes in the rectangle from topLeft to bottomRight change |
horizontalOffset() * |
Returns the view's horizontal offset |
indexAt(point)* |
Returns the model index of the item at position point in the view's viewport |
isIndexHidden(index)* |
Returns true if the item at index is a hidden item (and therefore should not be shown) |
mousePressEvent(event) |
Typically used to set the current model index to the index of the clicked item |
moveCursor(how,
modifiers)
|
Returns the model index of the item after navigating how (e.g., up, down, left, or right), and accounting for the keyboard modifiers |
paintEvent(event) |
Paints the view's contents on the viewport |
resizeEvent(event) |
Typically used to update the scrollbars |
rowsAboutToBeRemoved( parent, start, end) |
This slot is called when rows from start to end under parent are about to be removed |
rowsInserted(parent, start, end) |
This slot is called when rows from start to end are inserted under the parent model index |
scrollContentsBy(dx, dy) |
Scrolls the view's viewport by dx and dy pixels |
scrollTo(index, hint)* |
Scrolls the view to ensure that the item at the given model index is visible, and respecting the scroll hint as it scrolls |
setModel(model) |
Makes the view use the given model |
setSelection(rect, flags)* |
Applies the selection flags to all of the items in or touching the rectangle rect |
updateGeometries() |
Typically used to update the geometries of the view's child widgets, e.g., the scrollbars |
verticalOffset() |
Returns the view's vertical offset |
visualRect(index)* |
Returns the rectangle occupied by the item at the given model index |
visualRegionForSelection(
selection)
|
Returns the viewport region for the items in the selection |
Before we look at the TiledListView class, here is how an instance is created and initialized.
TiledListView *tiledListView = new TiledListView; tiledListView->setModel(model);
As these two lines make clear, the TiledListView is used in exactly the same way as any other view class.
Since the API that must be implemented is shown in Table 6.1, we won't show the class's definition in the header file, apart from the private data, all of which is specific to the TiledListView.
private: mutable int idealWidth; mutable int idealHeight; mutable QHash<int, QRectF> rectForRow; mutable bool hashIsDirty;
The idealWidth and idealHeight are the width and height needed to show all the items without the need for scrollbars. The rectForRow hash returns a QRectF of the correct position and size for the given row. (Note that since the TiledListView is designed for showing lists, a row corresponds to an item.) All these variables are concerned with behind-the-scenes bookkeeping, and since they are used in const methods we have been forced to make them mutable.
Rather than updating the rectForRow hash whenever a change takes place, we do lazy updates—that is, we simply set hashIsDirty to true when changes occur. Then, whenever we actually need to access the rectForRow hash, we recalculate it only if it is dirty.
We are now almost ready to review the TiledListView implementation, and will do so, starting with the constructor, and including the private supporting methods as necessary. But first we must mention an important conceptual point about QAbstractItemView subclasses.
The QAbstractItemView base class provides a scroll area for the data it displays. The only part of the widget that is a QAbstractItemView subclass that is visible is its viewport, that is, the part that is shown by the scroll area. This visible area is accessible using the viewport() method. It doesn't really matter what size the widget actually is; all that matters is what size the widget would need to be to show all of the model's data (even if that is far larger than the screen). We will see how this affects our code when we look at the calculateRectsIfNecessary() and updateGeometries() methods.
TiledListView::TiledListView(QWidget *parent) : QAbstractItemView(parent), idealWidth(0), idealHeight(0), hashIsDirty(false) { setFocusPolicy(Qt::WheelFocus); setFont(QApplication::font("QListView")); horizontalScrollBar()->setRange(0, 0); verticalScrollBar()->setRange(0, 0); }
The constructor calls the base class and initializes the private data. Initially the view's "ideal" size is 0 x 0 since it has no data to display.
Unusually, we call setFont() to set the widget's font rather than do what we normally do in custom widgets and just use the inherited font. The font returned by the QApplication::font() method, when given a class name, is the platform-specific font that is used for that class. This makes the TiledListView use the correct font even on those platforms (such as Mac OS X) that use a slightly different-sized font from the default QWidget font for QListViews.*
Since there is no data we set the scrollbars' ranges to (0, 0); this ensures that the scrollbars are hidden until they are needed, while leaving the responsibility for hiding and showing them to the base class.
void TiledListView::setModel(QAbstractItemModel *model) { QAbstractItemView::setModel(model); hashIsDirty = true; }
When a model is set we first call the base class implementation, and then set the private hashIsDirty flag to true to ensure that when the calculateRectsIfNecessary() method is called, it will update the rectForRow hash.
The indexAt(), setSelection(), and viewportRectForRow() methods all need to know the size and position of the items in the model. This is also true indirectly of the mousePressEvent(), moveCursor(), paintEvent(), and visualRect() methods, since all of them call the methods that need the sizes and positions. Rather than compute the rectangles dynamically every time they are needed, we have chosen to trade some memory for the sake of speed by caching them in the rectForRow hash. And rather than keeping the hash up to date by calling calculate-RectsIfNecessary() whenever a change occurs, we simply keep track of whether the hash is dirty, and only recalculate the rectangles when we actually need to access the hash.
const int ExtraHeight = 3; void TiledListView::calculateRectsIfNecessary() const { if (!hashIsDirty) return; const int ExtraWidth = 10; QFontMetrics fm(font()); const int RowHeight = fm.height() + ExtraHeight; const int MaxWidth = viewport()->width(); int minimumWidth = 0; int x = 0; int y = 0; for (int row = 0; row < model()->rowCount(rootIndex()); ++row) { QModelIndex index = model()->index(row, 0, rootIndex()); QString text = model()->data(index).toString(); int textWidth = fm.width(text); if (!(x == 0 || x + textWidth + ExtraWidth < MaxWidth)) { y += RowHeight; x = 0; } else if (x != 0) x += ExtraWidth; rectForRow[row] = QRectF(x, y, textWidth + ExtraWidth, RowHeight); if (textWidth > minimumWidth) minimumWidth = textWidth; x += textWidth; } idealWidth = minimumWidth + ExtraWidth; idealHeight = y + RowHeight; hashIsDirty = false; viewport()->update(); }
This method is the heart of the TiledListView, at least as far as its appearance is concerned, since—as we will see shortly—all the painting is done using the rectangles created in this method.
We begin by seeing if the rectangles need to be recalculated at all. If they do we begin by calculating the height needed to display a row, and the maximum width that is available to the viewport, that is, the available visible width.
In the method's main loop we iterate over every row (i.e., every item) in the model, and retrieve the item's text. We then compute the width needed by the item, and compute the x- and y-coordinates where the item should be displayed—these depend on whether the item can fit on the same line (i.e., the same visual row) as the previous item, or if it must start a new line. Once we know the item's size and position, we create a rectangle based on that information and add it to the rectForRow hash, with the item's row as the key.
Notice that during the calculations in the loop, we use the actual visible width, but assume that the available height is whatever is needed to show all the items given this width. Also, to retrieve the model index we want, we pass a parent index of QAbstractItemView::rootIndex() rather than an invalid model index (QModelIndex()). Both work equally well for list models, but it is better style to use the more generic rootIndex() in QAbstractItemView subclasses.
At the end we recompute the ideal width (the width of the widest item plus some margin), and the ideal height (the height necessary to show all the items at the viewport's current width, no matter what the viewport's actual height is)—at this point the y variable holds the total height of all the rows. The ideal width may be greater than the available width, for example, if the viewport is narrower than the width needed to display the longest item—in which case the horizontal scrollbar will automatically be shown. Once the computations are complete, we call update() on the viewport (since all painting is done on the viewport, not on the QAbstractItemView custom widget itself), so that the data will be repainted.
At no point do we refer to or care about the actual size of the QAbstractItemView custom widget itself—all the calculations are done in terms of the viewport and of the ideal width and height.
QRect TiledListView::visualRect(const QModelIndex &index) const { QRect rect; if (index.isValid()) rect = viewportRectForRow(index.row()).toRect(); return rect; }
This pure virtual method must return the rectangle occupied by the item with the given model index. Fortunately, its implementation is very easy because we pass the work on to our private viewportRectForRow() method that makes use of the rectForRow hash.
QRectF TiledListView::viewportRectForRow(int row) const { calculateRectsIfNecessary(); QRectF rect = rectForRow.value(row).toRect(); if (!rect.isValid()) return rect; return QRectF(rect.x() - horizontalScrollBar()->value(), rect.y() - verticalScrollBar()->value(), rect.width(), rect.height()); }
This method is used by the visualRect() method and by the moveCursor() and paintEvent() methods. It returns a QRectF for maximum accuracy (e.g., for the paintEvent() method); other callers convert the returned value to a plain integer-based QRect using the QRectF::toRect() method.
The calculateRectsIfNecessary() method must be called by methods that access the rectForRow hash, before the access takes place. If the rectForRow hash is up to date, the calculateRectsIfNecessary() method will do nothing; otherwise it will recompute the rectangles in the hash ready for use.
The rectangles in the rectForRow hash have the x- and y-coordinates of their rows (items) based on the ideal width (usually the visible width) and the ideal height (the height needed to display all the items at the current width). This means that the rectangles are effectively using widget coordinates based on the ideal size of the widget (the actual size of the widget is irrelevant). The viewportRectForRow() method must return a rectangle that is in viewport coordinates, so we adjust the coordinates to account for any scrolling. Figure 6.2 illustrates the difference between widget and viewport coordinates.
bool isIndexHidden(const QModelIndex&) const { return false; }
Figure 6.2 Widget vs. viewport coordinates
We must reimplement this pure virtual method, and have done so in the header since it is so trivial. This method is designed for data that can have hidden items—for example, a table with hidden rows or columns. For this view, no items are hidden because we don't offer support for hiding them, so we always return false.
void TiledListView::scrollTo(const QModelIndex &index, QAbstractItemView::ScrollHint) { QRect viewRect = viewport()->rect(); QRect itemRect = visualRect(index); if (itemRect.left() < viewRect.left()) horizontalScrollBar()->setValue(horizontalScrollBar()->value() + itemRect.left() - viewRect.left()); else if (itemRect.right() > viewRect.right()) horizontalScrollBar()->setValue(horizontalScrollBar()->value() + qMin(itemRect.right() - viewRect.right(), itemRect.left() - viewRect.left())); if (itemRect.top() < viewRect.top()) verticalScrollBar()->setValue(verticalScrollBar()->value() + itemRect.top() - viewRect.top()); else if (itemRect.bottom() > viewRect.bottom()) verticalScrollBar()->setValue(verticalScrollBar()->value() + qMin(itemRect.bottom() - viewRect.bottom(), itemRect.top() - viewRect.top())); viewport()->update(); }
This is another pure virtual method that we are obliged to implement. Fortunately, the implementation is straightforward (and is almost the same as that used in Qt's chart example).
If the item to be scrolled to has a rectangle that is left of the viewport's left edge, then the viewport must be scrolled. The scrolling is done by changing the horizontal scrollbar's value, adding to it the difference between the item rectangle's left edge and the viewport's left edge. All the other cases work in an analogous way.
Note that this method calls visualRect() which in turn calls viewportRectForRow() which in turn calls calculateRectsIfNecessary()—as already noted, this last method recalculates the rectangles in the rectForRow hash if the hash is dirty.
QModelIndex TiledListView::indexAt(const QPoint &point_) const { QPoint point(point_); point.rx() += horizontalScrollBar()->value(); point.ry() += verticalScrollBar()->value(); calculateRectsIfNecessary(); QHashIterator<int, QRectF> i(rectForRow); while (i.hasNext()) { i.next(); if (i.value().contains(point)) return model()->index(i.key(), 0, rootIndex()); } return QModelIndex(); }
This pure virtual method must return the model index of the item at the given point. The point is in viewport coordinates, but the rectangles in rectForRow are in widget coordinates. Rather than convert each rectangle that we check to see if it contains the point, we do a one-off conversion of the point into widget coordinates.
The QPoint::rx() and QPoint::ry() methods return non-const references to the point's x- and y-coordinates, making it easy to change them. Without these methods we would have to do, for example, point.setX(horizontalScrollBar()->value() + point.x()).
We make sure that the rectForRow hash is up to date, and then we iterate over every row (item) in the hash—in an arbitrary order since hashes are unordered. If we find a value, that is, a rectangle, that contains the point, we immediately return the corresponding model index.
For models with large numbers of items (beyond the low thousands), this method might run slowly since in the worst case every item's rectangle must be checked, and even in the average case, half of the items must be checked. For the TiledListView this is unlikely to be a problem, since putting thousands of items in a list model of any kind is probably unhelpful to users—a tree model that grouped the items and made the top-level list of items a more manageable size would almost certainly be better.
void TiledListView::dataChanged(const QModelIndex &topLeft, const QModelIndex &bottomRight) { hashIsDirty = true; QAbstractItemView::dataChanged(topLeft, bottomRight); }
This method is called whenever model data changes. We set hashIsDirty to true to make sure that when calculateRectsIfNecessary() is called it will update the rectForRow hash when the hash is next needed, and then we call the base class implementation. Notice that we do not call viewport->update() to schedule a repaint. The changed data might not be visible so a repaint might be unnecessary, and if it were necessary, the dataChanged() base class implementation would schedule the repaint for us.
void TiledListView::rowsInserted(const QModelIndex &parent, int start, int end) { hashIsDirty = true; QAbstractItemView::rowsInserted(parent, start, end); } void TiledListView::rowsAboutToBeRemoved(const QModelIndex &parent, int start, int end) { hashIsDirty = true; QAbstractItemView::rowsAboutToBeRemoved(parent, start, end); }
If new rows are inserted into the model, or if rows are going to be removed, we must make sure that the view responds appropriately. These cases are easily handled by passing the work on to the base class; all that we must do is ensure that the rectForRow hash is marked as dirty so that it will be recalculated if necessary—for example, if the base class methods schedule a repaint.
QModelIndex TiledListView::moveCursor( QAbstractItemView::CursorAction cursorAction, Qt::KeyboardModifiers) { QModelIndex index = currentIndex(); if (index.isValid()) { if ((cursorAction == MoveLeft && index.row() > 0) || (cursorAction == MoveRight && index.row() + 1 < model()->rowCount())) { const int offset = (cursorAction == MoveLeft ? -1 : 1); index = model()->index(index.row() + offset, index.column(), index.parent()); } else if ((cursorAction == MoveUp && index.row() > 0) || (cursorAction == MoveDown && index.row() + 1 < model()->rowCount())) { QFontMetrics fm(font()); const int RowHeight = (fm.height() + ExtraHeight) * (cursorAction == MoveUp ? -1 : 1); QRect rect = viewportRectForRow(index.row()).toRect(); QPoint point(rect.center().x(), rect.center().y() + RowHeight); while (point.x() >= 0) { index = indexAt(point); if (index.isValid()) break; point.rx() -= fm.width("n"); } } } return index; }
Just as the calculateRectsIfNecessary() method is at the heart of the TiledListView's appearance, this method is at the heart of its behavior. The method must return the model index of the item that the requested move action should navigate to—or an invalid model index if no move should occur.
If the user presses the left (or right) arrow key we must return the model index of the previous (or next) item in the list—or of the current item if the previous (or next) item is the list model's first (or last) item. This is easily achieved by creating a new model index based on the current model index but using the previous (or next) row.
Handling the up and down arrow keys is slightly more subtle than handling the left and right arrow keys. In both cases we must compute a point above or below the current item. It doesn't matter if the computed point is outside the viewport, so long as it is within an item's rectangle.
If the user presses the up (or down) arrow key we must return the model index of the item that appears above (or below) the current item. We begin by getting the current item's rectangle in the viewport. We then create a point that is exactly one row above (or below) the current item vertically, and at the item's center horizontally. We then use the indexAt() method to retrieve the model index for the item at the given point. If we get a valid model index, there is an item above (or below) the current one, and we have its model index, so we are finished and can return that index.
But the model index might be invalid: this is possible because there may not be an item above (or below). Recall from the screenshot (208 ) that the items at the right-hand edge are ragged, because lines are of different lengths. If this is the case, we move the point left by the width of one "n" character and try again, repeatedly moving left until either we find an item (i.e., until we get a valid model index), or until we move beyond the left edge which means that there is no item above (or below). There will be no item above (or below) when the user presses the up (or down) arrow on an item that is in the first (or last) line.
If the moveCursor() method returns an invalid QModelIndex, the QAbstractItemView base class harmlessly does nothing.
We have not written any code for handling selections—and we don't need to since we are using the QAbstractItemView API. If the user moves with the Shift key held down, the selection will be extended to create a selection of contiguous items. Similarly, while the user holds down the Ctrl key ( key on Mac OS X), they can click arbitrary items and each one will be selected to create a selection that may include non-contiguous items.
We have left the implementation of support for the Home, End, PageUp, and PageDown keys as an exercise—they just require that the moveCursor() method be extended to handle more CursorActions (such as QAbstractItemView::MoveHome and QAbstractItemView::MovePageUp).
int TiledListView::horizontalOffset() const { return horizontalScrollBar()->value(); } int TiledListView::verticalOffset() const { return verticalScrollBar()->value(); }
These pure virtual methods must be reimplemented. They must return the x- and y-offsets of the viewport within the (ideal-sized) widget. They are trivial to implement since the scrollbars' values are the offsets we need.
void TiledListView::scrollContentsBy(int dx, int dy) { scrollDirtyRegion(dx, dy); viewport()->scroll(dx, dy); }
This method is called when the scrollbars are moved; its responsibility is to ensure that the viewport is scrolled by the amounts given, and to schedule an appropriate repaint. Here we set up the repaint by calling the QAbstractItemView::scrollDirtyRegion() method, before performing the scroll. Alternatively, instead of calling scrollDirtyRegion(), we could call viewport->update(), after performing the scroll.
The base class implementation simply calls viewport->update() and doesn't actually scroll. Note that if we want to scroll programmatically we should do so by calling QScrollBar::setValue() on the scrollbars, not by calling this method.
void TiledListView::setSelection(const QRect &rect, QFlags<QItemSelectionModel::SelectionFlag> flags) { QRect rectangle = rect.translated(horizontalScrollBar()->value(), verticalScrollBar()->value()).normalized(); calculateRectsIfNecessary(); QHashIterator<int, QRectF> i(rectForRow); int firstRow = model()->rowCount(); int lastRow = -1; while (i.hasNext()) { i.next(); if (i.value().intersects(rectangle)) { firstRow = firstRow < i.key() ? firstRow : i.key(); lastRow = lastRow > i.key() ? lastRow : i.key(); } } if (firstRow != model()->rowCount() && lastRow != -1) { QItemSelection selection( model()->index(firstRow, 0, rootIndex()), model()->index(lastRow, 0, rootIndex())); selectionModel()->select(selection, flags); } else { QModelIndex invalid; QItemSelection selection(invalid, invalid); selectionModel()->select(selection, flags); } }
This pure virtual method is used to apply the given selection flags to all the items that are in or touching the specified rectangle. The actual selection must be made by calling QAbstractItemView::selectionModel()->select(). The implementation shown here is very similar to the one used by Qt's chart example.
The rectangle is passed using viewport coordinates, so we begin by creating a rectangle that uses widget coordinates since those are the ones used by the rectForRow hash. We then iterate over all the rows (items) in the hash—in arbitrary order—and if an item's rectangle intersects the given rectangle, we expand the first and last rows that the selection spans to include the item if it isn't already included.
If we have valid first and last selection rows, we create a QItemSelection that spans these rows (inclusively) and update the view's selection model. But if one or both rows are invalid, we create an invalid QModelIndex and update the selection model using it.
QRegion TiledListView::visualRegionForSelection( const QItemSelection &selection) const { QRegion region; foreach (const QItemSelectionRange &range, selection) { for (int row = range.top(); row <= range.bottom(); ++row) { for (int column = range.left(); column < range.right(); ++column) { QModelIndex index = model()->index(row, column, rootIndex()); region += visualRect(index); } } } return region; }
This pure virtual method must be reimplemented to return the QRegion that encompasses all the view's selected items as shown in the viewport and using viewport coordinates. The implementation we have used is very similar to that used by Qt's chart example.
We start by creating an empty region. Then we iterate over all the selections—if there are any. For each selection we retrieve a model index for every item in the selection, and add each item's visual rectangle to the region.
Our visualRect() implementation calls viewportRectForRow() which in turn retrieves the rectangle from the rectForRow hash and returns it transformed into viewport coordinates (since rectForRow's rectangles use widget coordinates). In this particular case we could have bypassed the visualRect() call and made direct use of the rectForRow hash, but we preferred to do a more generic implementation that is easy to adapt for other custom views.
void TiledListView::paintEvent(QPaintEvent*) { QPainter painter(viewport()); painter.setRenderHints(QPainter::Antialiasing| QPainter::TextAntialiasing); for (int row = 0; row < model()->rowCount(rootIndex()); ++row) { QModelIndex index = model()->index(row, 0, rootIndex()); QRectF rect = viewportRectForRow(row); if (!rect.isValid() || rect.bottom() < 0 || rect.y() > viewport()->height()) continue; QStyleOptionViewItem option = viewOptions(); option.rect = rect.toRect(); if (selectionModel()->isSelected(index)) option.state |= QStyle::State_Selected; if (currentIndex() == index) option.state |= QStyle::State_HasFocus; itemDelegate()->paint(&painter, option, index); paintOutline(&painter, rect); } }
Painting the view is surprisingly straightforward since every item's rectangle has already been computed and is available in the rectForRow hash. But notice that we paint on the widget's viewport, not on the widget itself. And as usual, we explicitly switch on antialiasing since we cannot assume what the default render hints are.
We iterate over every item and get each one's model index and its rectangle in viewport coordinates. If the rectangle is invalid (it shouldn't be), or if it is not visible in the viewport—that is, its bottom edge is above the viewport, or its y-coordinate is below the viewport—we don't bother to paint it.
For those items we do paint, we begin by retrieving the QStyleOptionViewItem supplied by the base class. We then set the option's rectangle to the item's rectangle—converting from QRectF to QRect using QRectF::toRect()—and update the option's state appropriately if the item is selected or is the current item.
Most importantly, we do not paint the item ourselves! Instead we ask the view's delegate—which could be the base class's built-in QStyledItemDelegate, or a custom delegate set by the class's client—to paint the item for us. This ensures that the view supports custom delegates.
The items are painted in lines, packing them in to make as much use of the available space as possible. But because each item's text could contain more than one word we need to help the user to be able to visually distinguish between different items. We do this by painting an outline around each item.
void TiledListView::paintOutline(QPainter *painter, const QRectF &rectangle) { const QRectF rect = rectangle.adjusted(0, 0, -1, -1); painter->save(); painter->setPen(QPen(palette().dark().color(), 0.5)); painter->drawRect(rect); painter->setPen(QPen(Qt::black, 0.5)); painter->drawLine(rect.bottomLeft(), rect.bottomRight()); painter->drawLine(rect.bottomRight(), rect.topRight()); painter->restore(); }
The outline is drawn by painting a rectangle, and then painting a couple of lines—one just below the bottom of the rectangle, and one just to the right of the rectangle—to provide a very subtle shadow effect.
void TiledListView::resizeEvent(QResizeEvent*) { hashIsDirty = true; calculateRectsIfNecessary(); updateGeometries(); }
If the view is resized we must recalculate all the items' rectangles and update the scrollbars. We have already seen the calculateRectsIfNecessary() method (212 ), so we just need to review updateGeometries().
void TiledListView::updateGeometries() { QFontMetrics fm(font()); const int RowHeight = fm.height() + ExtraHeight; horizontalScrollBar()->setSingleStep(fm.width("n")); horizontalScrollBar()->setPageStep(viewport()->width()); horizontalScrollBar()->setRange(0, qMax(0, idealWidth - viewport()->width())); verticalScrollBar()->setSingleStep(RowHeight); verticalScrollBar()->setPageStep(viewport()->height()); verticalScrollBar()->setRange(0, qMax(0, idealHeight - viewport()->height())); }
This protected slot was introduced with Qt 4.4 and is used to update the view's child widgets—for example, the scrollbars.
The widget's ideal width and height are calculated in calculateRectsIfNecessary(). The height is always sufficient to show all the model's data, and so is the width, if the viewport is wide enough to show the widest item. As mentioned earlier, it does not really matter what the view widget's actual size is, since the user only ever sees the viewport.
We make the horizontal scrollbar's single step size (i.e., how far it moves when the user clicks one of its arrows) the width of an "n", that is, one character. And we make its page step size (i.e., how far it moves when the user clicks left or right of the scrollbar's slider) the width of the viewport. We also set the horizontal scrollbar's range to span from 0 to the widget's ideal width, not counting the viewport's width (because that much can already be seen). The vertical scrollbar is set up in an analogous way.
void TiledListView::mousePressEvent(QMouseEvent *event) { QAbstractItemView::mousePressEvent(event); setCurrentIndex(indexAt(event->pos())); }
This is the last event handler that we need to implement. We use it to make the item the user clicked the selected and current item. Because our view is a QAbstractItemView subclass, which itself is a QAbstractScrollArea subclass, the mouse event's position is in viewport coordinates. This isn't a problem since the indexAt() method expects the QPoint it is passed to be in viewport coordinates.
One final point to note about the TiledListView class is that it assumes that the user is using a left to right language, such as English. Arabic and Hebrew users will find the class confusing because they use right to left languages. We leave modifying the class to work both left to right and right to left as an exercise for the reader. (The widget's left to right or right to left status is available from QWidget::layoutDirection(); this is normally the same as QApplication::layoutDirection() but it is best to use the QWidget variant to be strictly correct.)
Like all of Qt's standard view classes, TiledListView has a one to one correspondence between data items and display items. But in some situations we might want to visualize two or more items combined together in some way—but this isn't supported by the QAbstractItemView API, nor can it be achieved by using custom delegates. Nonetheless, we can still produce a view that visualizes our data exactly as we want—as we will see in the next section—but in doing so we must eschew the QAbstractItemView API and provide our own API instead.