シンプルなツリーモデルの例

シンプルなツリーモデルの例では、Qtの標準的なビュークラスで階層モデルを使用する方法を示しています。

Qtのモデル/ビューアーキテクチャは、ビューがデータソースの情報を操作するための標準的な方法を提供し、データへのアクセス方法を単純化し標準化するために、データの抽象モデルを使用します。単純なモデルは、データを項目のテーブルとして表現し、ビューがインデックスベースのシステムを通じてこのデータにアクセスできるようにします。より一般的には、モデルを使用して、各項目を子項目のテーブルの親として動作させることで、ツリー構造の形式でデータを表現することができます。

ツリーモデルを実装しようとする前に、データが外部ソースから供給されるのか、それともモデル自身の中で保持されるのかを検討する価値があります。この例では、外部ソースからのデータをパッケージ化する方法について議論するのではなく、データを保持する内部構造を実装します。

設計と概念

データの構造を表現するために使用するデータ構造は、TreeItem オブジェクトから構築されるツリーの形をとります。各TreeItem はツリー・ビューの項目を表し、いくつかの列のデータを含んでいる。

単純なツリーモデル構造

データは、ポインタベースのツリー構造でリンクされたTreeItem オブジェクトを使用してモデル内部に格納されます。一般的に、TreeItem はそれぞれ親項目を持ち、いくつかの子項目を持つことができます。しかし、ツリー構造のルートアイテムは親アイテムを持たず、モデル外部で参照されることはありません。

TreeItem には、ツリー構造内の位置に関する情報が含まれており、親項目と行番号を返すことができます。この情報をすぐに利用できるようにすることで、モデルの実装が容易になります。

通常、ツリービューの各項目はいくつかの列のデータ(この例ではタイトルとサマリー)を含むので、各項目にこの情報を格納するのは自然なことです。簡単のために、QVariant オブジェクトのリストを使用して、項目の各列のデータを格納します。

ポインタベースのツリー構造を使うということは、モデルのインデックスをビューに渡すときに、対応する項目のアドレスをインデックスに記録しておき(QAbstractItemModel::createIndex ()を参照)、後でQModelIndex::internalPointer ()で取り出すことができるということです。これにより、モデルの記述が簡単になり、同じ項目を参照するすべてのモデルインデックスが同じ内部データポインタを持つことが保証されます。

適切なデータ構造があれば、モデル・インデックスとデータを他のコンポーネントに提供するための余分なコードを最小限に抑えて、ツリー・モデルを作成することができます。

TreeItem クラスの定義

TreeItem クラスは以下のように定義されています:

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

    void appendChild(std::unique_ptr<TreeItem> &&child);

    TreeItem *child(int row);
    int childCount() const;
    int columnCount() const;
    QVariant data(int column) const;
    int row() const;
    TreeItem *parentItem();

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

このクラスは基本的なC++クラスです。このクラスは基本的なC++クラスであり、QObject を継承したり、シグナルやスロットを提供したりはしない。このクラスは、列データとツリー構造内の位置に関する情報を含むQVariantsのリストを保持するために使用されます。関数は以下の機能を提供する:

  • appendChildItem() は、モデルの最初の構築時にデータを追加するために使用され、通常の使用時には使用されません。
  • child()childCount() 関数は、モデルが子項目に関する情報を取得できるようにします。
  • 項目に関連付けられている列の数に関する情報はcolumnCount() で提供され、各列のデータは data() 関数で取得できます。
  • row() 関数とparent() 関数を使用して、項目の行番号と親項目を取得します。

親項目と列のデータは、parentItemitemData のプライベート・メンバ変数に格納されます。childItems 変数には、項目自身の子項目へのポインタのリストが格納されます。

TreeItemクラスの実装

コンストラクタは、アイテムの親と各カラムに関連するデータを記録するためだけに使用されます。

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

このアイテムに属する各子アイテムへのポインタは、childItems のプライベート・メンバ変数に std::unique_ptr として格納されます。クラスのデストラクタが呼ばれると、子アイテムは自動的に削除され、メモリが再利用されるようになります:

各子項目はモデルにデータが最初に投入されるときに構築されるので、子項目を追加する関数は簡単です:

void TreeItem::appendChild(std::unique_ptr<TreeItem> &&child)
{
    m_childItems.push_back(std::move(child));
}

各項目は、適切な行番号が与えられると、その子項目のいずれかを返すことができます。例えば、上の図では、「A」と書かれた項目は、row = 0 を持つルート項目の子に対応し、「B」項目は、row = 1 を持つ「A」項目の子であり、「C」項目は、row = 1 を持つルート項目の子である。

child() 関数は、項目の子項目リストの指定された行番号に対応する子を返す:

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

