На практике, иногда бывает необходимость показывать в QComboBox древовидную структуру данных.

Стандартным компонентом в Qt для такой структуры данных является QTreeView, более того,
QComboBox умеет отображать этот компонент внутри себя, но как всегда, в документации существуют небольшие пробелы, ведь нужно не только отображать дерево, но и устанавливать текущим, выбранный пользователем элемент.

Давайте разберём как правильно это делать

Во первых, создадим сам компонент, который будет отображать данные, для этого наследуемся от QComboBox и наделяем его нужными нам свойствами.

Объявим в закрытой части класса переменную m_view класса QTreeView, которая будет отображать дерево в QComboBox, переопределим 2 функции, которые отвечают за поведение компонента, при раскрытии и закрытии:

  • void showPopup() override; — выполняется, когда пользователь раскрывает список
  • void hidePopup() override; — выполняется, когда пользователь выбрал элемент, кликнув по нему

Так же добавим функцию hideColumn(int n), которая будет скрывать нужные вам колонки в QTreeView, так как если ваша модель состоит из нескольких колонок, QComboBox покажет их все (стандартный компонент использует список), что будет выглядеть очень некрасиво

treecombobox.h

#ifndef TREECOMBOBOX_H
#define TREECOMBOBOX_H
#include <QtWidgets/QComboBox>
#include <QtWidgets/QTreeView>

class TreeComboBox final : public QComboBox
{
public:
    TreeComboBox();

    void showPopup() override;
    void hidePopup() override;

    void hideColumn(int n);
    void expandAll();
    void selectIndex(const QModelIndex &index);

private:
    QTreeView *m_view = nullptr;

};

treecombobox.cpp

TreeComboBox::TreeComboBox()
{
    m_view = new QTreeView;

    m_view->setFrameShape(QFrame::NoFrame);
    m_view->setEditTriggers(QTreeView::NoEditTriggers);
    m_view->setAlternatingRowColors(true);
    m_view->setSelectionBehavior(QTreeView::SelectRows);
    m_view->setRootIsDecorated(false);
    m_view->setWordWrap(true);
    m_view->setAllColumnsShowFocus(true);
    m_view->setItemsExpandable(false);
    setView(m_view);
    m_view->header()->setVisible(false);

}

void TreeComboBox::hideColumn(int n)
{
    m_view->hideColumn(n);
}

void TreeComboBox::expandAll()
{
    m_view->expandAll();
}

void TreeComboBox::selectIndex(const QModelIndex &index)
{
    setRootModelIndex(index.parent());
    setCurrentIndex(index.row());
    m_view->setCurrentIndex( index );
}

void TreeComboBox::showPopup()
{
    setRootModelIndex(QModelIndex());
    QComboBox::showPopup();
}

void TreeComboBox::hidePopup()
{
    setRootModelIndex(m_view->currentIndex().parent());
    setCurrentIndex(  m_view->currentIndex().row());
    QComboBox::hidePopup();
}

В конструкторе, мы устанавливаем у дерева нужный нам вид, чтобы оно выглядело «встроенным» в QComboBox, убираем заголовки, скрываем элементы раскрытия и устанавливаем его как элемент отображения.

Вся хитрость для правильной установки выбранного пользователем элемента в QComboBox, заключается в функциях showPopup() и hidePopup().

Так как QComboBox работает с «плоским» модельным представлением, он не может установить правильный индекс, выбранного пользователем элемента в древовидных моделях, так как они используют индекс относительно родительского элемента, для этого:
showPopup()

корневым элементом — мы устанавливаем корневым индексом недействительный индекс модели, чтобы QComboBox отобразил все элементы модели.
hidePopup()

корневым элементом — мы устанавливаем индекс родителя выбранного полльзователем элемента модели, а затем уже относительно родительского элемента, устанавливаем выбранный пользовательский элемент по индексу.

Используется это всё примерно так:

int main(int argc, char *argv[])
{
    QApplication a(argc, argv);
    QWidget w;


    QStandardItemModel model;
    QStandardItem *parentItem = model.invisibleRootItem();
    for (int i = 0; i < 4; ++i) {
        QStandardItem *item = new QStandardItem(QString("item %0").arg(i));
        parentItem->appendRow(item);
        parentItem = item;
    }

    TreeComboBox t;
    t.setModel(&model);
    t.expandAll();


    auto lay = new QVBoxLayout;
    lay->addWidget( &t);
    w.setLayout(lay);
    w.show();

    return a.exec();
}

Комментарии (10)


  1. Tantrido
    22.08.2018 20:41
    +1

    Хорошая статья, но… хотелось бы увидеть код showPopup() и hidePopup(), а также скриншот того, что получилось. Не понятно также как будут работать методы
    int currentIndex() const
    и
    void setCurrentIndex(int index)


    QCombobox-a, как они будут связаны с QModelIndex представления и модели?


    1. sborisov Автор
      23.08.2018 09:45

      Код функций приведён в листинге treecombobox.cpp

      Не понятно также как будут работать методы
      int currentIndex() const и void setCurrentIndex(int index)


      Они работают, но главная проблема в том, что QComboBox возвращает индекс строки относительно «корня», он не понимает древовидную модель, поэтому над setCurrentIndex, лучше сделать обёртку, которая будет заниматься установкой нужного индекса.
      void TreeComboBox::selectIndex(const QModelIndex &index)
      {
          setRootModelIndex(index.parent());
          setCurrentIndex(index.row());
          m_view->setCurrentIndex( index );
      }


      Получение данных из модели делается стандартными средствами модели, примерно так:
       
      auto id   = model->data(model->index(combobox->currentIndex(), 0 ,  combobox->view()->currentIndex().parent()),  Qt::DisplayRole).toInt();
      


      1. Tantrido
        23.08.2018 18:56

        Спасибо!


  1. GoldKeeper
    23.08.2018 03:58

    del


  1. Sazonov
    23.08.2018 10:18

    Спасибо, отлично получилось. Вы меня вдохновили, чтобы тоже что-нибудь про Qt написать из своего опыта :)


    1. sborisov Автор
      23.08.2018 10:27

      А сколько таится нюансов в древовидных моделях…
      Обычно я использовал QStandardItemModel, но понадобилась очень сложная и развесистая модель — написал свою. Стандартная Qt-шная — довольно медленная, и навязывает свою архитектуру, что может оказаться очень неудобным в определённых моментах, информации по ним тоже кот наплакал, если табличные модели расписаны в документации «от и до», то древовидные почти не раскрыты.
      Но там материала думаю на несколько статей хватит


      1. Sazonov
        23.08.2018 10:34

        Древовидные достаточно сложно унифицировать. Поэтому про них и мало написано. Тем более, что некоторые варианты использования сложно визуализировать, например, когда есть дети у ненулевого столбца и т.п.
        Главное — это понять что Qt-шная модель, это не модель данных в общепринятом значении, а, скорее часть логики представления. Лучше даже сказать — адаптер.


        1. sborisov Автор
          23.08.2018 10:42

          Пришлось собирать модельку из «частей» из документации и из книги Бланше, а также дописывать много своих методов


        1. GoldKeeper
          23.08.2018 13:02

          Поэтому про них и мало написано.

          Вот в этой книге довольно подробно и много.
          Qt. Профессиональное программирование. Разработка кроссплатформенных приложений на С++
          www.ozon.ru/context/detail/id/6364884


  1. 4Draive
    24.08.2018 10:32

    Возможность хорошая, но чем она лучше чем просто комбобокс с пробелами (и спецсимволом на свой вкус для стрелочки)? И не надо возиться с новым классом и пересчётами индекса.

    Заголовок спойлера
    image