感想など。
最初に感想を書いて、あとで記録を書く。Qt を勉強するために、最初にテキストだけ表示する物を作り、次にリストを色んなウィジェットで表示する物を作り、次にリストの表示の仕方を変えてみた。やりたい事の複雑さに応じてコードも増えて行くが、簡単な例ではとても簡単に書く事が出来る。
さらに複雑に振る舞いを定義するためには選択には QItemSelectionModel、編集にはデリゲーションという物を使うらしい。しかし適切なデフォルトが定義されているので、単純な場合気にする必要が無い。これはとても親切に設計された API だと思った。
Smalltalk の MVC では、モデルは「何か値を持っている物」くらいの意味だが、Qt のモデルはもっと限定的に「何かのリスト」という事になってる(まだサワリしかやってないので MVC みたいなモデルもあるのかも知れないけど)。これはなかなか面白いデザイン上の選択だと思う。
はっきりさせる為に他の実装可能性を考える。例えばモデルがリストじゃなくて、リスト内の要素だと考えても良い。このモデルをテーブルに表示させようとすると、テーブル内の各セルがビューで、それぞれの要素であるモデルと対応する事になる。セルと要素モデルがはっきりしていてこの方が素直な設計だ。Morphic はこの方式だし(PluggableListMorph は Qt と同じだが、各ウィジェットがオブジェクトを表現するのが本来の Morphic の思想だと思う) は、昔の Qt のウィジェットもこれを採用していた。しかしこの方式は効率に欠点がある。要素が増えるにつれて画面に表示しきれない無駄なセルまで作らないといけないからだ。Qt ではこれを解決するするために、リストだけをモデルとし、各要素へは必要に応じてリスト内のインデックスから生成される QModelIndex という一時的な要素を通じてアクセスする。
リスト状のデータを供給するモデルはデータソースと呼ばれる事もある。Qt に限らず AppKit でも .NET でも同じような仕組みを使っているので、多分経験的に一番実用的なのだろう。
この、効率のために「要素ではなくリストをモデルとする」というのはなかなか面白いテーマだ。解決のためにも色々な手法がある。Qt では QModelIndex を通じて要素にアクセスする事で、あたかも QModelIndex が要素のモデルのように振る舞う。Squeak の String と Character の関係も似ていて、効率の為に String の内部はただのバイト列なのに API 上 Character の要素のように振る舞う。こういうのもしかしてデザインパターン名があるのかな?
(追記: QModelIndex は文字通りインデックスとして使われるだけで、要素の内容は示さない。私が「こうだったらいいなー」という願望を持っていたので間違って書いてしまった。Qt ではモデル(リスト)の各要素はオブジェクトでは無い。モデルの各要素にアクセスするためには、QModelIndex と role の二つのキーから QVariant (高級な void* みたいな物) の値を取り出す。)
この手法は、必要になってからオブジェクトを作る遅延評価の特殊例と考える事が出来る。GUI では見えない部分の処理をゴマカスために色々工夫がある。テーブルの例では、テーブルが大きすぎて画面からはみ出す分は必要になってから作っている。
という事は、最初から GUI ツールキット全体を関数型言語の遅延評価で作れば良いかも知れない。遅延評価を使うと、「全自然数を含むテーブル」とか、「フィボナッチ数列テーブル」等無限要素が表現出来る(スクロールバーの位置に工夫が必要だが)。関数型言語の GUI ツールキットは既存のツールキットへのバインディングが多いからかイベント処理に注目が行きがちだけど、イベント処理からビットマップ出力まで全部関数と考えると結構使いやすい物が出来そう。
実装
昨日 http://d.hatena.ne.jp/propella/20110616/p1 は Qt で単純な Hello hworld! と model-view プログラミングを試してみた。今日はもう少し実用的にモデルを自分で実装してみる。今回複数のファイルからプログラムを作るので、.pro ファイルにビルド方法を書かないといけない。プロジェクト名を model にしたので、model というディレクトリの中に model.pro を作る。今回メインに main.cpp、自作モデルに MyModel.h と MyModel.cpp というファイルを使う。
# model.pro SOURCES = MyModel.cpp main.cpp HEADERS = MyModel.h
こうすると qmake が適当な Makefile を作ってくれる。次に main.cpp。これは昨日のソースの QStringListModel を MyModel に入れ替えただけ。
// main.cpp #include "MyModel.h" int main(int argc, char **argv) { QApplication app(argc, argv); QSplitter widget; QStringList colors; colors << "Red" << "Orange" << "Yellow" << "Green" << "Blue" << "Purple"; MyModel model(colors); QListView list(&widget); QTreeView tree(&widget); QTableView table(&widget); QComboBox combo(&widget); list.setModel(&model); tree.setModel(&model); table.setModel(&model); combo.setModel(&model); widget.show(); return app.exec(); }
次にモデルのヘッダーファイル。QAbstractListModel から継承してカスタマイズしたいメソッドを宣言する。
// MyModel.h #include <QtGui> // QAbstractListModel のサブクラスを定義する。 class MyModel : public QAbstractListModel { // おまじない: Qt 拡張のシグナルやスロットが使えるようになる。 Q_OBJECT public: MyModel(const QStringList &strings, QObject *parent = 0); int rowCount(const QModelIndex &parent = QModelIndex()) const; int columnCount(const QModelIndex &parent = QModelIndex()) const; QVariant data(const QModelIndex &index, int role) const; Qt::ItemFlags flags(const QModelIndex &index) const; bool setData(const QModelIndex &index, const QVariant &value, int role = Qt::EditRole); // 実際のデータはプライベート変数の stringList に格納 private: QStringList stringList; };
最後に MyModel の本体。今回、文字列を単に表示するだけじゃなくて色んな方法で表示出来るようにしている。
- 最初の桁: 入力した文字列
- 最初の桁のアイコン: 文字列を色名と解釈する
- 二番目の桁: さらにHTML 用の色名に変換
- 文字列は編集出来るように。
いくつか面白い特徴をコメントの中に書いた。
// MyModel.cpp #include "MyModel.h" // お約束として、コンストラクタで親オブジェクトを渡す。 // 親オブジェクトが削除されると子オブジェクトも削除されるしくみ。 MyModel::MyModel(const QStringList &strings, QObject *parent) : QAbstractListModel(parent) { stringList = strings; } // 行数を返す int MyModel::rowCount(const QModelIndex &) const { return stringList.count(); } // 桁数を返す int MyModel::columnCount(const QModelIndex &) const { return 2; } // 位置と種類に応じて適切なデータを返す // index.column : データの位置を示す。 // role == Qt::DisplayRole : 文字列の表示に使うデータ // role == Qt::EditRole : 文字列編集の初期値 // role == Qt::DecorationRole : アイコン(色や図形) QVariant MyModel::data(const QModelIndex &index, int role) const { QString str = stringList.at(index.row()); QColor color = QColor(str); if (index.column() == 0 && role == Qt::DecorationRole) return color; if (index.column() == 0 && role == Qt::EditRole) return str; if (index.column() == 0 && role == Qt::DisplayRole) return str; if (index.column() == 1 && role == Qt::DisplayRole) return color.name(); return QVariant(); } // 編集可能かどうかを答える。 Qt::ItemFlags MyModel::flags(const QModelIndex &index) const { if (index.column() == 0) return QAbstractItemModel::flags(index) | Qt::ItemIsEditable; else return QAbstractItemModel::flags(index) | Qt::ItemIsEnabled; } // 編集が完了すると呼ばれる。 bool MyModel::setData(const QModelIndex &index, const QVariant &value, int role) { if (role != Qt::EditRole) return false; stringList.replace(index.row(), value.toString()); emit dataChanged(index, index); return true; }
ファイル一覧。gist https://gist.github.com/1030560