QTableView 与 QWidget 作为 QStyledItemDelegate

QTableView with QWidget as QStyledItemDelegate

我正在编写一个 Python CRUD 应用程序,它在地图和 QTableView 上显示无线电探空仪。我正在使用 QStyledItemDelegate 为每一列设置一个编辑器和正则表达式验证器,它工作得很好。但对于几何列,我想解析二进制数据并将其显示在自定义表单(lat、lng、elevation)上,能够编辑它们,如果单击“确定”,则将它们编码回 WKB 格式并更新数据。

当我单击“确定”时,该字段没有更新,而是变成空的。如果我在此之后尝试编辑任何其他单元格,则不会发生任何事情,如果我尝试编辑那个确切的单元格,应用程序会崩溃。如果我单击“取消”,也会发生同样的情况。

setData 方法 returns True,数据库中的数据得到更新。

我尝试在 QSqlTableModel 上使用 dataChanged.emit() 并在 QTableView 上使用 update() 方法。

main2.py:

from PyQt5.Qt import QStyledItemDelegate
from PyQt5.QtWebEngineWidgets import QWebEngineView
from PyQt5.QtCore import Qt
from shapely import wkb, wkt
import folium
import io



class Ui_RadioSondes(object):

    def setupUi(self, RadioSondes):
        self.centerCoord = (44.071800, 17.578125)

        RadioSondes.setObjectName("RadioSondes")
.
.
.
        self.tableView_2.setItemDelegate(ValidatedItemDelegate())
.
.
.
    
class ValidatedItemDelegate(QStyledItemDelegate):
    def createEditor(self, widget, option, index):
        if not index.isValid():
            return 0
        if index.column() == 0: #only on the cells in the first column
            editor = QtWidgets.QLineEdit(widget)
            validator = QtGui.QRegExpValidator(QtCore.QRegExp('[\w]{1,10}'), editor)
            editor.setValidator(validator)
            return editor
        if index.column() == 2:
            editor = QtWidgets.QSpinBox(widget)
            editor.setMaximum(360)
            editor.setMinimum(1)
            return editor
.
.
.
        if index.column() == 9:
            self.form = QtWidgets.QWidget()
            self.formLayout = QtWidgets.QFormLayout(self.form)
            self.formLayout.setVerticalSpacing(12)
            self.formLayout.setObjectName("formLayout")
            ###__________ Latitude__________###
            self.latLabel = QtWidgets.QLabel(self.form)
            self.latLabel.setObjectName("latLabel")
            self.latLabel.setText("Latitude")
            self.latLabel.adjustSize()
            self.formLayout.setWidget(0, QtWidgets.QFormLayout.LabelRole, self.latLabel)
            self.latEdit = QtWidgets.QLineEdit(self.form)
            # lineEdit.textChanged.connect(validateFields)
            self.latEdit.setObjectName("latEdit")
            self.formLayout.setWidget(0, QtWidgets.QFormLayout.FieldRole, self.latEdit)
            ###__________ Longitude__________###
            self.lngLabel = QtWidgets.QLabel(self.form)
            self.lngLabel.setObjectName("lngLabel")
            self.lngLabel.setText("Longitude")
            self.lngLabel.adjustSize()
            self.formLayout.setWidget(1, QtWidgets.QFormLayout.LabelRole, self.lngLabel)
            self.lngEdit = QtWidgets.QLineEdit(self.form)
            # lineEdit.textChanged.connect(validateFields)
            self.lngEdit.setObjectName("lngEdit")
            self.formLayout.setWidget(1, QtWidgets.QFormLayout.FieldRole, self.lngEdit)
            ###__________ Elevation__________###
            self.elevationLabel = QtWidgets.QLabel(self.form)
            self.elevationLabel.setObjectName("elevationLabel")
            self.elevationLabel.setText("Elevation")
            self.elevationLabel.adjustSize()
            self.formLayout.setWidget(2, QtWidgets.QFormLayout.LabelRole, self.elevationLabel)
            self.elevationEdit = QtWidgets.QLineEdit(self.form)
            # lineEdit.textChanged.connect(validateFields)
            self.elevationEdit.setObjectName("elevationEdit")
            self.formLayout.setWidget(2, QtWidgets.QFormLayout.FieldRole, self.elevationEdit)

            self.buttonBox = QtWidgets.QDialogButtonBox(self.form)
            self.buttonBox.setOrientation(QtCore.Qt.Horizontal)
            self.buttonBox.setStandardButtons(QtWidgets.QDialogButtonBox.Cancel | QtWidgets.QDialogButtonBox.Ok)
            self.buttonBox.setObjectName("buttonBox")
            self.formLayout.addWidget(self.buttonBox)

            self.form.resize(200, 300)

            self.prevData = index.data()
            self.index = index
            self.widget = widget

            self.model = self.widget.parent().parent().parent().parent().parent().parent().parent().objModel
            self.t_view = self.widget.parent().parent().parent().parent().parent().parent().parent().tableView_2

            data = self.model.data(self.index)
            geomWkb = wkb.loads(bytes.fromhex(data))
            self.latEdit.setText(str(geomWkb.x))
            self.lngEdit.setText(str(geomWkb.y))
            self.elevationEdit.setText(str(geomWkb.z))

            self.buttonBox.accepted.connect(self.generateGeom)
            self.buttonBox.rejected.connect(self.cancelGeomEdit)
            return self.form
        return super(ValidatedItemDelegate, self).createEditor(widget, option, index)

    def generateGeom(self):
        print(self.latEdit.text())
        print(self.lngEdit.text())
        print(self.elevationEdit.text())

        geomStr = "POINT Z (" + self.latEdit.text() + " " + self.lngEdit.text() + " " + self.elevationEdit.text() + ")"
        geom = wkt.loads(geomStr)
        geomWkb = wkb.dumps(geom, hex=True, srid=4326)

        try:
            self.model.setData(self.index, geomWkb, Qt.EditRole)

            self.form.close()
            #self.t_view.update()
        except AssertionError as error:
            print(error)

    def cancelGeomEdit(self):
        self.form.destroy(destroyWindow=True)

