如何在Python和Qt Quick QML应用程序中实现简化的双向数据绑定

Posted

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了如何在Python和Qt Quick QML应用程序中实现简化的双向数据绑定相关的知识,希望对你有一定的参考价值。

我正在尝试首次使用Python和Qt创建桌面应用。因为这是我第一次创建基于gui的应用程序,所以我希望有经验的人可以在一些问题上为我提供正确的指导。

背景:我正在尝试使用Python和Qt创建桌面应用程序。 Python逻辑用于与硬件(传感器和电动机)接口以控制过程。前端gui应该允许用户监视过程以及在过程发生时更改过程参数。以前,我曾使用LabVIEW实现目标,但现在我想迁移到Python。但是,我发现用Python开发gui不像LabVIEW那样容易。经过一些研究,我发现基于QML和Qt Quick的gui相对于使用Qt(pyqt / pyside)或wxPython的基于小部件的gui最简单。

问题:我很难理解我如何在过程变量和前端gui之间获得两个绑定。可以为过程中的各种硬件(例如传感器和电机)输入/输出这些过程变量。我读了this tutorial,它使用基于属性的方法来获得双向绑定。还解释了另一种数据绑定方法here。但是,似乎必须为该过程中的每个变量编写很多代码(信号,插槽,设置器,获取器)。当后端逻辑可能需要在前端gui中显示和修改数十个或数百个过程参数时,代码可能会变得非常复杂。

我编写了一个示例程序,允许用户根据给定的输入长度和宽度来计算面积。所有这些输入/输出参数都在gui上更新。尽管此示例可能无法捕获控制某些硬件和并行进程的后端逻辑的真正复杂性,但我希望它可以帮助回答我所遇到的一些问题。

问题:

  • 我创建了一个名为twoWayBindedParam的类,该类将使我为每种过程参数类型的signal,slot,setter,getter编写的代码最少。这是在后端逻辑参数和前端gui之间实现双向绑定的最佳方法吗?有什么可以改善的吗?
  • 在twoWayBindedParam类中,我定义了属性qml_prop_*,该属性将提供给前端gui。但是,我遇到的问题是我必须指定它发出的信号/插槽的类型(例如:int,float,str等)。我不能只是简单地将其指定为“对象”类型。另外,创建此属性时,我无法引用self.typeOfParamdocumentation on the QtCore Property module被限制。我如何创建属性可以与前端QML代码进行接口,而不必指定正在创建信号插槽的确切类型的值(例如:int,float,str等)。
  • 在main()函数中,我必须将QQmlApplicationEngine对象注入到myBackendLogic对象实例中。没有这个我就不能做rootContext()。setContextProperty()。有没有一种方法可以在myBackendLogic类中获取QQmlApplicationEngine()的当前运行实例,而无需在创建myBackendLogic对象时将其作为实例化参数提供?
  • 如何使QML前端类成为不属于类的函数?例如,在下面的示例代码中,我想在按下“计算”按钮时运行函数doSomething()。但是,我不确定如何调用此doSomething()函数,因为rootContext()。setContextProperty()需要类对象的名称。
  • 是否有一种建议的或简单的后端逻辑方法,当gui应用程序启动时,我可以加载存储在某些文件(例如json,xml,csv)中的数十个数百个配置参数。
  • 我当前的GUI可以按预期工作,但是当我将GUI窗口拖到第二台监视器时,所有控件都被冻结。冻结后,我需要将窗口绘制回主监视器。

我希望回答我的问题的人谨记使用Python而不是C ++作为示例代码。

main.py

import sys
import os
import random

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

class twoWayBindedParam(QObject):
   # A class to represent a variable that is two-binded in the Python logic and in QML gui

    def __init__(self, value):
        QObject.__init__(self)
        self.value = value
        self.typeOfParam = type(self.value) # Determine if its a str, int, float, etc...

    @Signal
    def valueChanged(self):
        pass

    @Slot(int)
    def set(self, value):
        self.value = value
        self.valueChanged.emit()

    def get(self):
        return self.value

    # Problem: I must create different properties for each type
    # In the QML gui, I must use the correct type of property
    # The problem is when creating the Property() object,
    # I can NOT refer to the self.typeOfParam
    # Chagning the type to 'object' doesn't work: https://stackoverflow.com/a/5186587/4988010
    qml_prop_int = Property(int, get, set, notify=valueChanged)
    qml_prop_float = Property(float, get, set, notify=valueChanged)
    qml_prop_bool = Property(bool, get, set, notify=valueChanged)

