4

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.
Steve Lorimer
  • 27,059
  • 17
  • 118
  • 213
  • 1
    It's certainly possible by having extra elements in the tree, and not using a header view. You might do it by a viewmodel proxy - a proxy that's used only for the purposes of viewing the data the way you want it viewed. – Kuba hasn't forgotten Monica Sep 23 '15 at 23:12
  • Wouldn't extra elements be shown as additional rows rather than part of the already existing rows? – Steve Lorimer Sep 23 '15 at 23:21
  • I don't know anything about view model proxy, I'll read up on that. Thanks for the suggestion – Steve Lorimer Sep 23 '15 at 23:21
  • There is no Qt documentation on that per se. The viewmodel is an abstract concept, with multiple possible implementations, and even multiple classes of implementations. One is when the viewmodel provides data to lay out controls. Another is when the viewmodel adapts the data from another model for a particular display scenario. – Kuba hasn't forgotten Monica Sep 24 '15 at 12:26
  • Thanks for the suggestion - I have pretty much managed exactly what I was looking for - kudos to you! I've added an answer with the details of putting your suggestions to work, for posterity – Steve Lorimer Sep 24 '15 at 15:58

1 Answers1

5

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

Community
  • 1
  • 1
Steve Lorimer
  • 27,059
  • 17
  • 118
  • 213