这是 GitHub 上的完整代码:https://github.com/draugnim/pyCrud

编辑

我设法通过在 generateGeom() 和 cancelGeomEdit() 结束时调用 self.model.selet() 使其工作。但是,如果我单击 X 按钮并关闭表单,已编辑的单元格将变为空白,并且此单元格和所有其他单元格也将变得不可编辑。

项目委托使用编辑器的user属性,这被认为是Qt对象的主要默认属性。对于 QLineEdit,它是 text(),对于 QSpinBox,它是 value(),等等

如果您想提供一个自定义的高级编辑器,解决方案是创建一个子class 和一个 自定义 用户 属性。
请注意,Qt 处理项目数据的内部方式对类型有点严格,并且由于 PyQt 不公开 QVariant class,唯一的选择是使用合适的类型。对于您的情况,QVector3D 是一个完美的选择。

然后,使用 外部 window 有点棘手,因为代表通常应该是存在于 内部 视图。要解决此问题,必须考虑以下因素:

  • 编辑器必须通知代理插入的数据已被接受,并且它必须在关闭时自行销毁;
  • 必须在过滤器(return False)中忽略按键事件,以便编辑器能够正确处理;
  • 焦点和隐藏事件也必须忽略,因为默认情况下委托会尝试在编辑器未被“拒绝”但它失去焦点或隐藏时更新模型;
  • 必须使用父级的顶级 window() 设置几何图形,并且必须忽略 updateEditorGeometry(),因为每当视图在隐藏后再次显示时,委托将尝试更新几何图形或它已调整大小;

由于给定的代码不是 minimal, reproducible example,我将提供该概念的通用示例。

from PyQt5 import QtCore, QtGui, QtWidgets
from random import randrange

class CoordinateEditor(QtWidgets.QDialog):
    submit = QtCore.pyqtSignal(QtWidgets.QWidget)
    def __init__(self, parent):
        super().__init__(parent)
        self.setWindowModality(QtCore.Qt.WindowModal)

        layout = QtWidgets.QFormLayout(self)
        self.latitudeSpin = QtWidgets.QDoubleSpinBox(minimum=-90, maximum=90)
        layout.addRow('Latitude', self.latitudeSpin)
        self.longitudeSpin = QtWidgets.QDoubleSpinBox(minimum=-180, maximum=180)
        layout.addRow('Longitude', self.longitudeSpin)
        self.elevationSpin = QtWidgets.QDoubleSpinBox(minimum=-100, maximum=100)
        layout.addRow('Elevation', self.elevationSpin)

        buttonBox = QtWidgets.QDialogButtonBox(
            QtWidgets.QDialogButtonBox.Ok|QtWidgets.QDialogButtonBox.Cancel)
        layout.addRow(buttonBox)
        buttonBox.accepted.connect(self.accept)
        buttonBox.rejected.connect(self.reject)
        self.finished.connect(self.deleteLater)

    def accept(self):
        super().accept()
        self.submit.emit(self)

    @QtCore.pyqtProperty(QtGui.QVector3D, user=True)
    def coordinateData(self):
        return QtGui.QVector3D(
            self.longitudeSpin.value(), 
            self.latitudeSpin.value(), 
            self.elevationSpin.value()
        )

    @coordinateData.setter
    def coordinateData(self, data):
        self.longitudeSpin.setValue(data.x())
        self.latitudeSpin.setValue(data.y())
        self.elevationSpin.setValue(data.z())

    def showEvent(self, event):
        if not event.spontaneous():
            geo = self.geometry()
            geo.moveCenter(self.parent().window().geometry().center())
            self.setGeometry(geo)
            QtCore.QTimer.singleShot(0, self.latitudeSpin.setFocus)


class DialogDelegate(QtWidgets.QStyledItemDelegate):
    def createEditor(self, parent, option, index):
        if index.column() == 1:
            editor = CoordinateEditor(parent)
            editor.submit.connect(self.commitData)
            return editor
        else:
            return super().createEditor(parent, option, index)

    def initStyleOption(self, option, index):
        super().initStyleOption(option, index)
        if index.column() == 1 and index.data() is not None:
            coords = index.data()
            option.text = '{:.02f}, {:.02f}, {:.02f}'.format(
                coords.y(), coords.x(), coords.z())

    def eventFilter(self, source, event):
        if isinstance(source, CoordinateEditor):
            if event.type() in (event.KeyPress, event.FocusOut, event.Hide):
                return False
        return super().eventFilter(source, event)

    def updateEditorGeometry(self, editor, option, index):
        if not isinstance(editor, CoordinateEditor):
            super().updateEditorGeometry(editor, option, index)


if __name__ == '__main__':
    import sys
    app = QtWidgets.QApplication(sys.argv)
    test = QtWidgets.QTableView()
    test.setItemDelegate(DialogDelegate(test))
    model = QtGui.QStandardItemModel(0, 2)
    for row in range(10):
        coordItem = QtGui.QStandardItem()
        coords = QtGui.QVector3D(
            randrange(-180, 181), 
            randrange(-90, 91), 
            randrange(-100, 101))
        coordItem.setData(coords, QtCore.Qt.DisplayRole)
        model.appendRow((
            QtGui.QStandardItem('Data {}'.format(row + 1)), 
            coordItem, 
        ))
    test.setModel(model)
    test.resizeColumnsToContents()
    test.show()
    sys.exit(app.exec_())