PyQtでのMVC その3 (データ追加・ドックウインドウ)
データを追加をする機能を追加する
その2では削除機能を追加しましたので、次はデータを追加する機能を追加します。
要領はデータの削除と同じ
データを追加するためには、以下の手続が必要になります。
- データを追加するためのインタフェースを用意する
- 1.で得られたデータをModelに追加してもらう
1. データを追加するためのインタフェースを用意する
追加可能なデータが入力できるのであれば何でも良いので、GUIである必要もありません。 標準入力でも良いし、ネットワークからの入力でも良いわけです。 しかしせっかくなのでここはPyQtらしく、GUIの入力インタフェースを用意します。
入力用のGUIを作る方針は沢山あるので悩みどころです。 すぐ思いつくものでは、ダイアログからの入力や、入力用のドックウインドウを用意するといった方法が挙げられます。
2. 1.で得られたデータをModelに追加してもらう
ここは前回の削除処理に非常に似ています。
実際に行を追加をする処理なので、Modelに追加処理のメソッドを追加します。 ここで重要なのは、Viewがどのタイミングで表示を更新するかを判断するために、 Modelが管理しているデータが実際に追加される前に、データの追加位置をViewに伝える必要があります。 さらに、追加完了時に、追加が完了したということもViewに伝える必要があります。 まとめると以下のようになります。
- Viewに対する追加するデータ位置の通知: Modelの
beginInsertRows()
を呼ぶ - 実際のデータ追加処理
- Viewに対するデータ追加完了通知: Modelの
endInsertRows()
を呼ぶ
サンプル
前回のサンプルから拡張しています。 「追加」ボタンを押すと入力用のドックウインドウが表示され、 トッピング、麺、温・冷を選択して「確定」ボタンを押すと、データが追加されます。
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() 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 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) 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()
ドックウインドウについて
ドックウインドウを追加するには、QDockWidget
インスタンスを生成してから、
QMainWindow
のインスタンスのaddDockWidget()
の引数に渡します。
サンプルでは1つのドックウインドウを作りましたが、
IDEやオーサリングツールの画面のように、役割別のQDockWidget
を用意するといった用途などがあります。
ドックウインドウの特性として、必要に応じて表示非表示ができるので、画面スペースを有効に使うことができます。
プログラミングデザインについても少し
依存関係を弱めるために、入力用画面は現在の文字列ををただ返却していることと、 モデルを操作する処理は全てMainWindowが行っていることに注目してください。
ドックインドウを表示するためのQDockWidget
を継承してウィジェットを配置するよりも、
より汎用性のあるQWidget
にウィジェットを配置することで、
QDockWidget
以外の、例えばQDialog
へ表示するための変更が容易になります。