Связывание виджета PyQT/PySide с локальной переменной в Python

Я новичок в PySide/PyQt, я иду из С#/WPF. Я много искал в этой теме, но ничего хорошего ответа не видно.

Ii хочу спросить, есть ли способ, с помощью которого я могу связать/подключить QWidget к локальной переменной, при которой каждый объект обновляется самостоятельно при изменении.

Пример: если у меня есть QLineEdit, и у меня есть локальная переменная self.Name в данном классе, как мне связать эти два, когда при срабатывании textChanged() или просто скажем, что изменение текста на QLineEdit переменная обновляется и в то же время, когда переменная обновляется, QLineEdit получает обновление без вызова какого-либо метода.

В С# существует свойство зависимости с преобразователями и коллекция Observable для списка, который обрабатывает эту функцию.

Буду рад, если кто-нибудь сможет дать ответ с хорошим примером

Ответ 1

Здесь вы просите о двух разных вещах.

  • Вы хотите иметь простой объект python, self.name подписаться на изменения в QLineEdit.
  • Вы хотите, чтобы ваш QLineEdit подписался на изменения на простой объект python self.name.

Подписывание изменений на QLineEdit легко, потому что это то, что для системы сигналов/слотов Qt. Вы просто делаете это

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

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

Более сложная часть - это изменение текста в QLineEdit при изменении self.name. Это сложно, потому что self.name - просто простой объект python. Он ничего не знает о сигналах/слотах, и у python нет встроенной системы для наблюдения за изменениями на объектах так, как это делает С#. Вы все равно можете делать то, что хотите.

Используйте функцию getter/setter с функцией свойства Python

Проще всего сделать self.name a 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

Вы можете просто добавить строку для обновления QLineEdit в методе setter. Таким образом, всякий раз, когда что-либо изменяет значение x, будет обновляться QLineEdit. Например

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

Обратите внимание, что данные имени фактически хранятся в атрибуте _name, потому что он должен отличаться от имени получателя/сеттера.

Использовать реальную систему обратного вызова

Слабость всего этого заключается в том, что вы не можете легко изменить этот шаблон наблюдателя во время выполнения. Для этого вам нужно что-то действительно похожее на то, что предлагает С#. Две системы наблюдения стиля С# в python obsub и мой собственный проект observed. Я использую наблюдаемые в своих проектах pyqt с большим успехом. Обратите внимание, что версия наблюдаемого на PyPI находится за версией на 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, вы можете добавить обратные вызовы к любому методу, который вы хотите во время выполнения. Вот простой пример:

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 и подписаться на него.

Ответ 2

Другим подходом было бы использовать библиотеку публикации-подписки, такую ​​как pypubsub. Вы сделали бы QLineEdit подпиской на тему по вашему выбору (скажем, "event.name" ), и всякий раз, когда ваш код меняет self.name, вы отправляете сообщение для этой темы (выберите событие, чтобы представлять, какое имя меняется, например "roster.name-changed" ). Преимущество заключается в том, что все слушатели данной темы будут зарегистрированы, а QLineEdit не обязательно будет знать, какое имя оно прослушивает. Эта свободная муфта может быть слишком свободной для вас, поэтому она может оказаться непригодной, но я просто выбрасываю ее в качестве другого варианта.

Кроме того, две ошибки, которые не специфичны для стратегии публикации-подписки (т.е. также для obsub и т.д., упомянутых в другом ответе): вы можете оказаться в бесконечном цикле, если вы слушаете QLineEdit, который устанавливает self.name, который уведомляет слушателей, которые изменили имя self.name, которое заканчивается вызовом QtextEdit settext и т.д. Вам понадобится защитник или проверьте, что если self.name уже имеет значение, указанное в QLineEdit, ничего не делать; аналогично в QLineEdit, если показанный текст идентичен новому значению self.name, тогда не устанавливайте его, чтобы вы не генерировали сигнал.

Ответ 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 связывается с сигналами valueChanged экземпляров QSpinBox и использует метод setValue для обновления значения. Он также понимает, как привязываться к свойствам python, предполагая, что существует соответствующее свойствоNameChanged (соглашение об именах) pyqtSignal на экземпляре конечной точки привязки.

update Я получил больше энтузиазма и создал для него правильный репозиторий: https://github.com/jkokorian/pyqt2waybinding

Для установки:

pip install pyqt2waybinding