Help us understand the problem. What is going on with this article?

QAbstractItemModelを実装してツリービューを作る(Read Only編)

More than 3 years have passed since last update.

はじめに

この記事はQtが提供している QFileSystemModelのように、データモデルをQAbstractItemModelから自作する際に必要なことをまとめたメモです。

QAbstractItemModelとQModelIndex

QAbstractItemModelは、QListViewQTableViewQTreeViewなどのビュークラスに対するモデルのクラスです。行(row)、列(column)、親(parent)の3つの情報でアイテムを特定する柔軟な構造になっていてリスト・表・木をまとめて扱えます。

image
(Qt Documentation - Model/View Programmingから引用)

モデル中のアイテムを特定する3つの情報をまとめてインデックスと呼び、QModelIndexオブジェクトで表現します。デフォルトコンストラクタで作成したQModelIndex()は無効なインデックスを表し、特別な意味を持っています。

  • ルートアイテムのインデックスを表す
    • あるアイテムの親のQModelIndexが無効な場合、そのアイテムがトップレベル(ルートアイテムの子)であることを表します
  • 無効なアイテムを表す
    • 親以外で無効なインデックスが返った場合、そのようなアイテムが無いことを表します

QModelIndexは、内部表現のポインタ(internalPointer)またはID(internalId)を格納できます。

基本クラスを決める

表現するデータ構造がリストの場合はQAbstractListModel、表の場合はQAbstractTableModelが使用できます。これらはQAbstractItemModelの派生クラスで、メンバー関数をある程度実装してくれています。

データ構造が木の場合はQAbstractItemModelを直接拡張します。

データ構造 基本クラス
リスト QAbstractListModel または QAbstractItemModel
QAbstractTableModel または QAbstractItemModel
QAbstractItemModel

サンプルの設計

サンプルとしてウィジェットの階層構造を表すツリーモデルWidgetHierarchyModelを作成します。仕様は以下の通りです。

  • QWidget*を直接internalPointerに格納
  • 列数は1
  • 行数は子ウィジェットの数
  • トップレベルのアイテム(ウィジェット)は1つ、メンバー変数topWidgetで保持
  • 各アイテムの文字列表現はウィジェットのクラス名

仮想関数をオーバーライドする

QAbstractItemModelが持つ仮想関数は主に3種類に分けられます。

  • アイテムデータハンドリング(必須)
    アイテムの個数や内容を読み書きする関数
  • ナビゲーションとインデックス生成(必須)
    モデル内のインデックスを表すQModelIndexオブジェクトを生成する関数
  • ドラッグアンドドロップサポート
    今回は扱いません

アイテムデータハンドリング(必須)

読み取り専用の場合、data()rowCount()columnCount()を実装します。これらは純粋仮想関数なので実装は必須です。

ヘッダーを出せるようにするにはheaderData()を実装します(基本的にはした方が良いでしょう)。

flags()は、各アイテムのフラグ(編集可能か、選択可能か、など)を返す関数です。デフォルトの実装ではQt::ItemIsEnabled | Qt::ItemIsSelectableを返します。

data()

QVariant QAbstractItemModel::data(const QModelIndex &index, int role = Qt::DisplayRole) const

アイテムのデータを返す関数です。

第1引数indexは要求されているデータのインデックスです。インデックスが無効なら範囲外です。

第2引数roleは返すデータの使用目的です。ツールチップ用の文字列を別に返したい場合などはこれで分岐します。最低でもQt::DisplayRoleの場合は値を返さなければ実用性がありません。

今回はroleQt::DisplayRoleのみ対応することにし、ウィジェットのクラス名を返します。internalPointer()からのstatic_castは頻出なのでラップしました。

WidgetHierarchyModel.cpp
QWidget* widget(const QModelIndex &index) const
{
    return static_cast<QWidget*>(index.internalPointer());
}

QVariant data(const QModelIndex &index, int role) const override
{
    if( role != Qt::DisplayRole || !index.isValid() ) return QVariant();
    return widget(index)->metaObject()->className();
}

headerData()

QVariant QAbstractItemModel::headerData(int section, Qt::Orientation orientation, int role = Qt::DisplayRole) const

orientationQt::HorizontalまたはQt::Verticalです。それに合わせて、sectionは列または行を表します。

今回は固定値を返します。

WidgetHierarchyModel.cpp
QVariant WidgetHierarchyModel::headerData(int, Qt::Orientation orientation, int role) const override
{
    if( orientation != Qt::Horizontal || role != Qt::DisplayRole ) return QVariant();
    return QString("Widget Hierarchy");
}

rowCount()・columnCount()

int QAbstractItemModel::rowCount(const QModelIndex &parent = QModelIndex()) const
int QAbstractItemModel::columnCount(const QModelIndex &parent = QModelIndex()) const

parentを親に持つアイテムの行数・列数を返します。parentが無効なインデックスの場合、親がない(トップレベルである)ことを表します。

WidgetHierarchyModelでは行数を子ウィジェットの数とし、列数は1とします。ただし、ルート直下の行数は1とします。

findChildrenは頻出のためラップしました。

WidgetHierarchyModel.cpp
QList<QWidget*> WidgetHierarchyModel::childrenOf(const QWidget* parent)
{
    return parent->findChildren<QWidget*>(QString(), Qt::FindDirectChildrenOnly);
}

