PySideでリストをカスタマイズするぞ! ~応用編「delegate」~
2015/4/13
みなさんこんにちは。TDの小野です。今回もPySideのお話。
前回、投稿してからかなり間が空いてしまいましたが、引き続きPySideのmodel、view、delegateについて、サンプルプログラムを用いながら解説していきます。
ちょっとおさらい
前回の「PySideでリストをカスタマイズするぞ! ~基礎編「model/viewアーキテクチャ」~」では、model、viewの仕組みを解説しながら、サンプルリストを作成しました。作成したリストがこれです。
このときmodelは文字と色の情報を持っていました(下図左)。そして、viewに対して「文字はこれだよ」、「色はこれだよ」とそれらの情報を渡して、viewが表示していました(下図右)。
今回はviewに渡した情報を、好きなよ~にカスタマイズして表示する方法を解説します。
各アイテムの中に文字だけじゃなくて、サムネイル画像なども入っていますね。さて、どうやって作りましょうか…。ここからがdelegateの出番です。
delegateの役割
上のデザインのように各アイテムの表示を細かくカスタマイズする際、活躍するのがdelegateです。また、テーブルやリストを編集する際にアイテムをダブルクリックして出てくるテキストボックスなどの表示や、編集後、入力されたデータをmodelに渡すのもdelegateの仕事です。QTableView
やQListWidget
もデフォルトではQStyledItemDelegate
というdelegateが内部でその役割を担っています。そこでオリジナルのdelegateを作ってリストの表示に使ってみましょう。
オリジナルのdelegateを作ってみる
前回のサンプルリストで使ったデータは以下のようなものでした。
data = [ {"name": "Lion", "color": [237,111,112]}, {"name": "Monkey", "color": [127,197,195]} ]
これから作るリストでは、modelが保持しているデータはこのままで、表示上だけ「Name: ○○」と表示されるものを作ってみます。まずは以下のようにQStyledItemDelegate
を継承して、シンプルなオリジナルdelegateクラスを作成してみましょう。
class CustomListDelegate(QtGui.QStyledItemDelegate): def __init__(self, parent=None): super(CustomListDelegate, self).__init__(parent) def paint(self, painter, option, index): # indexからデータを取り出す name = index.data(QtCore.Qt.DisplayRole) color = index.data(QtCore.Qt.ForegroundRole) # ペンを作って持たせる pen = QtGui.QPen(color, 0.5, QtCore.Qt.SolidLine) painter.setPen(pen) # テキストを描く painter.drawText(option.rect, QtCore.Qt.AlignVCenter|QtCore.Qt.AlignLeft, "Name : " + name)
最低限必要なのはこのpaint
メソッドです。このメソッドがdelegate内での描画を主に担当します。
paintメソッドは以下の3つの引数を取り、これらの情報を用いてテキストを表示させます。
painter
(QtGui.QPainter
) : お絵かきさんです。絵を描くのに特化したクラス。option
(QtGui.QStyleOptionViewItem
) : viewの各アイテムの情報を持っているクラス。各アイテムの表示されているサイズや、そのアイテムが選択状態かどうかなどの情報を持っています。index
(QtCore.QModelIndex
) : modelが持っている各アイテムの情報が入っています。
では上のコードの解説を。まずは、名前と色の情報を取得する必要があるので、index
からそれらを取得します。必要な情報のRole(役割)をdata
メソッドに渡すと値が取得できます。前回作成したmodelでは、DisplayRole
として名前情報、ForegroundRole
として色情報を返すようにしているので、9行目、10行目のように取得します。
あとはお絵かきさんにペンを持たせて描かせるだけです。13行目で色や太さ、線の種類を指定して、ペンを作っています。そして、14行目でそのペンをpainter
にセットしています。17行目が実際にテキストを描画しているところです。このdrawText
メソッドに文字列を渡してテキストを表示するのですが、このときに「Name : 」という接頭辞をつけています。描画の範囲を決めているのがoption.rect
(17行目) です。ここにはQRect
というクラスを渡すのですが、そのポジションや大きさを変えれば描画の位置を変更できます。
では、delegateができたので、ListViewにセットしましょう。
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) # デリゲートを生成 myListDelegate = CustomListDelegate() # 作ったdelegateをListViewにセット myListView.setItemDelegate(myListDelegate) myListView.show() sys.exit(app.exec_())
7行目で使っているCustomListModel
は前回作成したオリジナルmodelです。
11行目でオリジナルdelegateを作成し、myListView
のdelegateとして13行目でセットしています。
実行すると以下のように表示されます。
もっとカスタマイズしてみる
では次に、選択したときに背景の色が変わるようにしてみます。また、各セルの高さも変えてみます。
アイテムが選択状態かどうかはpaint
メソッドのoption
引数が持っています。選択状態のときだけ、背景を描画するように変更しましょう。
def paint(self, painter, option, index): # stateプロパティがセレクトかどうかチェック if option.state & QtGui.QStyle.State_Selected: # 背景を描く bgBrush = QtGui.QBrush(QtGui.QColor(60,60,60)) bgPen = QtGui.QPen(QtGui.QColor(60,60,60), 0.5, QtCore.Qt.SolidLine) painter.setPen(bgPen) painter.setBrush(bgBrush) painter.drawRect(option.rect) # indexからデータを取り出す name = index.data(QtCore.Qt.DisplayRole) color = index.data(QtCore.Qt.ForegroundRole) # ペンを作って持たせる pen = QtGui.QPen(color, 0.5, QtCore.Qt.SolidLine) painter.setPen(pen) # テキストを書く painter.drawText(option.rect, QtCore.Qt.AlignVCenter|QtCore.Qt.AlignLeft, "Name : " + name)
4行目が選択状態を判断している部分で、選択状態であれば6から10行目で背景を描いています。
また、セルの高さを変更するためにはQStyledItemDelegate
のsizeHint
メソッドをオーバーライドします。
class CustomListDelegate(QtGui.QStyledItemDelegate): def __init__(self, parent=None): """省略""" def paint(self, painter, option, index): """省略""" def sizeHint(self, option, index): return QtCore.QSize(100, 40)
これを実行すると以下のようにアイテムの高さが高く、選択すると色が変わるリストが表示されます。
画像を表示してみる
最後に、アイテム内に画像を表示してみましょう。まずはセル内で描画する位置をなんとなーく考えます。今回は下図のように左側に画像が来てその横にテキストが表示されるように作ってみます。

