Одной из наиболее интересных особенностей Qt 4 является структура модель/представление. Она обеспечивает разработчика большой гибкость при работе с элементами отображения списков, таблиц и деревьев. Хотя возможно создание особым образом пользовательских представлений и моделей для представления данных, в этой статье уделено внимание более удобному подходу, использовнию пользовательских делегатов.
When looking at modern user interfaces, it is common to notice expanding lists. In the Windows world, we usually find task boxes to the left in the File Explorer as well as the list for removing installed programs in the Control Panel. You can shrink the boxes in the Explorer by clicking the titles, while items in the Control Panel list are automatically expanded as they are selected.
These widgets are often described as lists because that is how the user experiences them. However, when discussing the problem with Host Mobility in Gothenburg, I realized that tree is a more appropriate term for them. Looking at what actually takes place—an item is expanded to reveal more items—this makes perfect sense, and provides the basis for an implementation.
The reason for we rely on a tree instead of simply implementing a resizable delegate is due to the model/view architecture of Qt. Delegates have no means of telling the view that their sizes have changed when their states change. Such a method would mean that each view would have to query all its items, including those not shown, for their size; this would reduce many of the advantages of using the model/view approach.
So, our goal is to create a header item that can be clicked. When clicked, it will expand to show a number of sub-items. These items can display information or perform an action when clicked. Finally, to group these items in a logical way, we will allow for whitespace (or blank) items in the tree.
We use two classes: a delegate for the visuals and a helper for the functionality. Prior to that, we need to create a QTreeView and provide it with a model. The source code here shows how a group of items is populated.
void TreeDialog::populateTree() { QStandardItemModel *model = new QStandardItemModel(); QStandardItem *parentItem = model->invisibleRootItem(); for (int i = 0; i < 4; ++i) { QStandardItem *item = new QStandardItem( QString("item %0").arg(i)); parentItem->appendRow(item); if (i == 0) parentItem = item; else { item->setData( QImage(":/trolltech/styles/commonstyle/" "images/viewlist-16.png"), Qt::DecorationRole); } item->setData(QColor(Qt::green), Qt::BackgroundColorRole); item->setData(ExpHelper::Item, ExpHelper::TypeRole); item->setData(i, ExpHelper::SignalValueRole); } ...
The whole model is based on QStandardItemModel. It allows us to populate the model and set a different data role for each item. As the group is shown at the top level, the parent of the first item is the invisibleRootItem of the model. The other tree items are children of the first item.
Each item is given a DisplayItem through the QStandardItem() constructor. Then the DecorationRole and a BackgroundColorRole are set, followed by the custom TypeRole and SignalValueRole. The latter two roles come from our helper class, ExpHelper.
The TypeRole can either be a Spacer or an Item, both of which will be handled by our delegate. The SignalValueRole allows us to set the value that is provided with a signal when an item is clicked. Both the roles and the types are defined as enumerations in ExpHelper as shown below.
class ExpHelper : public QObject { Q_OBJECT public: ExpHelper(QTreeView *parent); enum ItemType{Spacer, Item}; enum ItemRole{TypeRole = Qt::UserRole + 1000, SignalValueRole = Qt::UserRole + 1001}; signals: void itemClicked(int); private slots: void itemClicked(const QModelIndex &index); private: QPointer<QTreeView> viewPtr; };
The role of the helper class is to make a given QTreeView behave like an expanding tree. The constructor accepts a QTreeView as its parent; we define enumerations for the types and the roles, as well as the itemClicked(int) signal. The integer passed as an argument to this signal is the value given as the SignalValueRole when creating the item.
The end of the class is part of the private implementation: a slot corresponding to the itemClicked(int) signal and a pointer to the view which the helper belongs to. To see how these fit into the picture, have a look at the constructor implementation:
ExpHelper::ExpHelper(QTreeView *parent) : QObject(parent), viewPtr(parent) { if (!viewPtr) return; viewPtr->setIndentation(0); viewPtr->setRootIsDecorated(false); viewPtr->header()->hide(); connect(viewPtr, SIGNAL(clicked(QModelIndex)), this, SLOT(itemClicked(QModelIndex))); }
The viewPtr is initialized from the parent; if the helper has no parent, we return from the constructor. Notice that the parent variable does not have a default value—that approach would not make sense because the helper relies on having a view to work with. We use a QPointer instead of a regular pointer because QPointer keeps track of the object being pointed to. If the view is deleted and the helper survives for some reason, the helper can easily detect that the view is gone. Given a view to work with, three things are done:
void ExpHelper::itemClicked(const QModelIndex &index) { if (!viewPtr) return; if (index.parent().isValid()) { if (index.model()->data(index, SignalValueRole).isValid()) emit itemClicked(index.model()->data(index, SignalValueRole).toInt()); } else { if (viewPtr->isExpanded(index)) viewPtr->setExpanded(index, false); else { viewPtr->setExpanded(index, true); int childIndex = -1; while (index.child(childIndex+1, 0).isValid()) childIndex++; if (childIndex != -1) viewPtr->scrollTo(index.child(childIndex, 0)); } } }
The slot implementation is similar to the constructor—we return immediately unless we have a view to work with. The slot works in two different ways. If the clicked item has a parent and a valid SignalValueRole, the itemClicked(int) signal is emitted. If the item has no parent, its expanded state is toggled. When the item is expanded, the view shows the last of the children, ensuring that all items of the newly expanded group can be seen.
Let's have a look at the dialog using the helper. We have already looked at a part of the populateTree() member function of the TreeDialog class. Below you can see the where that method is called from the constructor.
TreeDialog::TreeDialog(QWidget *parent) : QWidget(parent) { ui.setupUi(this); populateTree(); ExpDelegate *delegate = new ExpDelegate(this); ui.treeView->setItemDelegate(delegate); ExpHelper *helper = new ExpHelper(ui.treeView); connect(helper, SIGNAL(itemClicked(int)), this, SLOT(itemClicked(int))); }
The TreeDialog class uses a dialog design created using Qt Designer. This design is first set up and the model populated before we start working with the tree view. We create and set up an item delegate and the helper. Finally the itemClicked(int) signal from the helper is connected to a slot of the same name in TreeDialog. The slot implements actions taken when a specific item is clicked.
Stepping back, we can see that the helper not only provides the functionality but also the appearance, in the form of a delegate.
A delegate can be used to visualize and provide editing capabilities for a model item. In this case, we use it to draw items and provide a size hint.
The class declaration of our delegate, ExpDelegate, is shown below. It consists of three parts: a constructor, the necessary methods and two private convenience methods.
class ExpDelegate : public QAbstractItemDelegate { Q_OBJECT public: ExpDelegate(QObject *parent = 0); void paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const; QSize sizeHint(const QStyleOptionViewItem &option, const QModelIndex &index) const; private: bool hasParent(const QModelIndex &index) const; bool isLast(const QModelIndex &index) const; };
The convenience methods come in handy when implementing all types of delegates. They provide the means to tell if a given item has a parent or if it is the last item among its siblings. This implementation, however simple, is shown next:
bool ExpDelegate::hasParent(const QModelIndex &index) const { if (index.parent().isValid()) return true; return false; } bool ExpDelegate::isLast(const QModelIndex &index) const { if (index.parent().isValid()) if (!index.parent().child(index.row()+1, index.column()).isValid()) return true; return false; }
The constructor of the delegate is empty—it only initializes the base class. The next interesting piece of code is found in the sizeHint() and paint functions. The sizeHint() function tells Spacer items apart from non-Spacer items. Spacers are used to divide items into groups and are small, while non-Spacer items are hinted to be 30 pixels high.
QSize ExpDelegate::sizeHint( const QStyleOptionViewItem &option, const QModelIndex &index) const { if (index.model()->data(index, ExpHelper::TypeRole) == ExpHelper::Spacer) return QSize(10, 10); else return QSize(100, 30); }
The paint method divides the items into four groups: spacers, top items, middle items and bottom items. Looking back at the screenshot at the beginning of this article, you can easily tell them apart from the way that their backgrounds are drawn. This is done using the TypeRole and convenience methods.
void ExpDelegate::paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const { if (index.model()->data(index, ExpHelper::TypeRole) == ExpHelper::Spacer) { // No need to draw spacer items return; } // Setup pens and colors if (!hasParent(index)) { // Paint the top-item } else if (isLast(index)) { // Paint the bottom item } else { // Paint middle items } // Draw common parts here (decoration and text) }
As the background is cleared to white before the paint method is called, we simply do nothing if a Spacer item is encountered. For the other types of items, the backgrounds are painted differently before the text and decoration are drawn by a common piece of code. These backgrounds are painted using a gradient brush and painter paths. Please refer to the downloadable source code that accompanies this article for the details.
One drawback with the solution that we have so far is that the delegate cannot tell whether the item is expanded or not. This limitation exists because the same delegate can be used with multiple views at once, and the state may vary in different views. If we introduce a limitation that the delegate can only be used with one view at a time, the viewPtr solution from the helper class can be used here as well.
This is implemented in the AdvExpDelegate class, which is the ExpDelegate class with a few changes. Let's start by looking at the interesting lines of the class declaration.
class AdvExpDelegate : public QAbstractItemDelegate { Q_OBJECT public: AdvExpDelegate(QTreeView *parent); ... private: ... bool isExpanded(const QModelIndex &index) const; QPointer<QTreeView> viewPtr; };
Firstly, we require the parent to be a QTreeView and there is no default parent value. Secondly, there is a private QPointer keeping track of the view used—this allows us to add the isExpanded() convenience method. The first implementation change is that the viewPtr is intialized by the constructor:
AdvExpDelegate::AdvExpDelegate(QTreeView *parent) : QAbstractItemDelegate(parent), viewPtr(parent) { }
The isExpanded() method is implemented using the viewPtr:
bool AdvExpDelegate::isExpanded(const QModelIndex &index) const { if (!viewPtr) return false; return viewPtr->isExpanded(index); }
The method is used in the paint() function for top-level items:
void AdvExpDelegate::paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const { if (!viewPtr) return; if (index.model()->data(index, ExpHelper::TypeRole) == ExpHelper::Spacer ) { // No need to draw spacer items return; } // Setup pens and colors if (!hasParent(index)) { // Paint the top-item if (isExpanded(index)) { // Expanded } else { // Closed } } else if (isLast(index)) { // Paint the bottom item } else { // Paint middle items } // Draw common parts here (decoration and text) }
Now that I have shown you how to use the delegate, I would also like to show you a trick you can play with it. Just by adding the following two lines of code to the itemClicked(QModelIndex), before the current item is expanded, you can make sure that only one group of items is expanded at once.
if (index.model()->data(index, TypeRole) != Spacer) viewPtr->collapseAll();
Note that if you call collapseAll() for a Spacer, you will make the items collapse. This could happen if the user misses an item and clicks on white space instead.
I'm sure that you can find numerous other tricks for both the delegate and the helper. For example, items that expand with a timer, expandable sub-items, and so on. Feel free to download the source code for this article and play around with the examples.
Обсудить...