株式会社デジタル・フロンティア-Digital Frontier

Header

Main

  • TOP
  • DF TALK
  • PySideでリストをカスタマイズするぞ! ~応用編「delegate」~

PySideでリストをカスタマイズするぞ! ~応用編「delegate」~

2015/4/13

Tag: ,,,

みなさんこんにちは。TDの小野です。今回もPySideのお話。
前回、投稿してからかなり間が空いてしまいましたが、引き続きPySideのmodel、view、delegateについて、サンプルプログラムを用いながら解説していきます。

ちょっとおさらい

前回の「PySideでリストをカスタマイズするぞ! ~基礎編「model/viewアーキテクチャ」~」では、model、viewの仕組みを解説しながら、サンプルリストを作成しました。作成したリストがこれです。

前回作成したリスト
 
このときmodelは文字と色の情報を持っていました(下図左)。そして、viewに対して「文字はこれだよ」、「色はこれだよ」とそれらの情報を渡して、viewが表示していました(下図右)。
今回はviewに渡した情報を、好きなよ~にカスタマイズして表示する方法を解説します。

model、viewの概念
 
目標はあくまでこんなリストです。

リストのUIデザイン
 
各アイテムの中に文字だけじゃなくて、サムネイル画像なども入っていますね。さて、どうやって作りましょうか…。ここからがdelegateの出番です。

delegateの役割

上のデザインのように各アイテムの表示を細かくカスタマイズする際、活躍するのがdelegateです。また、テーブルやリストを編集する際にアイテムをダブルクリックして出てくるテキストボックスなどの表示や、編集後、入力されたデータをmodelに渡すのもdelegateの仕事です。QTableViewQListWidgetもデフォルトでは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行目でセットしています。

実行すると以下のように表示されます。

オリジナルdelegateを使用したリスト
 

もっとカスタマイズしてみる

では次に、選択したときに背景の色が変わるようにしてみます。また、各セルの高さも変えてみます。
アイテムが選択状態かどうかは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行目で背景を描いています。
また、セルの高さを変更するためにはQStyledItemDelegatesizeHintメソッドをオーバーライドします。

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行目です。その後painterdrawPixmapメソッドで画像を描いています。
テキストの表示位置も同様に変更しなければなりません。THUMB_WIDTHMARGINでどれだけ右にズラすかを計算しています。32行目はズラしたぶんだけ表示範囲を狭める計算をしています。

これを実行すると以下のようなリストが表示されます。

サムネイルのあるリスト
 
いかがでしょうか。少しは初めの手書きデザインに近づいてきたのではないでしょうか。
あとはpaintメソッド内で各データの位置の指定と描画の繰り返しです。根気あるのみです!

最終的に僕はこんな感じのリストを作りました。

カスタムリスト
 
ソースコードはこちら。→ダウンロード
ぜひ試してみてください。

おわりに

はじめは取っ掛かりにくいmodel、view、delegateですが、仕組みを理解しておくだけでも開発の柔軟性が格段に上がります。
また、このような概念は今流行のモバイルアプリやウェブアプリなどでもデータ管理の手法として取り入れられていますので、他の分野へも応用していけます。
delegate自体は今回解説した機能以外にもユーザーとのやり取りを担当したりと、奥の深いクラスです。そのあたりはまた機会があれば紹介していきたいと思います。

では、また次回。


※免責事項※
本記事内で公開している全ての手法・コードの有用性、安全性について、当方は一切の保証を与えるものではありません。
これらのコードを使用したことによって引き起こる直接的、間接的な損害に対し、当方は一切責任を負うものではありません。
自己責任でご使用ください。

Pocket

コメント

コメントはありません

コメントフォーム

コメントは承認制ですので、即時に反映されません。ご了承ください。

*