メインコンテンツまでスキップ

TableViewの編集画面でDialogを使用する

前回の をさらに拡張して
TableView の編集画面で、Dialog を表示して編集する機能を追加してみます。

TableView のセルは、Widgets オブジェクトの場合はセルの中にぴたっとはまるように
Widgets が作成作成されます。
しかし、TextEdit のように長い文章を編集したい場合などは
セルの中に Widgets が埋め込まれてしまうととてつもなく編集がしづらくなってしまいます。

ので、今回は、セルをクリックすると編集用 Dialog を表示して、セルの中身を編集してくれるようにします。

https://snippets.cacher.io/snippet/ac2de40c53468c8a2f94

全コードはながいので ↑ にアップしました。

解説

Dialog を作る

class TextEditDialog(QtWidgets.QDialog):

def __init__(self, uiPos, parent=None):
super(TextEditDialog, self).__init__(parent)
layout = QtWidgets.QVBoxLayout()
self.textEdit = QtWidgets.QTextEdit(self)
layout.addWidget(self.textEdit)
self.setLayout(layout)

btn = QtWidgets.QPushButton("Close")
btn.clicked.connect(self.close)
layout.addWidget(btn)

self.uiPos = uiPos

self.setWindowFlags(QtCore.Qt.Popup)

def showEvent(self, event):
self.setGeometry(self.uiPos.x(), self.uiPos.y(), 400, 300)
super(TextEditDialog, self).showEvent(event)

まず、クリックしたときに表示される Dialog を作成します。
基本は、QDialog に対して TextEdit を配置しているだけですが2つポイントがあります。

1 つ目が、 setWindowFlags を使用して  Popup  扱いの Window にしています。
こうすると、Window の上のバーの部分がなくなるのと、Window が選択から外れると Window がクローズするように
なります。
2つめが、showEvent でデフォルトのウィンドウ配置位置を指定しているところ。

今回は、クリックしたセルの上に表示したいので
起動時に Window 配置位置を引数で指定しているのですが
initの中に setGeometry を入れてもうまく動きませんでした。

かわりに、showEvent をオーバーライドして、 setGeometry で配置したい位置を
セットするようにしています。

UI を呼び出す

class TableDelegate(QtWidgets.QItemDelegate):

def __init__(self, parent=None):
super(TableDelegate, self).__init__(parent)

def createEditor(self, parent, option, index):
"""
編集したいCellに対して、編集用のWidgetsを作成する
"""
if index.column() == 0:
return QtWidgets.QLineEdit(parent)

if index.column() == 2:
spin = QtWidgets.QSpinBox(parent)
spin.setMinimum(0)
spin.setMaximum(100)
return spin
if index.column() == 3:
edit = TextEditDialog(parent.mapToGlobal(option.rect.topLeft()), parent)
return edit

作成した UI を呼び出すには、 createEditor で UI オブジェクトを作成し
return で返すようにします。

UI の表示位置は、編集したいセルの左上にぴったり合わせるように表示したいので
option.rect.topLeft() を使用して座標を取得しています。

しかし、このままだと TableView のローカル座標扱いになってしまい
変な位置に表示されてしまいます。

ので、正しい位置に表示されるように parent.mapToGlobal(~~~) を使用して
グローバル座標の数値を取得し、Dialog に対して表示位置の QPoint をセットするようにしています。

値をセットする

Model 内の関数として、↓ を作成

    def setData(self, index, value, role=QtCore.Qt.EditRole):

if role == QtCore.Qt.EditRole:
index.data(QtCore.Qt.UserRole).setData(index.column(), value)
self.headerDataChanged.emit(QtCore.Qt.Vertical, index.row(), index.row())
self.dataChanged.emit(index, index)
    def setModelData(self, editor, model, index):
"""
編集した値を、Modelを経由して実体のオブジェクトに対してセットする
"""

value = None

if index.column() == 0:
value = editor.text()

if index.column() == 2:
value = editor.value()

if index.column() == 3:
value = editor.textEdit.toPlainText()

if value is not None:
model.setData(index, value)

Dialog を閉じると、setModelData 関数が呼ばれます。
ので、あとはほかの Widget と同じく editor(これは Widget のオブジェクト)を
使用して、値を取得し、モデルに対してセットします。

    def sizeHint(self, option, index):

if index.column() == 0:
return QtCore.QSize(100, 30)

if index.column() == 2:
return QtCore.QSize(200, 30)

if index.column() == 3:
document = QtGui.QTextDocument(index.data())
document.setDefaultFont(option.font)
return QtCore.QSize(document.idealWidth() + 50, 15 * (document.lineCount() + 1))

sizeHint で指定をしておけば、入力した行に応じてセルのサイズを変更できるようになります。
このあたりの説明は に説明がありますので
そちらを参照してください。

補足

今回は TextEdit を使用しただけですが
この TableView から呼ばれる Dialog は、通常の UI と同じく好きに Widget を配置することができます。
なので、いろいろいい感じにセットするような GUI を作り、
setModelData で、その Dialog からいい感じに情報を取得して
Model にセットするようにすれば
TableView の各セルをがっつり編集する Dialog を作ることなんかもできます。

Delegate はいろいろ拡張すると好き放題できるのが良いですね。