My PyQt5 application contains a bunch of input fields of different types: QLineEdit, QSpinBox, QComboBox, QTableView etc.
I want to alert the user if they input data or change the content of one or more of the fields and try to close the window without saving.
Do I have to connect all the different variations of the textChanged
signal to some kind of bookkeeping function, or is there an easier way?
EDIT: More details on sequence of events:
- The UI gets build from a Qt Designer .ui file, so some fields have default values (like QSpinBox, QDateEdit)
- The models of different QTableViews get initialized with certain default data structures, like a 2d array of
None
, or a dict whose keys all returnNone
- A bunch of documents get loaded from a document store, and the fields are set to the values of these documents. It might happen that no corresponding key exists in the document, so the field won't be set. But it's also possible that a value in the documents just happens to be the default value.
- The user changes the content of some of the fields, and on saving the document in the store will be updated accordingly.
I want to be able to tell if any field has been modified after step 3 which is to say a user made change. I'd like to avoid having to compare all fields against the document store.
CodePudding user response:
For simple widgets that store a single property, the solution is to user the user property of the meta object of each widget.
Item views require a custom checking to compare the model.
You need to create a list of the widgets you need to monitor for changes, get the value during initialization and then verify it when closing.
To simplify things, you can set a dynamic property for all widgets that need checking, so that you can iter through all widgets, check if the property is set, and add those widgets to the list. To add a custom property to a widget in Designer, select the widget and click on the " " symbol in the property editor; in the following example I used a boolean property ("validate") with a basic truthfulness check: remember that properties that are not set return None
, so in this case you must also set the property to True
.
class Test(QtWidgets.QDialog):
def __init__(self, parent=None):
super().__init__(parent)
uic.loadUi('mapper.ui', self)
self.loadData()
def loadData(self):
# some stuff setting the initial values
# ...
# save the current values
self.fieldData = []
for widget in self.findChildren(QtWidgets.QWidget):
if not widget.property('validate'):
continue
if isinstance(widget, QtWidgets.QAbstractItemView):
model = widget.model()
if not model:
continue
data = []
for row in range(model.rowCount()):
rowData = []
data.append(rowData)
for column in range(model.columnCount()):
rowData.append(model.index(row, column).data())
self.fieldData.append((widget, data))
else:
property = widget.metaObject().userProperty()
self.fieldData.append((widget, widget.property(property.name())))
def ignoreChanges(self):
for widget, default in self.fieldData:
if isinstance(widget, QtWidgets.QAbstractItemView):
model = widget.model()
for row, rowData in enumerate(default):
for column, itemData in enumerate(rowData):
if model.index(row, column).data() != default:
break
else:
property = widget.metaObject().userProperty()
if widget.property(property.name()) != default:
break
else:
return True
res = QtWidgets.QMessageBox.question(self, 'Ignore changes',
'Fields have been modified, do you want to ignore?',
QtWidgets.QMessageBox.Ok|QtWidgets.QMessageBox.Cancel)
if res != QtWidgets.QMessageBox.Ok:
return False
return True
def reject(self):
if self.ignoreChanges():
super().reject()
def closeEvent(self, event):
if not self.ignoreChanges():
event.ignore()