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

Header

Main

  • TOP
  • DF TALK
  • PySideでリストをカスタマイズするぞ! ~基礎編「model/viewアーキテクチャ」~

PySideでリストをカスタマイズするぞ! ~基礎編「model/viewアーキテクチャ」~

2014/10/20

Tag: ,,,,

はじめまして。最近TDチームに加わった小野です。
入って以来、PySideを使ってツール作りに励んでいるのですが、PySideって楽しいですね!今やPySideの虜です。

PySideは非常にツール開発しやすいフレームワークだと私自身思うのですが、たまに「これって何するクラス?」という時があります。そんな少しとっつきにくい機構のひとつがmodel/viewアーキテクチャではないでしょうか。これはデータのリスト、テーブル、ツリー構造などを表現するための概念なのですが、これを使わなくてもリストなどは作れます(厳密に言うと裏で使っていますが…)。しかし、この仕組みを理解することによって私自身、UI表現の幅が格段に上がりました。

そこで今日はmodel/view(とdelegate)の簡単な解説をしていきたいと思います。

きっかけ

リストやテーブルを使ったUIを作ったことがある方なら、QListWidgetQTableWidgetというクラスとQListViewQTableViewというクラスがあることに気づくと思います。PySideのドキュメントを見れば継承関係や、どう違うのかもきちんと解説されているのですが、初心者の私には「??…何が違うの?」という印象でした。少し調べて「Widgetの方が使いやすそうだな」ということでWidgetを採用したことを覚えています。
でも、QListWidgetQTableWidgetだと、できることが限られてくるんですよね…。

model/viewを使おうと思ったきっかけは、以下のようなUIを作ろうと思ったことでした。

リストのUIデザイン

リストのUIデザイン

「これ、リストで作りたいけど、リストウィジェットじゃ無理だよね…。」
というわけで少し調べてみると、こんなときは「modelとdelegateを使うといいよ」という文献があったので少し勉強することにしました。

model/viewアーキテクチャって?

model/viewアーキテクチャというのは、簡単に言うと「データの管理」と「表示」は別にしようという考え方です。
たとえば二つのリストがある場合、QListWidgetItemを使った構造では各リストに対してデータごとのアイテムインスタンスが作られます。この場合、片方のリストのデータを変更しても、もう片方のリストには反映されません(図左)。model/viewを使うと、2つのリストに対し、modelという共通のインスタンスを使用します。viewはこのmodelの情報を参照することで表示を行います。データの変更があった場合はその信号がmodelに伝わり、modelが情報を変更します(図右)。

model/viewの概念

model/viewの概念

こうすることで、アプリケーション全体でひとつのデータ構造を利用することができ、各view間の矛盾も無くなります。データを変更するたびに、「同じデータを表示しているWidgetすべてにSignalを出して…」ということもしなくて済み、メンテナンスもしやすくなります。

delegateはなにをするのか

オリジナルのリストを作るため、もうひとつ必要な概念がdelegateです。日本語では代理人とかいう意味で、ここでは各セルの表示や編集を担当します。例えば、「このセルは右寄せにするよ」とか、「このセルの編集にはコンボボックス使うよ」といった部分をコントロールします。つまり、ユーザー、model、viewの間のやり取りを代理で行ってくれます。これらの関係を簡単に図で示します。

ユーザーとmodel、view、delegateの関係性

ユーザーとmodel、view、delegateの関係性

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を作ってみましょう。このQAbstractListModelQAbstractItemModelを継承しており、リストの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にする際はsetDataflagsメソッドを実装するようにとも書かれています。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が追加されています。ここに変更後の値が入っています。行っていることは単純で、roleEditRoleのときは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を使って実際にこのようなオリジナルリストを作成する方法を紹介したいと思います。

では、お楽しみに。


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

Pocket

コメント

コメントはありません

コメントフォーム

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

*