編集可能なツリーモデルの例

この例では、モデル/ビューフレームワークの他のクラスと一緒に使うことができる、シンプルなアイテムベースのツリーモデルを実装する方法を示します。

このモデルは、編集可能な項目、カスタムヘッダー、行と列の挿入と削除の機能をサポートしています。これらの機能により、新しい子アイテムを挿入することも可能です。

概要

モデルのサブクラスリファレンスで説明されているように、モデルはモデル関数の標準セット(flags()、data()、headerData()、columnCount()、rowCount())の実装を提供しなければなりません。さらに、このモデルのような階層モデルは、index ()とparent ()の実装を提供する必要があります。

編集可能なモデルは、setData ()とsetHeaderData ()の実装を提供する必要があり、flags ()関数から適切なフラグの組み合わせを返す必要があります。

この例ではモデルの寸法を変更できるため、insertRows ()、insertColumns ()、removeRows ()、removeColumns ()も実装する必要があります。

設計

単純なツリーモデルの例と同様に、モデルは単にTreeItem クラスのインスタンスのコレクションのラッパーとして動作します。各TreeItem は、ツリービューのアイテムの行のデータを保持するように設計されているので、各列に表示されるデータに対応する値のリストを含んでいます。

QTreeView はモデルに対する行指向のビューを提供するので、この種のビューにモデルを介してデータを供給するデータ構造に行指向の設計を選択するのは自然なことです。このため、ツリーモデルは柔軟性に欠け、より洗練されたビューで使用する際の有用性が低くなる可能性がありますが、その分設計が複雑でなくなり、実装が容易になります。

内部項目間の関係

parentカスタムモデルで使用するためのデータ構造を設計する場合、TreeItem::parent() のような関数を通して各アイテムの親を公開することは有用です。同様に、TreeItem::child() のような関数は、モデルのindex() 関数を実装する際に役立ちます。その結果、各TreeItem はその親と子に関する情報を保持し、ツリー構造をたどることができるようになります。

この図は、TreeItem インスタンスがparent()関数とchild()関数によってどのように接続されているかを示しています。

この例では、2 つのトップレベル・アイテムAB は、child() 関数を呼び出すことで、ルート・アイテムから取得することができます。

TreeItem は、それが表す行の各列のデータをitemData プライベート・メンバ(QVariant オブジェクトのリスト)に格納します。ビューの各列とリストの各エントリーの間には1対1のマッピングがあるので、itemData リストのエントリーを読み込むための単純なdata()関数と、それらを変更できるようにするためのsetData()関数を提供します。項目の他の関数と同様に、これはモデルのdata() とsetData() 関数の実装を単純化します。

アイテムのツリーのルートにアイテムを配置します。このルートアイテムは、モデルインデックスを扱う際にトップレベルアイテムの親を表すために使用されるヌルモデルインデックス、QModelIndex ()に対応します。ルートアイテムは、標準的なビューでは目に見える表現を持ちませんが、QVariant オブジェクトの内部リストを使用して、水平ヘッダタイトルとして使用するためにビューに渡される文字列のリストを格納します。

モデルを介したデータへのアクセス

図に示したケースでは、aで表される情報の一部は、標準のモデル/ビュー API を使って取得することができます:

QVariant a = model->index(0, 0, QModelIndex()).data();

各項目は与えられた行の各カラムのデータの断片を保持するので、同じTreeItem オブジェクトにマッピングされる多くのモデルインデックスが存在し得ます。例えば、bで表される情報は以下のコードで取得できます:

QVariant b = model->index(1, 0, QModelIndex()).data();

bと同じ行の他のモデルインデックスの情報を得るために、同じTreeItem

モデルクラスTreeModel では、TreeItem オブジェクトをモデルインデックスに関連付けます。これは、index()parent() の実装で、QAbstractItemModel::createIndex() を使用して対応するモデルインデックスを作成する際に、各項目のポインタを渡すことによって行います。このようにして格納されたポインタは、関連するモデル・インデックスのinternalPointer() 関数を呼び出すことで取得することができます。この処理を行うために独自のgetItem()関数を作成し、data()およびparent() の実装から呼び出します。

アイテムへのポインタを保存することは、アイテムの生成と破棄を制御する場合に便利です。なぜなら、internalPointer() から取得したアドレスは有効なポインタであると仮定できるからです。しかし、モデルによっては、システム内の他のコンポーネントから取得したアイテムを処理する必要があり、多くの場合、アイテムの作成方法や破棄方法を完全に制御することはできません。このような状況では、純粋なポインタベースのアプローチに、モデルが削除されたアイテムにアクセスしようとしないことを保証するセーフガードを追加する必要があります。