class myBackendLogic(QObject):

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

        # The app_engine object is needed to use the function rootContext().setContextProperty()
        # Is there a way to get the current instance of the app_engine that is created in the main
        # without it having to be passed as a paramter to the myBackendLogic() object?
        self.eng = app_engine

        self.init_default_params()

    def init_default_params(self):

        random.seed(23)
        length = random.randint(0,100)
        width = random.randint(0,100)
        area = self.whatIsArea(length,width)

        self.length_param = twoWayBindedParam(length)
        self.eng.rootContext().setContextProperty("length_param", self.length_param)

        self.width_param = twoWayBindedParam(width)
        self.eng.rootContext().setContextProperty("width_param", self.width_param)

        self.area_param = twoWayBindedParam(area)
        self.eng.rootContext().setContextProperty("area_param", self.area_param)

        self.continuous_calc_param = twoWayBindedParam(False)
        self.eng.rootContext().setContextProperty("continuous_calc_param", self.continuous_calc_param)

    def whatIsArea(self, l,w):
        result = float(l*w) + random.random() # Add some noise
        return result

    @Slot()
    def calculate_area_param(self):
        area = self.whatIsArea(self.length_param.get(),self.width_param.get())
        self.area_param.set(area)

def doSomething():
    print('Do something')

def main():
    app = QGuiApplication(sys.argv)
    engine = QQmlApplicationEngine()

    mylogic = myBackendLogic(engine)
    engine.rootContext().setContextProperty("mylogic", mylogic)

    engine.load(os.path.join(os.path.dirname(__file__), "main.qml"))

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

if __name__ == "__main__":

    main()

main.qml

import QtQuick 2.13
import QtQuick.Window 2.13
import QtQuick.Controls 2.2

Window 
    title: qsTr("Hello World")
    width: 640
    height: 480
    visible: true

    Column 
        id: column
        x: 131
        y: 63
        width: 72
        height: 263

        TextInput 
            id: textInput_length
            width: 80
            height: 20
            text: length_param.qml_prop_int
            font.pixelSize: 12
        

        Slider 
            id: slider_length
            to: 100
            orientation: Qt.Vertical
            value: length_param.qml_prop_int
            onValueChanged: 
                length_param.set(value)
                if (continuous_switch.checked)  mylogic.calculate_area_param() 
            
        


    

    Column 
        id: column1
        x: 249
        y: 63
        width: 72
        height: 263

        TextInput 
            id: textInput_width
            width: 80
            height: 20
            text: width_param.qml_prop_int
            font.pixelSize: 12
        

        Slider 
            id: slider_width
            to: 100
            value: width_param.qml_prop_int
            orientation: Qt.Vertical
            onValueChanged: 
                width_param.set(value)
                if (continuous_switch.checked)  mylogic.calculate_area_param() 
            
        
    

    Row 
        id: row
        x: 110
        y: 332
        width: 274
        height: 53


        Slider 
            id: slider_area
            to: 10000
            value: area_param.qml_prop_float
        

        Label 
            id: label_area
            text: area_param.qml_prop_float
        
    


    Switch 
        id: continuous_switch
        x: 343
        y: 149
        text: qsTr("Continuous calculate")
        checked: continuous_calc_param.qml_prop_bool
    

    Button 
        id: button
        x: 383
        y: 205
        text: qsTr("Calculate")
        onClicked: 
            mylogic.calculate_area_param()
        
    

    Label 
        id: label
        x: 131
        y: 23
        text: qsTr("Length")
        font.pointSize: 12
    

    Label 
        id: label1
        x: 249
        y: 23
        text: qsTr("Width")
        font.pointSize: 12
    

    Label 
        id: label3
        x: 196
        y: 377
        text: qsTr("Area")
        font.pointSize: 12
    



