QTableView 与 QWidget 作为 QStyledItemDelegate

Posted

技术标签:

【中文标题】QTableView 与 QWidget 作为 QStyledItemDelegate【英文标题】:QTableView with QWidget as QStyledItemDelegate 【发布时间】:2021-09-17 12:07:56 【问题描述】:

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

编辑

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

【问题讨论】:

请提供minimal reproducible example 【参考方案1】:

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

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

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

编辑器必须通知代理插入的数据已被接受,并且它必须在关闭时自行销毁; 必须在过滤器中忽略按键事件(返回False),以便编辑器正确处理它们; focus 和 hide 事件也必须忽略,因为默认情况下,当编辑器没有被“拒绝”但失去焦点或被隐藏时,委托会尝试更新模型; 必须使用父级的***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_())

【讨论】:

非常感谢您的广泛回答和示例! @eyllanesc 我打算做一个最小的可重现示例,但我无法弄清楚如何在没有数据库的情况下使其工作或如何有效地共享数据库,所以我决定把代码和GitHub 存储库上的 DB 转储文件,供任何愿意深入了解的人使用。如果有人能向我澄清如何解决这个问题,我将不胜感激,所以我未来的问题不缺少最小的可重复示例。 @Draugnim:好吧,考虑一下我的例子。您的问题是关于从数据库中检索数据吗?不,那何必在乎呢?如您所见,我创建了一个基于完全随机数据的示例(我只确保它们具有适当的范围),因为问题在于数据的编辑,并且仅针对单个列,所以数据库转储对此完全没用。 MRE 应该只关注实际问题,其他任何事情都只是分散注意力。

以上是关于QTableView 与 QWidget 作为 QStyledItemDelegate的主要内容,如果未能解决你的问题,请参考以下文章

如何优化QTableView的性能

QTableView/QTableWidget 中的类似 Ktorrent 的小部件

如何在 QWidget 中居中背景图像

停止焦点陷入 QTableView

QMainWindow和QWidget分别作为主窗体时对Layout的影响

从 QTableView 读取和写入文件