首页 > 解决方案 > QTableView 与 QWidget 作为 QStyledItemDelegate

问题描述

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

在此处输入图像描述

当我单击确定时,该文件未更新,而是变为空。如果我在此之后尝试编辑任何其他单元格,则不会发生任何事情,并且如果我尝试编辑该确切单元格,则应用程序将崩溃。如果我单击取消,也会发生同样的情况。

setData 方法返回 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

编辑

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

标签: pythonpyqtpyqt5

解决方案


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

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

然后,使用外部窗口有点棘手,因为委托通常应该是存在视图中的简单字段编辑器。为了解决这个问题,必须考虑以下几点:

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

由于给定的代码不是最小的、可重现的示例,因此我将提供该概念的通用示例。

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_())

推荐阅读