保持している子項目の数は、childCount() で調べることができる:

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

TreeModel は、この関数を使用して、指定された親項目に存在する行の数を調べます。

row() 関数は、親アイテムのリスト内のアイテムの位置を報告します:

int TreeItem::row() const
{
    if (m_parentItem == nullptr)
        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;
}

ルート項目 (親項目を持たない) には自動的に行番号 0 が割り当てられますが、この情報がモデルで使用されることはないことに注意してください。

項目内のデータの列数は、columnCount() 関数によって簡単に返されます。

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

列データはdata() 関数で返されます。QList::value() 便利関数を使用します。この関数は境界をチェックし、境界違反の場合はデフォルトで構築されたQVariant を返します:

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

項目の親はparent() で見つかります:

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

モデルのルートアイテムは親を持たないので、この関数はゼロを返すことに注意してください。TreeModel::parent() 関数を実装する際には、モデルがこのケースを正しく処理するようにする必要があります。

TreeModel クラスの定義

TreeModel クラスは以下のように定義されています:

class TreeModel : public QAbstractItemModel
{
    Q_OBJECT

public:
    Q_DISABLE_COPY_MOVE(TreeModel)

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

    QVariant data(const QModelIndex &index, int role) const override;
    Qt::ItemFlags flags(const QModelIndex &index) 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;

private:
    static void setupModelData(const QList<QStringView> &lines, TreeItem *parent);

    std::unique_ptr<TreeItem> rootItem;
};

このクラスは、読み取り専用モデルを提供するQAbstractItemModel の他のほとんどのサブクラスに似ています。コンストラクタの形式とsetupModelData() 関数のみがこのモデルに固有です。さらに、モデルが破壊されたときに後始末をするためのデストラクタを提供します。

TreeModel クラスの実装

簡単のために、このモデルはデータの編集を許しません。その結果、コンストラクタはモデルがビューやデリゲートと共有するデータを含む引数を取ります:

TreeModel::TreeModel(const QString &data, QObject *parent)
    : QAbstractItemModel(parent)
    , rootItem(std::make_unique<TreeItem>(QVariantList{tr("Title"), tr("Summary")}))
{
    setupModelData(QStringView{data}.split(u'\n'), rootItem.get());
}

モデルのルート項目を作成するのはコンストラクタ次第です。この項目は、便宜上、垂直方向のヘッダーデータのみを含みます。また、モデルデータを含む内部データ構造を参照するために使用し、モデル内のトップレベルアイテムの架空の親を表すために使用します。ルート項目はstd::unique_ptrで管理され、モデルの削除時に項目のツリー全体が削除されるようになっています。

モデルの内部データ構造には、setupModelData() 関数によってアイテムが投入されます。この関数については、このドキュメントの最後に別途説明します。

デストラクタは、モデルが破棄されるときに、ルートアイテムとそのすべての子孫が削除されるようにします。ルートアイテムはunique_ptrに格納されているので、これは自動的に行われます。

TreeModel::~TreeModel() = default;

モデルが構築されセットアップされた後は、モデルにデータを追加することができないので、アイテムの内部ツリーを管理する方法が単純化されます。

ビューやデリゲートがデータにアクセスする際に使用するインデックスを提供するために、モデルはindex() 関数を実装しなければなりません。インデックスは、他のコンポーネントが行番号と列番号、そして親モデルのインデックスによって参照されるときに作成されます。親として無効なモデルインデックスが指定された場合、モデル内の最上位アイテムに対応するインデックスを返すかどうかはモデル次第です。

モデルインデックスが提供されると、まずそれが有効かどうかをチェックします。そうでない場合は、internalPointer ()関数でモデルインデックスからデータポインタを取得し、TreeItem オブジェクトを参照するために使用します。我々が構築するすべてのモデル・インデックスには、既存のTreeItem へのポインタが含まれることに注意してください。したがって、我々が受け取る有効なモデル・インデックスには有効なデータ・ポインタが含まれることが保証されます。

