I'd like to point out some further details besides the proper answer eyallesc already provided.
Why the signal is not emitted?
The notify signal argument is often mistaken by those who are not yet familiar with the Qt Meta-Object System and the property declaration. I myself was puzzled for the same reason years ago.
But it makes sense: Qt doesn't emit the signal for you, nor it should. A notify signal should be emitted only when a property has actually changed, and it's up to the developer to check if (and how) the value should be set and then considered changed or not: you might need to do a sanity check, or you could prevent setting a value under certain conditions.
Suppose you have a property that only accepts integer values greater or equal to zero, and for some reason a function tries to set that value to -1: you might need to consider if the new value should be ignored, or sanitized to 0.
You could even use an internal data type that is exposed as as a different type; imagine a property that has to be a string representation of a boolean: it would be exposed as "True"
or "False"
, but what if the current value (returned by the getter) is "True"
and the setter is called using "true"
, which you could still consider a valid value? The value for the setter is different, but the final value of the property won't change, so it should not emit the signal.
Then, what's it for?
Now, you're probably wondering what's the use of specifying a notifier signal, since you have to emit it on your own anyway.
One of the important aspects of QObject is that they provide a powerful interface through the meta object system. Every instance of a QObject subclass has a metaObject()
function that returns a QMetaObject, which provides full inspection of the class, including the full list of its methods and properties.
This is very useful for dynamic interfaces that have to interact with widgets that are created on the fly, or for complex UIs that should all connect to a function that notifies about user changes.
The meta object system is how Qt item views show editors depending on the data of the index, so that a unique abstract interface can be used to directly interact with the model without having to manually connect to signals and functions.
It's also how Designer is able to show the property editor for each selected widget, including custom plugins: it cycles through the whole list of properties (splitted by class inheritance, which is also provided by the meta object's super classes) and provides an appropriate editor for each property type.
A pseudo-real world example
I created an example program that shows the concept: it builds a basic UI with a few randomly chosen widgets, sets a random value for their user property and connects its notify signal to a common function that verifies if the values are identical to the initialization. A "restore" function is also added to reset the initial state, and an "apply" one to set the current values as new defaults.
One of the benefits of using such approach is that you can create advanced modular interfaces (for instance, a settings dialog) without having to manually connect all signals.
from PySide2 import QtCore, QtWidgets
from random import choice, randrange
from string import ascii_letters, digits
chars = ascii_letters + digits + ' '
class TestChanged(QtWidgets.QDialog):
def __init__(self):
super().__init__()
layout = QtWidgets.QFormLayout(self,
fieldGrowthPolicy=QtWidgets.QFormLayout.AllNonFixedFieldsGrow)
classes = (QtWidgets.QPushButton, QtWidgets.QCheckBox,
QtWidgets.QLineEdit, QtWidgets.QSpinBox)
self.defaults = {}
for i in range(10):
widget = choice(classes)()
meta = widget.metaObject()
prop = meta.userProperty()
layout.addRow('{}:{}'.format(meta.className(), prop.name()), widget)
if prop.typeName() == 'int':
value = randrange(100)
elif prop.typeName() == 'QString':
value = ''.join(choice(chars) for i in range(randrange(5, 16)))
elif prop.typeName() == 'bool':
widget.setCheckable(True) # for QPushButton
widget.setText(
''.join(choice(chars) for i in range(randrange(5, 16))))
value = choice((False, True))
else:
continue
widget.setProperty(prop.name(), value)
metaSignal = prop.notifySignal()
if metaSignal.isValid():
signal = getattr(widget, str(metaSignal.name(), 'utf8'))
signal.connect(self.valueChanged)
self.defaults[widget] = value
buttonBox = QtWidgets.QDialogButtonBox(
QtWidgets.QDialogButtonBox.RestoreDefaults|
QtWidgets.QDialogButtonBox.Apply|
QtWidgets.QDialogButtonBox.Close
)
layout.addRow(buttonBox)
self.applyButton = buttonBox.button(buttonBox.Apply)
self.applyButton.setEnabled(False)
self.restoreButton = buttonBox.button(buttonBox.RestoreDefaults)
self.restoreButton.setEnabled(False)
self.restoreButton.clicked.connect(self.restore)
self.applyButton.clicked.connect(self.apply)
buttonBox.rejected.connect(self.close)
self.infoLabel = QtWidgets.QLabel('Default values',
alignment=QtCore.Qt.AlignCenter)
layout.addRow(self.infoLabel)
def apply(self):
for widget, default in self.defaults.items():
prop = widget.metaObject().userProperty()
value = widget.property(prop.name())
if value != default:
self.defaults[widget] = value
self.applyButton.setEnabled(False)
self.restoreButton.setEnabled(False)
self.infoLabel.setText('New defaults set')
def restore(self):
for widget, default in self.defaults.items():
prop = widget.metaObject().userProperty()
if widget.property(prop.name()) != default:
# prevent emitting the notify signal that triggers valueChanged
blocker = QtCore.QSignalBlocker(widget)
widget.setProperty(prop.name(), default)
# for PyQt:
# with QtCore.QSignalBlocker(widget):
# widget.setProperty(prop.name(), default)
# alternatively, disconnect from valueChanged, set the value,
# and then connect it again
self.applyButton.setEnabled(False)
self.restoreButton.setEnabled(False)
self.infoLabel.setText('Default values restored')
def valueChanged(self, value):
if self.defaults[self.sender()] != value:
changed = True
else:
changed = False
for widget, default in self.defaults.items():
prop = widget.metaObject().userProperty()
if default != widget.property(prop.name()):
changed = True
break
if changed:
self.infoLabel.setText('Settings changed')
else:
self.infoLabel.setText('Default values')
self.applyButton.setEnabled(changed)
self.restoreButton.setEnabled(changed)
def closeEvent(self, event):
for widget, default in self.defaults.items():
prop = widget.metaObject().userProperty()
if default != widget.property(prop.name()):
res = QtWidgets.QMessageBox.question(
self, 'Settings changed',
'Settings has been modified, but not applied!\n'
'Ignore changes?',
QtWidgets.QMessageBox.Ignore|QtWidgets.QMessageBox.Cancel
)
if res != QtWidgets.QMessageBox.Ignore:
event.ignore()
break
def reject(self):
if self.close():
super().reject()
if __name__ == '__main__':
import sys
app = QtWidgets.QApplication(sys.argv)
w = TestChanged()
w.show()
sys.exit(app.exec_())