このページでは

単純なツリーモデルの例

「簡易ツリーモデル」の例では、Qtの標準ビュークラスと階層モデルを組み合わせて使用する方法について説明しています。

章見出しと簡単な説明を含むドキュメントの構成

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

ツリーモデルを実装する前に、データが外部ソースから提供されるのか、それともモデル自体の中で管理されるのかを検討しておく価値があります。この例では、外部ソースからのデータの取り込み方法について論じるのではなく、データを保持するための内部構造を実装します。

設計と概念

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

ルートとツリー要素からなるツリー構造単純なツリーモデルの構造

データは、ポインタベースのツリー構造で相互にリンクされた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++ クラスです。QObject を継承しておらず、シグナルやスロットも提供しません。このクラスは、列データを含む QVariant のリストと、ツリー構造内でのその位置に関する情報を保持するために使用されます。このクラスは以下の機能を提供します:

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

親アイテムと列データは、parentItem およびitemData というプライベートメンバー変数に格納されます。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() 関数によって返されます。ここでは、境界をチェックし、境界が違反されている場合にデフォルト構築されたQVariant を返すQList::value()という利便関数を使用します:

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

項目の親は、parent() を使用して特定されます:

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

なお、モデル内のルートアイテムには親が存在しないため、その場合はこの関数が0を返します。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 のインスタンスを作成するだけで十分です。

モデルのテスト

QAbstractItemModelTester アイテムモデルを正しく実装するのは難しい場合があります。 Qt Test モジュールに含まれるxml-ph-0000@deepl.internalクラスは、モデルインデックスの作成や親子関係など、モデルの整合性をチェックします。

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

classTestSimpleTreeModel :publicQObject
{
    Q_OBJECT

private slots:
    voidtestTreeModel();
};

voidTestSimpleTreeModel::testTreeModel()
{
    constexprautofileName= ":/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_tester"
    PREFIX
        "/"
    FILES
        ${simpletreemodel_resource_files}
)

add_test(NAME simpletreemodel_tester
         COMMAND simpletreemodel_tester)

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

© 2026 The Qt Company Ltd. Documentation contributions included herein are the copyrights of their respective owners. The documentation provided herein is licensed under the terms of the GNU Free Documentation License version 1.3 as published by the Free Software Foundation. Qt and respective logos are trademarks of The Qt Company Ltd. in Finland and/or other countries worldwide. All other trademarks are property of their respective owners.