QModelIndex TreeModel::index(int row, int column, const QModelIndex &parent) const
{
    if (!hasIndex(row, column, parent))
        return {};

    TreeItem *parentItem = parent.isValid()
        ? static_cast<TreeItem*>(parent.internalPointer())
        : rootItem.get();

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

この関数の行と列の引数は、対応する親項目の子項目を参照しているので、TreeItem::child() 関数を使用して項目を取得します。createIndex() 関数は、返されるモデルインデックスを作成するために使用されます。行番号と列番号、そして項目自体へのポインタを指定します。このモデル・インデックスは、後で項目のデータを取得するために使用することができます。

TreeItem オブジェクトの定義方法によって、parent() 関数を簡単に書くことができます:

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

    auto *childItem = static_cast<TreeItem*>(index.internalPointer());
    TreeItem *parentItem = childItem->parentItem();

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

ルート項目に対応するモデルインデックスを決して返さないようにするだけでよいのです。index() 関数の実装方法と矛盾しないように、モデル内のトップレベル項目の親に対しては無効なモデルインデックスを返します。

返すモデルインデックスを作成する際には、親アイテムの親内の行番号と列番号を指定しなければなりません。行番号はTreeItem::row() 関数を使えば簡単にわかりますが、列番号は0を親の列番号として指定する慣例に従っています。モデル・インデックスは、index() 関数と同じように、createIndex() で作成します。

rowCount() 関数は、単純に、与えられたモデルインデックスに対応するTreeItem の子項目の数、または無効なインデックスが指定された場合は最上位項目の数を返します:

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

    const TreeItem *parentItem = parent.isValid()
        ? static_cast<const TreeItem*>(parent.internalPointer())
        : rootItem.get();

    return parentItem->childCount();
}

各項目はそれ自身の列データを管理するので、columnCount() 関数は、与えられたモデルインデックスにいくつの列が存在するかを決定するために、項目自身のcolumnCount() 関数を呼び出す必要があります。rowCount() 関数と同様に、無効なモデルインデックスが指定された場合、返される列の数はルートアイテムから決定されます:

int TreeModel::columnCount(const QModelIndex &parent) const
{
    if (parent.isValid())
        return static_cast<TreeItem*>(parent.internalPointer())->columnCount();
    return rootItem->columnCount();
}

データはdata() を介してモデルから取得されます。項目はそれ自身の列を管理するので、TreeItem::data() 関数でデータを取得するために列番号を使用する必要があります:

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

    const auto *item = static_cast<const TreeItem*>(index.internalPointer());
    return item->data(index.column());
}

この実装ではDisplayRole のみをサポートしており、無効なモデルインデックスに対しては無効なQVariant オブジェクトも返すことに注意してください。

モデルが読み取り専用であることをビューに確実に知らせるために、flags() 関数を使います:

Qt::ItemFlags TreeModel::flags(const QModelIndex &index) const
{
    return index.isValid()
        ? QAbstractItemModel::flags(index) : Qt::ItemFlags(Qt::NoItemFlags);
}

headerData() 関数は、ルートアイテムに格納されているデータを返します:

QVariant TreeModel::headerData(int section, Qt::Orientation orientation,
                               int role) const
{
    return orientation == Qt::Horizontal && role == Qt::DisplayRole
        ? rootItem->data(section) : QVariant{};
}

コンストラクタで指定するか、headerData() 関数にハードコードするかです。

モデルのデータのセットアップ

モデルの初期データを設定するために、setupModelData() 関数を使います。この関数はテキストファイルを解析し、モデルで使用するテキスト文字列を抽出し、データとモデル全体の構造を記録するアイテムオブジェクトを作成します。当然ながら、この関数はこのモデル特有の動作をします。この関数の動作を以下に説明し、詳細についてはサンプルコードを参照してください。

まず、以下の形式のテキストファイルから始めます:

Getting Started                         How to familiarize yourself with Qt Widgets Designer
    Launching Designer                  Running the Qt Widgets Designer application
    The User Interface                  How to interact with Qt Widgets Designer
    ...
Connection Editing Mode                 Connecting widgets together with signals and slots
    Connecting Objects                  Making connections in Qt Widgets Designer
    Editing Connections                 Changing existing connections

このテキストファイルを次の2つのルールで処理する:

  • 各行の文字列のペアごとに、ツリー構造のアイテム(またはノード)を作成し、各文字列をアイテムのデータ列に配置する。
  • ある行の最初の文字列が前の行の最初の文字列に対してインデントされている場合、その項目を前に作成された項目の子にする。

モデルが正しく動作することを確認するためには、正しいデータと親アイテムを持つTreeItem のインスタンスを作成することだけが必要です。

モデルのテスト

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

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

class TestSimpleTreeModel : public QObject
{
    Q_OBJECT

private slots:
    void testTreeModel();
};

void TestSimpleTreeModel::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()));
    TreeModel model(QString::fromUtf8(file.readAll()));

    QAbstractItemModelTester tester(&model);
}

QTEST_APPLESS_MAIN(TestSimpleTreeModel)

#include "test.moc"

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

# Unit Test

include(CTest)

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

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

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

qt_add_resources(simpletreemodel_tester "simpletreemodel"
    PREFIX
        "/"
    FILES
        ${simpletreemodel_resource_files}
)

add_test(NAME simpletreemodel_tester
         COMMAND simpletreemodel_tester)

サンプルプロジェクト @ code.qt.io

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