PyQtでのMVC その4 (データ編集)
データの編集ができるようにする
これまで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
であるインスタンスを返すことになっており、
データが編集できる機能が期待されます。
(よく使われるものでは、QTextEdit
やQComboBox
といったクラスが挙げられます。)
編集前のデータは次のsetEditorData()
であらかじめ入力するようになっています。
setEditorData()
では、createEditor()
が生成したインスタンスと、
データのインデックスが引数として渡されてきます。
編集前のデータをインデックスを手掛かりに取り出し、
createEditor()
で生成したインスタンスにセットしておきます。
setModelData()
では、createEditor()
が生成したインスタンスと、Modelのインスタンスと、
データのインデックスが引数として渡されてきます。
この時点では編集が完了した状態でcreateEditor()
が生成したインスタンスが渡されます。
ほとんどの場合は、ModelのsetData()
の呼び出しで、引数に編集完了したデータをそのまま渡し、Modelに更新してもらいます。
まとめると、Viewからあらかじめデータがセットされたエディタを都度出現させ、 エディタで編集し、役目が終わるとエディタの情報からModelにデータを反映させるというサイクルになります。
サンプル
このサンプルでは、編集したいセルをダブルクリックすると、QTextEdit
のインスタンスを表示させ、
自由に編集できようになっています。まずはコードの単純さを優先ということで次回に続けようと思います。
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()
PyQtでのMVC その5 (データ編集のつづき)
編集機能の変更サンプル
前回のサンプルでは編集用のエディタが素のQTextEdit
でしたが、
整合性の観点から、QComboBox
を使ったプルダウンで選択する方式に変えておきます。
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): toppings = 'きつね', 'たぬき', '天ぷら', '月見', '肉', 'カレー' noodles = 'うどん', 'そば' hotcold = '温', '冷' columns = toppings, noodles, hotcold def __init__(self, parent=None): super(InputWidget, self).__init__(parent) layout = QVBoxLayout() self.toppingInput = InputWidget.comboBox(InputWidget.toppings) layout.addWidget(self.toppingInput) grpbox, self.noodles = InputWidget.radioButtons(InputWidget.noodles) layout.addWidget(grpbox) grpbox, self.hotcold = InputWidget.radioButtons(InputWidget.hotcold) layout.addWidget(grpbox) self.addButton = QPushButton('確定') layout.addWidget(self.addButton) layout.addStretch() self.setLayout(layout) @staticmethod def comboBox(values): comboBox = QComboBox() for value in values: comboBox.addItem(value) return comboBox @staticmethod def radioButtons(values): grpbox = QGroupBox() layout = QHBoxLayout() buttons = [] for value in values: rb = QRadioButton(value) layout.addWidget(rb) buttons.append(rb) buttons[0].setChecked(True) grpbox.setLayout(layout) return grpbox, buttons def values(self): topping = self.toppingInput.currentText() udonsoba = '?' for btn in self.noodles: 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): editor = InputWidget.comboBox(InputWidget.columns[index.column()]) editor.setParent(parent) return editor def setEditorData(self, editor, index): value = index.model().data(index, Qt.DisplayRole) editor.setCurrentIndex(editor.findText(value)) def setModelData(self, editor, model, index): model.setData(index, editor.currentText()) 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()
プログラムの変更点
- 編集用のエディタに使うクラスを
QTextEdit
からQComboBox
にする - 追加と編集の機能で使えるデータをクラスフィールドで定義する
- 追加または編集で、複数の選択肢から一つ選択するとき、表示の都合にあわせてラジオボタンかプルダウンのどちらかを表示する