Search code examples
c++qtqtreeviewqabstractitemmodel

QTreeView: hierarchical context for multi-level columns


I'm looking for a better way to display multi-level hierarchical data in a tree where the meaning of each column changes depending on the level in the tree.

I am using QTreeView and QAbstractItemModel to display my model data.

Each model row has a different number of columns and different column names depending on its level in the hierarchy.

In order to give context to the data displayed in the tree, I need to have column headers for each level in the hierarchy.

The problem is that QTreeView only has 1 set of column headers.

Current method

At the moment I'm changing the headers each time the selected row changes.

I do this by connecting to the tree view's selectionModel, and emitting a signal with the new QModelIndex each time the selection changes

void Window::slotOnSelectionChanged(const QItemSelection& new_selection, const QItemSelection& old_selection)
{
    QItemSelectionModel* selection_model = _view->selectionModel();
    QModelIndex index = selection_model->currentIndex();
    if (index.isValid())
        emit selectedIndexChanged(index);
}

In my model I connect to this signal, and when its fires, store the selected row, and force a column header update

void Model::slotOnSelectedIndexChanged(QModelIndex index)
{
    assert(index.isValid());
    _selected_row = modelRow(index);
    emit headerDataChanged(Qt::Horizontal, 0, _root->numColumns());
}

In the QAbstrateItemModel::headerData callback I then use selected_row to get the header for the currently selected row

QVariant Model::headerData(int i, Qt::Orientation orientation, int role) const
{
    if (role == Qt::DisplayRole)
    {
        switch (orientation)
        {
            case Qt::Horizontal:
                return QVariant(_selected_row->header(i));
            ...

Result

The result can be seen below - notice how the column headers change as the selected row changes.

enter image description here enter image description here enter image description here

Problem

It's not immediately obvious by just looking at the view what each datum is, and therefore the user is required to change rows in order to see what each column actually means.

What I'd like is to have some sort of embedded column header row, 1 per level in the hierarchy.

Something like this:

enter image description here

Questions

  • Is this possible?
  • If there is a better way to give context to the data in the tree, please do offer a suggestion.

Solution

  • At the suggestion of @Kuba Ober I added an extra row at position 0 in each hierarchy of the tree. It has no children.

    The model is then configured to special case for index.row() == 0, knowing that this row is a header row rather than a data row.

    eg: in Model::flags the header row is not editable

    Qt::ItemFlags Model::flags(const QModelIndex& index) const
    {
        Qt::ItemFlags item_flags = Qt::ItemIsEnabled | Qt::ItemIsSelectable;
    
        // header row is not editable
        if (index.row() != 0)
            item_flags |= Qt::ItemIsEditable;
    
        return item_flags;
    }
    

    I now return empty strings for headerData as the headers are in the tree itself

    QVariant Model::headerData(int i, Qt::Orientation orientation, int role) const
    {
        if (role == Qt::DisplayRole)
        {
            switch (orientation)
            {
                case Qt::Horizontal:
                    return QVariant(); // no column header, it's already in the tree
                ...
    

    I also change the background color for the header so it stands out

    QVariant Model::data(const QModelIndex& index, int role)  const
    {
        switch (role)
        {
            case Qt::BackgroundColorRole:
                if (index.row() == 0) // header row
                    return QColor(Qt::darkGray);
                break;
            ...
    

    The result is almost exactly what I was looking for

    enter image description here