17

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

kevinarpe
  • 20,319
  • 26
  • 127
  • 154
Temitayo
  • 802
  • 2
  • 12
  • 28

3 Answers3

26

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.

Scott Mermelstein
  • 15,174
  • 4
  • 48
  • 76
DanielSank
  • 3,303
  • 3
  • 24
  • 42
  • 2
    Well, all this trouble to implement a binding, I guess these are the price to pay, I have already done all these, I just need something automatic because, I'm working on something that generate object on the fly and set some of its property to a widget element, thanks, at least I got some idea from your example. thanks again – Temitayo Feb 24 '14 at 17:00
  • 1
    @Temitayo: It might be worth your time to look at that package I linked, obsub. It's extremely light weight and pretty easy to understand the source because it's so small. In fact, obsub was born out of a desire to implement C# style bindings in python after someone asked a similar question to yours here on StackOverflow! Also, if you like my answer, please mark it as accepted. – DanielSank Feb 24 '14 at 17:03
  • 2
    @Temitayo: I would also point out that "all this effort to implement a binding" comes down to 18 lines of code as shown in my example with the event decorator. That's not so bad :) – DanielSank Feb 24 '14 at 17:06
  • Yea I kwo but Imagine I have 30 elements to bind, not too bad too.... But thanks I will check out the library – Temitayo Feb 24 '14 at 17:09
  • 1
    @Temitayo: Binding 30 elements shouldn't be much more of a pain than doing it in C#. In my example you just call ".addObserver(methodName, callback)" for each binding. That's not so much worse than "+= callback" as you would do in C#. The obsub package actually allows you to use the += notation so it's exactly like C#! The only problem is that you have to add the event decorator to things you want to make observable, but if you look at the obsub github page you'll see I submitted something to help fix that. Also, could you please mark my answer as accepted? – DanielSank Feb 24 '14 at 17:12
  • It seems that adding an event decorator to a property setter only works with your `Observed` class, not with the `obsub` package. Nice job on it! – Scott Mermelstein Jan 10 '17 at 15:50
  • 1
    @ScottMermelstein Thanks for the feedback! By the way, please do note that the version of `observed` on PyPI is a bit older than the version in github. I'd recommend using the github version if your workflow allows it. I've been putting off updating PyPI because I need to revise the documentation a bit. If enough people bug me... – DanielSank Jan 10 '17 at 18:49
  • @DanielSank I don't think I'll *bug* you, but at least you know someone is using your code. :-) Your github version did look easier to use than the PyPI one, but at this moment, the most helpful code I've found has been your "simple callback system" from this answer. It gives me a good starting point, and is currently the easiest one to hook up to a property's setter. – Scott Mermelstein Jan 10 '17 at 18:57
  • @ScottMermelstein The simple example given here has the (IMHO) massive drawback that it requires your observed classes to subclass the `Observed` class. The major benefit of the `observed` package is that you decorate individual functions/methods without any need for inheritance. `observed` uses python [descriptors](https://docs.python.org/2/howto/descriptor.html) so that management of observers is outsourced to a class separate from the one being observed. This has a lot of advantages which I'd be happy to discuss in chat or whatever. – DanielSank Jan 10 '17 at 19:02
  • I agree - I'd rather not have to subclass, but I haven't been able to get anything other than subclassing to work. So, if you're willing, please [join me in chat](http://chat.stackoverflow.com/rooms/info/132847/binding-a-pyqt-pyside-widget-to-a-local-variable-in-python?tab=general) – Scott Mermelstein Jan 10 '17 at 19:23
  • @ScottMermelstein just noticed your comment. I'm happy to chat any time (you can also find my email through my profile page with minimal internet hunting). – DanielSank Jul 01 '17 at 20:04
  • Thanks for the offer. That was so long ago, I think I hit the chat request because the system suggested it. I ended up sub classing, and everything works fine. Thanks for the follow up, though. – Scott Mermelstein Jul 01 '17 at 20:07
  • @ScottMermelstein ok, happy coding. – DanielSank Jul 01 '17 at 20:08
3

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.

Oliver
  • 27,510
  • 9
  • 72
  • 103
3

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
jkokorian
  • 2,905
  • 7
  • 32
  • 47