如何在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.typeOfParam
。 documentation 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
您的问题很有趣,但是对于一个帖子,它可能是广泛的,因此对于下一个问题,建议您为每个问题创建一个帖子。
也许Qt所需的逻辑在您看来似乎很复杂,并且也已转移到python绑定(如PyQt5和PySide2),但是如果您在Qt的世界中花更多的时间,您会意识到事实并非如此。] >
不同于LabView,后者具有代表仪器(机械,电气等)的专门元素。 Qt是一个通用库,因此,显然,您将需要投入更多的时间来实现所需的目标,如果您的目标是特定领域,那么如果您相信Qt之上的库,那就太好了。
在本节中,我将尝试回答每个问题,因此我将列出它们:
对于很多人来说,为这个琐碎的功能实现许多代码似乎是广泛且不必要的,如在此问题中所见:How to create PyQt Properties dynamically,因此您可以将答案作为基础。
PySide2 / PyQt5是C ++对象包装器,因此设置了严格的键入语言限制,因此您无法设置“对象”类型。这有一个优点和理由:Qt希望使用尽可能少的内存,因为它的许多应用程序都用于嵌入式应用程序,并且它还减少了每个任务的等待时间。另一方面,PySide2文档仍在工作,因此您可以将Qt文档作为基础,例如,Property与Q_PROPERTY等效,或者documentation of its PyQt5 brother
。] >要使用从QML在python中实现的QObject,不必使用setContextProperty()。有两种方法可以将QObjects暴露给QML:
通过setContextProperty()公开QObject,它将表现为具有全局范围的单例。
公开通过qmlRegisterType(请参阅here)从QObject继承的类,然后您可以创建该QML类的对象,如TextInput,Slider等其他项目。]
因此,不必访问引擎即可将类公开给QML。
无法向QML公开函数,只能公开QObjects对象,一些基本类型(如int,string等)和基于QObject的类。一种解决方案是创建一个具有该方法的类并调用它。
取决于Qt中的数据结构,建议使用模型(QAbstract Item,Table,List Model,QStandardItemModel,QSql Query,Table Model等)并使用QListView,QGridView创建小部件,中继器。这些模型可以从json,xml,csv等来源获取信息,例如,已经有一个名为XmlListModel的项,并且基于json或csv构建模型通常并不复杂。
我没有第二个监视器,因此无法测试您的指示,但是如果是,则它是Qt错误,因此建议报告。
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应用程序中实现简化的双向数据绑定的主要内容,如果未能解决你的问题,请参考以下文章
Qt5 和 QML:如何使用 WebEngine Quick Nano Browser 自动输入用户名和密码
qt quick QML 应用程序的自定义样式页面(如 HTML 和 CSS)