# 构建应用程序
在本章中,将了解如何将Python和QML结合起来。 与将C++和QML结合起来类似,将这两个方面结合起来最自然的方式是用Python实现逻辑、用QML呈现内容。
为此,需要了解如何将QML和Python结合到一个程序中,然后了解如何实现两个方面之间的接口。 在下面的小节中,将看到如何完成的这样的功能。 本节将从简单开始,并通过一个示例演示Qt项目模型将Python模块的功能暴露给QML。
# 用Python运行QML
第一步,创建一个Python程序,它承载了如下所示Hello World的QML程序。
import QtQuick
import QtQuick.Window
Window {
width: 640
height: 480
visible: true
title: qsTr("Hello Python World!")
}
为此,需要QtGui
模块中的QGuiApplication
提供的Qt主循环,还需要来自QtQml
模块的QQmlApplicationEngine
(QML应用程序引擎)。 为了将源文件的引用传递给QML应用程序引擎,还需要来自QtCore
模块的QUrl
类。
下面的代码中,模仿了Qt Creator为QML项目生成C++样板代码的功能。 它实例化了应用程序对象,并创建一个QML应用程序引擎。 然后它加载QML,并通过检查是否创建了根对象来确保加载了QML。 最后,它退出并返回应用程序对象的exec
方法的返回值。
import sys
from PySide6.QtGui import QGuiApplication
from PySide6.QtQml import QQmlApplicationEngine
from PySide6.QtCore import QUrl
if __name__ == '__main__':
app = QGuiApplication(sys.argv)
engine = QQmlApplicationEngine()
engine.load(QUrl("main.qml"))
if not engine.rootObjects():
sys.exit(-1)
sys.exit(app.exec())
运行该示例会生成一个标题为Hello Python World的窗口。
提示
该示例假定:在它的执行目录中包含main.qml
源文件。 可以使用__file__
变量确定正在执行的Python文件的位置。 这可用于定位相对于Python文件的QML文件,如此博客文章 (opens new window)中所示。
# Python对象暴露到QML
在Python和QML之间共享信息的最简单方法是将Python对象暴露给QML。 这是通过QQmlApplicationEngine
注册context property(上下文属性)来完成的。 在这样做之前,需要定义一个类,以便有一个要暴露的对象。
Qt类带有许多希望能够被使用的特性,它们是:信号、槽和属性。 在第一个示例中,将限定使用一对基本的信号和槽;其余的将在后面的示例中介绍。
# 信号与槽
先从NumberGenerator
类分析,它有一个构造函数、一个名为updateNumber
的方法和一个名为nextNumber
的信号。思路是,当调用updateNumber
时,会发出带有新随机数的信号nextNumber
。在下面可以查看该类的代码,但先了解一些细节。
首先,确保构造函数中调用了QObject.__init__
。这非常重要,因为没有调用它,该示例将无法运行。
然后通过用从PySide6.QtCore
模块导入的Signal
类来创建实例声明一个信号。在这种情况下,信号携带一个整数值参数,因此是int
;信号参数名称number
,定义在arguments
参数中。
最后,用@Slot()
装饰器装饰updateNumber
方法,从而把它变成一个槽。 Qt for Python中没有invokables的概念,所以所有可调用的方法都必须是槽。
在updateNumber
方法中,使用emit
方法发出nextNumber
信号。这与QML或C++的语法略有不同,因为信号是用对象表示的,而不是可调用的函数。
import random
from PySide6.QtCore import QObject, Signal, Slot
class NumberGenerator(QObject):
def __init__(self):
QObject.__init__(self)
nextNumber = Signal(int, arguments=['number'])
@Slot()
def giveNumber(self):
self.nextNumber.emit(random.randint(0, 99))
接下来是将刚刚创建的类与用于组合QML和Python的样板代码结合起来。 这提供了如下的入口点代码。
首先,实例化NumberGenerator
;然后,使用QML引擎的rootContext
的setContextProperty
方法将此对象暴露给QML。 这会将对象作为名为numberGenerator
的全局变量暴露给QML。
import sys
from PySide6.QtGui import QGuiApplication
from PySide6.QtQml import QQmlApplicationEngine
from PySide6.QtCore import QUrl
if __name__ == '__main__':
app = QGuiApplication(sys.argv)
engine = QQmlApplicationEngine()
number_generator = NumberGenerator()
engine.rootContext().setContextProperty("numberGenerator", number_generator)
engine.load(QUrl("main.qml"))
if not engine.rootObjects():
sys.exit(-1)
sys.exit(app.exec())
继续查看QML代码,可以看到创建了一个Qt Quick Controls 2的用户界面,其中包含一个Button
(按钮)和一个Label
(标签)。 在按钮的onClicked
处理器中,会调用numberGenerator.updateNumber()
函数,这个函数是在Python端实例化的对象的槽。
要从已在QML之外实例化的对象上接收信号,需要使用Connections
元素,它允许将信号处理器附加到已存在的目标上。
import QtQuick
import QtQuick.Window
import QtQuick.Controls
Window {
id: root
width: 640
height: 480
visible: true
title: qsTr("Hello Python World!")
Flow {
Button {
text: qsTr("Give me a number!")
onClicked: numberGenerator.giveNumber()
}
Label {
id: numberLabel
text: qsTr("no number")
}
}
Connections {
target: numberGenerator
function onNextNumber(number) {
numberLabel.text = number
}
}
}
# 属性
向QML暴露状态的常用方法并不是单纯依赖信号和槽,而是通过属性暴露。属性是setter、getter和通知信号的组合。 setter是可选的,因为也存在只读属性。
为了尝试这一点,将上个示例中的NumberGenerator
更新为基于属性的版本。它将有两个属性:number
,一个只读属性,保存最后一个随机数;maxNumber
,一个读写属性,保存可以返回的最大值。它还将有一个槽updateNumber
,用于更新随机数。
为了深入了解属性的细节,在此之前,先创建了一个基本的Python类。它由相关的getter、setter组成,但不包括Qt信号。事实上,这里是唯一从QObject
继承的Qt部分。甚至连方法的名称也是Python风格的,也就是使用下划线而不是驼峰式。
注意,__set_number
方法的开头使用了下划线(“__
”),这意味着它是一个私有方法。 所以,即使number
属性是只读的,也提供了一个setter,只是不将其暴露出来。这允许在更改其值时采取行动(例如发出通知信号)。
class NumberGenerator(QObject):
def __init__(self):
QObject.__init__(self)
self.__number = 42
self.__max_number = 99
def set_max_number(self, val):
if val < 0:
val = 0
if self.__max_number != val:
self.__max_number = val
if self.__number > self.__max_number:
self.__set_number(self.__max_number)
def get_max_number(self):
return self.__max_number
def __set_number(self, val):
if self.__number != val:
self.__number = val
def get_number(self):
return self.__number
为了定义属性,需要从PySide2.QtCore
中导入Signal
、Slot
和Property
的概念。 在完整的示例中,有更多的导入,但这几个是与属性相关的。
from PySide6.QtCore import QObject, Signal, Slot, Property
现在,准备定义第一个属性number
。 首先声明信号numberChanged
,然后在__set_number
方法中调用它,以便在其值更改时发出信号。
之后,剩下的就是实例化Property
对象。 在这种情况下,Property
的构造函数接受三个参数:类型(int
)、getter(get_number
)和传递命名参数的通知信号(notify=numberChanged
)。 请注意,getter有一个Python格式的名称,也就是使用下划线命名而不使用驼峰命名法,因为它是用于从Python读取值的。 对于QML,使用属性名称number
。
class NumberGenerator(QObject):
# ...
# number
numberChanged = Signal(int)
def __set_number(self, val):
if self.__number != val:
self.__number = val
self.numberChanged.emit(self.__number)
def get_number(self):
return self.__number
number = Property(int, get_number, notify=numberChanged)
上面的过程指导添加下一个属性,maxNumber
。 这是一个读写属性,所以需要提供一个setter,以及上面为number
属性所做的一切操作。
首先,声明maxNumberChanged
信号。 这一次,使用@Signal
装饰器而不再是实例化Signal
对象。 提供了一个带有Qt名称(驼峰命名法)的setter——槽setMaxNumber
,它简单地调用Python命名格式的方法set_max_number
;还提供了一个带有Python命名格式的getter。 同样,当其值更新时,setter会发出更改信号。
最后,通过实例化一个将类型、getter、setter 和通知信号作为参数的Property
对象,它将这些部分组合成了一个读写属性。
class NumberGenerator(QObject):
# ...
# maxNumber
@Signal
def maxNumberChanged(self):
pass
@Slot(int)
def setMaxNumber(self, val):
self.set_max_number(val)
def set_max_number(self, val):
if val < 0:
val = 0
if self.__max_number != val:
self.__max_number = val
self.maxNumberChanged.emit()
if self.__number > self.__max_number:
self.__set_number(self.__max_number)
def get_max_number(self):
return self.__max_number
maxNumber = Property(int, get_max_number, set_max_number, notify=maxNumberChanged)
现在有了当前随机数number
和最大随机数maxNumber
的属性,剩下需要的就是一个产生新随机数的槽。 它的名称为updateNumber
,它简单地设置一个新的随机数。
class NumberGenerator(QObject):
# ...
@Slot()
def updateNumber(self):
self.__set_number(random.randint(0, self.__max_number))
最后,数字生成器通过根上下文属性暴露给QML。
if __name__ == '__main__':
app = QGuiApplication(sys.argv)
engine = QQmlApplicationEngine()
number_generator = NumberGenerator()
engine.rootContext().setContextProperty("numberGenerator", number_generator)
engine.load(QUrl("main.qml"))
if not engine.rootObjects():
sys.exit(-1)
sys.exit(app.exec())
在QML中,可以绑定到numberGenerator
对象的number
和maxNumber
属性。 在Button
的onClicked
处理器中,调用updateNumber
方法生成一个新的随机数;在Slider
的onValueChanged
处理器中,使用setMaxNumber
方法设置maxNumber
属性。 这样做因为直接通过Javascript更改属性会破坏与属性的绑定;通过显式使用setter方法,可以避免破坏属性的绑定。
import QtQuick
import QtQuick.Window
import QtQuick.Controls
Window {
id: root
width: 640
height: 480
visible: true
title: qsTr("Hello Python World!")
Column {
Flow {
Button {
text: qsTr("Give me a number!")
onClicked: numberGenerator.updateNumber()
}
Label {
id: numberLabel
text: numberGenerator.number
}
}
Flow {
Slider {
from: 0
to: 99
value: numberGenerator.maxNumber
onValueChanged: numberGenerator.setMaxNumber(value)
}
}
}
}
# 暴露Python类到QML
到目前为止,已经实例化了一个Python对象,并使用rootContext
的setContextProperty
方法使其在QML中能够使用。 在QML中实例化的对象可以更好地控制QML中对象的生命周期。 为了实现这一点,需要向QML暴露类而不是对象。
暴露给QML的类不受实例化位置的影响。 类的定义不需要更改。 但是,不再是调用setContextProperty
函数,而是使用了qmlRegisterType
函数。 该函数来自PySide2.QtQml
模块,它接受五个参数:
- 下面示例中,对类
NumberGenerator
的引用 - 模块名称,
Generators
- 模块版本,由主、次编号组成,
1
和0
表示1.0
- 类的QML名称,
NumberGenerator
import random
import sys
from PySide6.QtGui import QGuiApplication
from PySide6.QtQml import QQmlApplicationEngine, qmlRegisterType
from PySide6.QtCore import QUrl, QObject, Signal, Slot
class NumberGenerator(QObject):
def __init__(self):
QObject.__init__(self)
nextNumber = Signal(int, arguments=['number'])
@Slot()
def giveNumber(self):
self.nextNumber.emit(random.randint(0, 99))
if __name__ == '__main__':
app = QGuiApplication(sys.argv)
engine = QQmlApplicationEngine()
qmlRegisterType(NumberGenerator, 'Generators', 1, 0, 'NumberGenerator')
engine.load(QUrl("main.qml"))
if not engine.rootObjects():
sys.exit(-1)
sys.exit(app.exec())
在QML中,需要导入模块(例如,导入Generators 1.0
),然后将类实例化成NumberGenerator { ... }
。 现在,该实例可以像任何其他的QML元素一样工作。
import QtQuick
import QtQuick.Window
import QtQuick.Controls
import Generators
Window {
id: root
width: 640
height: 480
visible: true
title: qsTr("Hello Python World!")
Flow {
Button {
text: qsTr("Give me a number!")
onClicked: numberGenerator.giveNumber()
}
Label {
id: numberLabel
text: qsTr("no number")
}
}
NumberGenerator {
id: numberGenerator
}
Connections {
target: numberGenerator
function onNextNumber(number) {
numberLabel.text = number
}
}
}
# 来自Python的模型
从Python暴露给QML最有趣的类或者对象类型之一是项目模型。 它们与各种视图或Repeater
元素一起使用,以从模型内容动态构建用户界面。
在本节中,将采用现有的Python实用程序(psutil
)来监控CPU负载(以及更多功能),并通过名为CpuLoadModel
的定制项目模型将其暴露给QML。 正在运行的程序如下所示:
提示
可以在https://pypi.org/project/psutil/ (opens new window)中找到与psutil库相关资料。
“psutil(进程与系统实用程序)是一个跨平台库,用于在Python中检索有关正在运行的进程和系统利用率(CPU、内存、磁盘、网络、传感器)的信息。”
可以使用pip install psutil
命令来安装psutil。
每隔一秒都使用psutil.cpu_percent
函数(文档(documentation) (opens new window))对所有内核的CPU负载进行采样。为了驱动采样,使用一个QTimer
。所有这些内容都通过CpuLoadModel
(它是一个QAbstractListModel
类)暴露出来。
项目模型非常有趣。如果使用QAbstractItemModel
的话,它们允许表示二维数据集,甚至是嵌套数据集。使用的QAbstractListModel
允许表示项目列表,即一维数据集。它也可以实现一组嵌套的列表,创建一个树型结构,但这里只创建了一个层次。
要实现一个QAbstractListModel
类,需要实现rowCount
和data
方法。 rowCount
返回使用psutil.cpu_count
方法获得的CPU核数。 data
方法返回不同roles(角色)的数据。这里仅支持Qt.DisplayRole
,它对应于在QML的代理项目中引用display
时得到的内容。
查看模型的代码,可以看到实际数据存储在列表__cpu_load
中。如果对data
发出了有效的请求,即正确的行、列和角色,将会从列表__cpu_load
中返回正确的元素;否则,返回 None
,它对应于Qt端未初始化的QVariant
。
每当更新定时器(__update_timer
)超时,都会触发__update
方法。在这里,__cpu_load
列表被更新,同时发出dataChanged
信号,表示所有数据都已被更改。不执行modelReset
,是因为这也意味着项目的数量可能已经改变。
Finally, the CpuLoadModel
is exposed to QML are a registered type in the PsUtils
module.
最后,暴露给 QML 的 CpuLoadModel
是 PsUtils
模块中的注册类型。
import psutil
import sys
from PySide6.QtGui import QGuiApplication
from PySide6.QtQml import QQmlApplicationEngine, qmlRegisterType
from PySide6.QtCore import Qt, QUrl, QTimer, QAbstractListModel
class CpuLoadModel(QAbstractListModel):
def __init__(self):
QAbstractListModel.__init__(self)
self.__cpu_count = psutil.cpu_count()
self.__cpu_load = [0] * self.__cpu_count
self.__update_timer = QTimer(self)
self.__update_timer.setInterval(1000)
self.__update_timer.timeout.connect(self.__update)
self.__update_timer.start()
# The first call returns invalid data
psutil.cpu_percent(percpu=True)
def __update(self):
self.__cpu_load = psutil.cpu_percent(percpu=True)
self.dataChanged.emit(self.index(0,0), self.index(self.__cpu_count-1, 0))
def rowCount(self, parent):
return self.__cpu_count
def data(self, index, role):
if (role == Qt.DisplayRole and
index.row() >= 0 and
index.row() < len(self.__cpu_load) and
index.column() == 0):
return self.__cpu_load[index.row()]
else:
return None
if __name__ == '__main__':
app = QGuiApplication(sys.argv)
engine = QQmlApplicationEngine()
qmlRegisterType(CpuLoadModel, 'PsUtils', 1, 0, 'CpuLoadModel')
engine.load(QUrl("main.qml"))
if not engine.rootObjects():
sys.exit(-1)
sys.exit(app.exec())
在QML方面,使用ListView
来显示CPU负载,将模型绑定到model
属性。 对于模型中的每个项,将实例化一个delegate
项。 在这种情况下,这意味着一个带有绿色条(另一个Rectangle
)和一个显示当前负载的Text
元素的Rectangle
元素。
import QtQuick
import QtQuick.Window
import PsUtils
Window {
id: root
width: 640
height: 480
visible: true
title: qsTr("CPU Load")
ListView {
anchors.fill: parent
model: CpuLoadModel { }
delegate: Rectangle {
id: delegate
required property int display
width: parent.width
height: 30
color: "white"
Rectangle {
id: bar
width: parent.width * delegate.display / 100.0
height: 30
color: "green"
}
Text {
anchors.verticalCenter: parent.verticalCenter
x: Math.min(bar.x + bar.width + 5, parent.width-width)
text: delegate.display + "%"
}
}
}
}