基礎となるデータ構造への情報の格納

いくつかのデータはQVariant オブジェクトとして、各TreeItem インスタンスのitemData メンバに格納される。

このダイアグラムでは、前の 2 つのダイアグラムでabcというラベルで表された情報の断片が、基礎となるデータ構造の項目ABCにどのように格納されているかを示しています。モデル内の同じ行にある情報の断片は、すべて同じ項目から取得されることに注意してください。リスト内の各要素は、モデル内の与えられた行の各列によって公開される情報の一部分に対応します。

TreeModel の実装はQTreeView で使用するために設計されているので、TreeItem のインスタンスを使用する方法に制限を加えました:各項目は同じ数の列のデータを公開しなければなりません。これにより、モデルを一貫して表示できるようになり、任意の行の列数を決定するためにルートアイテムを使用できるようになり、列の総数に対して十分なデータを含むアイテムを作成するという要件が追加されただけです。その結果、列の挿入と削除は、すべての項目を変更するためにツリー全体を横断する必要があるため、時間のかかる操作になります。

別のアプローチとしては、TreeModel クラスを設計して、データの項目が変更されたときに、個々のTreeItem インスタンスのデータのリストを切り詰めたり展開したりするようにすることです。しかし、この "遅延 "リサイズ・アプローチでは、各行の最後に列を挿入・削除することしかできず、各行の任意の位置に列を挿入・削除することはできません。

モデルインデックスを使った項目の関連付け

シンプルツリーモデルの例と同様に、TreeModel は、モデルインデックスを受け取り、対応するTreeItem を見つけ、その親と子に対応するモデルインデックスを返すことができる必要があります。

この図では、モデルのparent()実装が、前の図で示した項目を使用して、呼び出し元から提供された項目の親に対応するモデル・インデックスをどのように取得するかを示します。

項目Cへのポインタは、QModelIndex::internalPointer ()関数を使用して、対応するモデル・インデックスから取得されます。このポインタは、インデックスが作成されたときに内部に格納されていました。子には親へのポインタが含まれているので、そのparent()関数を使用して項目B へのポインタを取得します。親モデル・インデックスは、QAbstractItemModel::createIndex()関数を使用して作成され、内部ポインタとして項目Bへのポインタを渡します。

TreeItem クラスの定義

TreeItem クラスは、親アイテムと子アイテムの情報を含む、いくつかのデータを含む単純なアイテムを提供します:

class TreeItem
{
public:
    explicit TreeItem(QVariantList data, TreeItem *parent = nullptr);

    TreeItem *child(int number);
    int childCount() const;
    int columnCount() const;
    QVariant data(int column) const;
    bool insertChildren(int position, int count, int columns);
    bool insertColumns(int position, int columns);
    TreeItem *parent();
    bool removeChildren(int position, int count);
    bool removeColumns(int position, int columns);
    int row() const;
    bool setData(int column, const QVariant &value);

private:
    std::vector<std::unique_ptr<TreeItem>> m_childItems;
    QVariantList itemData;
    TreeItem *m_parentItem;
};

私たちは、各アイテムに情報の列数を返す関数、データの読み書きをする関数、列の挿入と削除をする関数を与えることで、QAbstractItemModel によって提供されるものと同様のAPIを設計しました。ただし、「行」ではなく「子」を扱う関数を提供することで、項目間の関係を明確にしている。

各項目は、子項目へのポインタのリスト、親項目へのポインタ、モデル内の指定された行の列に保持される情報に対応するQVariant オブジェクトのリストを含む。

TreeItemクラスの実装

TreeItem は、データのリストとオプションの親アイテムで構成されます:

TreeItem::TreeItem(QVariantList data, TreeItem *parent)
    : itemData(std::move(data)), m_parentItem(parent)
{}

初期状態では、各アイテムは子アイテムを持ちません。これらの子は、後で説明するinsertChildren() 関数を使用して、項目の内部メンバchildItems に追加されます。

子メンバーはstd::unique_ptrに格納され、アイテムが削除されたときに追加された子メンバーも削除されるようになっています。

各アイテムはその親へのポインタを格納するので、parent() 関数は些細なものである:

