Issue
I'm coding a Python CRUD app that shows radiosondes on a map and a QTableView. I'm using QStyledItemDelegate to set an editor and regex validator for each column and it is working great. But for the geometry column I would like to parse the binary data and show it on a custom form (lat, lng, elevation), be able to edit them and if clicked OK encode them back to WKB format and update the data.
When I click OK the filed is not updated, instead it becomes empty. If I try to edit after that any other cell nothing happens and if I try to edit that exact cell the app crashes. The same happens if I click Cancel.
The setData method returns True and the data in the DB gets updated.
I tried with dataChanged.emit() on the QSqlTableModel and also with update() method on the QTableView.
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)
Here is the whole code on GitHub: https://github.com/draugnim/pyCrud
EDIT
I managed to get it working by calling self.model.selet()
at the end of generateGeom() and cancelGeomEdit(). But still if I click on the X button and close the form the edited cell becomes blank, also this and all other cells become uneditable.
Solution
The item delegate uses the editor's user property, which is considered the main default property of a Qt object. For QLineEdit it's text()
, for a QSpinBox it's value()
, etc.
If you want to provide a custom, advanced editor, the solution is to create a subclass with a custom user property.
Note that the Qt internal way of dealing with item data is a bit strict on types, and since PyQt doesn't expose the QVariant class, the only option is to use a suitable type. For you case, QVector3D is a perfect choice.
Then, using an external window is a bit tricky, as delegates are normally supposed to be simple field editors that exist inside the view. To work around that, the following must be considered:
- the editor must notify the delegate that the inserted data has been accepted, and it must destroy itself whenever it's closed;
- key press events must be ignored in the filter (return
False
), so that the editor can properly handle them; - focus and hide events must ignored too, as the delegate by default tries to update the model when the editor has not been "rejected" but it loses focus or is hidden;
- the geometry must be set using the parent's top level
window()
, and theupdateEditorGeometry()
must be ignored as the delegate would try to update the geometry whenever the view is shown again after being hidden or it's resized;
Since the given code is not a minimal, reproducible example, I'll provide a generic example of the concept.
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_())
Answered By - musicamante
0 comments:
Post a Comment
Note: Only a member of this blog may post a comment.