はじめに
この記事はQtが提供している QFileSystemModelのように、データモデルをQAbstractItemModelから自作する際に必要なことをまとめたメモです。
QAbstractItemModelとQModelIndex
QAbstractItemModelは、QListView
、QTableView
、QTreeView
などのビュークラスに対するモデルのクラスです。行(row)、列(column)、親(parent)の3つの情報でアイテムを特定する柔軟な構造になっていてリスト・表・木をまとめて扱えます。
(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
の場合は値を返さなければ実用性がありません。
今回はrole
はQt::DisplayRole
のみ対応することにし、ウィジェットのクラス名を返します。internalPointer()
からのstatic_cast
は頻出なのでラップしました。
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
orientation
はQt::Horizontal
またはQt::Vertical
です。それに合わせて、section
は列または行を表します。
今回は固定値を返します。
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
は頻出のためラップしました。
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
row
、column
、parent
で特定されるアイテムのインデックスを返します。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
ではQList
のindexOf
で求めます。
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
に設定して動作確認した結果がこちらです。
全コードは以下の通りです。
#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;
}