-
-
Save smallsong/b5655a0c89b1bb71a0c0646c92f487b0 to your computer and use it in GitHub Desktop.
PyQt observer class for 2-way binding
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import PyQt4.QtCore as q | |
import PyQt4.QtGui as qt | |
class BindingEndpoint(object): | |
""" | |
Data object that contains the triplet of: getter, setter and change notification signal, | |
as well as the object instance and it's memory id to which the binding triplet belongs. | |
Parameters: | |
instance -- the object instance to which the getter, setter and changedSignal belong | |
getter -- the value getter method | |
setter -- the value setter method | |
valueChangedSignal -- the pyqtSignal that is emitted with the value changes | |
""" | |
def __init__(self,instance,getter,setter,valueChangedSignal): | |
self.instanceId = id(instance) | |
self.instance = instance | |
self.getter = getter | |
self.setter = setter | |
self.valueChangedSignal = valueChangedSignal | |
class Observer(q.QObject): | |
""" | |
Create an instance of this class to connect binding endpoints together and intiate a 2-way binding between them. | |
""" | |
def __init__(self): | |
q.QObject.__init__(self) | |
self.bindings = {} | |
self.ignoreEvents = False | |
def bind(self,instance,getter,setter,valueChangedSignal): | |
""" | |
Creates an endpoint and call bindToEndpoint(endpoint). This is a convenience method. | |
Parameters: | |
instance -- the object instance to which the getter, setter and changedSignal belong | |
getter -- the value getter method | |
setter -- the value setter method | |
valueChangedSignal -- the pyqtSignal that is emitted with the value changes | |
""" | |
endpoint = BindingEndpoint(instance,getter,setter,valueChangedSignal) | |
self.bindToEndPoint(endpoint) | |
def bindToEndPoint(self,bindingEndpoint): | |
""" | |
2-way binds the target endpoint to all other registered endpoints. | |
""" | |
self.bindings[bindingEndpoint.instanceId] = bindingEndpoint | |
bindingEndpoint.valueChangedSignal.connect(self._updateEndpoints) | |
def bindToProperty(self,instance,propertyName): | |
""" | |
2-way binds to an instance property according to one of the following naming conventions: | |
@property, propertyName.setter and pyqtSignal | |
- getter: propertyName | |
- setter: propertyName | |
- changedSignal: propertyNameChanged | |
getter, setter and pyqtSignal (this is used when binding to standard QWidgets like QSpinBox) | |
- getter: propertyName() | |
- setter: setPropertyName() | |
- changedSignal: propertyNameChanged | |
""" | |
getterAttribute = getattr(instance,propertyName) | |
if callable(getterAttribute): | |
#the propertyName turns out to be a method (like value()), assume the corresponding setter is called setValue() | |
getter = getterAttribute | |
if len(propertyName) > 1: | |
setter = getattr(instance,"set" + propertyName[0].upper() + propertyName[1:]) | |
else: | |
setter = getattr(instance,"set" + propertyName[0].upper()) | |
else: | |
getter = lambda: getterAttribute() | |
setter = lambda value: setattr(instance,propertyName,value) | |
valueChangedSignal = getattr(instance,propertyName + "Changed") | |
self.bind(instance,getter,setter,valueChangedSignal) | |
def _updateEndpoints(self,*args,**kwargs): | |
""" | |
Updates all endpoints except the one from which this slot was called. | |
Note: this method is probably not complete threadsafe. Maybe a lock is needed when setter self.ignoreEvents | |
""" | |
sender = self.sender() | |
if not self.ignoreEvents: | |
self.ignoreEvents = True | |
for binding in self.bindings.values(): | |
if binding.instanceId == id(sender): | |
continue | |
binding.setter(*args,**kwargs) | |
self.ignoreEvents = False | |
class Model(q.QObject): | |
""" | |
A simple model class for testing | |
""" | |
valueChanged = q.pyqtSignal(int) | |
def __init__(self): | |
q.QObject.__init__(self) | |
self.__value = 0 | |
@property | |
def value(self): | |
return self.__value | |
@value.setter | |
def value(self, value): | |
if (self.__value != value): | |
self.__value = value | |
print "model value changed to %i" % value | |
self.valueChanged.emit(value) | |
class TestWidget(qt.QWidget): | |
""" | |
A simple GUI for testing | |
""" | |
def __init__(self): | |
qt.QWidget.__init__(self,parent=None) | |
layout = qt.QVBoxLayout() | |
spinbox1 = qt.QSpinBox() | |
spinbox2 = qt.QSpinBox() | |
button = qt.QPushButton() | |
self.model = Model() | |
valueObserver = Observer() | |
self.valueObserver = valueObserver | |
valueObserver.bindToProperty(spinbox1, "value") | |
valueObserver.bindToProperty(spinbox2, "value") | |
valueObserver.bindToProperty(self.model, "value") | |
button.clicked.connect(lambda: setattr(self.model,"value",10)) | |
layout.addWidget(spinbox1) | |
layout.addWidget(spinbox2) | |
layout.addWidget(button) | |
self.setLayout(layout) | |
## Start Qt event loop unless running in interactive mode or using pyside. | |
if __name__ == '__main__': | |
app = qt.QApplication([]) | |
w = TestWidget() | |
w.show() | |
import sys | |
if (sys.flags.interactive != 1) or not hasattr(q, 'PYQT_VERSION'): | |
qt.QApplication.instance().exec_() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment