# 构建应用程序

在本章中,将了解如何将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引擎的rootContextsetContextProperty方法将此对象暴露给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中导入SignalSlotProperty的概念。 在完整的示例中,有更多的导入,但这几个是与属性相关的。

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对象的numbermaxNumber属性。 在ButtononClicked处理器中,调用updateNumber方法生成一个新的随机数;在SlideronValueChanged处理器中,使用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对象,并使用rootContextsetContextProperty方法使其在QML中能够使用。 在QML中实例化的对象可以更好地控制QML中对象的生命周期。 为了实现这一点,需要向QML暴露而不是对象

暴露给QML的类不受实例化位置的影响。 类的定义不需要更改。 但是,不再是调用setContextProperty函数,而是使用了qmlRegisterType函数。 该函数来自PySide2.QtQml模块,它接受五个参数:

  • 下面示例中,对类NumberGenerator的引用
  • 模块名称,Generators
  • 模块版本,由主、次编号组成,10表示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类,需要实现rowCountdata方法。 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 的 CpuLoadModelPsUtils 模块中的注册类型。

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 + "%"
            }   
        }
    }
}
最后更新: 2/1/2022, 11:21:28 AM