PySideでリストをカスタマイズするぞ! ~基礎編「model/viewアーキテクチャ」~
2014/10/20
Tag: model/view,PySide,python,tool,UI
はじめまして。最近TDチームに加わった小野です。
入って以来、PySideを使ってツール作りに励んでいるのですが、PySideって楽しいですね!今やPySideの虜です。
PySideは非常にツール開発しやすいフレームワークだと私自身思うのですが、たまに「これって何するクラス?」という時があります。そんな少しとっつきにくい機構のひとつがmodel/viewアーキテクチャではないでしょうか。これはデータのリスト、テーブル、ツリー構造などを表現するための概念なのですが、これを使わなくてもリストなどは作れます(厳密に言うと裏で使っていますが…)。しかし、この仕組みを理解することによって私自身、UI表現の幅が格段に上がりました。
そこで今日はmodel/view(とdelegate)の簡単な解説をしていきたいと思います。
きっかけ
リストやテーブルを使ったUIを作ったことがある方なら、QListWidget
、QTableWidget
というクラスとQListView
、QTableView
というクラスがあることに気づくと思います。PySideのドキュメントを見れば継承関係や、どう違うのかもきちんと解説されているのですが、初心者の私には「??…何が違うの?」という印象でした。少し調べて「Widgetの方が使いやすそうだな」ということでWidgetを採用したことを覚えています。
でも、QListWidget
やQTableWidget
だと、できることが限られてくるんですよね…。
model/viewを使おうと思ったきっかけは、以下のようなUIを作ろうと思ったことでした。
「これ、リストで作りたいけど、リストウィジェットじゃ無理だよね…。」
というわけで少し調べてみると、こんなときは「modelとdelegateを使うといいよ」という文献があったので少し勉強することにしました。
model/viewアーキテクチャって?
model/viewアーキテクチャというのは、簡単に言うと「データの管理」と「表示」は別にしようという考え方です。
たとえば二つのリストがある場合、QListWidgetItem
を使った構造では各リストに対してデータごとのアイテムインスタンスが作られます。この場合、片方のリストのデータを変更しても、もう片方のリストには反映されません(図左)。model/viewを使うと、2つのリストに対し、modelという共通のインスタンスを使用します。viewはこのmodelの情報を参照することで表示を行います。データの変更があった場合はその信号がmodelに伝わり、modelが情報を変更します(図右)。
こうすることで、アプリケーション全体でひとつのデータ構造を利用することができ、各view間の矛盾も無くなります。データを変更するたびに、「同じデータを表示しているWidgetすべてにSignalを出して…」ということもしなくて済み、メンテナンスもしやすくなります。
delegateはなにをするのか
オリジナルのリストを作るため、もうひとつ必要な概念がdelegateです。日本語では代理人とかいう意味で、ここでは各セルの表示や編集を担当します。例えば、「このセルは右寄せにするよ」とか、「このセルの編集にはコンボボックス使うよ」といった部分をコントロールします。つまり、ユーザー、model、viewの間のやり取りを代理で行ってくれます。これらの関係を簡単に図で示します。
viewは、他のオブジェクトにシグナルを送ったりと忙しいオブジェクトなので、細かい表示の設定はdelegateに任せます。表示をするにあたり、delegateはmodelに「この情報ください」といって必要な情報をもらいます。そして、 delegateはデータの種類を判断し、表示方法をviewに指示します。また、ユーザから操作があったときはそれにも対応します。データの変更が起こると今度はmodelに「このデータ書き換えてください」という命令を出します。
このようにしてリストやテーブル、ツリーなどではデータの表示が行われています。
model/viewを使ってリストを作ってみる
では、model/viewの大まかな構造がわかったところで、実際にmodel/viewアーキテクチャを用いてリストを作ってみましょう。まずは二つのリストと、それらが共通で使用するmodelを作成し、表示してみます。
#-*- coding:utf-8 import sys from PySide import QtCore, QtGui def main(): app = QtGui.QApplication(sys.argv) # modelの生成 myListModel = QtGui.QStringListModel(["Lion", "Monkey", "Tiger", "Cat"]) myListView1 = QtGui.QListView() myListView2 = QtGui.QListView() # リストにmodelをセット myListView1.setModel(myListModel) myListView2.setModel(myListModel) myListView1.show() myListView2.show() sys.exit(app.exec_()) if __name__ == '__main__': main()
ここではQStringListModel
クラスをmodelに使っています。このクラスは、リストの文字列を与えると、各文字列をデータとして取り込んでくれます。14、15行目でviewにmodelをセットしています。実行すると、二つのリストが表示されると思います。片方のデータを変更するともう片方が変更されることを確認してみてください。
これがmodelの基本的な役割です。
文字の色情報も入れてみる
では次に文字色情報もデータに入れてみましょう。独自のmodelクラスを作って実現してみます。まずはQAbstractListModel
を継承したmodelを作ってみましょう。このQAbstractListModel
はQAbstractItemModel
を継承しており、リストのmodelを作る際にはこのクラスを継承するようにPySideのドキュメントに記載されています。→ QAbstractListModel PySide v1.0.7 documentation
ということで一番初期状態。
class CustomListModel(QtCore.QAbstractListModel): # データを受け取りitemsに格納する def __init__(self, parent = None, data = []): super(CustomListModel, self).__init__(parent) self.__items = data
4行目、イニシャライザの引数でmodelのデータを取得できるようにしておきました。これをインスタンス変数__items
に格納して保持しておきます。
ドキュメントを見ると、継承する際はrowCount
メソッドとdata
メソッドを実装するようにと書かれています。rowCount
メソッドはリストの行数を返すためのものなので、ここでは__items
の長さを返せばよさそうです。data
メソッドは実際のテキストデータや色情報など、尋ねられた情報を返します。これらのメソッドを実装してみましょう。
このとき入力データは以下のような辞書のリストであると仮定します。"color"
の値はRGBの値を表すリストです。
data = [ {"name":"Lion","color":[237,111,112]}, {"name":"Monkey","color":[127,197,195]} ]
class CustomListModel(QtCore.QAbstractListModel): # データを受け取りitemsに格納する def __init__(self, parent = None, data = []): super(CustomListModel, self).__init__(parent) self.__items = data # アイテムの数を返す def rowCount(self, parent = QtCore.QModelIndex()): return len(self.__items) # Roleに合わせてデータを返す def data(self, index, role = QtCore.Qt.DisplayRole): if not index.isValid(): return None if not 0 <= index.row() < len(self.__items): return None if role == QtCore.Qt.DisplayRole: return self.__items[index.row()].get("name") elif role == QtCore.Qt.ForegroundRole: color = self.__items[index.row()].get("color", []) return QtGui.QColor(*color) else: return None
data
メソッドを見てみてください。viewやdelegateはmodelに対して、テキスト情報だけでなく、文字の色、背景の色など表示に関するさまざまな情報を尋ねてくることがあります。PySideはこれらの役割の分類をroleという概念で管理しています。
例)
DisplayRole
: テキストデータでメインの文字列表示に使われるToolTipRole
: ツールチップ(マウスをホバーした際に出る補助テキスト)の表示に使われるBackgroundRole
: 背景の描画に使われるForegroundRole
: 文字の色などに使われる
引数index
は、ここではリストの各アイテムに相当し、「自分が何行何列目のアイテムか」といった情報を持っています。つまりこのメソッドでは 「x行目のテキスト(DisplayRole
)はなんですか」というメッセージに対して、modelのデータが持っているx番目の"name"
の情報を返すという処理(24行目)を行い、 「x行目の文字色(ForegroundRole
)はなんですか」というメッセージに対しては、"color"
の情報からQColor
オブジェクトを生成して返す処理(28行目)を行っています。
またドキュメントには、編集可能なmodelにする際はsetData
、flags
メソッドを実装するようにとも書かれています。setData
メソッドは変更後の値を受け取り、実際のデータを書き換える処理をし、flags
メソッドは各index
が成り得る状態を定義するためのものです。これらを下のように実装してみました。
# データの変更時の処理 def setData(self, index, value, role = QtCore.Qt.EditRole): if not index.isValid() or not 0 <= index.row() < len(self.__items): return False if role == QtCore.Qt.EditRole and value != "": self.__items[index.row()]["name"] = value self.dataChanged.emit(index, index) return True else: return False # 各セルのインタラクション def flags(self, index): return QtCore.Qt.ItemIsSelectable | QtCore.Qt.ItemIsEnabled | QtCore.Qt.ItemIsEditable
setData
メソッドの引数はdata
メソッドに加えvalue
が追加されています。ここに変更後の値が入っています。行っていることは単純で、role
がEditRole
のときはvalue
の値を__items
の適切な位置に格納しています。そして、viewを更新するためdataChanged
シグナルを送信しています。
コード全体はこんな感じになります。
#-*- coding:utf-8 import sys from PySide import QtCore, QtGui class CustomListModel(QtCore.QAbstractListModel): # データを受け取りitemsに格納する def __init__(self, parent = None, data = []): super(CustomListModel, self).__init__(parent) self.__items = data # アイテムの数を返す def rowCount(self, parent = QtCore.QModelIndex()): return len(self.__items) # Roleに合わせてデータを返す def data(self, index, role = QtCore.Qt.DisplayRole): if not index.isValid(): return None if not 0 <= index.row() < len(self.__items): return None if role == QtCore.Qt.DisplayRole: return self.__items[index.row()].get("name") elif role == QtCore.Qt.ForegroundRole: color = self.__items[index.row()].get("color", []) return QtGui.QColor(*color) else: return None # データの変更時の処理 def setData(self, index, value, role = QtCore.Qt.EditRole): if not index.isValid() or not 0 <= index.row() < len(self.__items): return False if role == QtCore.Qt.EditRole and value != "": self.__items[index.row()]["name"] = value self.dataChanged.emit(index, index) return True else: return False # 各セルのインタラクション def flags(self, index): return QtCore.Qt.ItemIsSelectable | QtCore.Qt.ItemIsEnabled | QtCore.Qt.ItemIsEditable def main(): app = QtGui.QApplication(sys.argv) data = [ {"name":"Lion","color":[237,111,112]}, {"name":"Monkey","color":[127,197,195]} ] myListModel = CustomListModel(data = data) myListView = QtGui.QListView() myListView.setModel(myListModel) myListView.show() sys.exit(app.exec_()) if __name__ == '__main__': main()
実行するとこのような、文字に色のついたリストができます。
おわりに
いかかでしたでしょうか。今回はmodel/viewの概念と、簡単なサンプルプログラムを紹介してみました。このレベルのリストならmodel/viewを使わなくてもできるのですが、この機構を使えばさらに発展したリストやテーブルを作ることができます。また、delegateと組み合わせることで、さまざまな表現が可能になります(delegateの解説はまた次回…)。
今回は長くなってきましたのでこの辺で留めておきますが、model/view、delegateを使えば、一番上のデザイン画像を元に、このようなオリジナルリストを作ることができました。
お知らせツールとかに使えそうですね。私のデザインセンスがイケているかどうかはさておき、model/viewのパワフルさは伝わったかなと思います。次回はdelegateを使って実際にこのようなオリジナルリストを作成する方法を紹介したいと思います。
では、お楽しみに。
※免責事項※
本記事内で公開している全ての手法・コードの有用性、安全性について、当方は一切の保証を与えるものではありません。
これらのコードを使用したことによって引き起こる直接的、間接的な損害に対し、当方は一切責任を負うものではありません。
自己責任でご使用ください。
コメント
コメントフォーム