Designing Delegates

Материал из Wiki.crossplatform.ru

Перейти к: навигация, поиск
Image:qt-logo_new.png Image:qq-title-article.png
Qt Quarterly | Выпуск 24 | Документация


Написал Johan Thelin.
Одной из наиболее интересных особенностей 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.


An Expanding Tree

Итак, наша цель - создание элемента заголовка, по которому можно щелкнуть. Когда по нему щелкаем, он раскроется, для того чтобы показать набор подэлементов. Когда по ним щелкаем, эти элементы могут выводить информацию или выполнять действие. Наконец, группируя эти элементы в логической последовательности, мы будем учитывать пустые пространства (whitespace) (или пробелы) в дереве.

center

Мы используем два класса: делегат (delegate) для обеспечения визуализации и помощник (helper) для обеспечения функциональности. До этого, нам необходимо создать QTreeView и подготовить для него модель. Здесь исходный код показывает как заполняется группа элементов.

    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);
      }
      ...

Вся модель основывается на QStandardItemModel. Она позволяет нам заполнить модель и установить различные роли данных для каждого элемента. Поскольку группа показана на верхнем уровне, родитель первого элемента в моделе - invisibleRootItem. Остальные элементы в дереве являются его детьми.

Каждому элементу в конструкторе QStandardItem() назначается DisplayItem. Затем устанавливаются DecorationRole и BackgroundColorRole, и далее следуют пользовательские TypeRole и SignalValueRole. Позднее две роли придут из нашего класса помощника, ExpHelper.

Класс Помощник

TypeRole может принимать значение либо Spacer, либо Item. Каждое из них будет обрабатываться нашим делегатом. SignalValueRole позволяет нам установить значение, формируемое сигналом, когда по элементу щелкают. Как показано далее, и роли, и типы определены как перечисления (enumerations) в ExpHelper.

    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;
    };

Класс помощник предназначен для того, чтобы сделать поведение данного QTreeView подобным раскрывающемуся дереву. Конструктор принимает QTreeView как родителя. Мы определяем перечисления для типов и ролей, подобно сигналу itemClicked(int). Аргумент integer, передавемый данному сигналу, является значением принятым SignalValueRole во время создания элемента. Завершающая private часть класса включает слот, соответствующий сигналу itemClicked(int) и указатель на представление (view), которому помощник принадлежит. Для того, чтобы увидеть как они вписываются в картину, рассмотрим конструктор:

    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:

  • The indentation property of the view is set to zero. This means that child items will not be indented, thus appearing right below their parent.
  • The rootIsDecorated property is set to false to prevent any unwanted decorations from being shown.
  • The header of the view is hidden.

The last thing that happens in the constructor is that the view's clicked( QModelIndex) signal is connected to the private itemClicked( QModelIndex) slot of the helper, shown here:

    void ExpHelper::itemClicked(const QModelIndex &amp;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.

The Appearance 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 &amp;option,
                 const QModelIndex &amp;index) const;
      QSize sizeHint(const QStyleOptionViewItem &amp;option,
                     const QModelIndex &amp;index) const;
 
    private:
      bool hasParent(const QModelIndex &amp;index) const;
      bool isLast(const QModelIndex &amp;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 &amp;index)
      const
    {
      if (index.parent().isValid())
        return true;
 
      return false;
    }
 
    bool ExpDelegate::isLast(const QModelIndex &amp;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 &amp;option,
      const QModelIndex &amp;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 &amp;option,
      const QModelIndex &amp;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.

Knowing Too Much

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.

center

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 &amp;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 &amp;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 &amp;option,
      const QModelIndex &amp;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)
    }

Making it Exclusive and Other Tricks

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.

Ссылки