読者です 読者をやめる 読者になる 読者になる

bravo's blog

マルチプラットフォームGUIツールキット+軽量言語であるPyQtのプログラミングとか

PyQtでのMVC その4 (データ編集)

PyQt PyQt5 Python Python3

データの編集ができるようにする

これまでCRUD処理の、Read、Delete、Createを作りました。 残りのUであるUpdate機能を追加します。

まず、このパートではデータのインデックスという言葉が頻出しますが、具体的にはQModelIndexインスタンスを指します。 以前のパートにも何度か出ていますが、 QModelIndexインスタンスにはModelから特定できるデータの位置情報が記録されており、 さらにこれ自体がどのModelを扱っているかがわかるようになっています。つまり、 QModelIndexのインスタンス1つでModelとデータの位置がわかるようになっています。

Modelのメソッドをオーバーライド

Modelクラスでは、編集情報を受け付けられるようにsetData()flags()の2つのメソッドをオーバーライドし、 編集処理に必要なコードを追加します。

setData()では、更新されるデータのインデックス、更新したいデータ、ロールが引数として渡されてきます。 ロールの値がQt.EditRoleである場合に、実際のデータを書き換えます。更新処理ができたら、Trueを返します。 それ以外はFalseを返します。

flags()では、データが更新可能なことを伝えるために、Qt.ItemIsEditableのビットを立てたフラグ情報を返します。

Viewに編集機能を加える

Viewでは直接データを編集することはできませんが、 Delegeteという仕組みを使うことで編集機能を実現しています。

Delegateの使い方は、Modelでのデータの入出力の方法にやや似ています。 QStyledItemDelegateを継承したDelegateクラスを作り、 ViewのsetItemDelegate()の引数に渡します。 DelegateクラスではcreateEditor()setEditorData()setModelData()の3つのメソッドをオーバーライドします。

createEditor()では、表示元のQWidgetインスタンス、書式などのスタイルオプション、 データのインデックスが引数として渡されてきます。 このメソッドでは基底クラスがQWidgetであるインスタンスを返すことになっており、 データが編集できる機能が期待されます。 (よく使われるものでは、QTextEditQComboBoxといったクラスが挙げられます。) 編集前のデータは次のsetEditorData()であらかじめ入力するようになっています。

setEditorData()では、createEditor()が生成したインスタンスと、 データのインデックスが引数として渡されてきます。 編集前のデータをインデックスを手掛かりに取り出し、 createEditor()で生成したインスタンスにセットしておきます。

setModelData()では、createEditor()が生成したインスタンスと、Modelのインスタンスと、 データのインデックスが引数として渡されてきます。 この時点では編集が完了した状態でcreateEditor()が生成したインスタンスが渡されます。 ほとんどの場合は、ModelのsetData()の呼び出しで、引数に編集完了したデータをそのまま渡し、Modelに更新してもらいます。

まとめると、Viewからあらかじめデータがセットされたエディタを都度出現させ、 エディタで編集し、役目が終わるとエディタの情報からModelにデータを反映させるというサイクルになります。

サンプル

このサンプルでは、編集したいセルをダブルクリックすると、QTextEditインスタンスを表示させ、 自由に編集できようになっています。まずはコードの単純さを優先ということで次回に続けようと思います。

f:id:bravo:20160117105954p: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 addRow(self, topping, menkind, hotcold):
        self.beginInsertRows(QModelIndex(), len(self.items), 1)
        self.items.append([topping, menkind, hotcold])
        self.endInsertRows()

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

    def flags(self, index):
        return super(Model, self).flags(index) | Qt.ItemIsEditable

    def setData(self, index, value, role=Qt.EditRole):
        if role == Qt.EditRole:
            self.items[index.row()][index.column()] = value
            return True
        return False

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 InputWidget(QWidget):
    def __init__(self, parent=None):
        super(InputWidget, self).__init__(parent)
        layout = QVBoxLayout()

        toppings = ('きつね', 'たぬき', 'てんぷら', '月見', '肉', 'カレー')
        self.toppingInput = QComboBox()
        for topping in toppings:
            self.toppingInput.addItem(topping)
        layout.addWidget(self.toppingInput)

        self.bgrp = QGroupBox()
        udon = QRadioButton('うどん')
        udon.setChecked(True)
        soba = QRadioButton('そば')
        btnlayout = QHBoxLayout()
        btnlayout.addWidget(udon)
        btnlayout.addWidget(soba)
        self.bgrp.setLayout(btnlayout)
        layout.addWidget(self.bgrp)
        self.udonsoba = udon, soba

        self.bgrp_temp = QGroupBox()
        hot = QRadioButton('温')
        hot.setChecked(True)
        cold = QRadioButton('冷')
        btnlayout_temp = QHBoxLayout()
        btnlayout_temp.addWidget(hot)
        btnlayout_temp.addWidget(cold)
        self.bgrp_temp.setLayout(btnlayout_temp)
        layout.addWidget(self.bgrp_temp)
        self.hotcold = hot, cold

        self.addButton = QPushButton('確定')
        layout.addWidget(self.addButton)

        layout.addStretch()

        self.setLayout(layout)

    def values(self):
        topping = self.toppingInput.currentText()

        udonsoba = '?'
        for btn in self.udonsoba:
            if btn.isChecked():
                udonsoba = btn.text()
                break

        hotcold = '?'
        for btn in self.hotcold:
            if btn.isChecked():
                hotcold = btn.text()
                break

        return topping, udonsoba, hotcold

class Delegate(QStyledItemDelegate):
    def __init__(self, parent=None):
        super(Delegate, self).__init__(parent)

    def createEditor(self, parent, option, index):
        return QLineEdit(parent)

    def setEditorData(self, editor, index):
        value = index.model().data(index, Qt.DisplayRole)
        editor.setText(value)

    def setModelData(self, editor, model, index):
        model.setData(index, editor.text())

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.view.setItemDelegate(Delegate())
        self.setCentralWidget(self.view)

        self.inputWidget = InputWidget()
        self.inputWidget.addButton.clicked.connect(self.addItem)
        self.addDock = QDockWidget('追加入力', self)
        self.addDock.setWidget(self.inputWidget)
        self.addDock.setAllowedAreas(Qt.AllDockWidgetAreas)
        self.addDockWidget(Qt.RightDockWidgetArea, self.addDock)
        self.addDock.hide()

        toolBar = QToolBar()
        self.addToolBar(toolBar)

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

        self.addButton = QPushButton('追加')
        self.addButton.clicked.connect(self.addDock.show)
        toolBar.addWidget(self.addButton)

    def addItem(self):
        self.model.addRow(*self.inputWidget.values())

    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()

広告