bravo's blog

マルチプラットフォームGUIツールキット+軽量言語であるPyQtのプログラミングとか | Pythonのお仕事も募集してます(twitterからDMできます)

PyQtでのMVC その2 (データ削除・ツールバー)

QTreeViewでの表示

その1ではQTableViewを使った表示をしました。このViewをQTreeViewに変えてみます。 QTreeView木構造のデータモデルを表示するのに適しますが、RDBのようなレコードを持つ構造にも適しています。 QTreeViewでは、データがノード単位で選択されます。ここでは「ノード単位 = 行単位」と考えてください。

通常、QTreeViewでは子ノードの表示を開閉するためのビュレット(三角形や[+]のようなグラフィック)がついていますが、 今回は子ノードの概念は無いので、描画しないように処理を追加します。Viewクラスの内容は以下のようになります。

class View(QTreeView):
    def __init__(self, parent=None):
        super(View, self).__init__(parent)
        self.setItemsExpandable(False)
        self.setIndentation(0)

    def drawBranches(self, painter, rect, index):
        return

f:id:bravo:20160107231206p:plain

このViewクラスの__init__()では、開閉処理ができないようにsetItemsExpandable(False)を呼び、 表示時のインデントも不要なのでsetIndentation(0)も呼んでいます。また、drawBranches()をオーバライドし、すぐにreturnさせることで、展開用のビュレットを描画しないようにしておきます。

f:id:bravo:20160107231206p:plain

ヘッダの内容を変える

現在ヘッダ情報はModelからは何も返していないので、デフォルトの列番号が表示されています。 列番号でなく、任意の値を表示するためには、ModelのクラスのheaderData()を実装します。

headerData()では、第4引数の値roleから、返すべきデータを判断します。 例えば、roleの値がQt.DisplayRoleである場合、データの内容を返すようにします。 詳細についてはこの記事の最後にあるサンプルに記載します。

※前回出てきたdata()では第5引数がroleに該当します。仕組みも前述のheaderData()と同様です。

f:id:bravo:20160108213607p:plain

データの削除をする機能の追加

表示処理はひとまず終わらせて、次に実装が簡単なデータの削除機能を追加します。

データの削除機能

データを削除するためには、以下の手続が必要になります。

  1. Viewで削除したいデータを選ぶ
  2. 1.で選んだインデックスを調べる
  3. 2.で得たインデックスのデータをModelに削除してもらう

1. Viewで削除したいデータを選ぶ

Viewから削除したいデータを選択状態にするだけなので、特に処理を追加するような部分はありませんが、 使い勝手の向上のため、Viewのクラスの初期化で以下のコードを1行追加して一度に複数行選択できるようにします。

self.setSelectionMode(QAbstractItemView.ExtendedSelection)

2. 1.で選んだインデックスを調べる

ViewのselectedIndexes()を呼び、選択されているインデックスの値を全て返してもらいます。 返却される値はQModelIndexインスタンスのシーケンスになっており、それぞれ行と列が格納されています。 全ての値から、列の値が0のものを選んで、行の値を追加します。これで選択されている行の値が全て揃いました。

3. 2.で得たインデックスのデータをModelに削除してもらう

実際に行を削除をする処理なので、Modelに削除処理のメソッドを追加します。 ここで重要なのは、Viewがどのタイミングで表示を更新するかを判断するために、 Modelが管理しているデータが実際に削除される前に、データの削除位置をViewに伝える必要があります。 さらに、削除完了時に、削除が完了したということもViewに伝える必要があります。 まとめると以下のようになります。

  1. Viewに対する削除するデータ位置の通知: ModelのbeginRemoveRows()を呼ぶ
  2. 実際のデータ削除処理
  3. Viewに対する削除完了通知: ModelのendRemoveRows()を呼ぶ

実際の削除処理では、整合性を保つために行インデックスの数値の大きい順から削除します。

サンプル

ここまで説明した部分を実際に実装したものです。 削除を実行するためのボタンはツールバーを用意し、その上に設置しました。

複数選択できます。

f:id:bravo:20160108213614p:plain

削除ボタンを押すと選択部分が削除されます。

f:id:bravo:20160108213617p:plain

import sys

from PyQt5.QtCore import *
from PyQt5.QtGui import *
from PyQt5.QtWidgets import *

class Model(QAbstractItemModel):
    headers = 'トッピング', 'うどん/そば', '温/冷'
    def __init__(self, parent=None):
        super(Model, self).__init__(parent)
        self.items = [
            ['たぬき','そば','温'],
            ['きつね','うどん','温'],
            ['月見','うどん','冷'],
            ['天ぷら','そば','温'],
            ]

    def index(self, row, column, parent=QModelIndex()):
        return self.createIndex(row, column, None)

    def parent(self, child):
        return QModelIndex()

    def rowCount(self, parent=QModelIndex()):
        return len(self.items)

    def columnCount(self, parent=QModelIndex()):
        return len(self.headers)

    def data(self, index, role=Qt.DisplayRole):
        if role == Qt.DisplayRole:
            try:
                return self.items[index.row()][index.column()]
            except:
                return
        return

    def headerData(self, section, orientation, role=Qt.DisplayRole):
        if role != Qt.DisplayRole:
            return
        if orientation == Qt.Horizontal:
            return self.headers[section]

    def removeRows(self, rowIndexes):
        for row in sorted(rowIndexes, reverse=True):
            self.beginRemoveRows(QModelIndex(), row, row + 1)
            del self.items[row]
            self.endRemoveRows()

class View(QTreeView):
    def __init__(self, parent=None):
        super(View, self).__init__(parent)
        self.setItemsExpandable(False)
        self.setIndentation(0)
        self.setSelectionMode(QAbstractItemView.ExtendedSelection)

    def drawBranches(self, painter, rect, index):
        return

class MainWindow(QMainWindow):
    def __init__(self, parent=None):
        super(MainWindow, self).__init__(parent)

        self.view = View(self)
        self.model = Model(self)
        self.view.setModel(self.model)
        self.setCentralWidget(self.view)

        toolBar = QToolBar()
        delButton = QPushButton('削除')
        delButton.clicked.connect(self.removeItems)
        toolBar.addWidget(delButton)
        self.addToolBar(toolBar)

    def selectedRows(self):
        rows = []
        for index in self.view.selectedIndexes():
            if index.column() == 0:
                rows.append(index.row())
        return rows

    def removeItems(self):
        self.model.removeRows(self.selectedRows())

def main():
    app = QApplication(sys.argv)
    w = MainWindow()
    w.show()
    w.raise_()
    app.exec_()

if __name__ == '__main__':
    main()

PR