enter image description here

答案

您的问题很有趣,但是对于一个帖子,它可能是广泛的,因此对于下一个问题,建议您为每个问题创建一个帖子。

也许Qt所需的逻辑在您看来似乎很复杂,并且也已转移到python绑定(如PyQt5和PySide2),但是如果您在Qt的世界中花更多的时间,您会意识到事实并非如此。] >

不同于LabView,后者具有代表仪器(机械,电气等)的专门元素。 Qt是一个通用库,因此,显然,您将需要投入更多的时间来实现所需的目标,如果您的目标是特定领域,那么如果您相信Qt之上的库,那就太好了。


在本节中,我将尝试回答每个问题,因此我将列出它们:

  1. 对于很多人来说,为这个琐碎的功能实现许多代码似乎是广泛且不必要的,如在此问题中所见:How to create PyQt Properties dynamically,因此您可以将答案作为基础。

  2. PySide2 / PyQt5是C ++对象包装器,因此设置了严格的键入语言限制,因此您无法设置“对象”类型。这有一个优点和理由:Qt希望使用尽可能少的内存,因为它的许多应用程序都用于嵌入式应用程序,并且它还减少了每个任务的等待时间。另一方面,PySide2文档仍在工作,因此您可以将Qt文档作为基础,例如,Property与Q_PROPERTY等效,或者documentation of its PyQt5 brother

  3. 。] >
  4. 要使用从QML在python中实现的QObject,不必使用setContextProperty()。有两种方法可以将QObjects暴露给QML:

    • 通过setContextProperty()公开QObject,它将表现为具有全局范围的单例。

    • 公开通过qmlRegisterType(请参阅here)从QObject继承的类,然后您可以创建该QML类的对象,如TextInput,Slider等其他项目。]

    • 因此,不必访问引擎即可将类公开给QML。

  5. 无法向QML公开函数,只能公开QObjects对象,一些基本类型(如int,string等)和基于QObject的类。一种解决方案是创建一个具有该方法的类并调用它。

  6. 取决于Qt中的数据结构,建议使用模型(QAbstract Item,Table,List Model,QStandardItemModel,QSql Query,Table Model等)并使用QListView,QGridView创建小部件,中继器。这些模型可以从json,xml,csv等来源获取信息,例如,已经有一个名为XmlListModel的项,并且基于json或csv构建模型通常并不复杂。

  7. 我没有第二个监视器,因此无法测试您的指示,但是如果是,则它是Qt错误,因此建议报告。

  8. customproperty.py

    from PySide2 import QtCore, QtGui, QtQml
    
    
    class PropertyMeta(type(QtCore.QObject)):
        def __new__(cls, name, bases, attrs):
            for key in list(attrs.keys()):
                attr = attrs[key]
                if not isinstance(attr, Property):
                    continue
                initial_value = attr.initial_value
                type_ = type(initial_value)
                notifier = QtCore.Signal(type_)
                attrs[key] = PropertyImpl(
                    initial_value, name=key, type_=type_, notify=notifier
                )
                attrs[signal_attribute_name(key)] = notifier
            return super().__new__(cls, name, bases, attrs)
    
    
    class Property:
        """ Property definition.
    
        This property will be patched by the PropertyMeta metaclass into a PropertyImpl type.
        """
    
        def __init__(self, initial_value, name=""):
            self.initial_value = initial_value
            self.name = name
    
    
    class PropertyImpl(QtCore.Property):
        """ Actual property implementation using a signal to notify any change. """
    
        def __init__(self, initial_value, name="", type_=None, notify=None):
            super().__init__(type_, self.getter, self.setter, notify=notify)
            self.initial_value = initial_value
            self.name = name
    
        def getter(self, inst):
            return getattr(inst, value_attribute_name(self.name), self.initial_value)
    
        def setter(self, inst, value):
            last_value = getattr(inst, self.name)
            if last_value != value:
                setattr(inst, value_attribute_name(self.name), value)
                notifier_signal = getattr(inst, signal_attribute_name(self.name))
                notifier_signal.emit(value)
    
    
    def signal_attribute_name(property_name):
        """ Return a magic key for the attribute storing the signal name. """
        return f"_property_name_prop_signal_"
    
    
    def value_attribute_name(property_name):
        """ Return a magic key for the attribute storing the property value. """
        return f"_property_name_prop_value_"
    

