Model-Specific Visualizing Views
In this section we will create a view class from scratch as a QWidget subclass, and will provide our own API that is different from the QAbstractItemView API. It would have been possible to create a QAbstractItemView subclass, but since the view we want to create is specific to one particular model and shows some of its items combined, there seemed little point in making it comply with an API that wasn't needed or relevant.
The visualizer we will create is designed to present a table of census data. The model that holds the data is a table model, where each row holds a year, a count of the males, a count of the females, and the total of males and females. Figure 6.3 shows the central area of the CensusVisualizer application (censusvisualizer). The area has two views of the data. On the left a standard QTableView presents the data in the conventional way. On the right a CensusVisualizer view is used to represent the data, and it does so by showing the males and females as colored bars proportional to their numbers and using gradient fills.
Figure 6.3 A QTableView and a CensusVisualizer view
We could not use Qt's QHeaderView to present the visualizer's headers because we have combined two columns. Because of this we have created the CensusVisualizer view as a QWidget that aggregates three other widgets inside itself: a custom CensusVisualizerHeader to provide the horizontal header, a custom CensusVisualizerView to visualize the data, and a QScrollArea to contain the CensusVisualizerView and provide support for scrolling and resizing. The relationships between these classes are shown in Figure 6.4.
Figure 6.4 The CensusVisualizer classes in relation to one another
We will start by looking at the creation of the visualizer in the application's main() function.
CensusVisualizer *censusVisualizer = new CensusVisualizer; censusVisualizer->setModel(model);
This looks and works exactly like we'd expect—the visualizer is created and we call CensusVisualizer::setModel() to give it the model. Later on in the program's main() function, the QTableView is created, both views are laid out, and various signal–slot connections are made to give the program its behavior. We will ignore all of that and just focus our attention on the design and coding of the visualizer class and its aggregated header and view classes.
The Visualizer Widget
The visualizer widget is the one that our clients will use directly, so we will start by reviewing the CensusVisualizer class. This will give us the context we need to then go on to look at the two custom classes that the visualizer aggregates to provide its appearance. Here's the CensusVisualizer's definition in the header file, but excluding its private data:
class CensusVisualizer : public QWidget { Q_OBJECT public: explicit CensusVisualizer(QWidget *parent=0); QAbstractItemModel *model() const { return m_model; } void setModel(QAbstractItemModel *model); QScrollArea *scrollArea() const { return m_scrollArea; } int maximumPopulation() const { return m_maximumPopulation; } int widthOfYearColumn() const { return m_widthOfYearColumn; } int widthOfMaleFemaleColumn() const; int widthOfTotalColumn() const { return m_widthOfTotalColumn; } int selectedRow() const { return m_selectedRow; } void setSelectedRow(int row); int selectedColumn() const { return m_selectedColumn; } void setSelectedColumn(int column); void paintItemBorder(QPainter *painter, const QPalette &palette, const QRect &rect); QString maleFemaleHeaderText() const; int maleFemaleHeaderTextWidth() const; int xOffsetForMiddleOfColumn(int column) const; int yOffsetForRow(int row) const; public slots: void setCurrentIndex(const QModelIndex &index); signals: void clicked(const QModelIndex&); private: ... };
Although the data isn't shown, it is worth noting that the aggregated CensusVisualizerHeader is held in the header private member variable and the CensusVisualizerView is held in the view private member variable—both are pointers, of course. The class also holds a pointer to the model and to the QScrollArea that contains the CensusVisualizerView. The other private member data are all integers most of whose getters are implemented inline and shown here, and whose setters—for those that are writable—we will review shortly.
The maximum population is used by the view to compute the maximum widths of the male–female bars to make the best use of the available space, and is calculated whenever setModel() is called.
The width getters are used by both the header and the view when they are painting themselves. The selected row and column are kept track of and their values are used by the header to highlight the selected column, and by the view to highlight the selected item (or the selected male–female item pair).
The signal is included so that if the selected item is changed by the user clicking on the view, we emit a clicked() signal to notify any interested objects.
The non-inline parts of the CensusVisualizer class are the constructor and ten methods. The paintItemBorder(), maleFemaleHeaderText(), and maleFemaleHeaderTextWidth() methods are used by the aggregated header and view, so we will defer our review of them until we see them used, but we will review all the others here.
const int Invalid = -1; CensusVisualizer::CensusVisualizer(QWidget *parent) : QWidget(parent), m_model(0), m_selectedRow(Invalid), m_selectedColumn(Invalid), m_maximumPopulation(Invalid) { QFontMetrics fm(font()); m_widthOfYearColumn = fm.width("W9999W"); m_widthOfTotalColumn = fm.width("W9,999,999W"); view = new CensusVisualizerView(this); header = new CensusVisualizerHeader(this); m_scrollArea = new QScrollArea; m_scrollArea->setBackgroundRole(QPalette::Light); m_scrollArea->setWidget(view); m_scrollArea->installEventFilter(view); QVBoxLayout *layout = new QVBoxLayout; layout->addWidget(header); layout->addWidget(m_scrollArea); layout->setContentsMargins(0, 0, 0, 0); layout->setSpacing(0); setLayout(layout); connect(view, SIGNAL(clicked(const QModelIndex&)), this, SIGNAL(clicked(const QModelIndex&))); }
We begin by setting fixed widths for the year and total columns based on the largest numbers we expect them to handle, plus some margin.* The width of the total column set here is just an initial default; the actual width is recalculated in the setModel() method and depends on the model's maximum population. We then create the aggregated view and header widgets. Although we pass this as their parent, because we use a QScrollArea to contain the view, the view will be reparented to the QScrollArea.
The QScrollArea class is unusual for Qt in that it is not designed to be subclassed. Instead the usage pattern is to aggregate the QScrollArea inside another widget as we have done here. Although this approach is by far the easiest to use, if we want to use inheritance, we can derive our subclass from QAbstractScrollArea as some of Qt's built-in classes do.
We install the view as an event filter for the scroll area—this means that every event that goes to the scroll area will first be sent to the view's eventFilter() method. We will see why this is necessary when we review the CensusVisualizerView class further on.
The layout is quite conventional except that we set the layout's margins and spacing to 0; this makes the CensusVisualizer have the same look as other widgets, with no extraneous border area, and with no gap between the CensusVisualizerHeader and the CensusVisualizerView (contained in the QScrollArea).
The connection is slightly unusual since it is a signal–signal connection. These set up a relationship whereby when the first signal is emitted the second signal is emitted as a consequence. So in this case, when the user clicks the view (i.e., to select an item), the view's clicked() signal goes to the CensusVisualizer, and this in turn emits a matching clicked() signal with the same QModelIndex parameter. This means that CensusVisualizer clients can connect to the CensusVisualizer's clicked() signal without having to know or care about the internals. This makes the CensusVisualizer much more of a self-contained component than it would be if, for example, it exposed the widgets it aggregates.
enum {Year, Males, Females, Total}; void CensusVisualizer::setModel(QAbstractItemModel *model) { if (model) { QLocale locale; for (int row = 0; row < model->rowCount(); ++row) { int total = locale.toInt(model->data( model->index(row, Total)).toString()); if (total > m_maximumPopulation) m_maximumPopulation = total; } QString population = QString::number(m_maximumPopulation); population = QString("%1%2") .arg(population.left(1).toInt() + 1) .arg(QString(population.length() - 1, QChar('0'))); m_maximumPopulation = population.toInt(); QFontMetrics fm(font()); m_widthOfTotalColumn = fm.width(QString("W%1%2W") .arg(population) .arg(QString(population.length() / 3, ','))); } m_model = model; header->update(); view->update(); }
When a new model is set we must tell the header and view to update themselves. But first we must calculate a suitable maximum population. We do this by finding the biggest total population in the data, and then rounding it up to the smallest number with a most significant digit that is one larger. For example, if the biggest total is 8 392 174, the maximum becomes 9 000 000.
The algorithm used is very crude, but effective: we create a string that starts with the number's first digit plus one, followed by one less than as many zeros as there are digits in the number, and convert this string to an int. For the zeros we used one of QString's two-argument constructors that takes a count and a character and returns a string that contains exactly count occurrences of the character.
Notice that we cannot retrieve the totals using model->data(model->index(row, Total).toInt(), because the model happens to hold the values as localized strings (e.g., "8,392,174" in the U.S. and UK, and "8.392.174" in Germany), rather than as integers. The solution is to use toString() to extract the data and then to use QLocale::toInt()—which takes an integer in the form of a localized string and returns the integer value.
The QLocale class also has corresponding toFloat() and toDouble() methods, as well as methods for other integral types—such as toUInt()—and also methods for extracting dates and times from localized strings. When a QLocale is constructed it defaults to using the application's current locale, but this can be overridden by using the one-argument constructor and a locale name that has the ISO 639 language code and ISO 3166 country code, or the two-argument constructor using Qt's language and country enums.
In the constructor we set an initial width for the total column, but here we can set one that is appropriate for the actual total. The width is set to be the number of pixels needed to show the maximum number, plus space for a couple of "W"s for padding, plus space for a comma (or other grouping marker) for every three digits.
const int ExtraWidth = 5; int CensusVisualizer::widthOfMaleFemaleColumn() const { return width() - (m_widthOfYearColumn + m_widthOfTotalColumn + ExtraWidth + m_scrollArea->verticalScrollBar()->sizeHint().width()); }
This method returns a suitable width for the male–female column. It calculates the width as the maximum available width given the width of the CensusVisualizer itself, the widths of the other two columns, the width of the scroll area's vertical scrollbar, and a little bit of margin. This ensures that when the CensusVisualizer is resized, any extra width is always given to the male–female column.
void CensusVisualizer::setSelectedRow(int row) { m_selectedRow = row; view->update(); } void CensusVisualizer::setSelectedColumn(int column) { m_selectedColumn = column; header->update(); }
If the selected row is changed programmatically, the view must update itself to show the correct highlighted item. Similarly, if the selected column is changed, the header must highlight the title of the selected column.
void CensusVisualizer::setCurrentIndex(const QModelIndex &index) { setSelectedRow(index.row()); setSelectedColumn(index.column()); int x = xOffsetForMiddleOfColumn(index.column()); int y = yOffsetForRow(index.row()); m_scrollArea->ensureVisible(x, y, 10, 20); }
This slot is provided as a service to clients, so that they can change the CensusVisualizer's selected item by using a signal–slot connection.
Once the row and column are set, we make sure that they are visible in the scroll area. The QScrollArea::ensureVisible() method takes x- and y-coordinates, and optionally some horizontal and vertical margin (which defaults to 50 pixels each). We've reduced the margins so as to avoid unwanted scrolling when the user clicks the top or bottom visible row.
There is actually a trade-off to be made here. If the vertical margin is too large, clicking the top or bottom item will cause unnecessary scrolling. And if the margin is too small, if the user Tabs to the widget and uses the down arrow to reach the bottom item, the item won't be shown fully.
int CensusVisualizer::xOffsetForMiddleOfColumn(int column) const { switch (column) { case Year: return widthOfYearColumn() / 2; case Males: return widthOfYearColumn() + (widthOfMaleFemaleColumn() / 4); case Females: return widthOfYearColumn() + ((widthOfMaleFemaleColumn() * 4) / 3); default: return widthOfYearColumn() + widthOfMaleFemaleColumn() + (widthOfTotalColumn() / 2); } }
This method is used to get a suitable x-offset for the current column. It does this by computing the given column's horizontal midpoint based on the column widths.
const int ExtraHeight = 5; int CensusVisualizer::yOffsetForRow(int row) const { return static_cast<int>((QFontMetricsF(font()).height() + ExtraHeight) * row); }
This method is used to get the y-offset for the given row, which it calculates by multiplying the height of one row by the given row index.
The x- and y-offsets returned by the xOffsetForMiddleOfColumn() and yOffsetForRow() methods assume that the CensusVisualizerView is exactly the size needed to show all the data. This assumption is valid because the CensusVisualizerView enforces it—as we will see when we look at the CensusVisualizerView:: eventFilter() method. This means that even though only a portion of the view might be displayed, we don't have to do any scrolling-related computations because the QScrollArea that contains the CensusVisualizerView takes care of them for us.
We have now finished reviewing the CensusVisualizer class. Apart from the constructor and the setModel() method, it has very little code. This is because all of the widget's appearance, and most of its behavior, are handled by the instances of the CensusVisualizerHeader and CensusVisualizerView classes that the CensusVisualizer creates and lays out in its constructor. We will now review each of these aggregated classes in turn, starting with the header.
The Visualizer's Aggregated Header Widget
The CensusVisualizerHeader widget provides the column headers for the CensusVisualizer, as Figure 6.3 illustrates (224 ). Since we are painting it ourselves we have taken the opportunity to give it a stronger three-dimensional look than QHeaderView normally provides by using a different gradient fill. (If we had wanted to exactly match QHeaderView, we could have done the painting using QStyle methods.)
The class's definition in the header file is quite simple; here is its complete public API:
class CensusVisualizerHeader : public QWidget { Q_OBJECT public: explicit CensusVisualizerHeader(QWidget *parent) : QWidget(parent) {} QSize minimumSizeHint() const; QSize sizeHint() const { return minimumSizeHint(); } protected: void paintEvent(QPaintEvent *event); ... };
The constructor has an empty body. The only methods that are implemented are the minimumSizeHint(), the sizeHint(), the paintEvent(), and a couple of private helper methods (covered later) that paintEvent() calls.
QSize CensusVisualizerHeader::minimumSizeHint() const { CensusVisualizer *visualizer = qobject_cast<CensusVisualizer*>( parent()); Q_ASSERT(visualizer); return QSize(visualizer->widthOfYearColumn() + visualizer->maleFemaleHeaderTextWidth() + visualizer->widthOfTotalColumn(), QFontMetrics(font()).height() + ExtraHeight); }
The column widths are available from the parent CensusVisualizer, so we must cast—using qobject_cast<>() as here, or dynamic_cast<>()—to get a pointer to the parent that we can use to access the data we require. (If dynamic_cast<>() is used the compiler must have RTTI—Run Time Type Information—turned on, which most do by default nowadays.) The minimum width we need is the sum of the widths of all the columns, and the minimum height is the height of a character in the widget's font plus some margin.
The maleFemaleHeaderTextWidth() method, and the method it depends on, are provided by the CensusVisualizer class since they are used by both of the aggregated custom widgets. We show them here for completeness.
int CensusVisualizer::maleFemaleHeaderTextWidth() const { return QFontMetrics(font()).width(maleFemaleHeaderText()); } QString CensusVisualizer::maleFemaleHeaderText() const { if (!m_model) return " - "; return QString("%1 - %2") .arg(m_model->headerData(Males, Qt::Horizontal).toString()) .arg(m_model->headerData(Females, Qt::Horizontal) .toString()); }
The maleFemaleHeaderTextWidth() method returns the width needed by the male–female column to show its title, and the maleFemaleHeaderText() method returns the title itself.
void CensusVisualizerHeader::paintEvent(QPaintEvent*) { QPainter painter(this); painter.setRenderHints(QPainter::Antialiasing| QPainter::TextAntialiasing); paintHeader(&painter, height()); painter.setPen(QPen(palette().button().color().darker(), 0.5)); painter.drawRect(0, 0, width(), height()); }
The paintEvent() sets up the painter, passes most of the work on to the paintHeader() method, and finishes off by drawing a rectangle around the entire header.
void CensusVisualizerHeader::paintHeader(QPainter *painter, const int RowHeight) { const int Padding = 2; CensusVisualizer *visualizer = qobject_cast<CensusVisualizer*>( parent()); Q_ASSERT(visualizer); paintHeaderItem(painter, QRect(0, 0, visualizer->widthOfYearColumn() + Padding, RowHeight), visualizer->model()->headerData(Year, Qt::Horizontal) .toString(), visualizer->selectedColumn() == Year); paintHeaderItem(painter, QRect(visualizer->widthOfYearColumn() + Padding, 0, visualizer->widthOfMaleFemaleColumn(), RowHeight), visualizer->maleFemaleHeaderText(), visualizer->selectedColumn() == Males || visualizer->selectedColumn() == Females); ... }
This method paints each column header in turn. For each one it calls paintHeaderItem(), passing it the painter, the rectangle in which to do the painting, the text to paint, and whether this item (i.e., this column) is selected. We have omitted the code for the total column since it is very similar to that used for the year column.
void CensusVisualizerHeader::paintHeaderItem(QPainter *painter, const QRect &rect, const QString &text, bool selected) { CensusVisualizer *visualizer = qobject_cast<CensusVisualizer*>( parent()); Q_ASSERT(visualizer); int x = rect.center().x(); QLinearGradient gradient(x, rect.top(), x, rect.bottom()); QColor color = selected ? palette().highlight().color() : palette().button().color(); gradient.setColorAt(0, color.darker(125)); gradient.setColorAt(0.5, color.lighter(125)); gradient.setColorAt(1, color.darker(125)); painter->fillRect(rect, gradient); visualizer->paintItemBorder(painter, palette(), rect); painter->setPen(selected ? palette().highlightedText().color() : palette().buttonText().color()); painter->drawText(rect, text, QTextOption(Qt::AlignCenter)); }
This is the method that actually paints each header item. We begin by getting a pointer to the CensusVisualizer since we use one of its methods. Then we create a linear gradient whose coloring depends on whether this item is selected. The gradient goes from a lighter color in the middle to a darker color at the top and bottom, using lighter and darker colors than the ones used by QHeaderView, to produce a stronger three-dimensional effect. Once the gradient is set up we use it to paint the item's background. Next we draw an outline around the item—actually we draw just two lines, one along the bottom, and the other on the right edge. And finally, we draw the text centered in the middle.
For completeness, here is the paintItemBorder() method:
void CensusVisualizer::paintItemBorder(QPainter *painter, const QPalette &palette, const QRect &rect) { painter->setPen(QPen(palette.button().color().darker(), 0.33)); painter->drawLine(rect.bottomLeft(), rect.bottomRight()); painter->drawLine(rect.bottomRight(), rect.topRight()); }
We chose to draw the "outline" using just two lines because in this example it produces a better effect than drawing a rectangle.
This completes our review of the CensusVisualizerHeader class. The class is surprisingly straightforward, with most of the work simply a matter of setting up the painter and gradient and doing some simple drawing. This is quite a contrast with the CensusVisualizerView class where we must implement both its appearance and its behavior, as we will see in the next subsection.
The Visualizer's Aggregated View Widget
The custom CensusVisualizerView widget is used to display the model's data. It doesn't matter as such what size the widget is since it is embedded in a QScrollArea which provides scrollbars when necessary and generally takes care of all scrolling-related matters for us. This leaves us free to concentrate on the widget's appearance and behavior. Here is the public part of the widget's definition from the header file:
class CensusVisualizerView : public QWidget { Q_OBJECT public: explicit CensusVisualizerView(QWidget *parent); QSize minimumSizeHint() const; QSize sizeHint() const; signals: void clicked(const QModelIndex&); protected: bool eventFilter(QObject *target, QEvent *event); void mousePressEvent(QMouseEvent *event); void keyPressEvent(QKeyEvent *event); void paintEvent(QPaintEvent *event); ... };
The class also has several private methods, all of which are used to support painting the data—and which we will review later—and one private data member, a pointer to the parent CensusVisualizer. We will briefly look at the public methods and the slot, and then work our way through the protected event handlers to see what they do and how they do it—but first we will look at the constructor.
CensusVisualizerView::CensusVisualizerView(QWidget *parent) : QWidget(parent) { visualizer = qobject_cast<CensusVisualizer*>(parent); Q_ASSERT(visualizer); setFocusPolicy(Qt::WheelFocus); setMinimumSize(minimumSizeHint()); }
The CensusVisualizerView is created inside the CensusVisualizer constructor and passed as parent the CensusVisualizer itself (227 ). Nonetheless we have chosen to keep a private CensusVisualizer pointer member (visualizer), to give us access to the CensusVisualizer, because after the view has been constructed it is given to a QScrollArea—and this takes ownership of the view and becomes the view's parent. (Alternatively we could avoid keeping a member variable and access the visualizer by calling qobject_cast<CensusVisualizer*>(parent() ->parent()) instead.)
Qt provides several different focus policies: Qt::NoFocus (useful for labels and other read-only widgets), Qt::TabFocus (the widget accepts focus when tabbed to), Qt::ClickFocus (the widget accepts focus when clicked), Qt::StrongFocus (this combines tab and click focus), and Qt::WheelFocus (this is strong focus plus accepting the focus when the mouse wheel is used). Here we have used Qt:: WheelFocus which is the usual choice for editable widgets.
We have omitted the minimumSizeHint() method's implementation since it is almost identical to CensusVisualizerHeader::minimumSizeHint() (232 ), the only difference being that here we have the visualizer member built into the class. (The CensusVisualizerHeader's parent is the CensusVisualizer and it isn't reparented, so it doesn't need a separate visualizer member variable.)
QSize CensusVisualizerView::sizeHint() const { int rows = visualizer->model() ? visualizer->model()->rowCount() : 1; return QSize(visualizer->widthOfYearColumn() + qMax(100, visualizer->maleFemaleHeaderTextWidth()) + visualizer->widthOfTotalColumn(), visualizer->yOffsetForRow(rows)); }
If a model has been set we allow enough room for all its rows; otherwise we allow room for a single row. The y-offset returned by the CensusVisualizer:: yOffsetForRow() method is the height we need since we pass it a row that is equal to the number of rows in the model. For the columns we use the fixed widths calculated when the CensusVisualizer was constructed, plus the computed width of the male–female column (or 100 pixels, whichever is greater).
bool CensusVisualizerView::eventFilter(QObject *target, QEvent *event) { if (QScrollArea *scrollArea = visualizer->scrollArea()) { if (target == scrollArea && event->type() == QEvent::Resize) { if (QResizeEvent *resizeEvent = static_cast<QResizeEvent*>(event)) { QSize size = resizeEvent->size(); size.setHeight(sizeHint().height()); int width = size.width() - (ExtraWidth + scrollArea->verticalScrollBar()->sizeHint() .width()); size.setWidth(width); resize(size); } } } return QWidget::eventFilter(target, event); }
The CensusVisualizerView was made an event filter for the QScrollArea that contains it (227 ). This means that every event that is sent to the QScrollArea goes to this method first.
The only event we are interested in is QEvent::Resize. When this event occurs, that is, when the scroll area is resized, we also resize the CensusVisualizerView widget. We always make the view the height needed to show all of its data, and we set its width to the available width while allowing for the width of the vertical scrollbar. This means that if the user has scrolled the view and, for example, clicks a row, we can work as if the entire widget is visible without having to account for the scrolling to compute which row was clicked.
Inside an eventFilter() reimplementation we are free, at least in principle, to do what we like with the event: we can change it, replace it, delete it, or ignore it. To stop an event from going further (whether or not we do anything with it), or if we delete an event, we must return true to indicate that it has been handled; otherwise we must return false. Here we make use of the event, but don't want to interfere with its behavior, so we leave the arguments unchanged and call the base class implementation at the end.
void CensusVisualizerView::mousePressEvent(QMouseEvent *event) { int row = static_cast<int>(event->y() / (QFontMetricsF(font()).height() + ExtraHeight)); int column; if (event->x() < visualizer->widthOfYearColumn()) column = Year; else if (event->x() < (visualizer->widthOfYearColumn() + visualizer->widthOfMaleFemaleColumn() / 2)) column = Males; else if (event->x() < (visualizer->widthOfYearColumn() + visualizer->widthOfMaleFemaleColumn())) column = Females; else column = Total; visualizer->setSelectedRow(row); visualizer->setSelectedColumn(column); emit clicked(visualizer->model()->index(row, column)); }
The QMouseEvent::y() method returns the mouse click's y-offset relative to the top of the widget. Thanks to the CensusVisualizerView being embedded in a QScrollArea, and thanks to it always being exactly high enough to hold all the data—something we ensure in the eventFilter()—we can work directly with the y-offset no matter whether the widget has been scrolled. So here, we determine the row by dividing the y-offset by the height of one row.
To work out the column, we compare the x-offset: if it is less than the width of the year column then the year column was clicked; if it is less than the width of the year column plus half the width of the male–female column then the male column was clicked; and so on.
Once the row and column are known we tell the CensusVisualizer to select them, safe in the knowledge that doing this will also result in update() being called both on this view and on the header so that the correct row and column are properly highlighted. And finally, we emit the clicked() signal with the model index—as computed by the model—of the selected item, which in turn will cause the CensusVisualizer to emit its own clicked() signal with the same model index for the benefit of any connected objects.
void CensusVisualizerView::keyPressEvent(QKeyEvent *event) { if (visualizer->model()) { int row = Invalid; int column = Invalid; if (event->key() == Qt::Key_Left) { column = visualizer->selectedColumn(); if (column == Males || column == Total) --column; else if (column == Females) column = Year; } ... else if (event->key() == Qt::Key_Up) row = qMax(0, visualizer->selectedRow() - 1); else if (event->key() == Qt::Key_Down) row = qMin(visualizer->selectedRow() + 1, visualizer->model()->rowCount() - 1); row = row == Invalid ? visualizer->selectedRow() : row; column = column == Invalid ? visualizer->selectedColumn() : column; if (row != visualizer->selectedRow() || column != visualizer->selectedColumn()) { QModelIndex index = visualizer->model()->index(row, column); visualizer->setCurrentIndex(index); emit clicked(index); return; } QWidget::keyPressEvent(event); }
This event handler is used to provide navigation inside the view by the use of the keyboard arrow keys.
Inside the CensusVisualizer we keep track of the selected row and column, but in the case of the male and female columns they are visually—and therefore from the user's perspective—a single column. To account for this, if the user presses the left arrow and the current column is either the male or the female column, we set the column to be the year column. If the current column is the year column, we do nothing, and if the current column is the total column we set the column to be the female column. The handling of right arrow presses is very similar (so we have omitted the code): if the current column is either the male or the female column, we set the column to be the total column. And if the current column is the year column we set it to be the male column, and if the current column is the total column we do nothing.
If the user presses the up arrow, we set the current row to be one less than the current row—or do nothing if they are already on the first row. And similarly, if the user presses the down arrow, we set the current row to be one more than the current row—or do nothing if they are already on the last row.
If the new selected row or column or both are different from the currently selected ones, we set the selected row and column. This will cause update() to be called on the view and the header, and will also ensure that the selected item is visible. We also emit a clicked() signal with the selected item's model index.
At the end, if we selected a new item, we must not call the base class implementation, since we have handled the key press ourselves and don't want it to go to the scroll area. This is because the scroll area handles the arrow keys itself, interpreting them as requests to scroll, which we don't want—or need—since we handle the scrolling ourselves. And conversely, if we didn't handle the key press, we call the base class implementation to handle it for us.
Compare this method with the mouse event handler where we set the row and column without having to ensure that the selected item is visible—since the user must have clicked it. But here, the user could be pressing, say, the down arrow, on the last visible row, so we must call QScrollArea::ensureVisible() (which is done by CensusVisualizer::setCurrentIndex(); 230 ) so that the view is scrolled appropriately.
Adding support for the Home, End, PageUp, and PageDown keys follows the same principles as the code used for the arrow keys, and is left as an exercise. (When implementing PageUp and PageDown, it is conventional to move up or down by the widget's visible height minus one line or row so that the user has one line of context by which they can orient themselves.)
The eventFilter(), mousePressEvent(), and keyPressEvent() methods that we have just reviewed provide the view's behavior. Now we will look at the paintEvent() and the private helper methods it uses to see how the view's appearance is rendered.
void CensusVisualizerView::paintEvent(QPaintEvent *event) { if (!visualizer->model()) return; QFontMetricsF fm(font()); const int RowHeight = fm.height() + ExtraHeight; const int MinY = qMax(0, event->rect().y() - RowHeight); const int MaxY = MinY + event->rect().height() + RowHeight; QPainter painter(this); painter.setRenderHints(QPainter::Antialiasing| QPainter::TextAntialiasing); int row = MinY / RowHeight; int y = row * RowHeight; for (; row < visualizer->model()->rowCount(); ++row) { paintRow(&painter, row, y, RowHeight); y += RowHeight; if (y > MaxY) break; } }
This method begins by computing some constants, in particular, the height to allow for each row, and the paint event's minimum and maximum y-coordinates, minus or plus one row's height to ensure that even if only a portion of a row is visible, it is still painted.
Since the widget is inside a QScrollArea and its height is always precisely that needed to show all the items, we do not need to compute any offsets or work out for ourselves what is visible and what isn't. However, for the sake of efficiency, we should paint only visible items.
The paint event that is passed in has a QRect that specifies the rectangle that needs repainting. For small widgets we often ignore this rectangle and just repaint the whole thing, but for a model-visualizing widget that could have large amounts of data we want to be more efficient and only paint what needs painting. So with the constants in place, we set up the painter and calculate the first row that needs painting, and that row's y-coordinate. (It may be tempting to initialize y with y = MinY; but MinY is not usually the same as row * RowHeight because of the—desired—integer truncation that occurs in the MinY / RowHeight expression.)
With everything in place, we iterate through the model's rows, starting at the first one that is visible, and painting each one until the y-coordinate takes us beyond the rectangle that needs repainting, at which point we stop. This ensures that we retrieve and paint at most the rows that are visible plus two extra rows, which could be a considerable savings if the model has thousands or tens of thousands of rows or more.
void CensusVisualizerView::paintRow(QPainter *painter, int row, int y, const int RowHeight) { paintYear(painter, row, QRect(0, y, visualizer->widthOfYearColumn(), RowHeight)); paintMaleFemale(painter, row, QRect(visualizer->widthOfYearColumn(), y, visualizer->widthOfMaleFemaleColumn(), RowHeight)); paintTotal(painter, row, QRect(visualizer->widthOfYearColumn() + visualizer->widthOfMaleFemaleColumn(), y, visualizer->widthOfTotalColumn(), RowHeight)); }
This method is used simply to create a suitable rectangle and call a column-specific paint method for each column.
void CensusVisualizerView::paintYear(QPainter *painter, int row, const QRect &rect) { paintItemBackground(painter, rect, row == visualizer->selectedRow() && visualizer->selectedColumn() == Year); painter->drawText(rect, visualizer->model()->data( visualizer->model()->index(row, Year)).toString(), QTextOption(Qt::AlignCenter)); }
Once the background is painted, all that remains is for the item's text to be drawn. The text is retrieved from the model and painted centered in its column.
The CensusVisualizerView::paintTotal() method is very similar to this one (so we don't show it), with the only difference being that we right-align the total.
void CensusVisualizerView::paintItemBackground(QPainter *painter, const QRect &rect, bool selected) { painter->fillRect(rect, selected ? palette().highlight() : palette().base()); visualizer->paintItemBorder(painter, palette(), rect); painter->setPen(selected ? palette().highlightedText().color() : palette().windowText().color()); }
Which background and foreground colors to use depends on whether the item is selected. This method paints the background and the border and sets the pen color ready for the caller to paint its text.
The paintMaleFemale() method is slightly longer so we will review it in three parts.
void CensusVisualizerView::paintMaleFemale(QPainter *painter, int row, const QRect &rect) { QRect rectangle(rect); QLocale locale; int males = locale.toInt(visualizer->model()->data( visualizer->model()->index(row, Males)).toString()); int females = locale.toInt(visualizer->model()->data( visualizer->model()->index(row, Females)).toString()); qreal total = males + females; int offset = qRound( ((1 - (total / visualizer->maximumPopulation())) / 2) * rectangle.width());
We begin by finding out how many males and females there are and the total they sum to. (We discussed the use of QLocale to get numbers from localized strings earlier; 229 .) Then we compute how much width the complete colored bar should occupy and use that to work out the offset by which the bar must be indented from the left and from the right to make the bar the right size within the available rectangle.
painter->fillRect(rectangle, (row == visualizer->selectedRow() && (visualizer->selectedColumn() == Females || visualizer->selectedColumn() == Males)) ? palette().highlight() : palette().base());
The first thing we paint is the background, with the color determined by whether the males or females column is selected.
visualizer->paintItemBorder(painter, palette(), rectangle); rectangle.setLeft(rectangle.left() + offset); rectangle.setRight(rectangle.right() - offset); int rectY = rectangle.center().y(); painter->fillRect(rectangle.adjusted(0, 1, 0, -1), maleFemaleGradient(rectangle.left(), rectY, rectangle.right(), rectY, males / total)); }
Toward the end, we paint the item's border and then resize the available rectangle—potentially making it smaller—so that it has the correct size and position to serve as the rectangle for drawing the colored bar. Finally, we draw the bar—with a tiny reduction in height—using a gradient fill which goes from dark green to light green (left to right) for the male part, and from light red to dark red (left to right) for the female part.
QLinearGradient CensusVisualizerView::maleFemaleGradient( qreal x1, qreal y1, qreal x2, qreal y2, qreal crossOver) { QLinearGradient gradient(x1, y1, x2, y2); QColor maleColor = Qt::green; QColor femaleColor = Qt::red; gradient.setColorAt(0, maleColor.darker()); gradient.setColorAt(crossOver - 0.001, maleColor.lighter()); gradient.setColorAt(crossOver + 0.001, femaleColor.lighter()); gradient.setColorAt(1, femaleColor.darker()); return gradient; }
This method is shown for completeness. It creates a linear gradient that goes from dark to light in one color and then from light to dark in another color with a crossover between the colors at the specified position. The crossover point is computed by the caller as males / total; this ensures that the widths of the male and female parts are in correct proportion to their populations.
Qt also has QConicalGradient and QRadialGradient classes with similar APIs.
We have now finished the CensusVisualizer class and its aggregated CensusVisualizerHeader and CensusVisualizerView classes that do so much of the work. Creating custom classes like this is ideal when we have a model that we want to visualize in a unique way and where items are shown combined in some way so that using a custom delegate or a custom view based on the QAbstractItemView API is not sufficient.
We have now completed our review of the TiledListView class, and of the CensusVisualizer class. The TiledListView is much shorter because it didn't have to show any column captions and because it could rely on the base class for some of its functionality. If we want to present model data in unique ways, for example, graphically, or if we want to present some model items combined, then a custom delegate is insufficient and we must use a custom view. If we take the approach used for the CensusVisualizer class, we get complete control, and only have to implement the features that we actually need. However, if we choose to create a QAbstractItemView subclass, we still get complete control, we get some functionality for free, and we get much more potential for reuse—but we are obliged to reimplement all the pure virtual methods, and in general at least those methods listed in Table 6.1 (210 ).
This chapter is the last of the four chapters dedicated to Qt's model/view architecture. In general, it is easiest to start by using a QStandardItemModel, subclassing it (or QStandardItem) to make the data serializable and deserializable. Later on, if the need arises, a custom model can always be used as a drop-in replacement. Similarly, using one of Qt's standard views is the best way to start viewing model data, and if the need for customizing the appearance or editing of items is required, it is best—and easiest—to use custom delegates. However, if no combination of standard view and custom delegate can visualize the data in the desired way, then we must create a custom view using one of the approaches shown in this chapter.