Question

I'm pretty new to PySide/PyQt, I'm coming from C#/WPF. I've googled alot on this topic but it no good answer seems to show up.

Ii want to ask is there a way where I can bind/connect a QWidget to a local variable, whereby each object update themselves on change.

Example: If I have a QLineEdit and I have a local variable self.Name in a given class, how do I bind these two whereby when a textChanged() is triggered or simply say the text change on the QLineEdit the variable is updated and at the same time, when the variable is updated the QLineEdit get updated without calling any method.

In C# there is dependency property with converters and Observable collection for list that handles this function.

I will be glad if anyone can give answer with good example

No correct solution

OTHER TIPS

You're asking for two different things here.

  1. You want to have a plain python object, self.name subscribe to changes on a QLineEdit.
  2. You want to have your QLineEdit subscribe to changes on a plain python object self.name.

Subscribing to changes on QLineEdit is easy because that's what the Qt signal/slot system is for. You just do like this

def __init__(self):
    ...
    myQLineEdit.textChanged.connect(self.setName)
    ...

def setName(self, name):
    self.name = name

The trickier part is getting the text in the QLineEdit to change when self.name changes. This is tricky because self.name is just a plain python object. It doesn't know anything about signals/slots, and python does not have a built-in system for observing changes on objects in the way that C# does. You can still do what you want though.

Use a getter/setter with Python's property feature

The simplest thing to do is make self.name a Property. Here's a brief example from the linked documentation (modified for clarity)

class Foo(object):

    @property
    def x(self):
        """This method runs whenever you try to access self.x"""
        print("Getting self.x")
        return self._x

    @x.setter
    def x(self, value):
        """This method runs whenever you try to set self.x"""
        print("Setting self.x to %s"%(value,))
        self._x = value

You could just add a line to update the QLineEdit in the setter method. That way, whenever anything modifies the value of x the QLineEdit will be updated. For example

@name.setter
def name(self, value):
    self.myQLineEdit.setText(value)
    self._name = value

Note that the name data is actually being held in an attribute called _name because it has to differ from the name of the getter/setter.

Use a real callback system

The weakness of all of this is that you can't easily change this observer pattern at run time. To do that you need something really like what C# offers. Two C# style observer systems in python are obsub and my own project observed. I use observed in my own pyqt projects with much success. Note that the version of observed on PyPI is behind the version on github. I recommend the github version.

Make your own simple callback system

If you want to do it yourself in the simplest possible way you would do something like this

import functools
def event(func):
    """Makes a method notify registered observers"""
    def modified(obj, *arg, **kw):
        func(obj, *arg, **kw)
        obj._Observed__fireCallbacks(func.__name__, *arg, **kw)
    functools.update_wrapper(modified, func)
    return modified


class Observed(object):
    """Subclass me to respond to event decorated methods"""

    def __init__(self):
        self.__observers = {} #Method name -> observers

    def addObserver(self, methodName, observer):
        s = self.__observers.setdefault(methodName, set())
        s.add(observer)

    def __fireCallbacks(self, methodName, *arg, **kw):
        if methodName in self.__observers:
            for o in self.__observers[methodName]:
                o(*arg, **kw)

Now if you just subclass Observed you can add callbacks to any method you want at run time. Here's a simple example:

class Foo(Observed):
    def __init__(self):
        Observed.__init__(self)
    @event
    def somethingHappened(self, data):
        print("Something happened with %s"%(data,))

def myCallback(data):
    print("callback fired with %s"%(data,))

f = Foo()
f.addObserver('somethingHappened', myCallback)
f.somethingHappened('Hello, World')
>>> Something happened with Hello, World
>>> callback fired with Hello, World

Now if you implement the .name property as described above, you can decorate the setter with @event and subscribe to it.

Another approach would be to use a publish-subscribe library like pypubsub. You would make QLineEdit subscribe to a topic of your choice (say, 'event.name') and whenever your code changes self.name you sendMessage for that topic (select event to represent what name is changing, like 'roster.name-changed'). The advantage is that all listeners of given topic will get registered, and QLineEdit does not need to know specific which name it listens to. This loose coupling may be too loose for you, so it may not be suitable, but I'm just throwing it out there as another option.

Also, two gotchas that are not specific to publish-subscribe strategy (i.e., also for the obsub etc mentioned in other answer): you could end up in an infinite loop if you listen for QLineEdit which sets self.name which notifies listeners that self.name changed which ends up calling QLineEdit settext etc. You'll either need a guard or check that if self.name already has value given from QLineEdit, do nothing; similarly in QLineEdit if text shown is identical to new value of self.name then don't set it so you don't generate a signal.

I've taken the effort of crafting a small generic 2-way binding framework for a pyqt project that I'm working on. Here it is: https://gist.github.com/jkokorian/31bd6ea3c535b1280334#file-pyqt2waybinding

Here is an example of how it's used (also included in the gist):

The model (non-gui) class

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)

The QWidget (gui) class

class TestWidget(qt.QWidget):
    """
    A simple GUI for testing
    """
    def __init__(self):
        qt.QWidget.__init__(self,parent=None)
        layout = qt.QVBoxLayout()

        self.model = Model()

        spinbox1 = qt.QSpinBox()
        spinbox2 = qt.QSpinBox()
        button = qt.QPushButton()
        layout.addWidget(spinbox1)
        layout.addWidget(spinbox2)
        layout.addWidget(button)

        self.setLayout(layout)

        #create the 2-way bindings
        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))

The Observer instance binds to the valueChanged signals of the QSpinBox instances and uses the setValue method to update the value. It also understands how to bind to python properties, assuming that there is a corresponding propertyNameChanged (naming convention) pyqtSignal on the binding endpoint instance.

update I got more enthousiastic about it and created a proper repository for it: https://github.com/jkokorian/pyqt2waybinding

To install:

pip install pyqt2waybinding
Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top