13.2 Qt Models and Views
Qt includes a model/view framework that maintains separation between the way data is organized and managed and the way that it is presented to the user. Classes for the three most common types of views (lists, trees, and tables) are provided. In addition, there are abstract and concrete data models that can be extended and customized to hold different kinds of data. It is not unusual for an application to have a model that can be viewed in several different ways simultaneously.
Views are objects for acquiring, changing, and displaying the data. Figure 13.3 shows four kinds of view types in the Qt model-view framework.
Figure 13.3 Qt View Classes
QAbstractItemModel defines the standard interface that enables views (and delegates) to access data. Item models store the actual data that is to be viewed and manipulated (e.g., sorted, edited, stored, retrieved, transmitted, and so on). Using signals and slots, they notify all associated views of changes to the data. Each view object holds a pointer to a model object. View objects make frequent calls to item model methods to get or set data or to do various other operations. Figure 13.4 shows the model classes that are designed to work closely with the various view classes.
Figure 13.4 Qt Model Classes
Selection models are objects that describe which items in the model are selected in the view. Each view has a pointer to a selection model. QModelIndex acts like a cursor, or a smart pointer, providing a uniform way to iterate through list, tree, or table items inside the model.
After setModel() is called, the view should automatically update itself whenever the model changes (assuming the model is written properly).
There are two approaches to implementing the data model. Each has advantages.
- Implement the passive interface of a QAbstractItemModel, including the data representation.
- Reuse a general-purpose concrete data model, such as QStandardItemModel, and fill in the data.
The passive interface offers more flexibility in implementation. It is possible to use data structures that are optimized for specific access patterns or data distributions.
The second approach, reusing the QStandardItem (Model) classes, makes it possible to write tree/item code in a style similar to that used by QListWidget, QTableWidget, and QTreeWidget.
Views
QAbstractItemView provides an interface for the common features of the three different model types:
- Lists in various arrangements
- Tables, perhaps with interactive elements
- Trees representing objects in a parent-child hierarchy
Model Index
Each piece of data in a model is represented by a model index. Model indexes give views and delegates indirect access to data items in the model without relying on knowledge of the underlying structure of the data. Only the model needs to know how to directly access its data. The QModelIndex class provides an interface for indexing and accessing data in models derived from QAbstractItemModel. It works equally well for lists, tables, and trees. Each index has a pointer to the model that created it and may have a parent index, in case of hierarchically structured data (e.g., in a tree). QModelIndex treats model data as if it were arranged in a rectangular array with row and column indices, regardless of what underlying data structure actually holds the data.
QModelIndex objects, created by the model, can be used by model, view, or delegate code to locate particular items in the data model. QModelIndex objects have short life spans and can become invalid shortly after being created, so they should be used immediately and then discarded.
QModelIndex::isValid() should be called before using a QModelIndex object that has existed for more than a few operations. QPersistentModelIndex objects have longer life spans but still should be checked with isValid() before being used.
13.2.1 QFileSystemModel
The QFileSystemModel can be viewed as a list, table, or tree. Figure 13.5 shows one in a QTreeView.
QFileSystemModel is already populated with data, so we can simply create one, create a view, and view->setModel(model). Example 13.1 shows what may be the simplest Qt model-view example.
Figure 13.5 QFileSystemModel in a QTreeView
Example 13.1. src/modelview/filesystem/main.cpp
#include <QtGui>
int main(int argc, char *argv[]) {
QApplication app(argc, argv);
QFileSystemModel model;
model.setRootPath("/");
QTreeView tree;
tree.setModel(&model);
tree.setSortingEnabled(true); 1
tree.header()->setResizeMode(QHeaderView::ResizeToContents);
tree.resize(640, 480);
tree.show();
return app.exec();
}
|
Enable HeaderView sort buttons. |
By setting the resizeMode in the headerView, the columns of the table or tree will be resized whenever the window is. These classes are the basic building blocks for writing a file browser widget.
13.2.2 Multiple Views
Example 13.2 is a function that creates a QStandardItemModel that can be viewed as a tree, a table, or a simple list. This function also demonstrates the use of QStandardItem.
Example 13.2. src/modelview/multiview/createModel.cpp
#include <QtGui>
QStandardItemModel* createModel(QObject* parent, int rows,
int cols, int childNodes) {
QStandardItemModel*
model = new QStandardItemModel(rows, cols, parent);
for( int r=0; r<rows; r++ )
for( int c=0; c<cols; c++) {
QStandardItem* item = new QStandardItem(
QString("Row:%0, Column:%1").arg(r).arg(c) );
if( c == 0 ) 1
for( int i=0; i<childNodes; i++ ) {
QStandardItem* child = new QStandardItem(
QString("Item %0").arg(i) );
item->appendRow( child );
}
model->setItem(r, c, item);
}
model->setHorizontalHeaderItem( 0, new QStandardItem( "Name" ));
model->setHorizontalHeaderItem( 1, new QStandardItem( "Value" ) );
return model;
}
|
Add child nodes to elements in the first column |
The main program, shown in Example 13.3, creates four different views: QListView, QTableView, QTreeView, and QColumnView. Notice that the QTableView and QListView do not display child nodes. In addition, the QColumnView and QListView do not display the table model's columns beyond column 0. The QColumnView displays tree children of selected nodes in the column to the right, similar to the way that the MacOS X Finder displays files in a selected folder.
All views share the same model, so edits in a cell are immediately visible in the other views. In addition, all views share a selection model so that selections happen simultaneously in all four views.
Example 13.3. src/modelview/multiview/multiview.cpp
[ . . . . ] #include "createModel.h" int main( int argc, char** argv ) { QApplication app( argc, argv ); QStandardItemModel* model = createModel(&app); QSplitter vsplitter(Qt::Vertical); QSplitter hsplitter;1
QListView list; QTableView table; QTreeView tree; QColumnView columnView; [ . . . . ] list.setModel( model ); table.setModel( model ); tree.setModel( model );2
columnView.setModel (model); [ . . . . ] list.setSelectionModel( tree.selectionModel() ); table.setSelectionModel( tree.selectionModel() );3
columnView.setSelectionModel (tree.selectionModel()); table.setSelectionBehavior( QAbstractItemView::SelectRows ); table.setSelectionMode( QAbstractItemView::SingleSelection );
|
By default, children lay out horizontally. |
|
Share the same model. |
|
Common selection model. |
When you run this code, you should see a window similar to Figure 13.6. Notice that selecting an item from one of the views causes selection to occur in the others. Because we use the concrete QStandardItemModel, items are editable from any of the views. Furthermore, changes from any view are automatically propagated to the other views, thus ensuring that each view is consistent with the model.
You can trigger an edit via the F2 key, a double-click, or by simply entering the cell, depending on how QAbstractItemView::EditTriggers has been set. You might notice that other keyboard shortcuts (cut, copy, paste, ctrl+cursor keys, and so on) familiar from your native window environment will also work inside these views and text areas.
Figure 13.6 Multiple Views, One Model
For this application, we used QSplitter widgets. A QSplitter has some of the features of a layout, but the widgets that it manages are its children. Splitters permit the user to resize the subspaces that contain child widgets at runtime by dragging the boundary between them. Example 13.4 includes the code that sets up the splitter widgets.
Example 13.4. src/modelview/multiview/multiview.cpp
[ . . . . ] hsplitter.addWidget( &list ); hsplitter.addWidget( &table ); vsplitter.addWidget( &hsplitter ); vsplitter.addWidget ( &tree ); vsplitter.addWidget ( &columnView ); vsplitter.setGeometry(300, 300, 500, 500); vsplitter.setWindowTitle("Multiple Views - Editable Model");
13.2.3 Delegate Classes
The delegate, shown in Figure 13.7, provides another level of indirection between the model and view, which increases the possibilities for customization.
Figure 13.7 Model, View, and Delegate
A delegate class, usually derived from QAbstractItemDelegate, adds several kinds of controller features to the Qt model-view framework. A delegate class can provide a factory method that enables view classes to create editors and virtual getters and setters for moving editor data to and from the model. It can also provide a virtual paint() method for custom display of items in the view. Delegates can be set up for an entire QAbstractItemView or for just one of its columns.
Figure 13.8 shows a modified version of the StarDelegate example from $QTDIR/examples/modelview/stardelegate.
Figure 13.8 Star Delegates
In Example 13.5, we extended from QStyledItemDelegate. This is a concrete class that is used by default in Q(List|Table|Tree)Views. It provides a QLineEdit for editing QString properties, and other appropriate editors for the native style, when the type is a boolean, QDate, QTime, int, or double. The custom delegate shows the virtual methods one must override to get "stars" instead of a simple integer in the table for a custom StarRating value.
Example 13.5. src/modelview/playlists/stardelegate.h
#ifndef STARDELEGATE_H #define STARDELEGATE_H #include <QStyledItemDelegate> #include <QStyleOptionViewItem> class StarDelegate : public QStyledItemDelegate { Q_OBJECT public: typedef QStyledItemDelegate SUPER; StarDelegate(QObject* parent=0) : SUPER(parent) {}; QWidget* createEditor(QWidget* parent, const QStyleOptionViewItem& option, const QModelIndex& index) const; void paint(QPainter* painter, const QStyleOptionViewItem& option, const QModelIndex& index) const; void setEditorData(QWidget* editor, const QModelIndex& index) const; void setModelData(QWidget* editor, QAbstractItemModel* model, const QModelIndex& index) const; }; #endif // STARDELEGATE_H
Delegates give full control over how the item in a view appears when displayed, via the paint() method, overridden in Example 13.6.
Example 13.6. src/modelview/playlists/stardelegate.cpp
[ . . . . ] void StarDelegate:: paint(QPainter* painter, const QStyleOptionViewItem& option, const QModelIndex& index) const { QString field = index.model()->headerData(index.column(), Qt::Horizontal).toString(); if (field == "length") { QVariant var = index.data(Qt::DisplayRole); Q_ASSERT(var.canConvert(QVariant::Time)); QTime time = var.toTime(); QString str = time.toString("m:ss"); painter->drawText(option.rect, str, QTextOption()); // can't use drawDisplay with QStyledItemDelegate: // drawDisplay(painter, option, option.rect, str); return; } if (field != "rating") { SUPER::paint(painter, option, index); return; } QVariant variantData = index.data(Qt::DisplayRole); StarRating starRating = variantData.value<StarRating>(); if (option.state & QStyle::State_Selected) painter->fillRect(option.rect, option.palette.highlight()); starRating.paint(painter, option.rect, option.palette, StarRating::ReadOnly); }
In addition, delegates can determine what kind of widget shows up when the user triggers editing of an item. For this, you need to either override createEditor(), as shown in Example 13.7, or supply a custom QItemEditorFactory.
Example 13.7. src/modelview/playlists/stardelegate.cpp
[ . . . . ] QWidget* StarDelegate:: createEditor(QWidget* parent, const QStyleOptionViewItem& option, const QModelIndex& index) const { QString field = index.model()->headerData(index.column(), Qt::Horizontal).toString(); if (field == "rating") { return new StarEditor(parent); } if (field == "length") { return new TimeDisplay(parent); } return SUPER::createEditor(parent, option, index); }v
When you trigger an edit request, you want to see an editor that initially has the value from the model for you to see or change. This is done by setEditorData(), as shown in Example 13.8.
Example 13.8. src/modelview/playlists/stardelegate.cpp
[ . . . . ] void StarDelegate:: setEditorData(QWidget* editor, const QModelIndex& index) const { QVariant val = index.data(Qt::EditRole); StarEditor* starEditor = qobject_cast<StarEditor*>(editor);1
if (starEditor != 0) { StarRating sr = qVariantValue<StarRating>(val);2
starEditor->setStarRating(sr); return; } TimeDisplay* timeDisplay = qobject_cast<TimeDisplay*>(editor);3
if (timeDisplay != 0) { QTime t = val.toTime(); timeDisplay->setTime(t); return; } SUPER::setEditorData(editor, index);4
return; }
|
Dynamic type checking. |
|
Extract user type value from QVariant. |
|
Dynamic type checking. |
|
Let base class handle other types. |
When the user finishes editing, setModelData(), shown in Example 13.9, is called to put the data back into the QAbstractItemModel.
Example 13.9. src/modelview/playlists/stardelegate.cpp
[ . . . . ] void StarDelegate:: setModelData(QWidget* editor, QAbstractItemModel* model, const QModelIndex& index) const { StarEditor* starEditor = qobject_cast<StarEditor*>(editor); if (starEditor != 0) { StarRating r = starEditor->starRating(); QVariant v; v.setValue<StarRating>(r); model->setData(index, v, Qt::EditRole); return; } TimeDisplay* td = qobject_cast<TimeDisplay*>(editor); if (td != 0) { QTime t = td->time(); model->setData(index, QVariant(t)); return; } SUPER::setModelData(editor, model, index); return; }