int WidgetHierarchyModel::rowCount(const QModelIndex &parent) const override
{
    return parent.isValid() ? childrenOf(widget(parent)).size() : 1;
}

int WidgetHierarchyModel::columnCount(const QModelIndex &) const override
{
    return 1;
}

ナビゲーションとインデックス生成(必須)

インデックスから親・子のインデックスを生成する関数です。便利なファクトリー関数createIndexを使ってインデックスを作成します。ここで第3引数ptrに渡したポインタがinternalPointerになります。

QModelIndex QAbstractItemModel::createIndex(int row, int column, void *ptr = nullptr) const

WidgetHierarchyModelではrowは0に固定です。

index()

QModelIndex QAbstractItemModel::index(int row, int column, const QModelIndex &parent = QModelIndex()) const

rowcolumnparentで特定されるアイテムのインデックスを返します。parentが無効なインデックスの場合、親がない(トップレベルである)ことを表します。

WidgetHierarchyModelでは親ウィジェットに対してfindChildrenして取得したリスト中のrow番目を返します。

QModelIndex WidgetHierarchyModel::index(int row, int column, const QModelIndex &parent) const override
{
    if( !parent.isValid() )
    {
        // トップレベルは0行0列目だけアイテムがある
        if( row == 0 && column == 0 )
        {
            return createIndex(0, 0, topWidget);
        }
        return QModelIndex();
    }
    // 列は1列だけ
    if( column != 0 || parent.column() != 0 )
    {
        return QModelIndex();
    }
    QList<QWidget*> children = childrenOf(widget(parent));
    return row < children.size() ? createIndex(row, 0, children.at(row)) : QModelIndex();
}

parent()

QModelIndex QAbstractItemModel::parent(const QModelIndex &index) const

indexが指すアイテムの親のインデックスを返します。indexが指すアイテムがトップレベルの場合はルートのインデックス(無効なインデックス)を返します。

createIndexを呼ぶには、親アイテムの中でindexが指すアイテムが何行何列にあるのか求める必要があります。

WidgetHierarchyModelではQListindexOfで求めます。

WidgetHierarchyModel.cpp
QModelIndex WidgetHierarchyModel::parent(const QModelIndex &index) const override
{
    if( index.isValid() )
    {
        QWidget* self = widget(index);
        if( self != topWidget )
        {
            QWidget* parent = self->parentWidget();
            int row = childrenOf(parent).indexOf(self);
            if( row > -1 )
            {
                return createIndex(row, 0, parent);
            }
        }
    }
    return QModelIndex();
}

完成

モデルをQTreeViewに設定して動作確認した結果がこちらです。
image

全コードは以下の通りです。

WidgetHierarchyModel.cpp
#include <QApplication>
#include <QVBoxLayout>
#include <QTreeView>

class WidgetHierarchyModel : public QAbstractItemModel
{
   public:
    WidgetHierarchyModel(QWidget* topWidget):topWidget(topWidget)
    {
    }

    QWidget* widget(const QModelIndex &index) const
    {
        return static_cast<QWidget*>(index.internalPointer());
    }

    QVariant data(const QModelIndex &index, int role) const override
    {
        if( role != Qt::DisplayRole || !index.isValid() ) return QVariant();
        return widget(index)->metaObject()->className();
    }

    QVariant headerData(int, Qt::Orientation orientation, int role) const override
    {
        if( orientation != Qt::Horizontal || role != Qt::DisplayRole ) return QVariant();
        return QString("Widget Hierarchy");
    }

    int rowCount(const QModelIndex &parent) const override
    {
        return parent.isValid() ? childrenOf(widget(parent)).size() : 1;
    }

    int columnCount(const QModelIndex &) const override
    {
        return 1;
    }

    QModelIndex index(int row, int column, const QModelIndex &parent) const override
    {
        if( !parent.isValid() )
        {
            if( row == 0 && column == 0 )
            {
                return createIndex(0, 0, topWidget);
            }
            return QModelIndex();
        }
        if( column != 0 || parent.column() != 0 )
        {
            return QModelIndex();
        }
        QList<QWidget*> children = childrenOf(widget(parent));
        if( row < children.size() )
        {
            return createIndex(row, 0, children.at(row));
        }
        return QModelIndex();
    }

    QModelIndex parent(const QModelIndex &index) const override
    {
        if( index.isValid() )
        {
            QWidget* const self = widget(index);
            if( self != topWidget )
            {
                QWidget* const parent = self->parentWidget();
                int row = childrenOf(parent).indexOf(self);
                if( row > -1 )
                {
                    return createIndex(row, 0, parent);
                }
            }
        }
        return QModelIndex();
    }

  private:
    static QList<QWidget*> childrenOf(const QWidget* parent)
    {
        return parent->findChildren<QWidget*>(QString(), Qt::FindDirectChildrenOnly);
    }

    QWidget* topWidget;
};

int main(int argc, char *argv[])
{
    QApplication a(argc, argv);

    auto window = new QWidget();
    auto layout = new QVBoxLayout();
    auto tree   = new QTreeView(window);

    layout->addWidget(tree);
    window->setLayout(layout);

    auto model = new WidgetHierarchyModel(tree);
    tree->setModel(model);
    tree->setHeaderHidden(false);
    window->show();

    a.exec();
    delete window;

    return 0;
}

参考文献

tetsurom
主にC++, Qtでアプリケーション開発をしています。
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away