まずはmodelに渡すデータに画像の情報を入れます。「thumbnail
」というキーに画像ファイル名を入れておきます。
data = [ {"name":"Lion", "color":[237,111,112], "thumbnail":"lion.png"}, {"name":"Monkey", "color":[127,197,195], "thumbnail":"monkey.png"} ]
modelも少し書き換えないといけません。前回のCustomListModel
を修正し、data
メソッドでthumbnail
情報を返せるようにしておきます。
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) # Thumbnailキーの画像ファイル名を返す elif role == QtCore.Qt.UserRole: return self.__items[index.row()].get("thumbnail", "") else: return None # 各セルのインタラクション def flags(self, index): return QtCore.Qt.ItemIsSelectable | QtCore.Qt.ItemIsEnabled
QtCore.Qt.UserRole
はPySideが用意している自由に使えるRoleです。これで、delegate内でindex.data(QtCore.Qt.UserRole)
のように取得するとthumbnail
の値が取得できます。
あとはdelegateのpaint
メソッドで画像の表示位置やテキストの表示位置を決め、描画していきます。
class CustomListDelegate(QtGui.QStyledItemDelegate): def __init__(self, parent=None): """省略""" def paint(self, painter, option, index): # 画像のサイズとマージンの定義 THUMB_WIDTH = 60 MARGIN = 5 # stateプロパティがセレクトかどうかチェック if option.state & QtGui.QStyle.State_Selected: """省略""" # indexからデータを取り出す name = index.data(QtCore.Qt.DisplayRole) color = index.data(QtCore.Qt.ForegroundRole) # 画像ファイル名を取ってくる thumbName = index.data(QtCore.Qt.UserRole) # Pixmapオブジェクトに変換 thumbImage = QtGui.QPixmap(os.path.join(CURRENT_PATH, "images", thumbName)).scaled(THUMB_WIDTH, THUMB_WIDTH) # 画像の表示場所を指定 r = QtCore.QRect(option.rect.left(), option.rect.top(), THUMB_WIDTH, THUMB_WIDTH) painter.drawPixmap(r, thumbImage) # ペンを作って持たせる """省略""" # テキストを書く r = QtCore.QRect(option.rect.left()+THUMB_WIDTH+MARGIN, option.rect.top(), option.rect.width()-THUMB_WIDTH-MARGIN, option.rect.height()) painter.drawText(r, QtCore.Qt.AlignVCenter|QtCore.Qt.AlignLeft, "Name : " + name) def sizeHint(self, option, index): return QtCore.QSize(100, 60)
まずは8行目で画像を表示させるサイズを定義しています。後でポジションの計算などいろいろな場所で使うので、このようにどこかに定義しておいたほうがいいです。そして、19行目でmodelインデックスのデータから画像ファイル名を取得し、次の行で、実ファイルパスに変換(スクリプトのフォルダ内にimagesというフォルダがあり、その中に画像がある状態です。)後、PySideで画像を扱うオブジェクトであるQPixmap
を作っています。
画像の表示位置を指定するQRect
を作成しているのが23行目です。その後painter
のdrawPixmap
メソッドで画像を描いています。
テキストの表示位置も同様に変更しなければなりません。THUMB_WIDTH
とMARGIN
でどれだけ右にズラすかを計算しています。32行目はズラしたぶんだけ表示範囲を狭める計算をしています。
これを実行すると以下のようなリストが表示されます。
いかがでしょうか。少しは初めの手書きデザインに近づいてきたのではないでしょうか。
あとはpaint
メソッド内で各データの位置の指定と描画の繰り返しです。根気あるのみです!
最終的に僕はこんな感じのリストを作りました。
ソースコードはこちら。→ダウンロード
ぜひ試してみてください。
おわりに
はじめは取っ掛かりにくいmodel、view、delegateですが、仕組みを理解しておくだけでも開発の柔軟性が格段に上がります。
また、このような概念は今流行のモバイルアプリやウェブアプリなどでもデータ管理の手法として取り入れられていますので、他の分野へも応用していけます。
delegate自体は今回解説した機能以外にもユーザーとのやり取りを担当したりと、奥の深いクラスです。そのあたりはまた機会があれば紹介していきたいと思います。
では、また次回。
※免責事項※
本記事内で公開している全ての手法・コードの有用性、安全性について、当方は一切の保証を与えるものではありません。
これらのコードを使用したことによって引き起こる直接的、間接的な損害に対し、当方は一切責任を負うものではありません。
自己責任でご使用ください。
コメント
コメントフォーム