Enlazando un widget PyQT/PySide a una variable local en Python
dependency-properties (3)
Soy bastante nuevo en PySide / PyQt, vengo de C # / WPF. He buscado mucho en este tema, pero parece que no aparece una buena respuesta.
Quiero preguntar si hay alguna manera en la que pueda enlazar / conectar un QWidget a una variable local, por lo que cada objeto se actualice en el cambio.
Ejemplo: Si tengo un QLineEdit
y tengo una variable local self.Name
en una clase determinada, ¿cómo puedo vincular estos dos mediante el cual cuando se textChanged()
un textChanged()
o simplemente digo el cambio de texto en el QLineEdit
la variable se actualiza y en Al mismo tiempo, cuando se actualiza la variable, QLineEdit
se actualiza sin llamar a ningún método.
En C # hay una propiedad de dependencia con conversores y una colección observable para la lista que maneja esta función.
Me alegrará si alguien puede dar una respuesta con buen ejemplo
Estás pidiendo dos cosas diferentes aquí.
- Desea tener un objeto python simple,
self.name
suscribirse a los cambios en unQLineEdit
. - Desea que su
QLineEdit
suscriba a los cambios en un objeto python simpleself.name
.
Suscribirse a los cambios en QLineEdit
es fácil porque para eso sirve el sistema de señal / ranura Qt. Simplemente te gusta esto
def __init__(self):
...
myQLineEdit.textChanged.connect(self.setName)
...
def setName(self, name):
self.name = name
La parte más complicada es hacer que el texto en el QLineEdit cambie cuando self.name
cambia. Esto es complicado porque self.name
es solo un objeto python simple. No sabe nada sobre señales / ranuras, y Python no tiene un sistema incorporado para observar los cambios en los objetos en la forma en que lo hace C #. Sin embargo, puedes hacer lo que quieras.
Utilice un getter / setter con la función de propiedad de Python
Lo más simple es hacer que self.name
una propiedad . Aquí hay un breve ejemplo de la documentación vinculada (modificado para mayor claridad)
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
Simplemente podría agregar una línea para actualizar el QLineEdit
en el método setter. De esta forma, cada vez que algo modifica el valor de x
, se actualizará QLineEdit
. Por ejemplo
@name.setter
def name(self, value):
self.myQLineEdit.setText(value)
self._name = value
Tenga en cuenta que los datos de nombre en realidad se mantienen en un atributo llamado _name
porque tiene que diferir del nombre del getter / setter.
Use un sistema de devolución de llamada real
La debilidad de todo esto es que no puede cambiar fácilmente este patrón de observador en tiempo de ejecución. Para hacer eso necesitas algo realmente como lo que C # ofrece. Dos sistemas de observadores de estilo C # en Python son obsub y se observó mi propio proyecto. Uso observado en mis propios proyectos pyqt con mucho éxito. Tenga en cuenta que la versión de observado en PyPI está detrás de la versión en github. Recomiendo la versión de github.
Haga su propio sistema de devolución de llamada simple
Si quieres hacerlo tú mismo de la manera más simple posible, harías algo como esto
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)
Ahora bien, si solo subclase Observed
, puede agregar devoluciones de llamada a cualquier método que desee en tiempo de ejecución. Aquí hay un ejemplo simple:
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
Ahora bien, si implementa la propiedad .name
como se describe arriba, puede decorar el setter con @event
y suscribirse a él.
Me he tomado el trabajo de diseñar un pequeño marco vinculante genérico de 2 vías para un proyecto pyqt en el que estoy trabajando. Aquí está: https://gist.github.com/jkokorian/31bd6ea3c535b1280334#file-pyqt2waybinding
Aquí hay un ejemplo de cómo se usa (también incluido en la esencia):
La clase modelo (no 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)
La clase 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))
La instancia de Observer
une a las señales valueChanged
de las instancias de QSpinBox
y utiliza el método setValue para actualizar el valor. También entiende cómo vincularse a las propiedades de python, suponiendo que hay una propiedad correspondienteChanged (convención de nomenclatura) pyqtSignal en la instancia de punto final de enlace.
actualización Me puse más entusiasta al respecto y creé un repositorio adecuado para él: https://github.com/jkokorian/pyqt2waybinding
Instalar:
pip install pyqt2waybinding
Otro enfoque sería usar una biblioteca de publicación y suscripción como pypubsub . Haría que QLineEdit se suscribiera a un tema de su elección (por ejemplo, ''event.name'') y siempre que su código cambie self.name, usted enviará Message para ese tema (seleccione evento para representar qué nombre está cambiando, como ''roster.name-changed ''). La ventaja es que todos los oyentes de un tema determinado se registrarán, y QLineEdit no necesita saber qué nombre escucha específico. Este acoplamiento flojo puede ser demasiado flojo para usted, por lo que puede no ser adecuado, pero lo estoy lanzando como otra opción.
Además, dos errores que no son específicos de la estrategia de suscripción-publicación (es decir, también para el obsub etc. mencionado en otra respuesta): podrías terminar en un bucle infinito si escuchas QLineEdit que establece self.name que notifica a los oyentes ese auto .name cambió, lo que termina llamando a QLineEdit settext, etc. Necesitarás un guardia o verificarás que si self.name ya tiene un valor dado por QLineEdit, no hagas nada; de manera similar en QLineEdit si el texto que se muestra es idéntico al nuevo valor de self.name, entonces no lo configure para que no genere una señal.