Как реализовать QAbstractItemModel для уже существующей древовидной структуры данных без копирования? - PullRequest
1 голос
/ 04 февраля 2020

После прочтения документов и примеров QAbstractItemModel и QModelIndex я все еще не понимаю, как правильно реализовать модель для QTreeView.

Поскольку я хочу предоставить модель для существующей иерархической структуры данных, я избегаю использования QTreeWidget или QStandardItemModel и связанные с этим проблемы с дублированием данных и синхронизацией. Однако мне не удалось реализовать модель рабочего элемента (слишком много вопросов, публиковать мой код бесполезно).

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

Предположим, структура данных такая простая:

struct Property {
    QString name;
    QString value;
};
struct Node {
    QString name;
    QVector<Property> properties;
};
struct Track {
    int length;
    QString channel;
};
struct Model {
    QVector<Node> nodes;
    QVector<Track> tracks;
};

, где Model - это верхний уровень, и она напоминает дерево. Дерево, отображаемое в QTreeView, может выглядеть следующим образом:

Model
  ├─Nodes
  │  ├─Node "node1"
  │  │  └─Properties
  │  │     ├─Property property1 = value1
  │  │     └─Property property2 = value2
  │  └─Node "node2"
  │     └─Properties
  │        └─Property property1 = someValue
  └─Tracks
      ├─Track 1, ...
      ├─Track 2, ...
      └─Track 3, ...

Как следует реализовать подкласс QAbstractItemModel для доступа к существующим данным без копирования

1 Ответ

0 голосов
/ 06 февраля 2020

Вот мое решение проблемы.

Прежде всего, мое первоначальное предположение, что QModelIndex не в состоянии сохранить отношения родитель-ребенок, является правильным. На самом деле метод QModelIndex::parent просто вызывает QAbstractItemModel::parent, и задача реализации родительского метода остается за классом модели. Когда базовой моделью является правильное дерево, указатель на узлы дерева может быть сохранен в классе QModelIndex, но в моем случае мы имеем дело с «виртуальным» деревом, и эта связь недоступна. Таким образом, мы вынуждены ввести какое-то дополнительное хранилище, чтобы иметь возможность определить, где мы находимся в дереве. Если бы QModelIndex изначально поддерживало наличие указателя на родительский индекс, эта проблема была бы решена гораздо проще. Но поскольку QModelIndex является классом значений, у нас не может быть указатель на родительский элемент, а скорее мы должны хранить все родительские индексы внутри класса QModelIndex, и, возможно, разработчики Qt нашли какой-то хороший способ не делать этого. Поэтому я сохранил QVector<QModelIndex> в поле внутреннего указателя QModelIndex. Есть некоторые вещи, о которых нужно позаботиться, например, избегать выделения больше, чем необходимо, таких индексов, а также помнить об освобождении памяти, когда они больше не нужны (здесь мы не можем использовать иерархию QObject). Могут возникнуть дополнительные проблемы, которые нужно решить, когда модель предназначена для чтения и записи, но в этом случае я имею дело с моделью только для чтения.

Моя реализация следует. Методы rowCount и data определяют это виртуальное дерево c. Другие методы можно абстрагировать в классе, который можно использовать повторно.

class MyModel : public QAbstractItemModel
{
    Q_OBJECT

private:
    struct IndexData
    {
        QVector<QModelIndex> parents;
    };

public:
    explicit MyModel(QObject *parent = nullptr);
    ~MyModel();

    QVariant data(const QModelIndex &index, int role) const override;
    QModelIndex index(int row, int column, const QModelIndex &parent = QModelIndex()) const override;
    QModelIndex parent(const QModelIndex &index) const override;
protected:
    IndexData * indexData(const QModelIndex &index) const;
    QList<int> indexPath(const QModelIndex &index) const;
    QString indexString(const QModelIndex &index) const;
    QString indexString(int row, int column, const QModelIndex &parent) const;
public:
    int indexDepth(const QModelIndex &index) const;
    int rowCount(const QModelIndex &parent = QModelIndex()) const override;
    int columnCount(const QModelIndex &parent = QModelIndex()) const override;
    Qt::ItemFlags flags(const QModelIndex &index) const override;

private:
    QMap<QString, IndexData*> indexData_;
    Model model;
};

реализация:

MyModel::MyModel(QObject *parent)
    : QAbstractItemModel(parent)
{
    model.nodes.resize(2);
    model.nodes[0].name = "node1";
    model.nodes[0].properties.resize(2);
    model.nodes[0].properties[0].name = "property1";
    model.nodes[0].properties[0].value = "value1";
    model.nodes[0].properties[1].name = "property2";
    model.nodes[0].properties[1].value = "value2";
    model.nodes[1].name = "node2";
    model.nodes[1].properties.resize(1);
    model.nodes[1].properties[0].name = "property1";
    model.nodes[1].properties[0].value = "someValue";
    model.tracks.resize(3);
    model.tracks[0].length = 2;
    model.tracks[0].channel = "A";
    model.tracks[1].length = 4;
    model.tracks[1].channel = "B";
    model.tracks[2].length = 3;
    model.tracks[2].channel = "C";
}

MyModel::~MyModel()
{
    for(auto v : indexData_) delete v;
}

QVariant MyModel::data(const QModelIndex &index, int role) const
{
    if(!index.isValid() || role != Qt::DisplayRole) return {};

    int d = indexDepth(index);
    auto path = indexPath(index);

    if(d == 1) return "Model";
    if(d == 2 && path[0] == 0 && path[1] == 0) return "Nodes";
    if(d == 2 && path[0] == 0 && path[1] == 1) return "Tracks";
    if(d == 3 && path[0] == 0 && path[1] == 0) return QString("Node \"%1\"").arg(model.nodes[path[2]].name);
    if(d == 4 && path[0] == 0 && path[1] == 0) return "Properties";
    if(d == 5 && path[0] == 0 && path[1] == 0 && path[3] == 0) return QString("Property %1 = %2").arg(model.nodes[path[2]].properties[path[4]].name, model.nodes[path[2]].properties[path[4]].value);
    if(d == 3 && path[0] == 0 && path[1] == 1) return QString("Track %1...").arg(index.row() + 1);
    return {};
}

QModelIndex MyModel::index(int row, int column, const QModelIndex &parent) const
{
    QString dataKey = indexString(row, column, parent);
    auto it = indexData_.find(dataKey);
    IndexData *data;
    if(it == indexData_.end())
    {
        data = new IndexData;
        const_cast<MyModel*>(this)->indexData_.insert(dataKey, data);
        if(parent.isValid())
        {
            data->parents.append(parent);
            data->parents.append(indexData(parent)->parents);
        }
    }
    else
    {
        data = it.value();
    }
    return createIndex(row, column, data);
}

QModelIndex MyModel::parent(const QModelIndex &index) const
{
    if(!index.isValid()) return {};
    auto data = indexData(index);
    if(data->parents.empty()) return {};
    return data->parents.at(0);
}

MyModel::IndexData * MyModel::indexData(const QModelIndex &index) const
{
    if(!index.internalPointer()) return nullptr;
    return reinterpret_cast<IndexData*>(index.internalPointer());
}

QList<int> MyModel::indexPath(const QModelIndex &index) const
{
    QList<int> path;
    auto data = indexData(index);
    for(int i = data->parents.size() - 1; i >= 0; i--)
        path.push_back(data->parents[i].row());
    path.push_back(index.row());
    return path;
}

QString MyModel::indexString(const QModelIndex &index) const
{
    return indexString(index.row(), index.column(), index.parent());
}

QString MyModel::indexString(int row, int column, const QModelIndex &parent) const
{
    QString pre = parent.isValid() ? indexString(parent) + "." : "";
    return pre + QString("[%1,%2]").arg(row).arg(column);
}

int MyModel::indexDepth(const QModelIndex &index) const
{
    if(!index.isValid()) return 0;
    return 1 + indexDepth(index.parent());
}

int MyModel::rowCount(const QModelIndex &parent) const
{
    if(!parent.isValid()) return 1; // root item

    int d = indexDepth(parent);
    auto path = indexPath(parent);

    //if(d == 0) return 1; // root item
    if(d == 1) return 2;
    if(d == 2 && path[0] == 0 && path[1] == 0) return model.nodes.size();
    if(d == 2 && path[0] == 0 && path[1] == 1) return model.tracks.size();
    if(d == 3 && path[0] == 0 && path[1] == 0) return 1;
    if(d == 4 && path[0] == 0 && path[1] == 0 && path[3] == 0) return model.nodes[path[2]].properties.size();
    return 0;
}

int MyModel::columnCount(const QModelIndex &parent) const
{
    return 1;
}

Qt::ItemFlags MyModel::flags(const QModelIndex &index) const
{
    if(index.isValid()) return Qt::ItemIsEnabled | Qt::ItemIsSelectable;
    return {};
}
...