Edited at

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

More than 1 year has 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;
}



参考文献