TreeItem *TreeItem::parent()
{
    return m_parentItem;
}

child() 、内部子リストから特定の子を返します:

TreeItem *TreeItem::child(int number)
{
    return (number >= 0 && number < childCount())
        ? m_childItems.at(number).get() : nullptr;
}

childCount() 関数は子の総数を返します:

int TreeItem::childCount() const
{
    return int(m_childItems.size());
}

childNumber() 関数は、親の子リストにおける子のインデックスを決定するために使用されます。この情報を得るために、親のchildItems メンバに直接アクセスします:

int TreeItem::row() const
{
    if (!m_parentItem)
        return 0;
    const auto it = std::find_if(m_parentItem->m_childItems.cbegin(), m_parentItem->m_childItems.cend(),
                                 [this](const std::unique_ptr<TreeItem> &treeItem) {
        return treeItem.get() == this;
    });

    if (it != m_parentItem->m_childItems.cend())
        return std::distance(m_parentItem->m_childItems.cbegin(), it);
    Q_ASSERT(false); // should not happen
    return -1;
}

ルート項目には親項目がありません。この項目については、他の項目との整合性を保つためにゼロを返します。

columnCount() 関数は、単にQVariant オブジェクトの内部リストitemData の要素数を返します:

int TreeItem::columnCount() const
{
    return int(itemData.count());
}

データの取得にはdata() 関数を使用し、itemData リスト内の適切な要素にアクセスします:

QVariant TreeItem::data(int column) const
{
    return itemData.value(column);
}

データはsetData() 関数を用いて設定されます。この関数は、モデル内の列の値に対応する有効なリストインデックスに対してのみ、itemData リストに値を格納します:

bool TreeItem::setData(int column, const QVariant &value)
{
    if (column < 0 || column >= itemData.size())
        return false;

    itemData[column] = value;
    return true;
}

モデルの実装を簡単にするために、データが正常にセットされたことを示すtrueを返します。

編集可能なモデルは、行や列を挿入したり削除したりできるように、しばしばリサイズ可能である必要があります。モデル内の与えられたモデルインデックスの下に行が挿入されると、対応するアイテムに新しい子アイテムが挿入され、insertChildren() 関数によって処理されます:

bool TreeItem::insertChildren(int position, int count, int columns)
{
    if (position < 0 || position > qsizetype(m_childItems.size()))
        return false;

    for (int row = 0; row < count; ++row) {
        QVariantList data(columns);
        m_childItems.insert(m_childItems.cbegin() + position,
                std::make_unique<TreeItem>(data, this));
    }

    return true;
}

これにより、新しい項目が必要な列数で作成され、内部childItems リスト内の有効な位置に挿入されることが保証されます。アイテムの削除はremoveChildren() 関数で行います:

bool TreeItem::removeChildren(int position, int count)
{
    if (position < 0 || position + count > qsizetype(m_childItems.size()))
        return false;

    for (int row = 0; row < count; ++row)
        m_childItems.erase(m_childItems.cbegin() + position);

    return true;
}

上述したように、列の挿入と削除のための関数は、子項目の挿入と削除のための関数とは異なる方法で使用されます。この関数は、アイテムの各子アイテムに対して再帰的に呼び出されます:

bool TreeItem::insertColumns(int position, int columns)
{
    if (position < 0 || position > itemData.size())
        return false;

    for (int column = 0; column < columns; ++column)
        itemData.insert(position, QVariant());

    for (auto &child : std::as_const(m_childItems))
        child->insertColumns(position, columns);

    return true;
}

TreeModel クラス定義

TreeModel クラスはQAbstractItemModel クラスの実装を提供し、編集とサイズ変更が可能なモデルに必要なインターフェイスを公開します。

class TreeModel : public QAbstractItemModel
{
    Q_OBJECT

public:
    Q_DISABLE_COPY_MOVE(TreeModel)

    TreeModel(const QStringList &headers, const QString &data,
              QObject *parent = nullptr);
    ~TreeModel() override;

コンストラクタとデストラクタはこのモデルに固有のものです。

    QVariant data(const QModelIndex &index, int role) const override;
    QVariant headerData(int section, Qt::Orientation orientation,
                        int role = Qt::DisplayRole) const override;

    QModelIndex index(int row, int column,
                      const QModelIndex &parent = {}) const override;
    QModelIndex parent(const QModelIndex &index) const override;

