Skip to content

Instantly share code, notes, and snippets.

@jkokorian
Created May 24, 2015 14:18
Show Gist options
  • Star 2 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save jkokorian/31bd6ea3c535b1280334 to your computer and use it in GitHub Desktop.
Save jkokorian/31bd6ea3c535b1280334 to your computer and use it in GitHub Desktop.
PyQt observer class for 2-way binding
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