在Python中将PyQT/PySide小部件绑定到本地变量

17

我对PySide/PyQt比较新,之前用过C#/WPF。我在这个主题上进行了很多谷歌搜索,但好的答案似乎并没有出现。

我想问是否有一种方法可以将QWidget绑定/连接到一个本地变量,从而使每个对象在更改时更新自己。

例如:如果我有一个QLineEdit和一个给定类中的本地变量self.Name,我该如何将它们绑定起来?当触发textChanged()或简单说QLineEdit的文本更改时,变量会被更新,同时当变量更新时,QLineEdit也会被更新,无需调用任何方法。

在C#中,有依赖性属性与转换器以及Observable集合用于列表处理此功能。

如果有人能给出良好的示例回答,我将不胜感激。

3个回答

26

您在这里要求两件不同的事情。

  1. 您希望一个普通的Python对象 self.name 订阅一个 QLineEdit 上的更改。
  2. 您希望您的 QLineEdit 订阅普通Python对象 self.name 上的更改。

订阅 QLineEdit 的更改很容易,因为这就是Qt信号/槽系统的作用。您只需像这样做:

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

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

更棘手的部分是当self.name发生变化时,如何让QLineEdit中的文本发生变化。这很困难,因为self.name只是一个纯Python对象。它不知道任何关于信号/槽的事情,并且Python没有内置的系统来观察对象上的更改,就像C#那样。不过,您仍然可以做到想要的效果。

使用Python的属性功能设置getter/setter

最简单的方法是将self.name设置为Property。下面是从链接文档中的简短示例(经过修改以增加清晰度):

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

你可以在setter方法中添加一行代码来更新QLineEdit。这样,无论什么修改了x的值,QLineEdit都会得到更新。例如:

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

请注意,数据名称实际上保存在名为_name的属性中,因为它必须与getter/setter的名称不同。

使用真正的回调系统

所有这些的弱点是您无法在运行时轻松更改此观察者模式。要做到这一点,您需要像C#所提供的那样。Python中两个类似于C#风格的观察者系统是obsub和我自己的项目observed。我在自己的PyQt项目中成功地使用了observed。 请注意,PyPI上的observed版本落后于github上的版本。我建议使用github版本。

创建自己的简单回调系统

如果您想以最简单的方式自己处理,可以尝试以下内容:

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)

现在,如果您只是子类化Observed,那么您可以在运行时向任何方法添加回调。这是一个简单的示例:

现在,如果你只是子类化Observed,你就可以在运行时为任何想要的方法添加回调函数。这里有一个简单的例子:

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

如果您按照上述方式实现了.name属性,您可以使用@event装饰setter并订阅它。


2
嗯,为了实现绑定而付出的所有这些麻烦,我想这就是要付出的代价。我已经完成了所有这些工作,我只需要一些自动化的东西,因为我正在处理一些动态生成对象并将其属性设置为小部件元素的工作。谢谢,至少我从你的例子中得到了一些想法。再次感谢。 - Temitayo
1
@Temitayo:看一下我链接的那个软件包obsub可能是值得你时间的。它非常轻量级,因为其源码非常小,所以很容易理解。实际上,obsub是受一个人在StackOverflow上问类似问题后想要在Python中实现C#样式绑定的愿望而诞生的!此外,如果你喜欢我的答案,请标记为接受。 - DanielSank
2
@Temitayo:我还要指出,“实现绑定的所有这些努力”归结为18行代码,就像我的事件装饰器示例一样。这并不太糟糕 :) - DanielSank
1
@Temitayo:绑定30个元素不应该比在C#中做更麻烦。在我的示例中,您只需为每个绑定调用“.addObserver(methodName,callback)”。这并不比在C#中使用“+= callback”更糟糕。obsub包实际上允许您使用+=符号,因此它与C#完全相同!唯一的问题是您必须向要使其可观察的内容添加事件装饰器,但如果您查看obsub github页面,您会看到我提交了一些东西来帮助解决这个问题。另外,请标记我的答案为已接受,好吗? - DanielSank
1
@ScottMermelstein 感谢您的反馈!顺便提醒一下,PyPI上的observed版本比github上的版本稍旧。如果您的工作流允许,我建议使用github版本。我一直拖延更新PyPI,因为我需要稍微修改一下文档。如果有足够多的人催促我... - DanielSank
显示剩余8条评论

3
我已经为我正在开发的pyqt项目创建了一个小型通用的双向绑定框架。这是它的链接:https://gist.github.com/jkokorian/31bd6ea3c535b1280334#file-pyqt2waybinding 以下是如何使用它的示例(也包含在上述链接中):

非GUI类模型

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)

QWidget (gui) 类

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))
Observer 实例将绑定到 QSpinBox 实例的 valueChanged 信号,并使用 setValue 方法来更新值。它也可以理解如何绑定到 Python 属性,假设在绑定端点实例上有相应的 propertyNameChanged(命名约定)pyqtSignal。

更新:我对此更加热衷,并为其创建了一个适当的存储库:https://github.com/jkokorian/pyqt2waybinding

安装方法:

pip install pyqt2waybinding

3
另一种方法是使用发布-订阅库,例如 pypubsub。您可以使QLineEdit订阅您选择的主题(例如“event.name”),并且每当您的代码更改self.name时,就会为该主题发送sendMessage(选择事件以表示正在更改的名称,例如“roster.name-changed”)。优点是所有给定主题的侦听器都将注册,并且QLineEdit不需要知道它监听哪个名称。这种松散耦合可能对您来说过于松散,因此可能不适用,但我只是提出另一种选择。
此外,还有两个小问题与发布-订阅策略无关(即,也适用于其他答案中提到的obsub等):如果您监听设置self.name的QLineEdit,则可能陷入无限循环,从而通知self.name已更改,从而调用QLineEdit settext等。您需要一个保护或检查,如果QLineEdit已经给出了值,则不执行任何操作;同样,在QLineEdit中,如果显示的文本与self.name的新值相同,则不要设置它,以便不生成信号。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接