如何在 PySide 中模块化创建属性

Posted

技术标签:

【中文标题】如何在 PySide 中模块化创建属性【英文标题】:How to modularize property creation in PySide 【发布时间】:2020-04-20 08:45:13 【问题描述】:

我在很大程度上遵循here 使用属性的说明,我可以只使用那里给出的 Person 对象作为后端,但这不是很有用。我想弄清楚如何做以下两件事:

    在后端使用多个此类类的多个实例,并以 PySide/QML 不会抱怨的方式连接它们 允许在运行时确定通过模块自定义后端(即,我最终想要组件化应用程序 - 有不同的组件实现接口,组件分别对 GUI 和后端做出贡献;但这个问题只涉及后端)

这与在主后端类上简单地定义所有这些属性以及它们的 setter 和 getter(我能够做到)形成对比,这就是我在问题中模块化的意思。

我修改了链接中的 Person 示例,使其成为 UI 可以更改的内容,并为它提供了一个额外的属性以用于踢球...

person.py

from PySide2.QtCore import QObject, Signal, Property

class Person(QObject):

    def __init__(self, name, age):
        QObject.__init__(self)
        self._name = name
        self._age = age

    def getName(self):
        return self._name

    def setName(self, name):
        print(f"Setting name to name")
        self._name = name

    def getAge(self):
        return self._age

    def setAge(self, age):
        print(f"Setting age to age")
        self._age = age

    @Signal
    def name_changed(self):
        pass

    @Signal
    def age_changed(self):
        pass

    name = Property(str, getName, setName, notify=name_changed)
    age = Property(str, getAge, setAge, notify=age_changed)

作为示例,我将创建两个 Person 实例。我作为类成员创建的第一个实例。这不是我真正想要的,但更类似于链接中使用属性的方式。第二个实例是我真正想要的,它的属性是实例成员,这样我就可以在运行时从应用程序的其他地方添加它们。目前这两种方法都不起作用

main.py

import sys
from os.path import abspath, dirname, join

from PySide2.QtCore import QObject, Property, Signal
from PySide2.QtGui import QGuiApplication
from PySide2.QtQml import QQmlApplicationEngine

from person import Person

class Backend(QObject):

    def __init__(self):
        QObject.__init__(self)

    def registerProperty(self, name : str, prop):
        setattr(self, name, prop)

    person1 = Person("Jill", 29)

if __name__ == '__main__':

    app = QGuiApplication(sys.argv)
    engine = QQmlApplicationEngine()
    context = engine.rootContext()

    # Instance of the Python object
    backend = Backend()

    # simulate properties added by another module
    backend.registerProperty("person2", Person("Jack", 30))

    qmlFile = join(dirname(__file__), 'view3.qml')
    engine.load(abspath(qmlFile))

    # Expose the Python object to QML
    context.setContextProperty("backend", backend)

    # I tried this but it did nothing
    # context.setContextProperty("backend.jack", backend.jack)
    # context.setContextProperty("backend.jill", backend.jill)

    if not engine.rootObjects():
        sys.exit(-1)

    sys.exit(app.exec_())

终于view3.qml文件很简单

import QtQuick 2.0
import QtQuick.Layouts 1.12
import QtQuick.Controls 2.12
import QtQuick.Window 2.12

ApplicationWindow 

    visible: true

    ColumnLayout 
        TextField 
            implicitWidth: 200
            onAccepted: 
                backend.person1.name = text
            
        
        TextField 
            implicitWidth: 200
            onAccepted: 
                backend.person1.age = text
            
        
        TextField 
            implicitWidth: 200
            onAccepted: 
                backend.person2.name = text
            
        
        TextField 
            implicitWidth: 200
            onAccepted: 
                backend.person2.age = text
            
        
    

当我尝试在 UI 中设置任何值时,错误总是相同的(错误出现在 QML 文件中)

TypeError: Value is undefined and could not be converted to an object

最终,我希望将此类对象嵌套到任意深度。有没有办法实现我在这里尝试做的事情?或者我可能完全偏离了我正在设置的方式?

【问题讨论】:

【参考方案1】:

我不知道我是否有资格就 GUI 应用程序的整体架构设计向您提供建议,但我想我可以解释发生了什么问题,并建议一种方法来执行您所描述的操作。您的 registerProperty 方法添加了一个 Python 属性,但正如您所见,这不会使其在 QML 中可见。

坏消息: Qt 属性在创建后无法添加到对象。好消息: 您可以创建一个 Qt 属性,它是一个列表 (或字典),然后添加到其中。

需要注意的一个陷阱是,要将列表公开给 QML,您将其类型指定为 'QVariantList'。 (对于字典,请使用'QVariantMap',并确保您的键是字符串。)

下面是您的 Backend 类的外观示例。 (使用super() 访问父类意味着您不必将self 传递给它的初始化程序。)

from Pyside2.QtCore import QObject, Property, Signal

class Backend(QObject):
    def __init__(self):
        super().__init__()
        self.people = []
    
    people_changed = Signal('QVariantList')

    @Property('QVariantList', notify=people_changed)
    def people(self):
        return self._people

    @value.setter
    def people(self, new_people):
        self._people = new_people
        self.people_changed.emit(new_people)

    def add_person(self, name, age):
        self.people.append(Person(name, age, self))
        # Note: only ASSIGNING will automatically fire the changed
        # signal. If you append, you have to fire it yourself.
        self.people_changed.emit(self.people)

这将使 QML 在您添加人员时保持最新;您还可以创建类似的方法来删除人员。将每个人作为 Backend 对象的父级确保只要您的 Backend 仍然存在,Qt 就会保留对他们的引用。

对于真正动态的属性集合,也许您可​​以为您的***后端对象提供一个字典,您的其他组件将添加到该字典中。所以backend.people 会变成backend.properties['people'],一个特定的模块会负责将该键添加到properties 字典中,然后在其中添加和删除条目。


指定所有这些 getter 和 setter 很麻烦,不是吗?我花了很长时间觉得肯定有更好的方法,直到最近才在 Stack Overflow 上遇到了一个解决方案。使用this metaclass,您的 Person 类和我在上面发布的 Backend 可以简化为:

from PySide2.QtCore import QObject

# Assuming you save the linked code as properties.py:
from properties import PropertyMeta, Property

class Person(QObject, metaclass=PropertyMeta):
    name = Property(str)
    age = Property(int)
    
    def __init__(self, name, age, parent=None):
        super().__init__(parent)
        self.name = name
        self.age = age


class Backend(QObject, metaclass=PropertyMeta):
    people = Property(list)
    
    def __init__(self):
        super().__init__()
        self.people = []

    def add_person(self, name, age):
        self.people.append(Person(name, age, self))
        # Automatically emits changed signal, even for in-place changes

(我也将age 更改为int,但如果需要,它仍然可以是str。)

【讨论】:

以上是关于如何在 PySide 中模块化创建属性的主要内容,如果未能解决你的问题,请参考以下文章

如何在PySide / PyQt中连接QApplication()和QWidget()对象?

如何创建一个新的窗口按钮 PySide/PyQt?

在 PySide 的 QTreeView 中更改时如何获取项目的先前名称

如何从 PySide 访问 QML\QtQuick 控件?

使用 cx_freeze 和 bdist_msi 为 PySide 应用程序创建 MSI

如何删除 PySide 创建的窗口?