main.py

import os
import random

from PySide2 import QtCore, QtGui, QtQml

from customproperty import Property, PropertyMeta


def whatIsArea(l, w):
    result = float(l * w) + random.random()
    return result


class Utils(QtCore.QObject):
    @QtCore.Slot()
    def doSomething(self):
        print("Do something")


class Backend(QtCore.QObject, metaclass=PropertyMeta):
    length = Property(0)
    width = Property(0)
    area = Property(0)
    is_continuous = Property(False)

    @QtCore.Slot()
    def calculate_area(self):
        self.area = whatIsArea(self.length, self.width)


CURRENT_DIR = os.path.dirname(__file__)

QtQml.qmlRegisterType(Backend, "MyLibrary", 1, 0, "Backend")


def main():
    import sys

    app = QtGui.QGuiApplication(sys.argv)

    engine = QtQml.QQmlApplicationEngine()

    utils = Utils()
    engine.rootContext().setContextProperty("Utils", utils)

    engine.load(os.path.join(CURRENT_DIR, "main.qml"))

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


if __name__ == "__main__":
    main()
import QtQuick 2.13
import QtQuick.Window 2.13
import QtQuick.Controls 2.2

import MyLibrary 1.0

Window 
    title: qsTr("Hello World")
    width: 640
    height: 480
    visible: true

    Backend
        id: backend
        length: 55
        width: 87
        Component.onCompleted: calculate_area()
    

    Column 
        id: column
        x: 131
        y: 63
        width: 72
        height: 263

        TextInput 
            id: textInput_length
            width: 80
            height: 20
            text: backend.length
            font.pixelSize: 12
        

        Slider 
            id: slider_length
            to: 100
            orientation: Qt.Vertical
            value: backend.length
            onValueChanged: 
                backend.length = value
                if (backend.is_continuous)  backend.calculate_area() 
            
        
    

    Column 
        id: column1
        x: 249
        y: 63
        width: 72
        height: 263

        TextInput 
            id: textInput_width
            width: 80
            height: 20
            text: backend.width
            font.pixelSize: 12
        

        Slider 
            id: slider_width
            to: 100
            value: backend.width
            orientation: Qt.Vertical
            onValueChanged: 
                backend.width = value
                if (backend.is_continuous)  backend.calculate_area() 
            
        
    

    Row 
        id: row
        x: 110
        y: 332
        width: 274
        height: 53


        Slider 
            id: slider_area
            to: 10000
            value: backend.area
        

        Label 
            id: label_area
            text: backend.area
        
    


    Switch 
        id: continuous_switch
        x: 343
        y: 149
        text: qsTr("Continuous calculate")
        checked: backend.is_continuous
        onCheckedChanged: backend.is_continuous = checked
    

    Button 
        id: button
        x: 383
        y: 205
        text: qsTr("Calculate")
        onClicked: 
            backend.calculate_area()
        
    

    Label 
        id: label
        x: 131
        y: 23
        text: qsTr("Length")
        font.pointSize: 12
    

    Label 
        id: label1
        x: 249
        y: 23
        text: qsTr("Width")
        font.pointSize: 12
    

    Label 
        id: label3
        x: 196
        y: 377
        text: qsTr("Area")
        font.pointSize: 12
    

    Component.onCompleted: Utils.doSomething()


        

以上是关于如何在Python和Qt Quick QML应用程序中实现简化的双向数据绑定的主要内容,如果未能解决你的问题,请参考以下文章

qt-quick(qml) 应用程序无法订阅 ros 主题

Qt5 和 QML:如何使用 WebEngine Quick Nano Browser 自动输入用户名和密码

qml 和 c++ 与 qt quick 2 应用程序

qt quick QML 应用程序的自定义样式页面(如 HTML 和 CSS)

✿4-The Basics-Qt Quick and QML

如何通过按下和拖动在 Qt Quick/QML 画布中绘制一个矩形