    int rowCount(const QModelIndex &parent = {}) const override;
    int columnCount(const QModelIndex &parent = {}) const override;

読み取り専用のツリーモデルは、上記の関数だけを提供すればよい。以下のパブリック関数は、編集とリサイズのサポートを提供します:

    Qt::ItemFlags flags(const QModelIndex &index) const override;
    bool setData(const QModelIndex &index, const QVariant &value,
                 int role = Qt::EditRole) override;
    bool setHeaderData(int section, Qt::Orientation orientation,
                       const QVariant &value, int role = Qt::EditRole) override;

    bool insertColumns(int position, int columns,
                       const QModelIndex &parent = {}) override;
    bool removeColumns(int position, int columns,
                       const QModelIndex &parent = {}) override;
    bool insertRows(int position, int rows,
                    const QModelIndex &parent = {}) override;
    bool removeRows(int position, int rows,
                    const QModelIndex &parent = {}) override;

private:
    void setupModelData(const QList<QStringView> &lines);
    TreeItem *getItem(const QModelIndex &index) const;

    std::unique_ptr<TreeItem> rootItem;
};

この例を簡単にするために、モデルによって公開されたデータは、モデルのsetupModelData()関数によってデータ構造に整理されます。実世界の多くのモデルは生データを全く処理せず、単に既存のデータ構造やライブラリAPIと連携します。

TreeModel クラスの実装

コンストラクターはルート項目を作成し、与えられたヘッダーデータで初期化します:

TreeModel::TreeModel(const QStringList &headers, const QString &data, QObject *parent)
    : QAbstractItemModel(parent)
{
    QVariantList rootData;
    for (const QString &header : headers)
        rootData << header;

    rootItem = std::make_unique<TreeItem>(rootData);
    setupModelData(QStringView{data}.split(u'\n'));
}

内部のsetupModelData()関数を呼び出し、与えられたテキストデータをモデルで使用できるデータ構造に変換します。他のモデルは、既製のデータ構造で初期化されるかもしれませんし、独自のデータを保持するライブラリのAPIを使うかもしれません。

TreeModel::~TreeModel() = default;

デストラクタはルートアイテムを削除するだけで、すべての子アイテムが再帰的に削除されます。ルート・アイテムは unique_ptr 内に格納されているため、デフォルトのデストラクタでは自動的にこの処理が行われます。

モデルの他のモデル/ビューコンポーネントへのインタフェースはモデルインデックスに基づいており、内部データ構造はアイテムベースであるため、モデルによって実装される関数の多くは、任意のモデルインデックスを対応するアイテムに変換できる必要があります。利便性と一貫性のために、この繰り返しタスクを実行するgetItem() 関数を定義しました:

TreeItem *TreeModel::getItem(const QModelIndex &index) const
{
    if (index.isValid()) {
        if (auto *item = static_cast<TreeItem*>(index.internalPointer()))
            return item;
    }
    return rootItem.get();
}

この関数に渡される各モデル・インデックスは、メモリ上の有効な項目に対応しなければなりません。インデックスが無効であるか、その内部ポインタが有効なアイテムを参照していない場合、代わりにルートアイテムが返されます。

モデルのrowCount() の実装はシンプルです。まずgetItem() 関数を使用して関連するアイテムを取得し、次にそれが含む子の数を返します:

int TreeModel::rowCount(const QModelIndex &parent) const
{
    if (parent.isValid() && parent.column() > 0)
        return 0;

    const TreeItem *parentItem = getItem(parent);

    return parentItem ? parentItem->childCount() : 0;
}

対照的に、columnCount() の実装では、すべての項目が同じ数の列を持つように定義されているため、特定の項目を探す必要はありません。

int TreeModel::columnCount(const QModelIndex &parent) const
{
    Q_UNUSED(parent);
    return rootItem->columnCount();
}

その結果、列の数はルート項目から直接得ることができます。

アイテムを編集して選択できるようにするには、flags() 関数を実装して、Qt::ItemIsEditableQt::ItemIsSelectable のフラグとQt::ItemIsEnabled を含むフラグの組み合わせを返すようにする必要があります:

Qt::ItemFlags TreeModel::flags(const QModelIndex &index) const
{
    if (!index.isValid())
        return Qt::NoItemFlags;

    return Qt::ItemIsEditable | QAbstractItemModel::flags(index);
}

モデルは、他のコンポーネントがその構造に関するデータや情報を要求できるように、モデルインデックスを生成できる必要があります。このタスクは、index() 関数によって実行されます。この関数は、与えられた親アイテムの子に対応するモデルインデックスを取得するために使用されます:

QModelIndex TreeModel::index(int row, int column, const QModelIndex &parent) const
{
    if (parent.isValid() && parent.column() != 0)
        return {};

このモデルでは、親インデックスが(ルートアイテムに対応する)無効な場合、または列番号がゼロの場合にのみ、子アイテムのモデルインデックスを返します。

カスタムgetItem()関数を使用して、指定されたモデルインデックスに対応するTreeItem インスタンスを取得し、指定された行に対応する子項目を要求します。

    TreeItem *parentItem = getItem(parent);
    if (!parentItem)
        return {};

    if (auto *childItem = parentItem->child(row))
        return createIndex(row, column, childItem);
    return {};
}

各項目はデータ行全体の情報を含むので、行番号と列番号と項目へのポインタを指定してcreateIndex() を呼び出すことで、それを一意に識別するためのモデルインデックスを作成します。data()関数では、項目ポインタと列番号を使用してモデルインデックスに関連付けられたデータにアクセスします。このモデルでは、行番号はデータを識別するために必要ありません。

parent() 関数は、与えられたモデル・インデックスに対応するアイテムを見つけ、parent()関数を使用してその親アイテムを取得し、親を表すモデル・インデックスを作成することによって、アイテムの親にモデル・インデックスを提供します。(上図参照)。

QModelIndex TreeModel::parent(const QModelIndex &index) const
{
    if (!index.isValid())
        return {};

    TreeItem *childItem = getItem(index);
    TreeItem *parentItem = childItem ? childItem->parent() : nullptr;

    return (parentItem != rootItem.get() && parentItem != nullptr)
        ? createIndex(parentItem->row(), 0, parentItem) : QModelIndex{};
}

ルートアイテムを含む親のないアイテムは、NULL モデルインデックスを返すことで処理されます。そうでない場合は、index()関数と同じようにモデル・インデックスが作成され、適切な行番号で返されますが、index() の実装で使用されているスキームと一致するように、列番号はゼロになります。

モデルのテスト

アイテムモデルを正しく実装するのは難しいことです。Qt TestモジュールのクラスQAbstractItemModelTester は、モデルのインデックス作成や親子関係のようなモデルの一貫性をチェックします。

例えば、Qtユニットテストの一部として、クラスのコンストラクタにモデルのインスタンスを渡すだけで、モデルをテストすることができます:

class TestEditableTreeModel : public QObject
{
    Q_OBJECT

private slots:
    void testTreeModel();
};

void TestEditableTreeModel::testTreeModel()
{
    constexpr auto fileName = ":/default.txt"_L1;
    QFile file(fileName);
    QVERIFY2(file.open(QIODevice::ReadOnly | QIODevice::Text),
             qPrintable(fileName + " cannot be opened: "_L1 + file.errorString()));

    const QStringList headers{"column1"_L1, "column2"_L1};
    TreeModel model(headers, QString::fromUtf8(file.readAll()));

    QAbstractItemModelTester tester(&model);
}

QTEST_APPLESS_MAIN(TestEditableTreeModel)

#include "test.moc"

ctest の実行ファイルを使用して実行できるテストを作成するには、add_test() を使用します:

# Unit Test

include(CTest)

qt_add_executable(editabletreemodel_tester
    test.cpp
    treeitem.cpp treeitem.h
    treemodel.cpp treemodel.h)

target_link_libraries(editabletreemodel_tester PRIVATE
    Qt6::Core
    Qt6::Test
)

if(ANDROID)
    target_link_libraries(editabletreemodel_tester PRIVATE
        Qt6::Gui
    )
endif()

qt_add_resources(editabletreemodel_tester "editabletreemodel_tester"
    PREFIX
        "/"
    FILES
        ${editabletreemodel_resource_files}
)

add_test(NAME editabletreemodel_tester
         COMMAND editabletreemodel_tester)

プロジェクト例 @ code.qt.io

©2024 The Qt Company Ltd. 本書に含まれる文書の著作権は、それぞれの所有者に帰属します。 ここで提供されるドキュメントは、Free Software Foundation が発行したGNU Free Documentation License version 1.3に基づいてライセンスされています。 Qtおよびそれぞれのロゴは、フィンランドおよびその他の国におけるThe Qt Company Ltd.の 商標です。その他すべての商標は、それぞれの所有者に帰属します。