bravo's blog

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

PyQtでのMVC その3 (データ追加・ドックウインドウ)

データを追加をする機能を追加する

その2では削除機能を追加しましたので、次はデータを追加する機能を追加します。

要領はデータの削除と同じ

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

  1. データを追加するためのインタフェースを用意する
  2. 1.で得られたデータをModelに追加してもらう

1. データを追加するためのインタフェースを用意する

追加可能なデータが入力できるのであれば何でも良いので、GUIである必要もありません。 標準入力でも良いし、ネットワークからの入力でも良いわけです。 ただし、ここはPyQtがメインなので、GUIの入力インタフェースを用意します。

入力用のGUIを作る方針は沢山あるので悩みどころです。 すぐ思いつくものでは、ダイアログからの入力や、入力用のドックウインドウを用意するといった方法が挙げられます。

2. 1.で得られたデータをModelに追加してもらう

ここは前回の削除処理に非常に似ています。

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

  1. Viewに対する追加するデータ位置の通知: ModelのbeginInsertRows()を呼ぶ
  2. 実際のデータ追加処理
  3. Viewに対するデータ追加完了通知: ModelのendInsertRows()を呼ぶ

サンプル

前回のサンプルから拡張しています。 「追加」ボタンを押すと入力用のドックウインドウが表示され、 トッピング、麺、温・冷を選択して「確定」ボタンを押すと、データが追加されます。

f:id:bravo:20160113190812p: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()


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へ表示するための変更が容易になります。

PR