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_())
我正在编写一个 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_())