🤖 作者:包瑞清(richie bao): lastmod: 2025-01-31T10:20:47+08:00

11.1 Python 应用开发工具

支持 Python 编程语言进行图形用户界面(GUI)开发的工具有TkinterPyGObjectPyQtPySide6KivywxPythonDear PyGui等。下面例举了几个主要的 Python 应用开发工具。

  • PySide6

PySide6Qt框架的 Python 绑定,基于 Qt6 版本构建,支持各种高级图形和用户界面功能,提供了一个用于开发跨平台桌面应用程序的完整工具集。PySide6 主要特点有:

  1. 跨平台支持:支持 Windows、macOS 和 Linux 等主流操作系统,允许开发者创建跨平台应用程序。
  2. 面向对象编程:采样面向对象的编程范式,提供了大量的控件(如按钮、文本框、标签、树形视图等)和功能组件。
  3. 丰富的控件库:UI 组件丰富,包括但不限于:按钮、文本框、列表视图、表格视图、图形视图、图形项等。支持复杂的布局管理,方便开发者设计高度定制化的界面。
  4. 集成图形和 2D/3D 渲染:除了常规的 GUI 控件,PySide6 还支持图形渲染,适用于需要高度定制化或具有交互性的图形界面。
  5. 事件系统:通过信号(Signals)和槽(Slots)机制,支持事件驱动的编程模型,使得用户交互流程和高效。
  6. Qt Designer 继承:PySide6 可以与 Qt Designer 配合使用。Qt Designer 是一个可视化的界面设计工具,可以快速拖拽控件,生成.ui文件,并通过pyside6-uic工具转换为 Python 代码。
  7. 多语言支持:通过 Qt 的翻译功能,支持多语言环境,轻松创建本地化应用。
  8. 性能:PySide6 继承了 Qt 的高性能特点,适合开发需要复杂图形界面或高性能需求的桌面应用。

PySide6 适合于开发文本编辑器、图形设计工具等桌面应用;管理软件、分析工具等企业级应用;显示实时数据、图表和图形的数据可视化应用;支持2D、3D 图形、OpenGL 等,适用于需要图形渲染的图形界面开发。

  • Tkinter

Tkinter 是 Python 的标准 GUI (图形用户界面)库,是对 Tcl/Tk GUI 库的封装。由于 TKinter 简单、轻量级的特性,成为 Python 开发者常用的 GUI 开发工具之一。Tkinter 的主要特点有:

  1. 轻量级和内置库:Tkinter 是 Python 的标准库之一,意味着不需要安装任何额外的依赖包,直接就可以在 Python 环境中使用。
  2. 跨平台支持:支持 Windows、macOS 和 Linux 等操作系统。通过 Tkinter 可以开发跨平台的桌面应用。
  3. 简洁易学:Tkinter 设计上非常简单,提供了基本的 GUI 控件,如按钮、标签、文本框、菜单等,并支持基本的事件驱动编程,适合初学者快速上手。
  4. 事件驱动编程模型:使用事件驱动的模型,支持通过回调函数来响应用户的输入(如点击按钮、键盘输入等)。
  5. 基础控件:提供的控件包括:按钮、标签、文本框、单选框、复选框、列表框、滚动条、菜单等。Tkinter 的控件相比 PySide6 和其他 GUI 库少,适用于简单的应用。
  6. 布局管理:支持三种布局管理方式:pack()grid()place(),这些布局方式使得开发者能够控制控件的排布和大小。
  7. 内建对话框:提供了一些标准的对话框,如文本选择对话框、消息框、颜色选择对话框等,可以帮助开发者快速实现常见的功能。
  8. 不支持高级图形功能:与 PySide6 等框架不同,Tkinter 不支持复杂的图形渲染(如 OpenGL 渲染、2D/3D 渲染等),仅适合制作基本的桌面工具。
  9. 相对较低的性能:性能适合小型应用,不适合于复杂界面或需要高性能的应用。

Tkinter 适用于开发如计算器、文本编辑器等小型应用的桌面工具;由于 Tkinter 简单易学,可以快速开发原型,验证想法;也适合于作为 Python 编程初学者的教育和学习工具。

  • Dear PyGui

Dear PyGui(DPG)是一个易于使用、动态,GPU 加速,跨平台的 Python 图形用户界面工具包(GUI)。功能除了包括传统的 GUI 元素,如按钮、单选按钮、菜单和各种创建功能布局的方法外,DPG 还支持各种动态绘图、表格、绘图、调试器和多个资源查看器等功能。DPG 的主要特点有:

  1. 图形渲染:DPG 提供了 OpenGL 支持,能够进行高效的图形渲染,处理复杂的图形界面和动态图形,适合开发需要强大图形处理能力的应用。
  2. 性能:由于基于 OpenGL,具有较高的性能,特别是在需要频繁更新界面的应用中,可以处理实时渲染的交互式应用,如图形编辑器、数据可视化等。
  3. 简洁的 API:DPG 的 API 设计简洁直观,可以快速上手,并提供了许多内建控件(如按钮、输入框、进度条、图形视图等)和布局管理功能。
  4. 实时交互:DPG 专为需要动态更新的应用设计,支持快速更新 UI 元素,适用于实时数据可视化、动态交互等场景。
  5. 内建主题支持:提供了一些内建的 UI 主题,可以帮助开发者快速设计具有现代感的界面、且支持自定义主题。
  6. 阔平台支持:支持 Windows、macOS 和 Linux 等主流操作系统,能够开发跨平台的桌面应用。
  7. Python API 和 C++ 绑定:提供了 Python API ,同时也支持 C++ 接口,这为需要更高性能或需要与其它 C++ 库集成的开发者提供了便利。
  8. 绘图和图形控件:DPG 提供了绘图功能,可以绘制 2D 图形、文本、曲线、图表等,适合于数据可视化和图形编辑工具。
  9. 事件驱动编程模型:采用事件驱动模式,支持响应用户的输入事件,配合回调函数可以实现交互式界面。
  10. 开发工具:内置了开发者调试工具,可以帮助开发者在开发过程中调试界面和渲染过程。

Dear PyGui 适用于构建动态显示实时数据、绘制图表、仪表盘等数据可视化应用;图形设计工具、3D 建模工具等图形编辑器;用于开发游戏引擎的开发工具、编辑器等游戏开发工具;创建需要频繁更新和高交互性的实时控制界面;需要动态显示复杂设计结果的实验室软件、仿真系统等,为开发科学、工程、游戏、数据科学和其它需要快速交互界面的应用程序提供了基础框架。

PySide6,Tkinter,Dear PyGUI 三种 GUI 应用开发工具的比较。

表 11-1 GUI 应用开发工具比较

特性 PySide6 Tkinter Dear PyGui
库类型 图形用户界面(GUI)框架 标准GUI库 图形用户界面(GUI)库
开发者 Qt Company Python 官方(由 Tcl/Tk 提供支持) Dear PyGui 团队
支持的操作系统 跨平台(Windows、macOS、Linux) 跨平台(Windows、macOS、Linux) 跨平台(Windows、macOS、Linux)
编程范式 面向对象 事件驱动式 事件驱动式
易用性 复杂,但功能丰富,适合专业开发者 简单,适合快速开发,学习曲线较平缓 现代且易用,适合开发高效的图形应用
支持的控件 提供大量控件(按钮、文本框、树形视图、表格等) 提供基本控件(按钮、标签、文本框等) 提供现代控件(按钮、进度条、输入框等)
界面设计工具 支持Qt Designer(可视化设计工具) 没有可视化设计工具 没有可视化设计工具
性能 高性能,适合复杂应用开发 较低,适合小型应用 性能较好,适合游戏和高效图形应用
文档和社区支持 强大,Qt社区活跃 非常活跃,文档完善 社区相对较小,但活跃
开源与商业 开源(LGPL),也有商业授权版本 完全开源 完全开源
跨平台支持 完全支持 完全支持 完全支持
高级特性支持 动画、绘图、2D/3D渲染、WebView等 不支持高级特性 支持现代图形功能和动态UI
学习曲线 较陡,功能多但需要时间掌握 简单,适合初学者 简单易用,但也有一些高级特性
应用场景 桌面应用,复杂的企业级应用 小型桌面工具和快速原型开发 游戏工具,实时交互应用,快速原型开发
扩展性 高,支持与其他Qt库的集成 较低,扩展功能有限 高,支持Python、C++等多语言绑定
图形渲染与交互性 支持2D/3D图形渲染,适合复杂图形应用 不支持图形渲染,适合简单应用 高效的实时渲染和图形交互,适合动态图形和数据可视化
内置图形支持 支持 OpenGL 和图形视图组件 无内置图形支持 强大的2D/3D图形绘制能力,适合动态图形界面
适用领域 企业级应用、大型桌面应用 小型工具、快速原型开发 数据可视化、游戏工具、实时交互应用

11.2 PySide6 与 QtCreator

11.2.1 Python 虚拟环境下安装 PySide6

Python 虚拟环境(Virtual Environment)是一个独立的工作环境,可以在其中安装和使用特定版本的 Python 包和库,而不会影响到系统全局的 Python 环境,有益于管理项目的依赖和避免版本冲突。

11.2.1.1 使用 Python 官网的安装包

  1. (安装 Python)从 Python 官网 下载对应系统的最新 Python 安装程序(试验中使用 Windows 系统)。安装过程中确保同时勾选了pip项,以方便下载和安装其它 Python 包。同时,需要注意勾选Add Python to environment variables(或为Add Python to PATH),这将允许从系统命令行(Terminal/PowerShell)中使用 Pythonpip 命令。
  2. (创建虚拟环境)安装 Python 完成后,按Win + R,敲入cmdpowershell打开 Terminal/PowerShell(或在开始Start图标上右键弹出菜单选择 Terminal)。在命令行中用cd命令定位到需要创建 Python 虚拟环境的文件夹路径下,如cd C:\Users\richie\Omen_RichieBao\omen_APP\PySide6_cases\envs;然后执行python -m venv env-venv创建虚拟环境,env-venv为示例中自定义的虚拟环境名称。创建完成后可以在目标文件夹中查看生成的文件。
  3. (激活虚拟环境)在命令行(Command Prompt)中敲入env-venv\Scripts\activate或在 PowerShell 中敲入.\env-venv\Scripts\Activate,运行激活新建的虚拟环境。如果要退出虚拟环境,则执行deactivate命令。
  4. (安装 PySide6)在命令行中执行pip install pyside6,安装最新版 PySide6 库。
  5. (测试)为了测试新建 Python 虚拟环境的 pySide6 库是否正常安装。使用Visual Studio Code书写如下代码,并存储至本地磁盘,命名为pySide6_test.py
import PySide6.QtCore

print(PySide6.__version__)
print(PySide6.QtCore.__version__)

通过打印版本信息来测试安装。执行上述 Python 代码文件,在激活的虚拟环境命令行中,定位到该文件所在文件夹,执行python pySide6_test.py,获得输出结果为6.8.2,表明虚拟环境和 PySide6 均安装成功。

上述创建 Python 虚拟环境所用的 venv模块支持创建轻量级的“虚拟环境”,每个虚拟环境都有自己独立的 Python 包集合,安装在各自的站点目录中。虚拟环境是在现有的 Python 安装基础上创建的,这个安装被称为虚拟环境的“base(基础)” Python,并且可以选择性地与基础环境中的包隔离,这样只有那些明确安装在虚拟环境中的包才可用。

除了使用 Python 标准库中的模块venv创建 Python 虚拟环境,也可以使用第三方工具virtualenv,但必须先在终端中通过pip install virtualenv安装virtualenv。然后定位到要创建虚拟环境的文件夹路径下,执行virtualenv env_virtualenv创建名为env_virtualenv的虚拟环境。虚拟环境的激活方法和 PySide6 库安装同venv步骤。因为venv为 Python 标准库的一部分,无需额外安装,为官方推荐的虚拟环境工具。如果需要更多控制选项,如选择 Python 版本,默认隔离环境等,则可以考虑使用virtualenv

11.2.1.2 使用 Anaconda

Anaconda官网下载 Distribution 版本安装。安装后打开 Anaconda, 在Environments下点击Create按钮,根据提示创建新的 Python 虚拟环境。Anaconda 创建的虚拟环境通常位于 Anaconda 安装路径下的envs文件夹下。新创建的虚拟环境会显示在Environments页面中,在其上点击右侧向的一个箭头打开弹出菜单,选择Open Terminal打开终端,敲入pip install pyside6运行安装 PySide6。

Anaconda 是一个开源的 Python 和 R 语言的数据科学和机器学习平台,旨在提供一个统一的环境,用于开发、运行和管理科学计算、数据分析、机器学习和大数据处理等应用程序。Anaconda 提供了conda包管理工具,允许用户安装、更新和卸载 Python 包和环境;创建和管理不同的虚拟环境,避免不同项目之间的依赖冲突。在 Anaconda 中可以安装 Spyder, 一个专为数据科学设计的 Python 集成开发环境(IDE),提供了调试工具、交互式控制台和可视化功能等。

11.2.2 Qt Creator

Qt Creator 是一个跨平台的集成开发环境(IDE),专为开发使用 Qt 框架的应用程序而设计,用于开发 GUI 应用程序,支持跨平台开发,适用于 Windows、macOS 和 Linux 操作系统。Qt Creator 作为 Qt 官方推荐的 IDE,提供了一系列功能,帮助开发者高效的创建 Qt 应用程序。Qt Creator 主要特点有,

  1. 跨平台支持:支持多种操作系统(Windows、macOS、Linux)和不同的编译器,如 GCC、Clang 和 MSVC等,允许用户在不同平台上开发、编译和调试应用程序。
  2. 集成的 Qt 框架:提供了对 Qt 库的完整支持,帮助开发者快速使用 Qt 提供的各种功能(如 GUI、网络、数据库、3D图形等)。
  3. 图形化界面设计:内置了 Qt Designer,一个可视化界面设计工具,允许开发者通过拖放方式创建应用程序的 GUI,以便快速布局窗口、控件,并生成相应的代码。
  4. C++ 和 Python 支持:支持 C++ 和 Python 编程语言,使用 Python 时,是基于 PySide6 库创建应用。在选择模板时选择Application(Qt for Python)
  5. Qt Quick 和 QML 支持:支持用于创建动态、响应式界面的框架 Qt Quick 和 Qt 的声明性编程语言 QML,以便帮助开发者更快的开发现代化的应用界面。
  6. 调试器和分析工具:集成 GDB 调试器、LLDB 支持和代码分析工具(如 Valgrind),可以辅助进行深度调试,查看内存泄漏、性能瓶颈等。
  7. 版本控制系统支持:集成如 Git、Bazaar 和 Mercurial 等版本控制工具,方便管理代码、进行团队协作。
  8. 性能分析:内置性能分析工具,如 Qt Creator Profiler,帮助分析应用程序的性能瓶颈。

Qt Creator 适合于开发桌面 GUI 应用程序,包括对跨平台的支持;也广泛用于嵌入式设备开发,Qt Creator 提供对嵌入式目标的调试和部署支持;结合 Qt Quick 或 QML 开发现代化、响应式的应用界面,支持实时预览和动态调试。

安装 Qt Creator 可以通过使用包管理器(Package Managers)安装(命令行)或直接下载安装包(Installation Packages)安装。如果使用包管理器,在 Windows 终端系统命令行中输入choco install qtcreator;在 macOS 中输入brew install --cask qt-creator进行安装。或从 https://download.qt.io/official_releases/qtcreator/latest/ Qt 官网下载 .exe(Windows)等安装程序直接安装。可以从 Qt Creator 的 GitHub 代码托管查看源码或获取相关信息。

11.3 创建项目和Hello world!

11.3.1 创建项目

打开 Qt Creator,在欢迎页面(Welcome to Qt Creator)下选择 Create Project...创建项目,根据新建项目向导完成项目建立。

步骤 用户界面 说明
1

PYC icon

Qt Creator 的欢迎页面,主要用于快速访问项目、示例、教程和其它资源;并可以选择创建一个新项目或打开一个现有项目;底部工具栏包含了快速访问编译、终端输出和其它与项目相关任务的标签。当前尚没有创建任何项目。

2

PYC icon

可以通过选择不同的项目模板来创建项目。每个模板都有详细的描述和支持的平台信息,帮助开发者根据需求选择合适的项目。这里选择Application(Qt for Python)->Window UI模板,创建一个 Qt for Python(使用 PySide6库)的应用程序,其中包括一个基于 Qt Widgets Designer 的可视化控件(或称为窗口部件)(Widget)。需要 .ui到 Python 代码的转换(由 Qt Creator 自动完成)。

3

PYC icon

在该向导界面配置项目名称和存储位置。可以将当前路径设置为默认的项目位置。
4

PYC icon

当前界面是 Qt Creator 的类定义界面,在此选择定义项目的基类(Base class)。可选项包括QWidget,用于创建自定义窗口或控件;QDialog,用于创建对话框;QMainWindow,用于创建主应用程序窗口(包括菜单栏、工具栏等)。

QWidgetQMainWindowQDialog 都是 Qt 框架中的窗口部件类,用于不同类型的窗口和用户界面组件。

  • QWidget

QWidget是 Qt 中所有图形用户界面(GUI)控件的基类。几乎所有的窗口控件、控件和可视化组件都继承自QWidget,是 Qt 中最基本的窗口部件类。QWidget可以作为一个独立的窗口,也可以作为其它控件的容器,并可以通过继承QWidget,创建自定义控件。适合用于创建简单的自定义控件或容器控件;用作应用程序中的基础窗口。

  • QMainWindow

QMainWindow是继承自QWidget的一个更高级的类,专门用于创建应用程序的主窗口,提供了更多的内置功能,适用于需要菜单栏、工具栏、状态栏等功能的复杂应用。QMainWindow提供了一个方法setCentralWidget(),用来设置主窗口的中心部件,通常这个部件是显示应用程序内容的区域。QMainWindow适合于创建包含菜单、工具栏、多个子窗口等功能应用程序的主窗口;适用于需要复杂布局和组织方式的桌面应用。

  • QDialog

QDialog 是 Qt 中用于创建对话框的类。对话框通常用于获取用户输入、显示警告或确认消息等。QDialogQWidgetQMainWindow更专注于短时间显示的临时窗口,通常是交互式的。QDialog可以式模态(阻止用户与其它窗口交互,直到对话框关闭)或非模态(允许用户与其它窗口交互)。QDialog提供了便捷的方法来设置和管理按钮、输入框等控件,适用于快速创建和显示对话框,如文件选择、确认对话框、消息框等;用于处理用户输入或显示简短的信息等。

QWidgetQMainWindowQDialog 的比较,

特性 QWidget QMainWindow QDialog
继承关系 所有控件的基类 QWidget 的子类 QWidget 的子类
用途 用于创建自定义控件或简单窗口 用于创建主窗口,适合功能复杂的应用程序 用于创建对话框和模态/非模态窗口
内置功能 没有内置菜单栏、工具栏、状态栏等 提供菜单栏、工具栏和状态栏等 提供用于处理用户输入和交互的控件
中心部件 无默认中心部件 提供 setCentralWidget() 方法设置中心部件 没有中心部件概念,通常只用于用户交互
适用场景 用于简单的窗口和自定义控件开发 用于复杂的主窗口应用,适合带有菜单和工具栏的应用 用于创建临时对话框或提示框
模态支持 无模态特性 无模态特性 可以设置为模态或非模态
5

PYC icon

PySide version选择PySide 6,为最新的 PySide 版本,支持 Qt 6。

6

PYC icon

A

PYC icon

B

PYC icon

C

PYC icon

D

A. 当前界面为 Qt Creator 的工具包选择(Kit Selection)界面,需要在此为项目选择合适的构建工具包。如果未找到合适的工具包,系统会提示通过选项(options)或 SDK 维护工具添加工具包。这里选择options

B. 工具包是 Qt Creator 中用于构建和运行项目的重要配置,需要根据项目需求选择合适的工具包。

C. 因为选择 Qt for Python(PySide6)创建 GUI 应用程序,因此从左侧列表中选择Python项;通过右侧工具栏的Add增加 Python解释器;配置名称NamePythonVenv;并选择 Python 解释器的可执行文件路径,这里选择了前文使用venv模块创建的 Python 虚拟环境env-venv。具体选择的文件为env-venv->Scripts->python.exe文件。最后需要点击Generate Kit为选择的解释器生成工具包。

D. 此时可以选择新创建的工具包PythonVenv

7

PYC icon

当前界面是 Qt Creator 项目总结界面,可以确认项目的配置和文件结构,检查项目的基本信息,及配置版本控制,这里选择了Git作为版本控制系统。Files to be added in列出了项目目录下将要创建的文件,示例所在路径为C:\Users\richie\Omen_RichieBao\omen_APP\PySide6_cases/first0tWidgetApp,包含的文件有,

  1. .gitignore:Git 忽略文件,用于指定不需要版本控制的文件。
  2. firstQtWidgetApp.pyproject:Qt Creator 的项目文件。
  3. form.ui:使用 Qt Designer 设计的界面文件。
  4. requirements.txt:Python 依赖文件,即列出项目所需的第三方库。
  5. widget.py:项目的主 Python 文件,包含应用程序的逻辑。
8

PYC icon

A

PYC icon

B

PYC icon

C

A. 完成项目的创建,生成有一个典型的 PySide6 应用程序的模板。

B. Qt Designer 可视化界面设计工具,通过拖放方式创建应用程序的 GUI。

执行界面左下角右向绿色三角形图标按钮(或Ctr + RRun,会打开如图 C 空的的应用界面。

11.3.2 Hello world!

基于上述创建的典型 PySide6 应用程序模板,增加显示一个带有文本“Hello world!”的标签,并为该标签设置字体和颜色,如图11-1。

PYC icon

图 11-1 Hello world! 应用界面

文件 widget.py ui_form.py firstQtWidgetApp.pyproject
代码
# This Python file uses the following encoding: utf-8
import sys

from PySide6.QtWidgets import QApplication, QWidget, QLabel
from PySide6.QtGui import QFont

# Important:
# You need to run the following command to generate the ui_form.py file
#     pyside6-uic form.ui -o ui_form.py, or
#     pyside2-uic form.ui -o ui_form.py
from ui_form import Ui_Widget


class Widget(QWidget):
    def __init__(self, parent=None):
        super().__init__(parent)
        self.ui = Ui_Widget()
        self.ui.setupUi(self)

        self.hello_world()

    def hello_world(self):
        label = QLabel("Hello world!", self)
        label.move(100, 100)

        font = QFont("Arial", 16, QFont.Bold)
        label.setFont(font)

        label.setStyleSheet("color: green;")

        label.show()


if __name__ == "__main__":
    app = QApplication(sys.argv)
    widget = Widget()
    widget.show()
    sys.exit(app.exec())
# -*- coding: utf-8 -*-

################################################################################
## Form generated from reading UI file 'form.ui'
##
## Created by: Qt User Interface Compiler version 6.8.2
##
## WARNING! All changes made in this file will be lost when recompiling UI file!
################################################################################

from PySide6.QtCore import (QCoreApplication, QDate, QDateTime, QLocale,
    QMetaObject, QObject, QPoint, QRect,
    QSize, QTime, QUrl, Qt)
from PySide6.QtGui import (QBrush, QColor, QConicalGradient, QCursor,
    QFont, QFontDatabase, QGradient, QIcon,
    QImage, QKeySequence, QLinearGradient, QPainter,
    QPalette, QPixmap, QRadialGradient, QTransform)
from PySide6.QtWidgets import (QApplication, QSizePolicy, QWidget)

class Ui_Widget(object):
    def setupUi(self, Widget):
        if not Widget.objectName():
            Widget.setObjectName(u"Widget")
        Widget.resize(800, 600)

        self.retranslateUi(Widget)

        QMetaObject.connectSlotsByName(Widget)
    # setupUi

    def retranslateUi(self, Widget):
        Widget.setWindowTitle(QCoreApplication.translate("Widget", u"Widget", None))
    # retranslateUi
{
    "files": [
        "widget.py",
        "form.ui"
    ]
}

🤖 代码解读

  1. # This Python file uses the following encoding: utf-8:为一个文件级的注释,声明该 Python 文件使用 UTF-8 编码,这在 Python 3 中是默认的编码方式。
  2. import sys:导入 Python 的sys模块,该模块提供与 Python 解释器及其环境的交互功能,例如获取命令行参数、退出等。
  3. from PySide6.QtWidgets import QApplication, QWidget, QLabel:从PySide6.QtWidgets模块导入QApplicationQWidgetQWidget类。QApplication是 PySide 6 应用程序的核心类,用于管理应用程序的控制流和设置;QWidget是所有窗口和控件的基类,创建的一个主窗口通常会继承该基类;QLabel是一个用来显示文本或图像的控件,这里用来显示“Hello world!”文本。
  4. from PySide6.QtGui import QFont:从PySide6.QtGui模块导入QFont类,QFont用于设置文本字体、大小和央视的等。
  5. # Important:部分的注释提醒开发者需要通过pyside6-uicpyside2-uic工具将.ui文件(由 Qt Designer 生成的界面文件)转换成 Python 文件ui_form.py(Qt Creator 自动转换),这个文件包含界面控件的类的定义。
  6. from ui_form import Ui_Widget:从ui_form.py文件导入Ui_Widget类,这是通过 Qt Designer 生成的类,包含了界面布局和控件的定义。
  7. class Widget(QWidget)::定义一个Widget类,继承自QWidget,表示一个自定义窗口或控件类。
  8. def __init__(self, parent=None)::这是类的构造函数,在创建Widget实例时,会被自动调用。parent参数默认为None,用于设置该窗口的父窗口。
  9. super().__init__(parent):调用父类QWidget的构造函数,以确保父类正确初始化。parent是传递给父类构造函数的参数。
  10. self.ui = Ui_Widget():创建一个Ui_Widget对象,该对象包含了通过 Qt Designer 生成的界面控件(例如按钮、标签、输入框等)。
  11. self.ui.setupUi(self):调用Ui_Widget类的setupUi方法,将界面控件添加到当前的Widget实例(即窗口)中。
  12. self.hello_world():调用hello_world()方法(该方法在类中定义),用于在窗口中显示一个带有样式和字体的标签。
  13. def hello_world(self)::定义hello_world()方法,创建并显示一个包含文本的标签。
  14. label = QLabel("Hello world!", self):创建一个QLabel控件,并设置其文本为"Hello world!"self表示标签将作为Widget窗口的子控件显示。
  15. label.move(100, 100):将标签移动到窗口内的(100, 100)坐标位置。
  16. font = QFont("Arial", 16, QFont.Bold):创建一个QFont对象,设置字体为Arial,大小为16,并将字体样式设置为粗体QFont.Bold
  17. label.setFont(font):将创建的字体应用到label标签上,使用使用 Arial 字体,大小为1 16,且为粗体。
  18. label.setStyleSheet("color: green;"):使用setStyleSheet方法为标签设置样式表,这里将标签的文本颜色设置为绿色。
  19. label.show():显示标签控件,使其在窗口中可见。
  20. if __name__ == "__main__"::这一行是 Python 模块保护块的一部分,确保以下代码仅在该文件作为主程序运行时执行,而不是作为模块导入时执行。
  21. app = QApplication(sys.argv):创建一个QApplication实例,是 PySide6 应用程序的核心类,必须在任何窗口或控件显示之前创建。sys.argv是命令行参数,传递给QApplication用来配置应用程序。
  22. widget = Widget():创建Widget类的实例,表示应用程序的主窗口。
  23. widget.show():调用show()方法显示窗口(Widget实例)。
  24. sys.exit(app.exec()):启动应用程序的事件循环,app.exec()会进入事件循环,等待用户的交互(例如点击按钮、输出文本等)。当退出事件循环时,sys.exit()会退出程序并返回退出状态码。

这段代码是通过pyside6-uic工具从.ui文件生成的 Python 代码,其定义了一个名为Ui_Widget的类,该类用于设置窗口的 UI。

  1. ## WARNING!部分的注释说明如果重新编译.ui文件,所有手动修改的内容将会丢失,因此应避免直接修改该部分代码。
  2. 导入模块部分导入了许多 PySide6 的模块和类,PySide6.QtCore模块包含了 Qt 的核心功能,如日期、时间、坐标、对象模型等;PySide6.QtGui提供了图形视图的相关类,如字体、颜色、图标、图像处理等;PySide6.QtWidgets包含创建 UI 控件所需的类,如窗口、按钮、布局等。
  3. class Ui_Widget(object)::定义了一个名为Ui_Widget的类,继承自object类,表示窗口的 UI 组件。这是由 Qt Designer 自动生成的类,用于设置和初始化 UI 组件。
  4. setupUi方法用来设置窗口的 UI 布局,其接受一个Widget参数,表示要设置的窗口(QWidget类的实例)。
  5. if not Widget.objectName():检查窗口对象是否已设置名称,如果没有设置,则设置为Widget
  6. Widget.resize(800, 600)设置窗口的大小为 (800, 600)像素。
  7. self.retranslateUi(Widget):调用retranslateUi方法,用于设置窗口和控件的文本内容。
  8. QMetaObject.connectSlotsByName(Widget):将信号和槽连接起来。Qt 使用信号和槽机制来处理事件,例如按钮点击事件等。
  9. retranslateUi方法用于设置窗口的文本内容,特别是在支持多语言的应用程序中。使用QCoreApplication.translate来翻译并设置窗口的标题。QCoreApplication.translate("Widget", u"Widget", None)设置窗口的标题为"Widget"。在支持多语言的情况下,这个方法会根据翻译文件(.ts)进行相应的翻译。

11.4 信号(Signals)与槽(Slots)

11.4.1 一个简单的按钮

信号(signals)和槽(slots)是 Qt 一个特性,为对象之间通信的机制,允许控件之间或与 Python 代码之间进行通信。信号用于发出某种事件的通知,而槽则是处理这些事件的函数,通过QObjectconnect方法将信号与槽连接。当一个信号被发射时,任何连接到该信号的槽都会被调用。这里实现了一个简单的 PySide6 GUI 窗口,包含一个按钮(按钮点击触发信号)和一个标签(接收槽函数处理后的结果)。点击按钮时,标签会显示一段文本,按钮的文本也会更新。

PYC icon

图 11-2 一个简单的按钮

文件 widget.py
代码
import sys

from PySide6.QtWidgets import QApplication, QWidget, QPushButton, QLabel, QVBoxLayout
from PySide6.QtCore import Slot, Qt
from PySide6.QtGui import QFont

from ui_form import Ui_Widget


class Widget(QWidget):
    def __init__(self, parent=None):
        super().__init__(parent)
        self.ui = Ui_Widget()
        self.ui.setupUi(self)

        self.layout = QVBoxLayout()
        self.setLayout(self.layout)
        self.setWindowTitle("A Simple Button")

        self.signal_sayHello()

    def signal_sayHello(self):
        self.button = QPushButton("Click me")
        self.layout.addWidget(self.button)
        self.layout.setAlignment(self.button, Qt.AlignmentFlag.AlignCenter)

        self.label = QLabel()
        self.label.setStyleSheet("color: green;")
        self.layout.addWidget(self.label)
        self.layout.setAlignment(self.label, Qt.AlignmentFlag.AlignCenter)

        self.button.clicked.connect(self.slot_sayHello)

    @Slot()
    def slot_sayHello(self):
        self.label.clear()
        self.label.setText("Hi, guys!")

        font = QFont("Arial", 24, QFont.Bold)
        self.label.setFont(font)

        self.button.setText("You already clicked me")


if __name__ == "__main__":
    app = QApplication(sys.argv)
    widget = Widget()
    widget.show()
    sys.exit(app.exec())
🤖 代码解读
self.layout = QVBoxLayout()
self.setLayout(self.layout)
  1. 创建一个垂直布局管理器QVBoxLayout,并将其设置为Widget的主布局。
  2. self.setWindowTitle("A Simple Button")设置窗口的标题为"A Simple Button"
  3. 调用self.signal_sayHello()方法,用于设置按钮和标签,并连接信号与槽。
self.button = QPushButton("Click me")
self.layout.addWidget(self.button)
  1. 创建一个按钮控件QPushButton,设置其文本为"Click me"。并将按钮添加到布局中。
  2. self.layout.setAlignment(self.button, Qt.AlignmentFlag.AlignCenter):设置按钮在布局中的对齐方式为居中。
self.label = QLabel()
self.label.setStyleSheet("color: green;")
self.layout.addWidget(self.label)
  1. 创建一个标签控件QLabel,设置其字体颜色为绿色。并将标签添加到布局中。
  2. self.layout.setAlignment(self.label, Qt.AlignmentFlag.AlignCenter):设置标签在布局中的对齐方式为居中。
  3. self.button.clicked.connect(self.slot_sayHello):将按钮的clicked信号与slot_sayHello槽函数连接。即,当按钮被点击时,将执行slot_sayHello函数。
@Slot()
def slot_sayHello(self):
  1. 使用@Slot()装饰器将slot_sayHello函数标记为槽函数。slot_sayHello是按钮点击时的处理函数。
self.label.clear()
self.label.setText("Hi, guys!")
  1. 情况标签文本,并设置标签的文本为 "Hi, guys!"
font = QFont("Arial", 24, QFont.Bold)
self.label.setFont(font)
  1. 创建一个QFont实例,设置字体为Arila,大小为24, 并且加粗。并将这个字体应用到标签。
  2. self.button.setText("You already clicked me")修改按钮的文本为"You already clicked me",提示用户按钮已经被点击。
  3. if __name__ == "__main__":部分为程序的入口点,包括QApplication(sys.argv)创建一个实例并传入命令行参数;widget = Widget()创建Widget类的实例(主窗口);widget.show()显示主窗口;sys.exit(app.exec())启动应用的事件循环,直到退出时返回退出码。

11.4.2 信号与槽

由于 Qt 的特性,QObject需要一种通信方式,因此信号和槽机制成为 Qt 核心功能之一。简单来说,可以将信号和槽理解为与家中灯光的互动。当灯的开关(信号)被操作时,结果可能是灯泡被点亮或熄灭(槽)。在上述一个简单的按钮示例中,可以通过点击按钮的效果来理解信号和槽:点击按钮就是信号,而槽则是按钮被点击后发生的事情,如显示标签并更新按钮文本。

在之前章节解释并应用过回调(callback)函数。抛开实现细节,回调通常指的是一个通知函数(notification function),通过传递一个函数指针来响应程序中的事件。这种方式与信号和槽相似,但信号和槽更易于理解,较为直观。所有继承自QObject或其子类(如QWidget)的类,都可以包含信号和槽。信号由对象发出,当对象的状态发生变化并且这种变化可能对其它对象有用时,信号就会被触发。发出信号的对象并不会关心是否有其它对象接收到信号,这体现了信息的封装,确保对象能够作为独立的软件组件使用。槽用于接收信号,但它们也只是普通的成员函数。正如发出信号的对象不关心是否有其他对象接收该信号,槽也不关心是否有信号连接到它。这保证了 Qt 中能够创建完全独立的组件。多个信号可以连接到同一个槽,一个信号也可以连接到多个槽。甚至可以将一个信号直接连接到另一个信号,这样,每当一个信号发射时,第二个信号也会立即发射。Qt 中的控件(部件)有许多预定义的信号和槽,例如QAbstractButton(Qt 中按钮的基类)有一个clicked()信号;而QLineEdit(单行输入框)有一个名为clear()的槽。因此,使用QToolButton放置在QLineEdit右侧,并将其clicked()信号连接到clear()槽,就可以实现一个带有清除文本功能的输入框如图11-3。这是通过信号的connect()方法实现,代码如下:

PYC icon

图 11-3 带有清除文本功能的输入框

文件 widget.py
代码
import sys

from PySide6.QtWidgets import QApplication, QWidget, QToolButton, QHBoxLayout, QLineEdit
from PySide6.QtCore import Slot

from ui_form import Ui_Widget


class Widget(QWidget):
    def __init__(self, parent=None):
        super().__init__(parent)
        self.ui = Ui_Widget()
        self.ui.setupUi(self)

        self.layout = QHBoxLayout()
        self.setLayout(self.layout)
        self.setWindowTitle("line_edit.clear")

        self.line_edit_clear()

    def line_edit_clear(self):
        line_edit = QLineEdit()
        self.layout.addWidget(line_edit)
        button = QToolButton()
        button.setText("Clear")
        self.layout.addWidget(button)

        button.clicked.connect(line_edit.clear)


if __name__ == "__main__":
    app = QApplication(sys.argv)
    widget = Widget()
    widget.show()
    sys.exit(app.exec())
🤖 代码解读

这段代码实现了一个包含文本输入框(QLineEdit)和清除按钮(QToolButton)的简单 UI 组件,点击按钮时会清空文本输入框。

  1. line_edit = QLineEdit():创建一个QLineEdit实例,QLineEdit是一个文本框控件,允许用户输入单行文本。
  2. self.layout.addWidget(line_edit):将line_edit添加到布局中。
  3. button = QToolButton():创建一个QToolButton实例,QToolButton是一个工具按钮控件,通常用作提供额外功能的按钮(如清除文本、执行操作等)。
  4. button.setText("Clear"):设置按钮的文本为"Clear"
  5. self.layout.addWidget(button):将按钮添加到布局中。
  6. button.clicked.connect(line_edit.clear):将按钮的clicked信号连接到line_edit.clear槽函数。这里的line_edit.clearQLineEdit控件的内置方法,用于清空文本框中的内容。当按钮被点击时,line_edit.clear()会被触发,从而清空文本框的内容。

11.4.2.1 自定义信号

在 Pyside6 中,自定义信号允许在类之间进行通信,尤其是在基于事件驱动的 GUI 应用程序中。信号是一种“通知”机制,当某些事情发生时,可以发出一个信号(signal),然后通过“槽”(slot)来响应这个信号。

  • 信号(Signal):由对象发出,表示某些事件或状态的变化。信号没有任何逻辑,只是表示事件的发生。
  • 槽(Slot):是一个函数或方法,当信号发出时,槽会被调用。槽可以执行任何操作,如更新界面、处理数据等。

Pyside6 中,信号和槽使用SignalSlot来实现。Signal用来定义信号;Slot用来装饰定义接收信号的函数。自定义信号通常在继承自QObject或其它子类的类中定义。信号的定义通过Signal()来完成,信号可以接收不同类型的参数。

信号类型

Signal是用于定义信号的类,支持传递各种类型的数据,常见的信号类型包括基本数据类型,如int(整数)、float(浮点)、str(字符串)和bool(布尔)等;及 Qt 类型,如QDate(日期)、QTime(事件)、QDateTime(日期和时间)、QPoint(二维坐标 (x,y))、QSize(大小,如宽度和高度)、QRect(矩形 (x,y,width,height))、QVariant(可以存储任何类型的数据)等。

Signal可以接受多个不同类型的参数,如complex_signal = Signal(int, str, float)。如果信号不需要任何参数,则可以定义一个不带任何参数的信号,如simple_signal = Signal()

下述程序展示了如何使用信号和槽机制在 PySide6 中进行自定义信号传递。用户可以通过下拉框选择不同类型的信号(如字符串str、列表list、字典dict、日期Qdate等),点击按钮,信号被发送,并且通过槽函数处理并显示相应的结果。

PYC icon

图 11-4 自定义信号

文件 widget.py

代码

import sys
import math

from PySide6.QtWidgets import (
    QApplication,
    QWidget,
    QLabel,
    QComboBox,
    QGridLayout,
    QPushButton,
)
from PySide6.QtCore import Slot, Signal, QObject, QUrl, QDate, Qt, QDate
from PySide6.QtGui import QIcon

from ui_form import Ui_Widget


class Signals(QObject):
    sg_message = Signal(str)
    sg_vals = Signal(int, str, int)
    sg_list = Signal(list)
    sg_dict = Signal(dict)
    sg_anything = Signal(object)
    sg_QUrl = Signal(QUrl)
    sg_date = Signal(QDate)


class Widget(QWidget):
    def __init__(self, parent=None):
        super().__init__(parent)
        self.ui = Ui_Widget()
        self.ui.setupUi(self)

        self.grid = QGridLayout()
        self.setLayout(self.grid)
        self.setWindowTitle("Signal Type")

        self.signals = Signals()
        self.signals_widgets()

    def signals_widgets(self):
        btn = QPushButton("Sending Signal")
        btn.clicked.connect(self.emit_custom_signal)
        self.grid.addWidget(btn, 0, 0, alignment=Qt.AlignmentFlag.AlignRight)

        self.combo_signals = QComboBox()
        self.signals_dict = {
            "message": [self.signals.sg_message, "message..."],
            "vals": [self.signals.sg_vals, (27, "message", 13)],
            "list": [self.signals.sg_list, [1, 2, 3, 4, 5]],
            "dict": [self.signals.sg_dict, {"a": 27, "b": 13}],
            "anything": [self.signals.sg_anything, math.pi],
            "QUrl": [self.signals.sg_QUrl, QUrl("https://coding-x.tech/")],
            "date": [self.signals.sg_date, QDate.currentDate()],
        }

        for key, val in self.signals_dict.items():
            self.combo_signals.addItem(QIcon(), key, val[0])

        self.grid.addWidget(
            self.combo_signals, 0, 1, alignment=Qt.AlignmentFlag.AlignLeft
        )

        self.label = QLabel("Waiting for signal...")
        self.grid.addWidget(
            self.label, 1, 0, 1, 2, alignment=Qt.AlignmentFlag.AlignCenter
        )

    @Slot()
    def emit_custom_signal(self):
        custom_signal = self.combo_signals.currentData()
        custom_signal_idx = self.combo_signals.currentText()

        custom_signal.connect(self.custom_slot)
        if custom_signal_idx == "vals":
            custom_signal.emit(*self.signals_dict[custom_signal_idx][1])
        else:
            custom_signal.emit(self.signals_dict[custom_signal_idx][1])

    @Slot()
    def custom_slot(self, *args):
        if isinstance(args[0], QDate):
            self.label.setText(args[0].toString())
        elif len(args) > 1:
            self.label.setText(str(args))
        else:
            self.label.setText(str(args[0]))


if __name__ == "__main__":
    app = QApplication(sys.argv)
    widget = Widget()
    widget.show()
    sys.exit(app.exec())
🤖 代码解读
  1. PySide6.QtWidgets模块中导入必要的类,其中QGridLayout是按网格布局排列的布局管理器,允许控件放置于指定的行和列中;QComboBox为下拉框控件。
  2. PySide6.QtCore模块中导入必要的类,其中Slot标记为槽的装饰器;Signal定义信号;QObject所有对象的基类;QUrl表示URL;QDate表示日期;Qt为 Qt 常量和枚举,包含对齐方式等。
  3. PySide6.QtGui模块中导入QIcon,用于设置图标。
class Signals(QObject):
  sg_message = Signal(str)
  sg_vals = Signal(int, str, int)
  sg_list = Signal(list)
  sg_dict = Signal(dict)
  sg_anything = Signal(object)
  sg_QUrl = Signal(QUrl)
  sg_date = Signal(QDate)
  1. 定义一个Signals类,继承自QObject,用于声明几个不同类型的信号:
  • sg_messagestr传递字符串类型数据。
  • sg_vals:传递intstrint三种类型的数据。
  • sg_listlist传递一个列表。
  • sg_dictdict传递一个字典。
  • sg_anythingobject用于传递任何类型的数据。
  • sg_QUrl:传递一个QUrlURL 类型的数据。
  • sg_date:传递一个QDate日期类型的数据。
class Widget(QWidget):
  def __init__(self, parent=None):
      super().__init__(parent)
      self.ui = Ui_Widget()
      self.ui.setupUi(self)

      self.grid = QGridLayout()
      self.setLayout(self.grid)
      self.setWindowTitle("Signal Type")

      self.signals = Signals()
      self.signals_widgets()
  1. 定义Widget类,继承自QWidget;调用父类构造函数,设置 UI 布局;创建一个QGridLayout布局管理器,将其设置为窗口的布局;创建一个Signals实例,用于访问信号;调用signals_widgets方法设置控件。
btn = QPushButton("Sending Signal")
btn.clicked.connect(self.emit_custom_signal)
self.grid.addWidget(btn, 0, 0, alignment=Qt.AlignmentFlag.AlignRight)
  1. 创建一个按钮btn,并将其点击事件连接到emit_custom_signal槽函数(方法);并将按钮添加到网格布局中,设置其对齐方式为右对齐。
  2. self.combo_signals = QComboBox():创建一个下拉框QComboBox,用于选择发送的信号类型。
self.signals_dict = {
    "message": [self.signals.sg_message, "message..."],
    "vals": [self.signals.sg_vals, (27, "message", 13)],
    "list": [self.signals.sg_list, [1, 2, 3, 4, 5]],
    "dict": [self.signals.sg_dict, {"a": 27, "b": 13}],
    "anything": [self.signals.sg_anything, math.pi],
    "QUrl": [self.signals.sg_QUrl, QUrl("https://coding-x.tech/")],
    "date": [self.signals.sg_date, QDate.currentDate()],
}
  1. 创建一个字典signals_dict,映射信号名称到信号对象和要发送的相应数据。每个键代表一个信号类型,每个值是一个列表,其中包含信号和信号对应的参数。
for key, val in self.signals_dict.items():
  self.combo_signals.addItem(QIcon(), key, val[0])
  1. 循环遍历signals_dict字典,将每个信号类型添加到下拉框中,显示信号的名称。QComboBoxaddItem(icon, text[, userData=None])方法传入有三个个参数。QIcon()为下拉框添加一个空图标。可以为icon项指定图标,如果不需要,则使用QIcon()创建一个空图标;text指定显示的文本内容;userData(存储在Qt::UserRole中)为对应标签的数据,类型为object,即可以是任何类型的数据。
  2. self.grid.addWidget(self.combo_signals, 0, 1, alignment=Qt.AlignmentFlag.AlignLeft):是将QComboBox控件self.combo_signals添加到布局中,并设置其在布局中的位置和对齐方式。self.grid是一个QGridLayout布局管理器,addWidget(widget, row, column[, alignment=Qt.Alignment()])方法用于将控件添加到布局中。该方法接受多个参数,self.combo_signals对应widget参数,是要添加到布局的控件;rowcolumn是控件在布局中的行号和列号;alignment=Qt.AlignmentFlag.AlignLef设置控件的对齐方式为左对齐。
self.label = QLabel("Waiting for signal...")
self.grid.addWidget(self.label, 1, 0, 1, 2, alignment=Qt.AlignmentFlag.AlignCenter)
  1. 创建一个标签label,初始化文本为 "Waiting for signal...",并将其添加到布局中,居中显示。这里的addWidget方法,为addWidget(widget, row, column, rowSpan, columnSpan[, alignment=Qt.Alignment()]),传入的参数包括row行号,column列号,及rowSpan行跨和columnSpan列跨大小。
  • 槽函数 emit_custom_signal

点击按钮时触发槽函数 emit_custom_signal,其作用是从下拉框中获取当前选择的信号,并将其发送/射(emit)。根据选择的信号类型,代码会调整信号发送的方式。

  1. custom_signal = self.combo_signals.currentData():从下拉框(QComboBox)中获取当前选中项的关联数据。currentData()返回的是与当前选项关联的信号对象,如用户在下拉框中选择了"message",则代码返回self.signals.sg_message信号。
  2. custom_signal_idx = self.combo_signals.currentText():获取当前选项的文本,即下拉框当前显示的选项名称,如用户在下拉框中选择了"message",则代码返回"message"
  3. custom_signal.connect(self.custom_slot):通过connect()方法将选中的信号连接到槽函数custom_slot,即当信号被发送时,custom_slot函数将被执行。
if custom_signal_idx == "vals":
  custom_signal.emit(*self.signals_dict[custom_signal_idx][1])
  1. 如果当前选中的信号类型是"vals"(即下拉框选择了"vals"),将触发custom_signal(即self.signals.sg_vals),并将signals_dict"vals"对应的数据作为参数传递。self.signals_dict["vals"]对应的值是一个列表[self.signals.sg_vals, (27, "message", 13),所以custom_signal.emit(*self.signals_dict["vals"][1])会将(27, "message", 13)中的三个元素依次作为参数发射信号。
  2. 如果当前选中的信号不是vals,则执行custom_signal.emit(self.signals_dict[custom_signal_idx][1]),将signals_dict中该信号对应的数据作为单一参数传递给信号。

该槽函数的作用是根据用户在下拉框中的选择,发射不同类型的信号。如果选择了vals,则发射sg_vals信号并传递多个参数;对于其它信号类型,发射相应的信号并传递一个参数。通过调用connect(self.custom_slot),信号被连接到槽custom_slot,接收到信号后,custom_slot会处理传递的参数并更新界面或执行其它操作。

  • 信号的槽函数custom_slot

槽函数custom_slot用来处理从自定义信号发射过来的数据,并更新界面的标签QLabel

  1. @Slot()装饰器标记该方法为一个槽函数,会被信号触发。
  2. *args允许接收不定数量的参数,这些参数是通过信号发射到该槽的。
if isinstance(args[0], QDate):
  self.label.setText(args[0].toString())
  1. 检查第一个参数(args[0])是否为QDate类型。如果是QDate对象,则调用toString()方法将其转换为字符串表示(例如,Sun Feb 2 2025),并将结果设置为标签的文本。
elif len(args) > 1:
  self.label.setText(str(args))
  1. 如果传入的参数列表中包含多个参数(即长度大于1),则将这些参数转换为字符串,并将字符串设置为标签的文本。str(args)将参数列表或元组转换为字符串格式(例如,"(27, 'message', 13)")。
else:
  self.label.setText(str(args[0]))
  1. 如果只有一个参数(args[0]),则将这个参数转换为字符串,并将其设置为标签的文本。

这个槽函数根据接收到的参数类型,决定如何显示信息到标签(QLabel)。

发送自定义数据

如果有自定义类型的数据对象,也可以将其作为信号的参数,但需要遵循 Qt 的对象模型(通常继承自QObject),确保自定义类型可以被正确的传递和接收。

下述代码演示了如何使用 PySide6 信号和槽机制传递自定义数据类型。

PYC icon

图 11-5 发送自定义数据

文件 widget.py
代码
import sys

from PySide6.QtWidgets import (
    QApplication,
    QWidget,
    QLabel,
    QVBoxLayout,
    QPushButton,
)
from PySide6.QtCore import Slot, Signal, QObject

from ui_form import Ui_Widget


class Person(QObject):
    def __init__(self, name, age):
        super().__init__()
        self.name = name
        self.age = age

    def __str__(self):
        return f"Name: {self.name}, Age: {self.age}"


class Widget(QWidget):
    person_signal = Signal(Person)

    def __init__(self, parent=None):
        super().__init__(parent)
        self.ui = Ui_Widget()
        self.ui.setupUi(self)

        self.setWindowTitle("Signal-Custom Data Type")
        self.resize(300, 300)

        self.label = QLabel("Waiting for signal...", self)
        btn = QPushButton("Send Person Data", self)
        btn.clicked.connect(self.send_person_data)
        self.person_signal.connect(self.handle_person_data)

        layout = QVBoxLayout()
        layout.addWidget(btn)
        layout.addWidget(self.label)
        self.setLayout(layout)

    @Slot()
    def send_person_data(self):
        person = Person("John Doe", 18)
        self.person_signal.emit(person)

    @Slot()
    def handle_person_data(self, person):
        self.label.setText(f"Received: {person}")


if __name__ == "__main__":
    app = QApplication(sys.argv)
    widget = Widget()
    widget.show()
    sys.exit(app.exec())
🤖 代码解读
  • 定义Person
  1. Person类表示一个人,继承自QObject,有两个属性nameage
  2. __str__方法返回该人的字符串表示。当Person对象(实例)传递到QLabel或其它地方时,str()方法会被调用,显示格式为Name: {name}, Age: {age}
  • 定义Widget
  1. person_signal = Signal(Person)person_signal是一个信号,信号类型为自定义的Person对象,用于传递Person数据。
  2. Widget类的构造函数中self.resize(300, 300)用于调整窗口的大小。
  3. btn.clicked.connect(self.send_person_data)为点击创建的按钮时会调用send_person_data方法。
  4. self.person_signal.connect(self.handle_person_data)person_signal信号连接到槽函数handle_person_data,当信号发射时,handle_person_data会被调用。
  5. 使用QVBoxLayout布局管理器将按钮和标签添加到布局中,并将布局设置为窗口的布局。
  • 定义槽函数
  1. send_person_data槽函数,person = Person("John Doe", 18)用于创建一个Person对象(姓名为"John Doe",年龄为18)。然后用self.person_signal.emit(person)发射person_signal信号,将person对象作为参数传递。
  2. handle_person_data是另一槽函数,接收一个Person对象作为参数。self.label.setText(f"Received: {person}")将接收到的Person对象的字符串表示显示在标签label中。

重载不同类型的信号和槽

下述代码展示了 PySide6 信号的重载(overloading)功能,即同一信号可以支持不同的数据类型。

PYC icon

图 11-6 重载不同类型的信号和槽

文件 widget.py
代码
import sys

from PySide6.QtWidgets import (
    QWidget,
    QApplication,
    QPushButton,
    QLabel,
    QVBoxLayout,
)
from PySide6.QtCore import Slot, Signal, QObject

from ui_form import Ui_Widget


class Widget(QWidget):
    speak = Signal((int,), (str,))

    def __init__(self, parent=None):
        super().__init__(parent)
        self.ui = Ui_Widget()
        self.ui.setupUi(self)

        self.click_counter = 0

        self.setWindowTitle("Signal-Overloading")
        self.resize(300, 300)

        self.label = QLabel("Waiting for signal...", self)
        self.btn = QPushButton("say something", self)

        layout = QVBoxLayout()
        layout.addWidget(self.btn)
        layout.addWidget(self.label)
        self.setLayout(layout)

        self.btn.clicked.connect(self.emit_signal)
        self.speak[int].connect(self.say_something)
        self.speak[str].connect(self.say_something)

    @Slot()
    def emit_signal(self):
        self.click_counter += 1

        if self.click_counter == 1:
            self.speak.emit(100)
        elif self.click_counter == 2:
            self.speak[str].emit("Hello everybody!")
            self.click_counter = 0

    @Slot(int)
    @Slot(str)
    def say_something(self, arg):
        if isinstance(arg, int):
            self.label.setText(f"This is a number: {arg}")
            self.btn.setText("Clicked once.")
        elif isinstance(arg, str):
            self.label.setText(f"This is a string: {arg}")
            self.btn.setText("Clicked twice.")


if __name__ == "__main__":
    app = QApplication(sys.argv)
    widget = Widget()
    widget.show()
    sys.exit(app.exec())
🤖 代码解读
  1. speak = Signal((int,), (str,))speak信号可以传递整数(int)类型的信号;也可以传递字符串(str)类型的信号。intstr写成(int,)(str,),是 Python 中,单个元素的元组必须带,(逗号),否则会被当成普通类型而不是元组。
  2. self.click_counter = 0:用于跟踪按钮点击次数,以决定发射哪种类型的信号。
  3. self.btn.clicked.connect(self.emit_signal):将按钮的点击信号(clicked())连接到emit_signal()槽(方法),使得每次点击按钮时,都会执行emit_signal()。在 PySide6 中,QPushButton继承自QAbstractButton,而QAbstractButton内部定义了clicked信号(clicked = Signal(bool))。这个信号在按钮被点击时自动触发。默认情况下,会传递一个布尔值checked(用于QPushButtoncheckable模式)。
  4. self.speak[int].connect(self.say_something)self.speak[str].connect(self.say_something):当speak信号发射intstr不同类型信号时,此示例均连接到同一say_something(str)槽函数,即绑定不同类型信号到say_something()槽。
  5. emit_signal发射信号的槽,根据click_counter的值决定发送int还是str类型的信号。当click_counter == 1时,调用self.speak.emit(100),发射int类型信号;当click_counter == 2时,调用self.speak[str].emit("Hello everybody!"),发射str类型信号,并重置click_counter = 0。注意发送str类型信号时,明确指明了该[str]类型。
  6. say_something处理speak信号的槽。@Slot(int)@Slot(str)明确告知 Qt 这个槽支持哪些类型的信号,从而优化信号槽的连接效率。arg参数为不同数据类型时,通过isinstance方法判断,进而更新界面上QLabelQPushButton文本为不同内容。

该部分代码主要包括信号重载、信号与槽的应用和按钮点击控制逻辑几个内容。信号重载是speak = Signal((int,), (str,))允许信号speak即可以发送int,也可以发送strself.speak[datatype].connect(slot)连接不同数据类型的信号;信号与槽的应用对应到负责发送信号的emit_signal()槽和接收信号并处理信号更新 UI 的say_something()槽;按钮点击控制逻辑,是由click_counter变量值,按点击1次,或点击2次(不断循环)来发送不同数据类型的信号。

11.4.2.2 拦截信号,发送额外参数

信号连接到槽,槽在信号每次触发时都会运行。信号除了触发槽,许多信号还会传输数据,提供有关状态变化或触发槽的控件信息。接收槽可以使用这些数据对相同的信号执行不同的操作。但是除了自定义信号,默认的信号只能发射 PySide6 内置设计的数据,例如 QPushButton的点击信号clicked,在按钮被点击时触发,并发送checked状态数据。槽接收到这个数据,但并不知道是哪个控件发射的信号或者希望接收一些别的有用的信息。因此希望信号能够添加额外的数据,发送到槽,并且槽能够接收,这就需要拦截信号。拦截信号并发送额外参数的方式可以通过中间拦截槽(interceptor slot),或者在connect()方法中使用lambda传递额外参数。

下述示例设计了两个按钮,通过信号和槽机制,利用拦截信号发送额外信息至同一个槽,槽根据接收到的额外信息判断是哪个按钮发射的信号。

PYC icon

图 11-7 拦截信号,发送额外参数

文件 widget.py
代码
import sys

from PySide6.QtWidgets import (
    QWidget,
    QApplication,
    QPushButton,
    QLabel,
    QVBoxLayout,
)
from PySide6.QtCore import Slot, Signal, QObject

from ui_form import Ui_Widget


class Widget(QWidget):
    def __init__(self, parent=None):
        super().__init__(parent)
        self.ui = Ui_Widget()
        self.ui.setupUi(self)
        self.setWindowTitle("Intercepting signal")
        self.resize(300, 300)

        self.label = QLabel("Pass additional parameters...")
        btn_a = QPushButton("Btn-A")
        btn_b = QPushButton("Btn-B")

        layout = QVBoxLayout()
        layout.addWidget(btn_a)
        layout.addWidget(btn_b)
        layout.addWidget(self.label)
        self.setLayout(layout)

        btn_a.clicked.connect(self.intercepting_signal)
        btn_b.clicked.connect(lambda checked: self.button_clicked(checked, btn_b))

    @Slot()
    def intercepting_signal(self, checked):
        self.button_clicked(checked, "Btn-A")

    @Slot()
    def button_clicked(self, checked, info):
        if isinstance(info, QObject):
            self.label.setText(f"You just hit button: {info.text()}")
        elif isinstance(info, str):
            self.label.setText(f"You just hit button: {info}")


if __name__ == "__main__":
    app = QApplication(sys.argv)
    widget = Widget()
    widget.show()
    sys.exit(app.exec())
🤖 代码解读
  • 使用中间拦截槽intercepting_signal()拦截信号,增加额外参数
  1. btn_a.clicked.connect(self.intercepting_signal)btn_aclicked信号默认会传递checked参数(bool类型)。通过中间拦截槽intercepting_signal()拦截信号,并对应button_clicked()槽,增加额外的参数(info),发射"Btn-A"字符串。
  • 使用 lambda 增加额外参数
  1. btn_b.clicked.connect(lambda checked: self.button_clicked(checked, btn_b)):直接调用button_clicked()槽,但通过lambda额外传递btn_b对象。

  2. button_clicked()是槽处理信号。如果info是按钮对象(isinstance(info, QObject)),用btn_b.text()返回"Btn-B";如果info是字符串(isinstance(info, str)),直接使用该字符串(含有发射信号所用按钮的信息)。

发送多个数据

这个示例展示了如何使用信号和槽机制传递多个参数,包括按钮的文本信息、随机数组(使用 NumPy 库)和一个QLabel控件label

在 Qt Creator 中的 Python 默认环境下需要安装 NumPy 库。在 Qt Creator 底部选择 Terminal 项,用 cd 定位到 Python 默认环境路径,如 cd C:\Users\richie\Omen_RichieBao\omen_APP\PySide6_cases\envs\env-venv,执行 Scripts\activate 激活虚拟环境,通过 pip install numpy 安装 NumPy 库

PYC icon

图 11-8 信号发送多个数据到槽

文件 widget.py
代码
import sys
import numpy as np

from PySide6.QtWidgets import (
    QWidget,
    QApplication,
    QPushButton,
    QLabel,
    QVBoxLayout,
)
from PySide6.QtCore import Slot, Signal, QObject

from ui_form import Ui_Widget


class Widget(QWidget):
    def __init__(self, parent=None):
        super().__init__(parent)
        self.ui = Ui_Widget()
        self.ui.setupUi(self)
        self.setWindowTitle("Signal-Overloading")
        self.resize(300, 300)

        label = QLabel("Pass additional parameters...")
        btn = QPushButton("Button_transfer_data")

        layout = QVBoxLayout()
        layout.addWidget(btn)
        layout.addWidget(label)
        self.setLayout(layout)

        data = np.random.randint(0, 10, size=7)
        btn.clicked.connect(lambda: self.handle_signal(btn.text(), data, label))

    @Slot()
    def handle_signal(self, *args):
        button_text, info, label_widget=args
        label_widget.setText(f"button text: {button_text}\ndata: {info}")


if __name__ == "__main__":
    app = QApplication(sys.argv)
    widget = Widget()
    widget.show()
    sys.exit(app.exec())
🤖 代码解读
  1. data = np.random.randint(0, 10, size=7):生成一个包含 7 个随机整数(范围 0 到 9)的data数组。
  2. btn.clicked.connect(lambda: self.handle_signal(btn.text(), data, label)):将按钮的点击信号连接到handle_signal槽函数,通过lambda传递三个参数,按钮文本(btn.text());生成的随机数据数组(data)和一个标签控件label,用于更新显示内容。

11.5 菜单、工具栏和动作,及资源集合

图11-9中的示例应用开发了一个简单的文本编辑器,界面和功能类似于记事本,具备菜单栏、工具栏和状态栏,支持基本的文件操作和编辑功能。主窗口界面包括文件编辑区域,用户可以在其中输入和编辑文本。窗口标题显示了当前打开的文件名(如MTSbar.txt),*号表示文件已修改但未保存;菜单栏(Menu Bar)包含有File(文件)(并包含New新建、Open打开、save保存、Save As...另存为和Exit退出等功能),Edit(编辑)(并包含Cut剪切、Copy复制、Paste粘贴等基本文本编辑功能)和Help(帮助)(并包含About显示关于应用程序的信息,About Qt显示 Qt 相关信息)等菜单;工具栏(Toolbar)具备文件操作和编辑相关的快捷按钮;状态栏(Status Bar)显示应用状态,如当前操作的提示信息。弹出对话框包括点击AboutAbout Qt弹出对应的弹出对话框,及文件修改警告弹出对话框,提供Save(保存)、Discard(放弃更新)和Cancel(取消)三种选项。

PYC icon

图 11-9 文本编辑器(菜单栏、工具栏、状态栏)

使用 Qt Creator 创建一个项目,但是在Define Class步骤,Base class下选择 QMainWindow。项目中使用了 QRC 资源文件(application.qrc)管理 UI 相关的图片资源,并由pyside6-rcc工具,在Terminal终端中执行pyside6-rcc application.qrc -o application_rc.py,将application.qrc资源文件编译为application_rc.py 代码,使 PySide6 能够直接访问其中的资源(需要先用import rc_application导入该文件),如icon = QIcon(QPixmap(":resources/images/new.png"))

PYC icon

图 11-10 应用的文件结构

建立 .qrc文件,是从Projects文件结构中项目(如本例为mainWindow)上右键弹出菜单中选择Add New...,选择弹出对话框中的Choose a template->Qt->Qt Resource File建立.qrc

文件 mainwindow.py
代码
# Copyright (C) 2013 Riverbank Computing Limited.
# Copyright (C) 2022 The Qt Company Ltd.
# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
import sys

from PySide6.QtWidgets import (
    QApplication,
    QMainWindow,
    QTextEdit,
    QMessageBox,
    QFileDialog,
)
from PySide6.QtGui import QIcon, QAction, QKeySequence, QPixmap
from PySide6.QtCore import (
    QSaveFile,
    QFile,
    QTextStream,
    Slot,
    QFileInfo,
    Qt,
    QSettings,
    QByteArray,
)

from ui_form import Ui_MainWindow

# pyside6-rcc application.qrc -o application_rc.py
import rc_application


class MainWindow(QMainWindow):
    def __init__(self, parent=None):
        super().__init__(parent)
        self.ui = Ui_MainWindow()
        self.ui.setupUi(self)
        self.resize(500, 500)

        self._cur_file = ""

        self._text_edit = QTextEdit()
        self.setCentralWidget(self._text_edit)

        self.create_actions()
        self.create_menus()
        self.create_tool_bars()
        self.create_status_bar()

        self.read_settings()

        self._text_edit.document().contentsChanged.connect(self.document_was_modified)

        self.set_current_file("")
        self.setUnifiedTitleAndToolBarOnMac(True)

    def create_actions(self):
        icon = QIcon(QPixmap(":resources/images/new.png"))
        self._new_act = QAction(
            icon,
            "&New",
            self,
            shortcut=QKeySequence.New,
            statusTip="Create a new file",
            triggered=self.new_file,
        )

        icon = QIcon(QPixmap(":resources/images/open.png"))
        self._open_act = QAction(
            icon,
            "&Open",
            self,
            shortcut=QKeySequence.Open,
            statusTip="Open an existing file",
            triggered=self.open,
        )

        icon = QIcon(QPixmap(":resources/images/save.png"))
        self._save_act = QAction(
            icon,
            "&Save",
            self,
            shortcut=QKeySequence.Save,
            statusTip="Save the document to disk",
            triggered=self.save,
        )

        self._save_as_act = QAction(
            "&Save &As...",
            self,
            shortcut=QKeySequence.SaveAs,
            statusTip="Save the document under a new name",
            triggered=self.save_as,
        )

        icon = QIcon.fromTheme(QIcon.ThemeIcon.ApplicationExit)
        self._exit_act = QAction(
            icon,
            "E&xit",
            self,
            shortcut="Ctrl+Q",
            statusTip="Exit the application",
            triggered=self.close,
        )

        icon = QIcon(QPixmap(":resources/images/cut.png"))
        self._cut_act = QAction(
            icon,
            "Cu&t",
            self,
            shortcut=QKeySequence.StandardKey.Cut,
            statusTip="Cut the current selection's contents to the clipboard",
            triggered=self._text_edit.cut,
        )

        icon = QIcon(QPixmap(":resources/images/copy.png"))
        self._copy_act = QAction(
            icon,
            "&Copy",
            self,
            shortcut=QKeySequence.StandardKey.Copy,
            statusTip="Copy the current selection's contents to the clipboard",
            triggered=self._text_edit.copy,
        )

        icon = QIcon(QPixmap(":resources/images/paste.png"))
        self._paste_act = QAction(
            icon,
            "&Paste",
            self,
            shortcut=QKeySequence.StandardKey.Copy,
            statusTip="Paste the clipboard's contents into the current selection",
            triggered=self._text_edit.paste,
        )

        icon = QIcon.fromTheme(QIcon.ThemeIcon.HelpAbout)
        self._about_act = QAction(
            icon,
            "&About",
            self,
            statusTip="Show the application's About box",
            triggered=self.about,
        )

        self._about_qt_act = QAction(
            "&About &Qt",
            self,
            statusTip="Show the Qt library's About box",
            triggered=qApp.aboutQt,
        )

    def create_menus(self):
        self._file_menu = self.menuBar().addMenu("&File")
        self._file_menu.addAction(self._new_act)
        self._file_menu.addAction(self._open_act)
        self._file_menu.addAction(self._save_act)
        self._file_menu.addAction(self._save_as_act)
        self._file_menu.addSeparator()
        self._file_menu.addAction(self._exit_act)

        self._edit_menu = self.menuBar().addMenu("&Edit")
        self._edit_menu.addAction(self._cut_act)
        self._edit_menu.addAction(self._copy_act)
        self._edit_menu.addAction(self._paste_act)

        self.menuBar().addSeparator()

        self._help_menu = self.menuBar().addMenu("&Help")
        self._help_menu.addAction(self._about_act)
        self._help_menu.addAction(self._about_qt_act)

    def create_tool_bars(self):
        self._file_tool_bar = self.addToolBar("File")
        self._file_tool_bar.addAction(self._new_act)
        self._file_tool_bar.addAction(self._open_act)
        self._file_tool_bar.addAction(self._save_act)

        self._edit_tool_bar = self.addToolBar("Edit")
        self._edit_tool_bar.addAction(self._cut_act)
        self._edit_tool_bar.addAction(self._copy_act)
        self._edit_tool_bar.addAction(self._paste_act)

    def create_status_bar(self):
        self.statusBar().showMessage("Ready")

    def closeEvent(self, event):
        if self.maybe_save():
            self.write_settings()
            event.accept()
        else:
            event.ignore()

    def read_settings(self):
        settings = QSettings("QtProject", "Application Example")
        geometry = settings.value("geometry", QByteArray())
        if geometry.size():
            self.restoreGeometry(geometry)

    def write_settings(self):
        settings = QSettings("QtProject", "Application Example")
        settings.setValue("geometry", self.saveGeometry())

    @Slot()
    def new_file(self):
        if self.maybe_save():
            self._text_edit.clear()
            self.set_current_file("")

    @Slot()
    def save(self):
        if self._cur_file:
            return self.save_file(self._cur_file)
        return self.save_as()

    @Slot()
    def save_as(self):
        fileName, filtr = QFileDialog.getSaveFileName(self)
        if fileName:
            return self.save_file(fileName)
        return False

    @Slot()
    def document_was_modified(self):
        self.setWindowModified(self._text_edit.document().isModified())

    @Slot()
    def open(self):
        if self.maybe_save():
            fileName, filtr = QFileDialog.getOpenFileName(self)
            if fileName:
                self.load_file(fileName)

    @Slot()
    def about(self):
        QMessageBox.about(
            self,
            "About Application",
            "The <b>Application</b> example demonstrates how to write modern GUI application using Qt, with a menu bar, toolbars, and a status bar.",
        )

    def maybe_save(self):
        if self._text_edit.document().isModified():
            ret = QMessageBox.warning(
                self,
                "Application",
                "The document has been modified. \nDo you want to save your changes?",
                QMessageBox.StandardButton.Save
                | QMessageBox.StandardButton.Discard
                | QMessageBox.StandardButton.Cancel,
            )
            if ret == QMessageBox.StandardButton.Save:
                return self.save()
            elif ret == QMessageBox.StandardButton.Cancel:
                return False
        return True

    def load_file(self, fileName):
        file = QFile(fileName)
        if not file.open(QFile.OpenModeFlag.ReadOnly | QFile.OpenModeFlag.Text):
            reason = file.errorString()
            QMessageBox.warning(
                self, "Application", f"Cannot read file {fileName}:\n{reason}."
            )
            return

        inf = QTextStream(file)
        with QApplication.setOverrideCursor(Qt.CursorShape.WaitCursor):
            self._text_edit.setPlainText(inf.readAll())

        self.set_current_file(fileName)
        self.statusBar().showMessage("File loaded", 2000)

    def save_file(self, fileName):
        error = None
        with QApplication.setOverrideCursor(Qt.CursorShape.WaitCursor):
            file = QSaveFile(fileName)
            if file.open(QFile.OpenModeFlag.WriteOnly | QFile.OpenModeFlag.Text):
                outf = QTextStream(file)
                outf << self._text_edit.toPlainText()
                if not file.commit():
                    reason = file.errorString()
                    error = f"Cannot write file {fileName}:\n{reason}."
            else:
                reason = file.errorString()
                error = f"Cannot open file {fileName}:\n{reason}."
        if error:
            QMessageBox.warning(self, "Application", error)
            return False

    def set_current_file(self, fileName):
        self._cur_file = fileName
        self._text_edit.document().setModified(False)
        self.setWindowModified(False)

        if self._cur_file:
            shown_name = self.stripped_name(self._cur_file)
        else:
            shown_name = "untitled.txt"

        self.setWindowTitle(f"{shown_name}[*] - Application")

    def stripped_name(self, fullFileName):
        return QFileInfo(fullFileName).fileName()


if __name__ == "__main__":
    app = QApplication(sys.argv)
    widget = MainWindow()
    widget.show()
    sys.exit(app.exec())
🤖 代码解读
  • 导入必要的模块
  1. PySide6.QtWidgets模块
  • QApplication:应用程序的入口,管理所有窗口。
  • QMainWindow:主窗口,支持菜单栏、工具栏和状态栏。
  • QTextEdit:文本编辑组件,提供多行文本输入。
  • QMessageBox:弹窗对话框组件。
  • QFileDialog:文件选择对话框(用于 打开/保存文件)
  1. PySide6.QtGui模块
  • QIcon:用于设置窗口或操作的图标。
  • QAction:用于菜单栏、工具栏中的交互操作/动作(Action)。
  • QKeySequence:主要用于表示和处理快捷键组合,广泛用于快捷键映射、菜单项、快捷方式等功能。
  • QPixmap:是 Qt 框架中的一个类,主要用于加载和显示图像。
  1. PySide6.QtCore
  • QSaveFile:安全的文件写入机制,避免数据丢失。

  • QFile:是一个用于文件操纵的类,提供了对文件的基本读写功能,可以用来打开、读取、写入和删除文件等操作;并提供了多个打开模式,如读取、写入、追加、文本或二进制模式等。

  • QTextStream:是一个用于文本数据流的类,简化了对文件的文本读取和写入操作。

  • Slot:信号槽机制装饰器。

  • QSettings:用于存储和恢复应用的用户设置(如窗口大小)。

  • QByteArray:用于存储二进制数据。

  • 加载 UI 资源

  1. from ui_form import Ui_MainWindow:通常由 Qt Designer 设计并转换为 Python 代码,自动生成的 UI 文件。
  2. import rc_application:资源文件,通常包含图标、图片、样式表,由pyside6-rcc工具从.qrc生成。
  • 定义MainWindow主窗口
  1. class MainWindow(QMainWindow)::继承QMainWindow,构造主窗口。
  2. self.ui = Ui_MainWindow():实例化 UI 设计类。
  3. self.ui.setupUi(self):应用 UI 设计,将Ui_MainWindow布局加载到MainWindow
  4. self.resize(500, 500):设置窗口初始大小。
  • 定义QTextEdit组件
  1. self._cur_file = "":用于存储当前文件路径,初始值为空字符串。
  2. self._text_edit = QTextEdit():创建文本编辑框。
  3. self.setCentralWidget(self._text_edit) :将QTextEdit设为窗口的中心组件。
  • 生成菜单栏、工具栏和状态栏
  1. self.create_actions():创建文件、编辑等操作/动作(如新建、打开和保存等)。
  2. self.create_menus():创建菜单栏。
  3. self.create_tool_bars():创建工具栏。
  4. self.create_status_bar():创建状态栏。

创建操作/动作(Action)def create_actions(self):

icon = QIcon(QPixmap(":resources/images/new.png"))
self._new_act = QAction(
    icon,
    "&New",
    self,
    shortcut=QKeySequence.New,
    statusTip="Create a new file",
    triggered=self.new_file,
)
  1. icon = QIcon(QPixmap(":resources/images/new.png"))QIcon用来表示图标,这里用来为操作(菜单项或按钮)设置图标。
  2. QPixmap:用来加载图像资源,":resources/images/new.png"是资源路径,指向项目中的resources/images/文件夹中的new.png图像文件,作为按钮或菜单项的图标。
  3. QAction:表示一个操作,通常与菜单项或工具栏按钮关联。
  4. icon:设置QAction的图标为上面创建的QIcon
  5. "&New":表示菜单项的文本,&表示N字母是该操作的快捷键(用户可以按Alt + N来选择此项)。显示文本为New
  6. selfQAction的父对象,这里是MainWindow,意味着该操作属于这个窗口。
  7. shortcut=QKeySequence.New:定义快捷键为Ctrl + N,当用户按下Ctrl + N时,这个操作会被触发。QKeySequence.New是 Qt 提供的标准快捷键之一,表示"新建"(New)操作的默认快捷键,等效于Ctrl + N(Windows & Linux)或⌘+N(Mac)。
  8. statusTip="Create a new file":设置在状态栏上显示的提示信息,当鼠标悬停在该动作上时,会显示此文本。
  9. triggered=self.new_file:设置该动作触发时调用的方法,当用户点击该动作或使用快捷键时,会调用self.new_file()方法。

创建菜单栏def create_menus(self):

self._file_menu = self.menuBar().addMenu("&File")
self._file_menu.addAction(self._new_act)
  1. self.menuBar():调用QMainWindow类的menuBar()方法,获取窗口的菜单栏(QMenuBar对象)。
  2. addMenu("&File"):在菜单栏上添加一个新的菜单项,菜单的名称是"File""&File"通过在"File"前加&,Qt 将会把F设置为快捷键字母。用户可以通过Alt + F快捷键快速激活这个菜单。
  3. self._file_menu:是一个成员变量,用来保存创建的"File"菜单,后续可以通过这个变量来访问和修改菜单。
  4. addAction(self._new_act):将之前创建的New操作(_new_act)添加到"File"菜单中。self._new_act是一个QAction对象,代表新建文件的操作(具有图标、快捷键、状态提示和触发事件等)。此操作添加到"File"菜单之后,可以在菜单中看到"New"选项,并通过点击或使用快捷键Ctrl + N来触发新建文件的操作。

创建状态栏def create_tool_bars(self):

self._file_tool_bar = self.addToolBar("File")
self._file_tool_bar.addAction(self._new_act)
  1. self.addToolBar("File"):在主窗口上添加一个新的工具栏,并指定该工具栏的名称为"File"addToolBar()QMainWindow类提供的一个方法,允许在窗口中添加工具栏(QToolBar)。工具栏通常用于放置常用操作的按钮,如新建、打开、保存等。
  2. self._file_tool_bar:是一个成员变量,用来保存创建的工具栏对象,可以通过这个变量对工具栏进行修改或添加更多的操作。
  3. addAction(self._new_act):将之前创建的New操作(_new_act)添加到"File"工具栏中。通过addAction将这个操作转化为工具栏中的按钮,可以通过点击工具栏按钮来触发新建文件操作。

创建状态栏def create_status_bar(self):

1.self.statusBar().showMessage("Ready"):调用QMainWindow类的statusBar()方法,获取窗口的状态栏self.statusBar()QStatusBar对象)。状态栏是窗口底部的区域,用来显示应用程序的状态信息,如当前操作的进度、提示信息、错误消息等。 2. showMessage("Ready"):调用QStatusBar对象的showMessage()方法,显示消息内容"Ready",表示程序处于就绪状态,即表示应用程序没有正在进行的任务,界面准备好接收用户的操作。

  • 读取和保存窗口几何信息

write_settings()方法是在程序退出时将当前窗口几何信息保存到配置文件中,以便下一次启动时可以恢复相同的窗口位置和大小。read_settings()方法从配置文件中读取保存的窗口几何信息(如大小和位置),如果存在有效的几何信息,就恢复窗口的状态。

write_settings()方法

def write_settings(self):
  settings = QSettings("QtProject", "Application Example")
  settings.setValue("geometry", self.saveGeometry())
  1. QSettings("QtProject", "Application Example"):通过QSettings类创建一个设置对象,该对象用于读取和写入应用程序的设置。第一个参数"QtProject"是组织名称;第二个参数"Application Example"是应用程序的名称。这两个参数标识设置的存储位置和名称,Qt 会将设置存储在适当的文件中,通常是操作系统的配置目录下。
  2. self.saveGeometry()QMainWindow提供的一个方法,用来返回窗口的几何信息(位置和大小),并以字节数组(QByteArray)的形式返回。
  3. 使用setValue()方法将生成的几何信息(字节数组)保存到设置中,键名为"geometry"。这样,下次程序启动时可以通过read_settings方法读取并恢复这些几何信息。

read_settings()方法

def read_settings(self):
  settings = QSettings("QtProject", "Application Example")
  geometry = settings.value("geometry", QByteArray())
  if geometry.size():
      self.restoreGeometry(geometry)
  1. QSettings("QtProject", "Application Example"):和write_settings方法中一样,创建一个QSettings对象,指向同样的存储位置。
  2. settings.value("geometry", QByteArray()):使用value()方法从设置中读取名为"geometry"的值,通常包含了窗口的大小和位置。如果该值不存在或无法找到,默认值QByteArray()(一个空的字节数组)将被返回。
  3. if geometry.size()::检查读取到的geometry是否有效(即是否包含有效的数据)。geometry.size()返回QByteArray的大小,如果大宇0, 说明设置中有有效的几何信息。
  4. elf.restoreGeometry(geometry):如果有有效的几何信息,使用restoreGeometry()方法恢复窗口的大小和位置。restoreGeometry()QMainWindow提供的一个方法,用于将窗口的几何信息(包括位置、大小等)恢复到之前保存的状态。
  • 槽(Slot)函数

new_file()save()save_as()document_was_modified()open()about()多个用于处理不同事件和操作/动作的槽函数(@Slot()),被用来实现文件操作(新建、保存、另存、打开),文档修改检查,及显示应用程序的对话框(about())等功能。每个方法都绑定到了相应的用户操作,如triggered=self.new_file等。

  1. new_file()槽:该方法用于创建一个新文件。执行新建操作之前,首先检查当前文件是否有未保存的更改(通过maybe_save()方法)。如果有未保存的更改,弹出提示对话框询问用户是否保存。如果用户选择保持或放弃,继续执行新建文件的操作。self._text_edit.clear()为清空文本编辑区的内容,准备开始新的文档。self.set_current_file("")将当前文件路径设置为空,表示新文件没有保存路径。
  2. save()槽:该方法用于保存文件。如果当前文件已经有文件路径(self._cur_file),则直接保存到该路径return self.save_file(self._cur_file)。如果没有文件路径(表示是一个新文件),则调用save_as()方法,弹出保存对话框,让用户选择保持的位置和文件名。
  3. save_as()槽:该方法用于保存文件的另存为操作。通过QFileDialog.getSaveFileName()弹出文件保存对话框,用户可以选择保存的文件名和路径。选择了文件后,调用save_file()方法将文件保存到选定的路径。
  4. document_was_modified()槽:该方法用于更新窗口的修改状态。每当文本内容发生变化时,_text_edit.document().isModified()会返回True,表示文档已被修改。调用setWindowModified()方法来更新窗口的修改标记,如在窗口标题栏显示一个型号[*]来提醒用户文件有修改。
  5. open()槽:该方法用于打开文件。在打开文件之前,先通过maybe_save()方法检查文档是否有未保存的更改,如果有更改会提示用户保存。用户选择保存后,弹出文件打开对话框,选择要打开的文件。QFileDialog.getOpenFileName(self)弹出文件选择对话框,返回选择的文件路径。如果用户选择了文件,调用load_file()方法加载文件内容。
  6. about()槽:该方法用于显示一个“关于”对话框,通常用于显示应用程序的版权信息、版本说明等。QMessageBox.about(self, "About Application", "....")通过QMessageBox.about()弹出一个包含应用程序信息的对话框。
  • 加载和保存文档

load_file()方法从指定的文件加载文本内容,并将其显示在文本编辑框中。如果加载失败,会弹出警告框提示错误。save_file()将当前编辑器中的文本保存到指定路径的文件中。如果保存过程中出现错误,会弹出警告框提示错误信息。

load_file()方法

def load_file(self, fileName):
  file = QFile(fileName)
  if not file.open(QFile.OpenModeFlag.ReadOnly | QFile.OpenModeFlag.Text):
      reason = file.errorString()
      QMessageBox.warning(
          self, "Application", f"Cannot read file {fileName}:\n{reason}."
      )
      return

  inf = QTextStream(file)
  with QApplication.setOverrideCursor(Qt.CursorShape.WaitCursor):
      self._text_edit.setPlainText(inf.readAll())

  self.set_current_file(fileName)
  self.statusBar().showMessage("File loaded", 2000)
  1. file = QFile(fileName):创建一个QFile对象,用于操作指定路径的文件fileName
  2. if not file.open(QFile.OpenModeFlag.ReadOnly | QFile.OpenModeFlag.Text)::尝试以QFile.OpenModeFlag.ReadOnly只读模式打开文件;或以QFile.OpenModeFlag.Text文本模式打开文件,即文件会被当做文本文件进行读取。如果文件无法成功打开,调用file.errorString()获取错误原因,并使用QMessageBox.warning()显示警告消息框,提示无法读取文件。
  3. inf = QTextStream(file):创建一个QTextStream对象,用于从文件中读取文本。QTextStream是一个高级文本流类,提供了对文本文件的读写操作。
  4. 通过setOverrideCursor(Qt.CursorShape.WaitCursor)设置一个等待光标,提示用户文件正在加载中。
  5. inf.readAll():读取文件的所有内容,并将其作为文本返回。
  6. self._text_edit.setPlainText():将读取到的文本设置到文本编辑框(_text_edit)中。 7.self.set_current_file(fileName):设置当前文件路径,调用set_current_file(fileName)方法更新当前文件状态。set_current_file会更新文件路径,且调整窗口标题以反映当前文件的名称。

save_file()方法

def save_file(self, fileName):
  error = None
  with QApplication.setOverrideCursor(Qt.CursorShape.WaitCursor):
      file = QSaveFile(fileName)
      if file.open(QFile.OpenModeFlag.WriteOnly | QFile.OpenModeFlag.Text):
          outf = QTextStream(file)
          outf << self._text_edit.toPlainText()
          if not file.commit():
              reason = file.errorString()
              error = f"Cannot write file {fileName}:\n{reason}."
      else:
          reason = file.errorString()
          error = f"Cannot open file {fileName}:\n{reason}."
  if error:
      QMessageBox.warning(self, "Application", error)
      return False
  1. error = None:初始化一个error变量,用于存储可能出现的错误信息。
  2. 通过setOverrideCursor(Qt.CursorShape.WaitCursor)设置等待光标,提示用户文件正在保存中。
  3. 创建一个QSaveFile对象,QSaveFileQFile的一个子类,主要用于处理“保存”操作,可以在保存时先写入文件再确认是否提交。
  4. if file.open(QFile.OpenModeFlag.WriteOnly | QFile.OpenModeFlag.Text)::尝试以写入模式(QFile.OpenModeFlag.WriteOnly)打开文件,或以文本模式(QFile.OpenModeFlag.Text)打开文件。
  5. outf = QTextStream(file):创建一个QTextStream对象outf,用于向文件写入文本。
  6. outf << self._text_edit.toPlainText():获取文本编辑框中的纯文本内容,并写入文件。
  7. if not file.commit()::尝试提交对文件的写入(即将文件保存到磁盘)。file.commit()会将写入的内容保存到文件。如果commit()返回False,说明保存失败。通过file.errorString()获取错误原因,并将错误信息保存到error变量。
  8. 如果文件无法打开(else部分),同样获取错误原因,并存储到error变量中。
  9. if error::如果存在任何错误(保存失败或文件无法打开),通过QMessageBox.warning()弹出一个警告框,显示错误信息。return False表示保存失败。
  • 辅助方法

maybe_save()方法

该方法用于检查当前文档是否已经被修改,如果修改了,就弹出一个对话框询问用户是否保存更改。

def maybe_save(self):
  if self._text_edit.document().isModified():
      ret = QMessageBox.warning(
          self,
          "Application",
          "The document has been modified. \nDo you want to save your changes?",
          QMessageBox.StandardButton.Save
          | QMessageBox.StandardButton.Discard
          | QMessageBox.StandardButton.Cancel,
      )
      if ret == QMessageBox.StandardButton.Save:
          return self.save()
      elif ret == QMessageBox.StandardButton.Cancel:
          return False
  return True
  1. if self._text_edit.document().isModified()::用于检查文档(_text_edit对象的文本内容)是否已被修改。self._text_edit.document()获取当前文本编辑框的文档对象。.isModified()返回一个布尔值,指示文档是否已经修改。如果返回True,表示文档自上次保存后有修改。
  2. 如果文档已修改,弹出QMessageBox.warning()警告消息框,包含自定义的提示文本和几个按钮供用户选择。第一个参数self,指定消息框的父窗口;第二个参数"Application"是消息框的标题;第三个参数是消息框的内容,提示用户文档已修改并询问是否保存。QMessageBox.StandardButton.SaveQMessageBox.StandardButton.DiscardQMessageBox.StandardButton.Cancel是用户可以选择的按钮,分别对应Save保存文档,Discard丢弃更改,不保存和Cancel取消操作,返回不保存或丢弃操作。
  3. 根据返回值ret,如果保存对应返回return self.save()尝试保存文档;如果取消对应返回return False
  4. 如果文档没有被修改,则直接返回return True,表示不需要左任何保存操作。

set_current_file()方法

该方法用于设置当前正在编辑的文件路径,并更新窗口标题和文档状态。

def set_current_file(self, fileName):
  self._cur_file = fileName
  self._text_edit.document().setModified(False)
  self.setWindowModified(False)

  if self._cur_file:
      shown_name = self.stripped_name(self._cur_file)
  else:
      shown_name = "untitled.txt"

  self.setWindowTitle(f"{shown_name}[*] - Application")
  1. self._cur_file = fileName:设置当前文件的路径。_cur_file是一个实例变量,用于存储当前文件的路径。如果是新建文件,则fileName可能为空字符串"",表示该文件尚未保存;如果是打开已有文件,则fileName将是该文件的路径。
  2. self._text_edit.document().setModified(False):将文档的“修改”状态设置为未修改。.document()获取当前文本编辑框的文档对象。.setModified(False)将文档的修改状态标记为未修改。意味着系统不再提示该文档有未保存的更改。
  3. self.setWindowModified(False):告诉应用程序当前窗口没有未保存的更改,从而更新窗口的状态栏或标题栏,确保在打开或保存文件后,窗口的修改状态与文档的修改状态一致。
  4. 通过判断self._cur_file是否为空,决定窗口标题中要显示的文件名。shown_name = self.stripped_name(self._cur_file)是调用stripped_name(self._cur_file)来获取该文件的名称。stripped_name()方法通过return QFileInfo(fullFileName).fileName()从完整的文件路径中提取文件名部分,不包括路径。QFileInfo是 Qt 提供的一个类,用于处理文件路径和文件信息。可以从一个完整的文件路径中提取出文件的各种信息,如文件名、扩展名、文件大小、修改时间等。QFileInfo.fileName()方法,用于获取文件的名称。
  5. self.setWindowTitle(f"{shown_name}[*] - Application"):更新窗口的标题。

11.6 QML、QtQuick + PySide6

QML(Qt Modeling Language)是一个用于构建图形用户界面(GUI)和应用程序行为的声明式语言(declarative language),主要与 QtQuick 一起使用,由 Qt 框架提供,适用于快速开发现代、动态、富交互(如触摸、点击、互动等多种交互方式)的用户界面,广泛用于桌面、移动设备和嵌入式系统的开发。QML允许开发者以声明的方式描述界面的结构和行为。与传统的命令式编程(如 C++ 和 Python)不同,QML 更加关注“做什么”(声明),而不是“怎么做”(过程和逻辑)。使用 QML,开发者可以轻松构建复杂的界面和交互,支持动态界面,允许在运行时,根据条件或用户输入调整界面元素。QML 与 JavaScript 紧密集成,可以在 QML 中嵌入 JavaScript 来处理逻辑、事件、动画等交互行为。通过 JavaScript, 开发者可以动态控制界面的元素和行为。这种集成,使得 QML 在描述 UI 的同时,也能处理一些复杂的逻辑或事件管理。QML 应用程序可可以跨多个平台运行,包括 Windows、Linux、maxOS、Android 和 iOS 等。开发者只需要编写一次 QML 代码,即可以在不同平台上运行。QML 的设计考虑到了性能优化,尤其适合于需要流畅动画和高响应的应用程序。

QtQuick 是 Qt 的一个模块,提供了一种高效的方式来创建流畅、动态的用户界面,是基于 QML 声明式语言,旨在简化复杂用户界面的开发,包含了用于布局、动画、控件、图形和多媒体的工具。QML 与 Python 紧密结合,用 QML 来设计界面,用 Python 来处理后端逻辑。用QGuiApplication来初始化应用,通过QQmlApplicationEngine加载和显示 QML 文件,并用 Python 来控制 QML 中的组件和行为。

11.6.1 Hello World!(QML)

在 Qt Creator 中,新建一个项目(Create Project)时,选择Choose a template->Application(Qt for Python)->Qt Quick Application - Empty 创建一个 Qt Quick 应用程序(名为 HelloWorld),其中包含一个空窗口。项目的文件包括HelloWorld.pyproject,用于保存项目配置;main.py,负责加载和运行 QML 界面,并处理应用逻辑和与 QML 的交互;main.qml,用于描述用户界面的布局和交互。

示例代码创建了一个窗口,窗口中央有一个标签和一个按钮,初始时,标签显示"Click the button!",按钮上写着"Click Me"。当点击按钮,标签上的文本会改变为"Hello World!"

PYC icon

图 11-11 应用 QML 的 Hello World 示例

文件 main.qml main.py HelloWorld.pyproject
代码
import QtQuick
import QtQuick.Window
import QtQuick.Controls

ApplicationWindow {
    width: 200
    height: 200
    visible: true
    title: qsTr("Hello World")

    Label{
        id: label
        text: "Click the button!"
        anchors.centerIn: parent
    }

    Button{
        text: "Click Me"
        anchors.bottom: parent.bottom
        anchors.horizontalCenter: parent.horizontalCenter
        onClicked:{
            label.text="Hello World!"
        }
    }
}
# This Python file uses the following encoding: utf-8
import sys
from pathlib import Path

from PySide6.QtGui import QGuiApplication
from PySide6.QtQml import QQmlApplicationEngine


if __name__ == "__main__":
    app = QGuiApplication(sys.argv)
    engine = QQmlApplicationEngine()
    qml_file = Path(__file__).resolve().parent / "main.qml"
    engine.load(qml_file)
    if not engine.rootObjects():
        sys.exit(-1)
    sys.exit(app.exec())
{
    "files": [
        "main.py",
        "main.qml"
    ]
}
🤖 代码解读
import QtQuick
import QtQuick.Window
import QtQuick.Controls
  1. import QtQuick:引入 QtQuick模块,提供了创建用户界面所需的基础组件和功能。
  2. import QtQuick.Window:引入QtQuick.Window模块,提供了窗口管理功能,可以创建窗口及其相关的功能。
  3. import QtQuick.Controls:引入QtQuick.Controls模块,提供了常见的 UI 控件(如按钮、标签等)。
ApplicationWindow {
  width: 200
  height: 200
  visible: true
  title: qsTr("Hello World")
  1. ApplicationWindow是 QtQuick 提供的一个特殊窗口类型,是一个常用于创建应用程序窗口的容器,通常包含了应用程序的主界面,用于应用程序主窗口,适合于构建具有菜单栏、工具栏、状态栏等常见功能的桌面应用。
  2. width: 200height: 200:设置窗口的宽度和高度分别为 200 像素。
  3. visible: true:设置窗口的可见性为true,即窗口默认是可见的。
  4. title: qsTr("Hello World"):设置窗口的标题为"Hello World"qsTr是 Qt 提供的函数,用于支持翻译和本地化,确保字符串可以被翻译。
Label{
    id: label
    text: "Click the button!"
    anchors.centerIn: parent
}
  1. Label:定义一个标签控件,显示一些文本。
  2. id: label:为这个标签控件指定一个 ID,使其在后续代码中可以被引用。
  3. text: "Click the button!":设置标签的文本为"Click the button!"
  4. anchors.centerIn: parent:使用锚点布局将标签居中放置于父容器中(即ApplicationWindow)。
Button{
    text: "Click Me"
    anchors.bottom: parent.bottom
    anchors.horizontalCenter: parent.horizontalCenter
    onClicked:{
        label.text="Hello World!"
    }
}
  1. Button:定义一个按钮控件。
  2. text: "Click Me":设置按钮的文本为"Click Me"
  3. anchors.bottom: parent.bottom:将按钮锚固在窗口底部。
  4. anchors.horizontalCenter: parent.horizontalCenter:将按钮水平居中放置于父容器中。
  5. onClicked:定义按钮被点击时触发的事件处理器,即当按钮被点击时,通过label.text="Hello World!"修改标签的文本为"Hello World!"
  1. import sys:导入sys模块,用于访问系统特定参数和功能。通常用于处理命令行参数和退出应用程序。
  2. from pathlib import Path:从pathlib模块导入Path类,用来处理文件路径。
  3. from PySide6.QtGui import QGuiApplication:从 PySide6 导入QGuiApplication类,用于创建和管理基于 Qt 的 GUI 应用程序,特别是不适用 widgets(小部件/控件),而依赖 QML 图形视图框架的应用。
  4. from PySide6.QtQml import QQmlApplicationEngine:用于加载和管理 QML 文件及其与 Python 代码的集成。
  5. if __name__ == "__main__"::用于检查脚本是否作为主程序运行。如果是,将执行代码块中的内容。这是 Python 中的标准约定,允许文件在被导入为模块时不会执行应用程序代码。
  6. app = QGuiApplication(sys.argv):创建一个QGuiApplication实例,这是运行任何 PySide6 应用程序所必需的。sys.argv参数将命令行参数传递给应用程序,可以根据需要自定义应用程序启动时的行为。
  7. engine = QQmlApplicationEngine():创建一个QQmlApplicationEngine实例,用于加载和管理 QML 文件。此对象处理 Python 和 QML 引擎之间的交互。
  8. qml_file = Path(__file__).resolve().parent / "main.qml":构造 QML 文件(main.qml)的路径。使用了__file__变量,该变量保存当前 Python 脚本的路径,并于Path类结合,以确保路径是绝对的并指向与 Python 脚本位于同一目录下的main.qml文件。
  9. engine.load(qml_file):加载 QML 文件到引擎中。QQmlApplicationEngineload()方法加载 QML 文件,并自动将 QML UI 元素与后端的 Python 代码连接起来(如果需要)。
  10. if not engine.rootObjects()::检查是否有根对象被 QML 引擎创建。如果没有根对象(即 QML 文件加载失败或没有有效的 QML 代码),将执行sys.exit(-1),即应用程序将以状态码-1退出,表示发生了错误。
  11. sys.exit(app.exec()):通过调用app.exec(),启动应用程序的事件循环,保持应用程序运行直至被关闭。sys.exit()用于确保脚本正确终止,并将app.exec()返回的退出代码传递给系统。

列出了构建应用所需的文件,main.pymain.qml

  • main.py:负责初始化 Qt 应用程序,创建 QML 引擎并加载 QML 文件以显示界面。
  • main.qml:是 Qt Quick 的 QML 文件,包含应用的 UI 结构,定义了界面上的元素(如按钮、标签等)及其行为和布局。

11.6.2 QML 的基本语法

基本语法 GUI .qml .py 说明
定义对象的层次结构

PYC icon

main.qml

import QtQuick
import QtQuick.Window

Window {
    width: 300
    height: 300
    visible: true
    title: qsTr("Hierarchy")

    Rectangle{
        width: 280
        height: 50
        color: "red"
        anchors.centerIn: parent

        Text {
            anchors.centerIn: parent
            font.family: "Helvetica"
            font.pointSize: 24
            text: qsTr("Hello QML.")
        }
    }

}

main.py

# This Python file uses the following encoding: utf-8
import sys
from pathlib import Path

from PySide6.QtGui import QGuiApplication
from PySide6.QtQml import QQmlApplicationEngine


if __name__ == "__main__":
    app = QGuiApplication(sys.argv)
    engine = QQmlApplicationEngine()
    qml_file = Path(__file__).resolve().parent / "main.qml"
    engine.load(qml_file)
    if not engine.rootObjects():
        sys.exit(-1)
    sys.exit(app.exec())

main.qml

这个 QML 代码展示了一个简单的层次结构,其中包括一个Window窗口元素,内部包含了一个Rectangle元素,再内部嵌套了一个Text元素。QML 是声明式语言,意味着不需要告诉程序怎么做,而是通过声明控件的属性来描述界面。QML 的层次结构通过嵌套控件的方式来组织,嵌套的元素在 UI 中也会体现为层级关系。该 QML 代码的层次结构可以用下述缩进方式描述为:

`Window`:根元素,整个应用程序的窗口。
  * `Rectangle`:矩形控件,`Window`的子元素,显示为一个红色矩形,并居中。
      *`Text`:文本控件,`Rectangle` 的子元素,显示在矩形中央。

QML 元素是嵌套的,父元素包含子元素。每个子元素会继承其父元素的一些特性(如大小和位置),并可以进一步定义。QML 的层次结构适合于构建负责且灵活的 UI,同时保持代码简洁和易于理解。

Window是 QtQuick 中最基本的窗口控件,不具备像ApplicationWindow内置如标题栏、菜单栏和状态栏等高级功能,只是简单的窗口容器,适合于自由设计窗口的样式和布局。

使用控件(Controls)创建 QML 应用程序[菜单栏+工具栏+状态栏]

PYC icon

PYC icon

                                            

main.qml

import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import QtQuick.Controls.Material

ApplicationWindow {
    width: 420
    height: 320
    visible: true
    title: qsTr("QtQuick Example with Menubar, ToolBar, and Statusbar")

    Material.theme: Material.Light
    Material.primary: Material.Green
    Material.accent: Material.Red

    ActionGroup{
        id: actionGroup
        exclusive: false

        Action{
            id: actionNew
            text: "New"
            shortcut: "Ctrl+N"
            onTriggered: {
                label.text="New file action triggered"
            }
        }
        Action{
            id: actionOpen
            text: "&Open"
            shortcut: "Ctrl+O"
            onTriggered: {
                label.text="Open file action triggered"
            }
        }
        Action{
            id: actionExit
            text: "&Exit"
            shortcut: "Ctrl+Q"
            onTriggered: {
                Qt.quit()
            }
        }
    }

    menuBar: MenuBar{
        Menu{
            title: qsTr("&File")
            MenuItem {
                action: actionNew
                onHoveredChanged: if(hovered) {statusBar.text="New file" } else {statusBar.text="Ready"}

            }
            MenuItem {
                action: actionOpen
                onHoveredChanged: if(hovered) {statusBar.text="Open file" } else {statusBar.text="Ready"}
            }
            MenuItem {
                action: actionExit
                onHoveredChanged: if(hovered) {statusBar.text="Exit file" } else {statusBar.text="Ready"}
            }
        }
        Menu{
            title: qsTr("&Help")
            MenuItem{
                text: qsTr("&About...")
                onTriggered: aboutDialog.open()
            }
        }
    }

    header: ToolBar{
        RowLayout{
            ToolButton{text: qsTr("New");icon.name: "document-new";action: actionNew}
            ToolButton{
                text: qsTr("Open")
                icon.name: "document-open"
                action: actionOpen
            }
            ToolButton{text: qsTr("Exit");icon.name: "❌";action: actionExit}
            ToolButton{text: qsTr("About");icon.name: "?"; onClicked: aboutDialog.open() }
        }
    }

    footer: ToolBar{
        RowLayout{
            anchors.fill: parent
            Label{
                id: statusBar
                text: "Ready"
            }
        }
    }

    ColumnLayout{
        anchors.centerIn: parent
        spacing: 10

        Label{
            id: label
            font.pointSize: 24
            text: "..."
            Layout.alignment: Qt.AlignCenter
        }

        Button{
            text: qsTr("Say Hi!")
            anchors.horizontalCenter: parent.horizontalCenter
            onClicked:{
                label.text="Hi, Guys!"
            }

        }
    }

    Item{
        id: dialogGroup

        Dialog{
            id: aboutDialog
            title: qsTr("About")

            contentItem: Rectangle{
                color: "transparent"
                implicitWidth: 180
                implicitHeight: 100
                Text {
                    text: "While Qt Quick provides basic graphical elements,\
Qt Quick Controls provides ready-made QML types for use within an application."
                    color:"green"
                    wrapMode: Text.WordWrap
                    width: 200
                    anchors.centerIn: parent
                }
            }
            standardButtons: Dialog.Ok
            parent: Overlay.overlay
            anchors.centerIn: parent
       }
    }

}

main.py

# This Python file uses the following encoding: utf-8
import sys
from pathlib import Path

from PySide6.QtGui import QGuiApplication
from PySide6.QtQml import QQmlApplicationEngine
from PySide6.QtQuickControls2 import QQuickStyle


if __name__ == "__main__":
    app = QGuiApplication(sys.argv)
    QQuickStyle.setStyle("Material")

    engine = QQmlApplicationEngine()
    qml_file = Path(__file__).resolve().parent / "main.qml"
    engine.load(qml_file)
    if not engine.rootObjects():
        sys.exit(-1)
    sys.exit(app.exec())

main.qml

  • 整体结构与主题
  1. ApplicationWindow:整个应用基于ApplicationWindow窗口类型,适合于构建具有菜单栏、工具栏和状态栏等常见功能的桌面应用。设置了固定尺寸(420 × 320)、标题,并通过visible: true显示窗口。
  2. Material 风格: 使用Material主题,采用浅色主题(Material.Light)、绿色主色(Material.primary)和红色强调色(Material.accent)。
  • ActionGroup 与操作/动作定义
  1. ActionGroup:定义了一个ActionGroup,用于集中管理多个动作,且设置exclusive: false,表示这些动作不互斥,可以同时存在。
  2. 动作定义,包含三个ActionactionNew,快捷键Ctrl+N,触发时将中间显示的label文本修改为“New file action triggered”actionOpen,快捷键Ctrl+O,触发时将label文本修改为 “Open file action triggered”actionExit,快捷键Ctrl_Q,触发时调用Qt.quit()退出程序。
  • 菜单栏(menuBar)
  1. File 菜单:包含三个菜单项,分别绑定上述的三个动作。每个菜单项的onHoveredChanged事件会根据鼠标悬停状态更新状态栏(statusBar)的文本。例如,当鼠标悬停在New菜单项上时,状态栏显示New file;否则显示“Ready”
  2. Help 菜单:包含一个About...菜单项,点击后触发打开关于对话框。
  • 工具栏(header)
  1. ToolBar内的按钮使用RowLayout布局放置四个工具按钮,NewOpenExit按钮均绑定对应的Action,实现与菜单项一致的行为(共用相同的动作和快捷键)。About按钮点击时直接调用aboutDialog.open()打开关于对话框。
  • 状态栏(footer)
  1. ToolBar中的Label :状态栏放置于窗口底部,通过Label显示当前状态(默认显示Ready),并在菜单项悬停事件中实时更新其文本。
  • 主内容区域
  1. 中间内容使用ColumnLayout布局,居中放置一个Labelidlabel)用于显示动态文本,及一个按钮“Say Hi!”。点击按钮时会修改label的文本为“Hi, Guys!”
  • 关于对话框(About Dialog)
  1. 对话框的组织:对话框被放在一个单独的Itemid: aboutDialog)中,便于集中管理。
  2. Dialog 定义:使用Dialog定义了About对话框,内容区域采用了一个透明背景的Rectangle包含多行Text。文本使用了\分隔长句,同时设置了自动换行(wrapMode: Text.WordWrap),确保内容在对话框中显示完整。对话框的标准按钮为Ok,点击后关闭对话框。对话框定位在Overlay.overlay,以确保对话框始终显示在所有其它内容之上(置顶,不会被其它 UI 遮挡)。

该段代码显示了如何在 Qt Quick 中使用QtQuick.ControlsMaterial主题构建一个综合应用界面,包括:

  1. 动作与快捷键管理:通过ActionGroup定义动作,并在菜单项和工具栏按钮中共用,保证一致性。
  2. 菜单栏、工具栏和状态栏的联动:菜单项通过鼠标悬停事件更新状态栏信息;工具栏按钮和菜单项共用相同的动作。
  3. 主内容区域交互:中心区域通过标签和按钮实现简单的交互效果。
  4. 弹出对话框:内置About对话框,通过对话框组件显示关于信息,并采用多行文本显示长句内容。

main.py

  1. from PySide6.QtQuickControls2 import QQuickStyleQQuickStyle是 Qt Quick Controls 2 的一部分,用于设置 QML 界面的全局样式,指定 UI 风格,如Material(谷歌 Material Deisgn,现代 android 设计风格)、Fusion(跨平台统一风格)、Imagine(自定义主题)、Universal(Windows 现代 UI 风格)、macOS(符合 macOS 原生风格)。
  2. QQuickStyle.setStyle("Material"):设置 UI 风格为 Material Design,统一 QML 控件的 UI 风格(如按钮、滑块、对话框等);提供 Material Design 主题支持(如深色、浅色等)。
处理用户输入

PYC icon

PYC icon

main.qml

import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import QtQuick.Controls.Material

ApplicationWindow {
    width: 500
    height: 500
    visible: true
    title: qsTr("Handling User Input")

    Text{
        id: hui
        text: "Hanlding User Input"
        parent:Overlay.overlay
        anchors.centerIn: parent
        font.pointSize: 18
        wrapMode: Text.WordWrap
        width: 400
    }

    Rectangle{
        width:400
        height:350
        color:"lightblue"
        anchors.centerIn: parent
        focus: true

        TapHandler{
            acceptedButtons: Qt.RightButton
            onTapped: {parent.color="blue"; hui.text="[Right btn]Tap event triggered!"}
            onDoubleTapped: {parent.color="red"; hui.text="[Right btn]Double tap event triggered!"}
            onLongPressed: {parent.color="green"; hui.text="[Right btn]Long-press event triggered!"}
        }

        Keys.onPressed: (event)=>{
                            hui.text="Key: "+event.key
                            if (event.key===Qt.Key_Enter || event.key===Qt.Key_Return){
                                hui.text="The enter key pressed!"
                            }
                        }
    }

    MouseArea{
        anchors.fill: parent
        onClicked: (mouse)=>{hui.text=qsTr('[Left btn]Mouse click position:\nx= %1 y=%2').arg( mouse.x).arg(mouse.y)}
        //onPressed: hui.text="Mouse down"
        //onReleased: hui.text="Mouse release"
    }

    TextField{
        width: 400
        placeholderText: "Please enter content..."
        anchors.bottom: parent.bottom
        anchors.horizontalCenter: parent.horizontalCenter
        onTextChanged: hui.text="<b>Current input:</b><br>"+text
    }
}

main.py

# This Python file uses the following encoding: utf-8
import sys
from pathlib import Path

from PySide6.QtGui import QGuiApplication
from PySide6.QtQml import QQmlApplicationEngine
from PySide6.QtQuickControls2 import QQuickStyle


if __name__ == "__main__":
    app = QGuiApplication(sys.argv)
    QQuickStyle.setStyle("Material")

    engine = QQmlApplicationEngine()
    qml_file = Path(__file__).resolve().parent / "main.qml"
    engine.load(qml_file)
    if not engine.rootObjects():
        sys.exit(-1)
    sys.exit(app.exec())

main.qml

这段代码展示了如何处理用户输入、包括鼠标点击、键盘事件、触摸事件和文本输入。

  • 整体布局与结构
  1. ApplicationWindow为应用的主窗口,设置高宽均为500像素,并设置窗口的标题为"Handling User Input"
  2. Text:用于显示用户输入的文本,默认显示"Handling User Input",并在用户触发事件时更新。
  • 事件处理和交互

RectangleTapHandler

  1. Rectangle:绘制了一个宽 400, 高 350 的矩形,背景颜色为浅蓝色,并且设置为可获取焦点(focus: true),使得它可以接收触摸和键盘事件。
  2. TapHandler:绑定了三个事件。onTapped为右键点击事件触发时,矩形变为蓝色,文本更新为"Tap event triggered!"onDoubleTapped为右键双击事件触发时,矩形变为红色,文本更新为"Double tap event triggered!"onLongPressed为右键长按事件触发时,矩形变为绿色,文本更新为"Long-press event triggered!"

键盘事件处理(Keys.onPressed

  1. Keys.onPressed:当任何键按下时,文本会更新为按下的键名(event.key),如果按下的是回车键(EnterReturn),则显示"The enter key pressed!"

MouseArea事件

  1. MouseAreaMouseArea绑定了一个onClicked事件。当鼠标左键点击时,显示鼠标点击位置的坐标(mouse.xmouse.y),并使用qsTr格式化显示。

TextField 输入处理

  1. TextField:提供了一个文本框,用户可以在其中输入内容。通过onTextChanged信号,每当用户输入文本时,更新文本显示,显示为"Current input: <输入内容>",并使用了 HTML 标签<b><br>来设置文本样式和换行。
属性绑定

PYC icon

PYC icon

main.qml

import QtQuick
import QtQuick.Controls
import QtQuick.Layouts

ApplicationWindow {
    width: 400
    height: 400
    visible: true
    title: qsTr("Property Bindings")

    Rectangle{
        id: box
        width: 200
        height: 100
        color: "lightblue"
        anchors.centerIn: parent
    }

    Rectangle{
        id: box2
        width: box.width/2
        height: box.height*0.5
        color: box.width > 100 ? "red" : "green"
        anchors.bottom: box.top
        anchors.horizontalCenter: parent.horizontalCenter
    }



    Slider{
        id: sizeSlider
        from: 50
        to: 300
        anchors.top: box.bottom
        anchors.horizontalCenter: parent.horizontalCenter
        width: 200
        onValueChanged: box.width=sizeSlider.value
    }

    Text{
        text: "Slide to change width: " + (sizeSlider.value).toFixed(2)
        anchors.top: sizeSlider.bottom
        anchors.horizontalCenter: parent.horizontalCenter
    }
}

main.py

# This Python file uses the following encoding: utf-8
import sys
from pathlib import Path

from PySide6.QtGui import QGuiApplication
from PySide6.QtQml import QQmlApplicationEngine
from PySide6.QtQuickControls2 import QQuickStyle


if __name__ == "__main__":
    app = QGuiApplication(sys.argv)
    QQuickStyle.setStyle("Material")

    engine = QQmlApplicationEngine()
    qml_file = Path(__file__).resolve().parent / "main.qml"
    engine.load(qml_file)
    if not engine.rootObjects():
        sys.exit(-1)
    sys.exit(app.exec())

main.qml

这个示例的 QML 代码展示了属性绑定(Property Bindings)的使用,主要包括矩形之间的绑定,滑块与界面互动和实时文本更新等。

  • 整体结构
  1. ApplicationWindow:应用程序的主窗口,设置了窗口的宽度和高度均为 400 像素,并设置了窗口的标题为"Property Bindings"
  • 矩形(Rectangle)和属性绑定
  1. 第一个矩形(box):宽度和高度,矩形的宽度为 200,高度为 100, 背景色为 lightblue,并通过anchors.centerIn将其居中放置在窗口中;属性绑定,这个矩形的widthheight是静态设置的,主要用于作为参考矩形供其它控件(如box2)进行绑定。
  2. 第二个矩形(box2):大小绑定, box2的宽度设置为box的宽度的一半(box.width/2),高度设置为box高度的 50% (box.height*0.5);颜色变化,使用条件表达式绑定颜色,如果box.width大于100, 则box2的颜色为红色(red),否则为绿色(green);位置绑定,通过anchors.bottomanchors.horizontalCenterbox2放置在box的顶部,并将其水平居中。
  • 滑块(Slider)控件
  1. Slider控件:滑块的最小值为 50,最大值为 300。滑块宽度为 200, 位置位于 box矩形的底部,并水平居中放置;属性绑定,当滑块的值发生变化时,box.width会动态地更新为sizeSlider.value,即滑块地当前值。通过onValueChanged事件更新矩形地宽度。
  • 文本显示
  1. Text 控件:用于显示滑块当前值地文本。文本格式为"Slide to change width: ",后跟滑块地当前值,并使用toFixed(2)保留两位小数;位置绑定,文本放置于sizeSlider底部,水平居中显示。
动画

PYC icon

main.qml

import QtQuick
import QtQuick.Controls
import QtQuick.Layouts

ApplicationWindow {
    width: 400
    height: 400
    visible: true
    title: qsTr("Animations")

    Rectangle {
        id: box
        color: "lightgray"
        width: 200
        height: 200
        anchors.centerIn: parent

        property int animationValue: 0
        SequentialAnimation on animationValue {
            loops: Animation.Infinite
            PropertyAnimation{to: 150; duration: 10000}
            PropertyAnimation{to: 0; duration: 100000}
        }

        Rectangle{
            id: box2
            width: 100
            height: 100
            color: "lightblue"
            anchors.centerIn: parent

            ParallelAnimation{
                running: true

                NumberAnimation{
                    target: box2
                    property: "width"
                    from: 100
                    to: 200
                    duration: 2500
                }

                NumberAnimation{
                    target: box2
                    property: "height"
                    from: 100
                    to: 200
                    duration: 2500
                }

                SequentialAnimation{
                    running: true
                    loops: Animation.Infinite

                    ColorAnimation {
                        target: box
                        property: "color"
                        from: "white"
                        to: "black"
                        duration: 5000
                    }

                    ColorAnimation {
                        target: box
                        property: "color"
                        from: "black"
                        to: "white"
                        duration: 5000
                    }
                }
            }

            Rectangle {
                id: box3
                width: 100
                height: 100
                color: "green"
                //radius: width/2
                anchors.centerIn: parent

                RotationAnimation{
                    target: box3
                    from: 0
                    to: 360
                    duration: 10000
                    running: true
                }
            }
        }

        Text{
            anchors.centerIn: parent
            text: parent.animationValue
            font.pointSize: 24
        }

        NumberAnimation {
            target: box
            property: "width"
            from: 200
            to: 300
            duration: 5000
            running: true
            easing.type: Easing.InOutQuad
        }

        NumberAnimation {
            target: box
            property: "height"
            from: 200
            to: 300
            duration: 5000
            running: true
        }
    }
}

main.py

# This Python file uses the following encoding: utf-8
import sys
from pathlib import Path

from PySide6.QtGui import QGuiApplication
from PySide6.QtQml import QQmlApplicationEngine
from PySide6.QtQuickControls2 import QQuickStyle


if __name__ == "__main__":
    app = QGuiApplication(sys.argv)
    QQuickStyle.setStyle("Material")

    engine = QQmlApplicationEngine()
    qml_file = Path(__file__).resolve().parent / "main.qml"
    engine.load(qml_file)
    if not engine.rootObjects():
        sys.exit(-1)
    sys.exit(app.exec())

main.qml

这段 QML 代码展示了多个动画效果,结合了SequentialAnimation(顺序动画,多个动画按顺序依次执行。用于控制animationValue的变化和box颜色的变化)、ParallelAnimation(并行动画,多个动画同时执行。用于同时改变box2的宽度、高度及box的颜色)、PropertyAnimation(属性动画,用于修改属性,如animationValue值的变化)、ColorAnimation(颜色动画,用于平滑过渡矩形颜色的变化)、NumberAnimation(数值动画,用于修改属性),及RotationAnimation(旋转动画,实现旋转效果,如给矩形应用旋转动画)来创建动态效果。

  • 主窗口
  1. 设置窗口属性:设置了box的宽度和高度均为 200 像素,标题为"Animations",并且窗口可见。
  • 第一个矩形(box
  1. 矩形设置:设置了box的高宽均为 200,背景颜色为浅灰色(lightgray),并且使用anchors.centerIn将其居中于父容器。
  2. animationValue:定义了一个自定义属性animationValue,并通过SequentialAnimation实现了连续的属性动画(PropertyAnimation{to: x; duration: x})。animationValue0150之间变化,持续事件为 10 秒;然后从 1500,持续时间为 100 秒。这个动画会无限循环(loops: Animation.Infinite)。
  • 第二个矩形(box2
  1. 矩形设置:box2的宽度和高度都设置为100, 颜色为浅蓝色(lightblue),并通过anchors.centerIn将其居中。
  2. ParallelAnimation:在box2上同时执行多个动画。NumberAnimation,控制box2的宽度和高度,从100动画到200,持续时间为 2500 毫秒;SequentialAnimation,包含两个ColorAnimation,将box的颜色在whiteblack之间交替变化,每个颜色变化持续 5000 毫秒,且该动画循环执行(loops: Animation.Infinite)。
  • 第三个矩形(box3
  1. 矩形设置:box3的宽度和高度为 100,颜色为绿色(green),并通过anchors.centerIn居中放置。
  2. RotationAnimation:使box30360度旋转,动画持续时间为 10000 毫秒(10 秒),并且动画会不断循环。
  • Text 控件
  1. 文本显示: 在父矩形内的居中位置显示animationValue的当前值,字体大小为24, 显示parent.animationValue,这是第一个矩形动画的值。
  • 矩形宽度和高度的数值动画
  1. NumberAnimation:box 的宽度和高度从 200 动画到 300,动画持续时间为 5000 毫秒。这里设置了easing.type: Easing.InOutQuad`来使动画的变化更加平滑。
定义可重用的自定义 QML 类型

PYC icon

MessageLabel.qml

import QtQuick

Rectangle{
    height: 80
    property string message: "debug message"
    property var msgType: ["debug", "warning", "critical"]
    color: "black"

    Column{
        anchors.fill: parent
        padding: 5.0
        spacing: 2
        Text{
            text: msgType.toString().toUpperCase() + ":"
            font.bold: msgType === "critical"
            font.family: "Terminal Regular"
            font.pointSize: 24
            color: msgType === "warning" || msgType === "critical" ? "red" : "yellow"

            ColorAnimation on color {
                running: msgType === "critical"
                from: "red"
                to: "black"
                duration: 1000
                loops:  msgType === "critical" ? Animation.Infinite : 1
            }
        }
        Text{
            text: message
            color: msgType === "warning" || msgType === "critical" ? "red" : "yellow"
            font.family: "Terminal Regular"
            font.pointSize: 20
        }
    }
}

CustomButton.qml

import QtQuick

Item {
    width: 280
    height: 50

    property string buttonText: "CLick Me"
    signal clicked()

    Rectangle{
        width: parent.width
        height: parent.height
        color: "lightblue"
        //border.color: "black"
        radius: 10

        Text{
            anchors.centerIn: parent
            text: parent.parent.buttonText
            font.pointSize: 20
            color: "white"
        }

        MouseArea{
            anchors.fill: parent
            onClicked: {
                parent.parent.clicked()
            }
        }
    }
}

main.qml

import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import QtQuick.Controls.Material

ApplicationWindow {
    width: 280
    height: 280 + 100
    visible: true
    title: qsTr("Custom QML Types")

    Column{
        width:280
        height: 280
        padding: 1.5
        topPadding: 10.0
        bottomPadding: 10.0
        spacing: 5

        MessageLabel{
            width:   parent.width - 2
            msgType: "debug"
        }
        MessageLabel{
            width:   parent.width - 2
            msgType: "warning"
            message: "This is a warning!"
        }
        MessageLabel{
            width:   parent.width - 2
            msgType: "critical"
            message: "A critical warning!"
        }

        Label{
            id: label
            font.pointSize: 20
            text: "..."
            anchors.horizontalCenter: parent.horizontalCenter
        }

        CustomButton{
            buttonText: "Press Me"
            //anchors.centerIn: parent
            onClicked: {
                label.text = "Button was clicked!"
            }
            anchors.horizontalCenter: parent.horizontalCenter
        }
    }
}

main.py

# This Python file uses the following encoding: utf-8
import sys
from pathlib import Path

from PySide6.QtGui import QGuiApplication
from PySide6.QtQml import QQmlApplicationEngine
from PySide6.QtQuickControls2 import QQuickStyle


if __name__ == "__main__":
    app = QGuiApplication(sys.argv)
    QQuickStyle.setStyle("Material")

    engine = QQmlApplicationEngine()
    qml_file = Path(__file__).resolve().parent / "main.qml"
    engine.load(qml_file)
    if not engine.rootObjects():
        sys.exit(-1)
    sys.exit(app.exec())

MessageLabel.qml

这段代码使用Qt Quick(QML)构建了一个简单的用户界面组件,主要用于展示不同类型的消息。代码定义了一个Rectangle元素,包含了一个消息类型和一个消息内容,通过Text元素动态的显示不同的文本和颜色。代码的主要功能是根据msgType来动态改变显示内容和样式。

  • 根元素Rectangle
  1. height: 80:设置矩形的高度为 80 像素。
  2. property string message: "debug message":定义一个字符串属性message,初始值为"debug message",用于显示消息内容。
  3. property var msgType: ["debug", "warning", "critical"]:定义一个属性msgType,是一个数组,包含三种消息类型:"debug""warning""critical"
  4. color: "black":设置Rectangle的背景色为黑色。
  • Column容器
  1. anchors.fill: parent:将Column的大小设置为填满父元素Rectangle
  2. padding: 5.0:给Column元素添加内边距,确保子元素不会紧贴容器边缘。
  3. spacing: 2:设置Column中子元素的垂直间距为 2 像素。
  • 第一个 Text 元素与后端的
  1. text: msgType.toString().toUpperCase() + ":":显示msgType数组的字符串表示,并将其转换为大小形式后添加冒号,例如DEBUG:
  2. font.bold: msgType === "critical":如果msgType"critical",则将字体设置为加粗。
  3. font.family: "Terminal Regular":设置字体为Terminal Regular,一种等宽字体。
  4. font.pointSize: 24:设置字体大小为 24.
  5. color: msgType === "warning" || msgType === "critical" ? "red" : "yellow":根据msgType设置字体颜色,"warning""critical"为红色,否则为黄色。
  • ColorAnimation 动画
  1. running: msgType === "critical":如果msgType"critical",则启动颜色动画。
  2. from: "red"to: "black":动画从红色开始,结束时变为黑色。
  3. duration: 1000:动画持续 1000 毫秒(1秒)。
  4. loops: msgType === "critical" ? Animation.Infinite : 1:如果msgType"critical",则动画会无限循环,否则只执行一次。
  • 第二个 Text 元素
  1. text: message:显示message属性的内容,即"debug message"(或其它设定的内容)。
  2. color: msgType === "warning" || msgType === "critical" ? "red" : "yellow":同样根据msgType设置文本颜色,"warning""critical"类型为红色,否则为黄色。
  3. font.family: "Terminal Regular":设置字体为Terminal Regular
  4. font.pointSize: 20:设置字体大小为 20。

创建的一个带有动态文本和动画效果的 UI 组件,根据 msgType的不同值,界面显示不同类型的消息,且颜色、字体样式及动画效果都会随之变化。对于msgType"critical",会显示红色并且执行动画;对于msgType"warning",显示红色文本但没有动画;对于msgType"debug",显示黄色文本。

CustomButton.qml

该部分自定义了一个按钮组件,通过Item和嵌套的RectangleTextMouseArea元素实现了一个具有点击事件的按钮。

  • 根元素:Item
  1. width: 280height: 50:分别设置按钮的宽度和高度。
  • 属性和信号
  1. property string buttonText: "CLick Me":定义一个buttonText属性,类型为字符串,初始值为"Click Me"。这个属性将用于显示按钮上的文本。
  2. signal clicked():定义一个clicked信号,当按钮被点击时会发出该信号。
  • Rectangle(按钮背景)
  1. width: parent.widthheight: parent.height:设置Rectangle的宽度和高度为其父元素(Item)的宽度和高度。
  2. color: "lightblue":设置矩形背景颜色为淡蓝色。
  3. radius: 10:设置矩形的圆角半径为 10 像素,圆角使得按钮看起来更加圆润。
  4. //border.color: "black":注释掉的代码(未启用),如果启用,会为按钮添加一个黑色的边框。
  • Text(按钮文本)
  1. anchors.centerIn: parent:将文本元素相对于Rectangle进行居中对齐。
  2. text: parent.parent.buttonText:设置文本内容为Item元素的buttonText属性。这样,按钮上的文本就能动态的由buttonText控制。
  3. font.pointSize: 20:设置字体大小为 20。
  4. color: "white":设置文本颜色为白色。
  • MouseArea(鼠标点击区域)
  1. anchors.fill: parent:将MouseArea填充整个父元素(即整个Rectangle),意味着鼠标点击事件将会覆盖整个按钮区域。
  2. onClicked: { parent.parent.clicked() }:当鼠标点击区域时,触发MouseAreaonClicked事件,进而调用parent.parent.clicked(),即发出Item元素上的clicked信号。

MouseArea用于捕捉点击事件,并触发clicked信号,其它组件可以监听并响应这个信号,通常用于绑定逻辑操作。

main.qml

该部分 QML 代码使用了ApplicationWindowColumn布局,创建一个具有自定义组件MessageLabel(用于展示不同类型的消息)和CustomButton(自定义按钮)的交互式界面。

  1. ApplicationWindow:由width: 280height: 280 + 100设置窗口宽高。用visible: true确保窗口可见。用title: qsTr("Custom QML Types")设置窗口标题。
  2. Column布局:Column是一个垂直排列元素的容器,用width: 280height: 280设置Column的宽高。用padding: 1.5topPadding: 10.0bottomPadding: 10.0设置内边距和上下内边距。用spacing: 5设置子元素之间的垂直间距。
  3. MessageLabel组件:这三个MessageLabel元素用于显示不同类型的消息("debug""warning""critical")。每个MessageLabel都设置了不同的msgType属性和消息内容。
  4. Label组件:用id: label定义 ID,方便引用。用font.pointSize: 20设置字体大小。用text: "..."设置初始化文本。用anchors.horizontalCenter: parent.horizontalCenter使Label水平居中对齐。
  5. CustomButton组件:使用自定义按钮(CustomButton),包含一个点击事件。用buttonText: "Press Me",通过属性buttonText修改按钮显示的文本。onClicked: { label.text = "Button was clicked!" },为当按钮被点击时,修改Label的文本。

QML 中重要的概念之一是类型重用。一个应用程序可能有多个相似的可视化类型(如多个按钮),QML 允许将这些类型定义为可重用的自定义类型,以最小化代码重复并最大化可读性。

11.6.3 Python-QML 集成

QML 是一种声明式语言,相较传统编程语言(如 C++) 可以更快的用于设计 UI (用户界面)。QtQml 和 QtQuick 模块为基于 QML 的 UIs 设计提供了基础设施。下述示例解释了 Python 与 QML 的集成,使用 Python 作为后端逻辑代码,处理来自前端 QML 接口中 UI 元素的信号。定义了多个槽函数,提供了颜色、字体样式和大小等属性的设置。文件结构如图。

PYC icon

图 11-12 Python-QML 集成界面

PYC icon

图 11-13 Python-QML 集成文件结构

文件 main.qml main.py qtquickcontrols2.conf HelloWorld.pyproject
代码
// Copyright (C) 2021 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause

import QtQuick 2.0
import QtQuick.Layouts 1.11
import QtQuick.Controls 2.1
import QtQuick.Window 2.1
import QtQuick.Controls.Material 2.1

import io.qt.textproperties 1.0

ApplicationWindow {
    id: page
    width: 800
    height: 400
    visible: true
    //Material.theme: Material.Light //Dark
    //Material.accent: Material.Red

    Bridge {
        id: bridge
    }

    GridLayout {
        id: grid
        columns: 2
        rows: 3

        ColumnLayout {
            spacing: 2
            Layout.columnSpan: 1
            Layout.preferredWidth: 400

            Text {
                id: leftlabel
                Layout.alignment: Qt.AlignHCenter
                color: "black" //"white"
                font.pointSize: 16
                text: "Qt for Python"
                Layout.preferredHeight: 100
                Material.accent: Material.Green
            }

            RadioButton {
                id: italic
                Layout.alignment: Qt.AlignLeft
                text: "Italic"
                onToggled: {
                    leftlabel.font.italic = bridge.getItalic(italic.text)
                    leftlabel.font.bold = bridge.getBold(italic.text)
                    leftlabel.font.underline = bridge.getUnderline(italic.text)

                }
            }
            RadioButton {
                id: bold
                Layout.alignment: Qt.AlignLeft
                text: "Bold"
                onToggled: {
                    leftlabel.font.italic = bridge.getItalic(bold.text)
                    leftlabel.font.bold = bridge.getBold(bold.text)
                    leftlabel.font.underline = bridge.getUnderline(bold.text)
                }
            }
            RadioButton {
                id: underline
                Layout.alignment: Qt.AlignLeft
                text: "Underline"
                onToggled: {
                    leftlabel.font.italic = bridge.getItalic(underline.text)
                    leftlabel.font.bold = bridge.getBold(underline.text)
                    leftlabel.font.underline = bridge.getUnderline(underline.text)
                }
            }
            RadioButton {
                id: noneradio
                Layout.alignment: Qt.AlignLeft
                text: "None"
                checked: true
                onToggled: {
                    leftlabel.font.italic = bridge.getItalic(noneradio.text)
                    leftlabel.font.bold = bridge.getBold(noneradio.text)
                    leftlabel.font.underline = bridge.getUnderline(noneradio.text)
                }
            }
        }

        ColumnLayout {
            id: rightcolumn
            spacing: 2
            Layout.columnSpan: 1
            Layout.preferredWidth: 400
            Layout.preferredHeight: 400
            Layout.fillWidth: true

            RowLayout {
                Layout.alignment: Qt.AlignVCenter | Qt.AlignHCenter


                Button {
                    id: red
                    text: "Red"
                    highlighted: true
                    Material.accent: Material.Red
                    onClicked: {
                        leftlabel.color = bridge.getColor(red.text)
                    }
                }
                Button {
                    id: green
                    text: "Green"
                    highlighted: true
                    Material.accent: Material.Green
                    onClicked: {
                        leftlabel.color = bridge.getColor(green.text)
                    }
                }
                Button {
                    id: blue
                    text: "Blue"
                    highlighted: true
                    Material.accent: Material.Blue
                    onClicked: {
                        leftlabel.color = bridge.getColor(blue.text)
                    }
                }
                Button {
                    id: nonebutton
                    text: "None"
                    highlighted: true
                    Material.accent: Material.BlueGrey
                    onClicked: {
                        leftlabel.color = bridge.getColor(nonebutton.text)
                    }
                }
            }
            RowLayout {
                Layout.fillWidth: true
                Layout.alignment: Qt.AlignVCenter | Qt.AlignHCenter
                Text {
                    id: rightlabel
                    color: "white"
                    Layout.alignment: Qt.AlignLeft
                    text: "Font size"
                    Material.accent: Material.White
                }
                Slider {
                    width: rightcolumn.width*0.6
                    Layout.alignment: Qt.AlignRight
                    id: slider
                    value: 0.5
                    onValueChanged: {
                        leftlabel.font.pointSize = bridge.getSize(value)
                    }
                }
            }
        }
    }
}
# Copyright (C) 2022 The Qt Company Ltd.
# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
from __future__ import annotations

import sys
from pathlib import Path

from PySide6.QtCore import QObject, Slot
from PySide6.QtGui import QGuiApplication
from PySide6.QtQml import QQmlApplicationEngine, QmlElement
from PySide6.QtQuickControls2 import QQuickStyle

import rc_style   # noqa F401

# To be used on the @QmlElement decorator
# (QML_IMPORT_MINOR_VERSION is optional)
QML_IMPORT_NAME = "io.qt.textproperties"
QML_IMPORT_MAJOR_VERSION = 1

@QmlElement
class Bridge(QObject):

    @Slot(str, result=str)
    def getColor(self, s):
        if s.lower() == "red":
            return "#ef9a9a"
        if s.lower() == "green":
            return "#a5d6a7"
        if s.lower() == "blue":
            return "#90caf9"
        return "white"

    @Slot(float, result=int)
    def getSize(self, s):
        size = int(s * 34)
        return max(1, size)

    @Slot(str, result=bool)
    def getItalic(self, s):
        return s.lower() == "italic"

    @Slot(str, result=bool)
    def getBold(self, s):
        return s.lower() == "bold"

    @Slot(str, result=bool)
    def getUnderline(self, s):
        return s.lower() == "underline"


if __name__ == '__main__':
    app = QGuiApplication(sys.argv)
    QQuickStyle.setStyle("Material")
    engine = QQmlApplicationEngine()
    qml_file = Path(__file__).resolve().parent / "main.qml"
    engine.load(qml_file)

    if not engine.rootObjects():
        sys.exit(-1)
    sys.exit(app.exec())
[Controls]
Style=Material

[Universal]
Theme=System
Accent=Red

[Material]
Theme=Dark
Accent=Red
{
    "files": [
        "main.py",
        "main.qml",
        "qtquickcontrols2.conf",
        "rc_style.py",
        "style.qrc"
    ]
}
🤖 代码解读

这段 QML 代码实现了一个交互式界面,允许用户通过选择不同的单选按钮来改变标签文本的样式(斜体、粗体、下划线或无样式),通过按钮选择颜色,并用滑动条调整字体大小。所有这些操作都通过bridge对象的方法进行控制和反馈。

  1. 应用窗口(ApplicationWindow):创建一个主应用窗口,大小为 800x400,并设置为可见。
  2. 布局(GridLayout):使用一个GridLayout布局,将界面分为两列(三行)。该布局包含两个ColumnLayout,分别管理左侧和右侧内容。
  3. 左侧控件(左侧列):标签(Text)显示文本"Qt for Python",文本颜色为黑色,字体大小为 16。单选按钮(RadioButton)包括四个单选按钮用于控制字体样式(斜体、粗体、下划线、无样式)。每个按钮的onToggled信号通过调用bridge对象,会触发一个后端代码 Python 槽函数(getItalic()getBold()getUnderline())来调整左侧标签的字体样式。
  4. 右侧控件(右侧列):有四个按钮(Button),分别用于设置文本的颜色(红色、绿色、蓝色、无色)。点击按钮时,颜色会通过bridge.getColor()方法设置到左侧的标签。滑动条(Slider)用于调整文本的字体大小,通过bridge.getSize()方法动态调整字体大小。
  5. 桥接对象(Bridge):代码中使用了一个Bridge,以实现前端界面与后端代码 Python 槽函数之间的交互。
  6. 使用了 Material 设计风格的控件和主题(如Material.accentMaterial.White),可以设置不同控件的主题颜色,提升用户界面的美观度

这段 PySide6 应用程序,使用 Qt 的 QML 引擎加载一个 QML 文件,并通过 Python 中的 Bridge类与 QML 进行交互。

  • 导入模块
  1. from __future__ import annotations:这一行启用了 Python 未来的功能,允许在类型注释中使用尚未定义的类类型。
  2. import sys:导入sys模块,用于处理系统相关的操作,例如传递命令行参数。
  3. from pathlib import PathPath类,用于文件路径操作。
  4. from PySide6.QtCore import QObject, SlotQObject用于创建 QML 对象的基类。Slot用于将 Python 函数作为 QML 信号的槽函数。
  5. from PySide6.QtGui import QGuiApplication:Qt 的 GUI 应用程序类。
  6. from PySide6.QtQml import QQmlApplicationEngine, QmlElementQQmlApplicationEngine用于加载和运行 QML 文件的引擎。QmlElement用于将 Python 类暴露给 QML 使用的装饰器。
  7. from PySide6.QtQuickControls2 import QQuickStyle:用于设置应用程序样式。
  8. import rc_style # noqa F401:导入了 rc_style模块,但没有在代码中使用。# noqa F401是一个代码审查工具的忽略指令,表示知道导入了但未使用。
  • 设置 QML 元素
QML_IMPORT_NAME = "io.qt.textproperties"
QML_IMPORT_MAJOR_VERSION = 1
  1. 定义了 QML 导入名称和版本,用于与 QML 进行交互。QML_IMPORT_NAMEQML_IMPORT_MAJOR_VERSION会在 QML 中导入对应的 Python 模块。
  • 定义Bridge类与
@QmlElement
class Bridge(QObject):
  1. 通过@QmlElement装饰器将Bridge类暴露给 QML 使用。Bridge继承自QObject,允许将 Python 函数暴露为 QML 可调用的槽函数。

getColor方法

@Slot(str, result=str)
def getColor(self, s):
    if s.lower() == "red":
        return "#ef9a9a"
    if s.lower() == "green":
        return "#a5d6a7"
    if s.lower() == "blue":
        return "#90caf9"
    return "white"
  1. @Slot(str, result=str)装饰器定义了一个槽函数getColor,接受一个字符串并返回一个字符串。
  2. 根据输入的颜色名称("red""green""blue"),返回对应的颜色代码。如果传入的颜色名不匹配,则返回"white"

getSize方法

@Slot(float, result=int)
def getSize(self, s):
    size = int(s * 34)
    return max(1, size)
  1. @Slot(float, result=int)装饰器定义了一个槽函数getSize,接受一个浮动数值并返回一个整数。
  2. 将输入值乘以 34 并返回结果,但确保返回的值至少为1。

getItalic方法

@Slot(str, result=bool)
def getItalic(self, s):
    return s.lower() == "italic"
  1. @Slot(str, result=bool)装饰器定义了一个槽函数getItalic,接受一个字符串并返回布尔值。
  2. 如果传入的字符串是"italic",则返回True,否则返回False

getBold方法

@Slot(str, result=bool)
def getBold(self, s):
    return s.lower() == "bold"
  1. getBold方法类似于getItalic,判断传入的字符串是否为"bold",如果是,返回True,否则返回False

getUnderline方法

@Slot(str, result=bool)
def getUnderline(self, s):
    return s.lower() == "underline"
  1. getUnderline方法类似于getItalicgetBold,判断传入的字符串是否为"underline"
  • 主程序
  1. app = QGuiApplication(sys.argv):创建一个 Qt GUI 应用程序实例,传入命令行参数。
  2. QQuickStyle.setStyle("Material"):设置应用程序的样式为Material风格。
  3. engine = QQmlApplicationEngine():创建一个QQmlApplicationEngine实例,用于加载 QML 文件。
  4. qml_file = Path(__file__).resolve().parent / "main.qml":获取当前脚本所在目录下的main.qml文件路径,并将其加载到引擎中。
  5. if not engine.rootObjects()::返回 QML 引擎加载的根对象。如果没有加载任何对象,则退出程序(sys.exit(-1))。
  6. sys.exit(app.exec()):启动应用程序的事件循环,直到退出。

此代码是 PySide6 中一个常见模式,用于将 Python 和 QML 无缝集成,以便在 QML 中动态调用 Python 函数。

  1. QML 与 Python 交互: 通过@QmlElement装饰器将 Python 中的 Bridge类暴露给 QML, 使得 QML 可以调用 Python 中的槽函数(如getColorgetSizegetItalicgetBoldgetUnderline)。
  2. QML 引擎:使用QQmlApplicationEngine来加载 QML文件,并且通过QQuickStyle设置Material风格主题。
  3. 槽函数:定义了多个槽函数,提供颜色、字体样式等属性的设置,并在 QML 中使用。
  4. 应用结构:在 Python 中设置 QML 引擎并启动事件循环,确保 QML 能够正常工作。

该配置文件为应用设置了界面风格、主题和强调色。

11.7 模型视图

模型视图(Model-View)架构是一种常用于图形用户界面(GUI)应用程序设计的设计模式,帮助分离用户界面的显示(视图)与数据的管理(模型)。这种模式使得程序的结构清晰、可维护、且使得数据和显示逻辑的修改可以互不影响。模型是应用程序的数据结构和业务逻辑的核心,负责存储和管理数据,并提供接口来访问和修改这些数据。模型不关心数据如何被显示,只关心数据的管理和操作。视图负责将数据以可视化的形式展示出来,显示模型中的数据并允许用户与其交互。视图不直接处理数据,只关注如何呈现数据。

11.7.1 [Q]ListView

[Q]ListViewListView都是 Qt 框架中的视图控件,用于显示列表数据,都是基于 Model-View(模型视图)架构,但分属于不同的 UI 构建方式:QListView属于基于QWidgets的传统桌面应用,而ListView属于 QML/QtQuick 框架。QListView用于显示从模型中获取的数据列表,通常与QAbstractListModel或类似的模型结合使用,在传统的桌面应用程序(基于 QWidgets)中用于显示垂直或水平的项列表。ListView是 QtQuick/QML 框架中的一个控件,用于显示从模型中获取的数据,通常与ListModel或其它自定义模型配合使用。

该示例程序实现了一个简单的待办事项应用,具有添加、删除,标记完成等功能。

方式选择 Qt Designer + PySide6 QML(QtQuick) + PySide6
界面

PYC icon

PYC icon

                                              

[Q]ListView

QListView-PySide6

QListView是 PySide6 中用于显示数据列表的控件,与 Qt模型/视图框架(Model-View Framework)紧密结合,允许从模型中提取数据,如通过与QAbstractListModel或其子类结合使用,QListView可以动态显示各种类型的数据。

  • 模型(Model):QListView本身不存储数据,依赖于一个模型来提供数据。模型负责管理数据并通知视图数据发生变化。
  • 视图(View):QListView是一个视图控件,用于渲染和展示模型的数据,能够显示列表项,并处理用户的交互(如点击、选择等)。
  • 代理(Delegate):用于渲染项内容或处理项的编辑。QListView默认使用内建的代理来渲染项。

QListView的显示和交互是通过不同的角色(roles)来控制,常见的角色包括,

常量 描述
Qt.DisplayRole 0 要以文本形式渲染的关键数据。(QString)
Qt.DecorationRole 1 要渲染为装饰的数据,通常是图标。(QColor, QIcon 或 QPixmap)
Qt.EditRole 2 适合在编辑器中编辑的数据。(QString)
Qt.ToolTipRole 3 项目的工具提示中显示的数据。(QString)
Qt.StatusTipRole 4 在状态栏中显示的数据。(QString)
Qt.WhatsThisRole 5 What's This?模式下显示的数据。(QString)
Qt.SizeHintRole 13 提供给视图的项的大小提示。(QSize)

ListView- PySide6 + QML(QtQuick)

QML(QtQuick)中,ListView控件用于展示可以滚动的列表数据,结合模型-视图 结构,使数据和视图分离,可更灵活的展示和处理数据。

  • 模型(Model):用于存储数据,可以使用ListModel(QML 内部的简单数据模型)或者通过 PySide6 提供的模型,如QAbstractListModel等。
  • 视图(View):即ListView,将视图与数据模型连接起来,负责呈现和管理滚动的 UI。
  • 代理(Delegate):定义列表项的外观和行为。delegate是通过一个组件来表示列表中的每个项。

当数据量较大时,或者需要与 Python 后端交互,QML 中的ListModel可能不足够灵活,而 PySide6 提供的QAbstractListModel可以构建更复杂的模型。

代码
  • Qt Designer

在 Qt Creator 下的 Qt Designer (Design)中设计用户界面如图,包括的控件(widgets)如表,

objectName 类型 描述
todoView QListView 当前待办事项的列表
todoEdit QLineEdit 用于创建新待办项的文本输入框
addButton QPushButton 创建新待办事项,并将其添加到待办事项列表
deleteButton QPushButton 删除当前选中的待办事项,并将其从待办事项列表中移除
completeButton QPushButton 将当前选中的待办事项标记为已完成

PYC icon

  • mainwindow.py
# This Python file uses the following encoding: utf-8
import sys, os
import json

from PySide6.QtWidgets import QApplication, QMainWindow
from PySide6.QtCore import QAbstractListModel, Qt, Slot
from PySide6.QtGui import QImage, QIcon

from ui_form import Ui_MainWindow

basedir = os.path.dirname(__file__)
tick = QImage(os.path.join(basedir, "data/accepts.png"))


class TodoModel(QAbstractListModel):
    def __init__(self, todos=None):
        super().__init__()
        self.todos = todos or []

    def data(self, index, role):
        if role == Qt.DisplayRole:
            status, text = self.todos[index.row()]
            return text
        if role == Qt.DecorationRole:
            status, text = self.todos[index.row()]
            if status:
                return tick

    def rowCount(self, index):
        return len(self.todos)


class MainWindow(QMainWindow):
    def __init__(self, parent=None):
        super().__init__(parent)
        self.ui = Ui_MainWindow()
        self.ui.setupUi(self)
        self.setWindowTitle("TODO")
        self.setWindowIcon(QIcon(os.path.join(basedir, "data/icons8-add-list-48.png")))

        self.model = TodoModel()
        self.load()
        self.ui.todoView.setModel(self.model)
        self.ui.addButton.pressed.connect(self.add)
        self.ui.deleteButton.pressed.connect(self.delete)
        self.ui.completeButton.pressed.connect(self.complete)

    def load(self):
        try:
            with open("data/data.json", "r") as f:
                self.model.todos = json.load(f)
        except Exception:
            pass

    @Slot()
    def add(self):
        text = self.ui.todoEdit.text()
        text = text.strip()
        if text:
            self.model.todos.append((False, text))
            self.model.layoutChanged.emit()
            self.ui.todoEdit.setText("")
            self.save()

    def save(self):
        with open("data/data.json", "w") as f:
            data = json.dump(self.model.todos, f)

    @Slot()
    def delete(self):
        indexes = self.ui.todoView.selectedIndexes()
        if indexes:
            index = indexes[0]
            del self.model.todos[index.row()]
            self.model.layoutChanged.emit()
            self.ui.todoView.clearSelection()
            self.save()

    @Slot()
    def complete(self):
        indexes = self.ui.todoView.selectedIndexes()
        if indexes:
            index = indexes[0]
            row = index.row()
            status, text = self.model.todos[row]
            self.model.todos[row] = (True, text)
            self.model.dataChanged.emit(index, index)
            self.ui.todoView.clearSelection()
            self.save()


if __name__ == "__main__":
    app = QApplication(sys.argv)
    widget = MainWindow()
    widget.show()
    sys.exit(app.exec())
  • main.qml
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts

import TodoModel

ApplicationWindow {
    id: page
    width: 400
    height: 600
    visible: true
    title: "TODO[QML]"

    ListView{
        id: todoList
        width: parent.width
        height: parent.height
        model: TodoModel
        spacing: 1

        delegate: Item{
            width: parent.width
            height: 50

            Rectangle{
                id: todoRect
                width: parent.width
                height: 50
                color: "lightgray"
                border.color: "blue"
                radius: 10
                property int toggleState: 0

                Row{
                    anchors.verticalCenter: parent.verticalCenter
                    spacing: 10
                    padding: 5

                    Image{
                        source: d_icon ? "data/accepts.png" : ""
                    }

                    Text{
                        text: d_todo
                    }
                }
            }

            MouseArea{
                anchors.fill: parent
                onClicked: {
                    TodoModel.toggleStatus(index, todoRect.toggleState)
                    todoRect.toggleState % 2 === 0 ? todoRect.color="blue" : todoRect.color="lightgray"
                    todoRect.toggleState++
                }
            }
        }
    }

    Rectangle{
        width: parent.width
        height: 100
        anchors.bottom: parent.bottom
        color: "#f2f2f2"

        ColumnLayout{
            anchors.fill: parent
            spacing: 10

            TextField{
                id: todoEdit
                placeholderText: "Enter a new todo"
                Layout.fillWidth: true
                font.pointSize: 10
            }

            RowLayout{
                Layout.fillWidth: true
                spacing: 10

                Button{
                    text:"Add"
                    onClicked: {
                        TodoModel.addTodo(todoEdit.text)
                        todoEdit.text = ""
                    }
                }

                Button{
                    text:"Delete"
                    onClicked: {
                        TodoModel.deleteSelected()
                    }
                }

                Button {
                    text: "Complete"
                    onClicked: {
                        TodoModel.completeSelected()
                    }
                }
            }
        }
    }
}
  • main.py
from __future__ import annotations

import os
import pickle
from dataclasses import dataclass
from pathlib import Path
import sys
from PySide6.QtCore import QAbstractListModel, Qt, QUrl, QByteArray, Slot

from PySide6.QtGui import QGuiApplication, QImage, QIcon
from PySide6.QtQml import QQmlApplicationEngine, QmlElement, QmlSingleton
from PySide6.QtQuick import QQuickView

basedir = os.path.dirname(__file__)
tick = QImage(os.path.join(basedir, "data/accepts.png"))

QML_IMPORT_NAME = "TodoModel"
QML_IMPORT_MAJOR_VERSION = 1


@dataclass
class tasks:
    status: bool
    todo: str


@QmlElement
@QmlSingleton
class TodoModel(QAbstractListModel):
    roleID_icon = Qt.ItemDataRole.UserRole + 1
    roleID_todo = Qt.ItemDataRole.UserRole + 2

    def __init__(self, todos):
        super().__init__()
        self._todos = todos
        self.selected_dict = {}
        self.load()

    def roleNames(self):
        roles = {
            TodoModel.roleID_todo: QByteArray(b"d_todo"),
            TodoModel.roleID_icon: QByteArray(b"d_icon"),
        }
        return roles

    def data(self, index, role):
        d = self._todos[index.row()]

        if role == TodoModel.roleID_todo:
            return d.todo
        if role == TodoModel.roleID_icon:
            if d.status:
                return tick
        return None

    def rowCount(self, index):
        return len(self._todos)

    @staticmethod
    def create(engine):
        data = [tasks(True, "task1"), tasks(False, "task2")]
        # data=[]
        return TodoModel(data)

    @Slot(str)
    def addTodo(self, text):
        if text.strip():
            self._todos.append(tasks(False, text))
            self.layoutChanged.emit()
            self.save()

    @Slot()
    def deleteSelected(self):
        filtered_list = [
            item for i, item in enumerate(self._todos) if i not in self.selected_dict
        ]
        self._todos = filtered_list

        self.layoutChanged.emit()
        self.save()
        self.selected_dict = {}

    @Slot(int, int)
    def toggleStatus(self, index, toggleState):
        if toggleState % 2 == 0:
            self.selected_dict[index] = toggleState
        else:
            del self.selected_dict[index]

    @Slot()
    def completeSelected(self):
        print(self._todos[0].status)
        for i, item in enumerate(self._todos):
            if i in self.selected_dict and item.status == False:
                item.status = True
            elif i in self.selected_dict and item.status == True:
                item.status = False

        self.layoutChanged.emit()
        self.save()

        self.selected_dict = {}

    def load(self):
        try:
            with open("data/data.pkl", "rb") as f:
                self._todos = pickle.load(f)
        except Exception:
            self._todos = TodoModel([])

    def save(self):
        with open("data/data.pkl", "wb") as f:
            print(self._todos)
            pickle.dump(self._todos, f)


if __name__ == "__main__":
    app = QGuiApplication(sys.argv)
    app.setWindowIcon(QIcon(os.path.join(basedir, "data/icons8-add-list-48.png")))

    engine = QQmlApplicationEngine()
    qml_file = Path(__file__).resolve().parent / "main.qml"
    engine.load(qml_file)
    if not engine.rootObjects():
        sys.exit(-1)
    sys.exit(app.exec())
🤖 代码解读

该示例使用了 Qt 的模型视图架构,通过QAbstractListModel自定义数据模型,利用 Qt Designer 和 QMainWindow创建应用界面。所有的待办事项数据都存储在本地的data/data.json文件中,并使用 JSON 格式进行读写。界面交互通过 Qt 控件(如按钮、视图、文本框)与数据模型进行绑定,用户的操作会直接影响数据并保存。

  • 导入模块
  1. import json:用于处理 JSON 数据格式的读写。
  2. from PySide6.QtCore import QAbstractListModel:用于创建模型,表示待办事项的数据。
  3. from PySide6.QtGui import QImage, QIcon:用于加载和设置图片(QImage)和图标(QIcon)。
  • 初始化文件路径
basedir = os.path.dirname(__file__)
tick = QImage(os.path.join(basedir, "data/accepts.png"))
  1. basedir获取当前脚本的文件夹路径。tick加载一个表示已完成任务的图标(accepts.png),用于标示完成的任务。
  • TodoModel模型类

TodoModel类通过继承QAbstractListModel来实现一个待办事项列表的数据模型,提供了两种数据角色:DisplayRole显示任务的文本;DecorationRole显示任务的状态图标(如果任务完成,同时显示一个tick图标)。rowCount方法返回列表中任务的总数。该模型将任务表示为一个元组(status, text),其中status表示任务是否完成(bool),text是任务的描述文本。

class TodoModel(QAbstractListModel):
  def __init__(self, todos=None):
      super().__init__()
      self.todos = todos or []

  def data(self, index, role):
      if role == Qt.DisplayRole:
          status, text = self.todos[index.row()]
          return text
      if role == Qt.DecorationRole:
          status, text = self.todos[index.row()]
          if status:
              return tick

  def rowCount(self, index):
      return len(self.todos)
  1. TodoModel继承自QAbstractListModel,是 Qt 的一个基础模型类,专门用于表示和管理一个列表的数据。通过继承该类,可以定制如何展示数据并与视图控件(如QListVie)连接。
  2. 构造函数接受一个todos参数,默认值为None
  3. self.todos = todos or []:如果todos参数为None,则将self.todos设置为空列表[]。如果提供了todos参数,则使用提供的列表。
  4. data方法是QAbstractListModel中用于返回数据的关键方法,,其收集两个参数,index表示数据项的位置,通常是一个QModelIndex对象;role表示请求数据的角色,通常是Qt.DisplayRole(用于显示文本)或Qt.DecorationRole(用于显示图标或装饰)。
  5. roleQt.DisplayRole时(即请求显示数据时),data方法从self.todos列表中获取指定行的数据(status, text),并返回任务的文本text,即待办事项的名称。
  6. roleQt.DecorationRole时(即请求显示图标或装饰时),data方法检查任务的状态(status),如果任务已完成(statusTrue),则返回一个tick图标。
  7. rowCount方法返回列表中的数据项的数量,通过self.todos列表的长度来返回待办事项的数量。
  • MainWindow
  1. 构造函数中设置了窗口标题为"TODO"。用self.setWindowIcon(QIcon(os.path.join(basedir, "data/icons8-add-list-48.png")))设置窗口图标。
  2. self.model = TodoModel()创建TodoModel实例作为模型,并用self.load()加载数据。同时用self.ui.todoView.setModel(self.model)将模型设置到todoView 控件中(显示任务列表)。连接按钮的点击事件到相应的草方法上(adddeletecomplete)。
  • 槽方法

self.model.layoutChanged.emit()self.model.dataChanged.emit(index, index)是用来通知视图更新的信号,在 Qt 模型-视图架构中有不同的用途。self.model.layoutChanged.emit()通知视图模型的布局发生了变化,通常用于模型结构发生变化时,如添加(append())、删除(remove())或重新排列了数据项,导致整个模型的布局发生变化;self.model.dataChanged.emit(index, index)通知视图数据已发生变化,通常用于某一项数据的修改(如修改了某一项任务的状态或文本)。dataChanged 信号允许指定变化的范围。

  1. add:获取文本框中的内容,去除多余的空格并添加到todos列表中。如果文本非空,更新模型并保存数据。
  2. delete:获取选中的任务,并从模型中删除该任务,更新视图并保存数据。
  3. complete:获取选中的任务,并将其状态标记为已完成(即将任务的状态设置为True),更新视图并保存数据。

前端(QML):使用 QML 定义应用的用户界面,包括展示待办事项的列表、列表项的交互效果;及界面底部区域提供输入框和按钮,通过按钮调用模型提供的槽函数,实现添加、删除、和完成任务的操作。

后端(PySide6):定义数据结构tasks和自定义模型TodoModel,继承自QAbstractListModel,用于存储和管理任务数据。通过使用@QmlElement@QmlSingleton装饰器将TodoModel模型暴露给 QML;通过roleNamesrowCountdata方法将任务数据以特定格式暴露给 QML。提供槽函数(addTododeleteSelectedtoggleStatuscompleteSelected)供 QML 使用,处理任务的添加、删除、状态切换和持久化保存。利用pickle实现数据的加载和存储。

这种设计将数据逻辑和界面展示分离,通过 QML 的数据绑定和 PySide6 的模型-视图架构,实现了一个功能简洁而功能齐全的待办事项应用。

  • main.qml
  1. ListView:用于显示待办事项列表,数据模型model: TodoModel直接指定为从 PySide6 暴露的 TodoModel单例。
  2. delegate:定义每个列表项以一个Item作为根容器,其中包含Rectangle,作为背景、初始颜色为浅灰色,边框为蓝色,并设置圆角。内部包含一个水平排列的Row,用来显示图标和文本;Image通过source: d_icon ? "data/accepts.png" : ""绑定d_icon角色(由后端模型提供),若任务状态为完成则显示图标;Text绑定d_todo角色,用来显示任务的文字说明。
  3. MouseArea:覆盖整个列表项,处理点击事件。当点击时,会调用后端TodoModel.toggleStatus(index, toggleState)槽函数,同时本地更新背景颜色和记录点击状态(toggleState),以模拟选中状态的切换。
  4. 底部操作区域Rectangle用于放置任务输入框和操作按钮。按钮Add,点击时调用TodoModel.addTodo(todoEdit.text)添加任务,并清空输入框;Delete,点击时调用TodoModel.deleteSelected()删除已选择的任务;Complete,点击时调用TodoModel.completeSelected()完成(或切换完成状态)所选任务。
  • main.py
  1. 使用@dataclass定义任务数据类型,包含任务状态(status)和任务描述(todo)。
  2. TodoMode类继承自QAbstractListModel,并通过装饰器将其暴露为 QML 可用的单例。定义了两个自定义角色(role),roleID_todo对应任务文本,映射到 QML 中的属性d_todoroleID_icon用于任务状态图标,映射到 QML 中的属性d_icon
  3. roleNames返回角色与 QML 中属性名的映射,使 QML 中可以通过d_todod_icon访问数据。
  4. rowCount返回任务列表的长度。
  5. data根据角色返回相应的数据。当请求任务文本时返回d.todo;当请求图标时,如果任务状态为 True,则返回预先加载的图标(tick)。
  6. 静态工厂方法create创建并初始化TodoModel实例,同时填充初始数据供 QML 显示。
  7. 操作槽函数有添加任务addTodo,新任务(状态为未完成)被添加到列表,模型发出layoutChanged信号更新视图,并将数据保存;删除选中任务deleteSelected,根据 QML 中记录的选中项(存储在selected_dict中),删除相应任务后更新模型和保存数据;切换任务选中状态toggleStatus,当 QML 中点击某个列表项时,调用toggleStatus来记录或取消记录该项为选中状态,利用toggleState实现状态切换;完成任务completeSelected对选中的任务进行状态切换(完成或未完成),更新模型后保存数据并重置选中记录。
  8. 利用 pickle 对任务数据加载load()和保存save(),使得任务数据在程序重启后依然可用。
  9. 应用程序入口初始化QGuiApplicationQQmlApplicationEngine,加载 QML主界面文件(main.qml),启动事件循环。

11.7.2 [Q]TabelView

QTableView(PySide6/Python)和TableView(QtQuick/QML)是 Qt 框架中两种不同的表格视图组件,分别适用于桌面应用程序(基于 QtWidgets)和 QML/QtQuick 应用程序(基于声明式语言构建 UI )。两者均用来显示和操作表格数据,但在使用方式、设计模式和实现上存在差异。

QTableView是 Qt 的一部分,用于在桌面应用中显示表格数据,依赖于 Qt 的 Model-View(模型视图) 架构,其中QTableView是 View, 用于显示数据,而 Model (如QAbstractTableModel)则负责提供数据。QTableView主要用于传统的桌面应用程序,采用基于QWidget的 UI。

TableView是 QtQuick 模块中的一个组件,专门用于 QML 应用程序,其同样遵循 Model-View架构,表格数据通过模型提供,而 QML 中的TableView控件用于显示数据。QML 是声明式语言,因此可以通过 QML 定义 UI 元素。

方式选择 Qt Designer + PySide6 QtQuick/QML + PySide6
界面

PYC icon

PYC icon

                                                            
[Q]TableView

QTableView[PySide6]

QTableView是基于 QtWidgets 的表格组件,适用于桌面应用程序,采用 Model-View-Controller(MVC)架构,通过QAbstractTableModel提供数据管理,并支持自定义渲染、排序、编辑等功能。在QTableView中,数据管理和显示是分开的,

  1. Model(数据模型):使用QAbstractTableModelQStandardItemModel提供数据。
  2. View(视图):QTableView负责显示数据。
  3. Delegate(代理)(可选):使用QStyledItemDelegate自定义渲染单元格,如复选框、按钮等。

TableView[QtQuick/QML]

TableView是用于展示二维表格数据的组件,适用于现代 UI 和跨平台应用(如桌面和移动端),与 QML 的动态绑定和动画特性结合,提供了灵活和直观的用户界面。

  1. Model(数据模型):ListModel(QML)适合简单数据;TableModel(QML)支持多列数据;QAbstractTableModel(通过 Python/C++ 实现),适合复杂数据。
  2. Delegate(代理):决定每个单元格的显示方式,通常是RectangleText等。
代码
  • mainwindow.py
# This Python file uses the following encoding: utf-8
import sys
import random
import pandas as pd
import numpy as np

from PySide6.QtWidgets import QApplication, QMainWindow, QVBoxLayout, QWidget
from PySide6.QtCore import Qt, QAbstractTableModel
from PySide6.QtWidgets import QTableView

from ui_form import Ui_MainWindow


class TableModel(QAbstractTableModel):
    def __init__(self, data):
        super().__init__()
        self._data = data

    def data(self, index, role):
        if role == Qt.DisplayRole:
            return self._data[index.row()][index.column()]

    def rowCount(self, index):
        return len(self._data)

    def columnCount(self, index):
        return len(self._data[0])


class TableMode_df(QAbstractTableModel):
    def __init__(self, data):
        super().__init__()
        self._data = data

    def data(self, index, role):
        if role == Qt.DisplayRole:
            value = self._data.iloc[index.row(), index.column()]
            return str(value)

    def rowCount(self, index):
        return self._data.shape[0]

    def columnCount(self, index):
        return self._data.shape[1]

    def headerData(self, section, orientation, role):
        if role == Qt.DisplayRole:
            if orientation == Qt.Horizontal:
                return str(self._data.columns[section])
            if orientation == Qt.Vertical:
                return str(self._data.index[section])


class MainWindow(QMainWindow):
    def __init__(self, parent=None):
        super().__init__(parent)
        self.ui = Ui_MainWindow()
        self.ui.setupUi(self)
        self.resize(550, 500)
        self.setWindowTitle("TableView")

        matrix = [[random.randint(0, 100) for _ in range(5)] for _ in range(5)]
        self.model = TableModel(matrix)
        self.table = QTableView()
        self.table.setModel(self.model)

        self.data_df = pd.DataFrame(
            np.random.randint(0, 100, size=(5, 5)),
            columns=[f"Col_{i}" for i in range(1, 6)],
            index=[f"Row_{i}" for i in range(1, 6)],
        )
        self.model_df = TableMode_df(self.data_df)
        self.table_df = QTableView()
        self.table_df.setModel(self.model_df)

        self.layout = QVBoxLayout()
        self.layout.addWidget(self.table)
        self.layout.addWidget(self.table_df)

        central_widget = QWidget(self)
        self.setCentralWidget(central_widget)
        central_widget.setLayout(self.layout)


if __name__ == "__main__":
    app = QApplication(sys.argv)
    widget = MainWindow()
    widget.show()
    sys.exit(app.exec())
  • main.qml
import QtQuick
import QtQuick.Window
import QtQuick.Controls
import Qt.labs.qmlmodels
import QtQuick.Layouts

ApplicationWindow {
    width: 480
    height: 400
    visible: true
    title: qsTr("TableView[QML]")

    TableModel {
        id: tableModelInside
        TableModelColumn {
            display: "name"
        }
        TableModelColumn {
            display: "color"
        }

        rows: [{
                "name": "cat",
                "color": "black"
            }, {
                "name": "dog",
                "color": "brown"
            }, {
                "name": "bird",
                "color": "white"
            }]
    }

    ColumnLayout {
        anchors.fill: parent
        spacing: 2

        Rectangle {
            Layout.fillWidth: true
            Layout.preferredHeight: parent.height / 2

            HorizontalHeaderView {
                id: horizontalHeader
                anchors.left: tableView.left
                anchors.top: parent.top
                syncView: tableView
                clip: true
            }

            VerticalHeaderView {
                id: verticalHeader
                anchors.top: tableView.top
                anchors.left: parent.left
                syncView: tableView
                clip: true

                model: ["Idx 1", "Idx 2", "Idx 3", "Idx 4"]
            }

            TableView {
                id: tableView
                anchors.top: horizontalHeader.bottom
                anchors.left: verticalHeader.right
                anchors.right: parent.right
                anchors.bottom: parent.bottom

                Layout.fillWidth: true
                Layout.preferredHeight: parent.height / 2
                columnSpacing: 1
                rowSpacing: 1
                clip: true
                model: tableModel

                columnWidthProvider: function (column) {
                    return [50, 150, 80, 120][column]
                } // Column width

                delegate: Rectangle {
                    implicitWidth: 100
                    implicitHeight: 40
                    border.color: "black"

                    Text {
                        anchors.centerIn: parent
                        text: model.display
                    }
                }
            }
        }

        Rectangle {
            Layout.fillWidth: true
            Layout.preferredHeight: parent.height / 2

            HorizontalHeaderView {
                id: horizontalHeaderInside
                anchors.left: tableViewInside.left
                anchors.top: parent.top
                syncView: tableViewInside
                clip: true
            }

            VerticalHeaderView {
                id: verticalHeaderInside
                anchors.top: tableViewInside.top
                anchors.left: parent.left
                syncView: tableViewInside
                clip: true
            }

            TableView {
                id: tableViewInside
                anchors.top: horizontalHeaderInside.bottom
                anchors.left: verticalHeaderInside.right
                anchors.right: parent.right
                anchors.bottom: parent.bottom

                Layout.fillWidth: true
                Layout.preferredHeight: parent.height / 2
                columnSpacing: 1
                rowSpacing: 1
                model: tableModelInside

                delegate: Rectangle {
                    implicitWidth: 100
                    implicitHeight: 40
                    border.color: "black"

                    Text {
                        anchors.centerIn: parent
                        text: model.display
                    }
                }
            }
        }
    }
}
  • main.py
# This Python file uses the following encoding: utf-8
import sys
from pathlib import Path

from PySide6.QtGui import QGuiApplication
from PySide6.QtQml import QQmlApplicationEngine
from PySide6.QtCore import Qt, QAbstractTableModel, QModelIndex


class TableModel(QAbstractTableModel):
    def __init__(self, headers, data):
        super().__init__()

        self.headers = headers
        self.data_list = data

    def rowCount(self, parent=QModelIndex()):
        return len(self.data_list)

    def columnCount(self, parent=QModelIndex()):
        return len(self.headers)

    def data(self, index, role=Qt.DisplayRole):
        if not index.isValid():
            return None

        if role == Qt.DisplayRole:
            return self.data_list[index.row()][index.column()]

        if role == Qt.TextAlignmentRole:
            return Qt.AlignCenter

        if role == Qt.BackgroundRole and index.column() == 0:
            return QColor("#f0f0f0")

        return None

    def headerData(self, section, orientation, role=Qt.DisplayRole):
        if role == Qt.DisplayRole and orientation == Qt.Horizontal:
            return self.headers[section]
        return None


if __name__ == "__main__":
    app = QGuiApplication(sys.argv)
    engine = QQmlApplicationEngine()

    headers = ["ID", "Name", "Age", "Country"]
    data_list = [
        [1, "Alice", 25, "USA"],
        [2, "Bob", 30, "UK"],
        [3, "Charlie", 22, "Canada"],
        [4, "David", 28, "Germany"],
    ]

    model = TableModel(headers, data_list)
    engine.rootContext().setContextProperty("tableModel", model)

    qml_file = Path(__file__).resolve().parent / "main.qml"
    engine.load(qml_file)
    if not engine.rootObjects():
        sys.exit(-1)
    sys.exit(app.exec())
🤖 代码解读

通过QAbstractTableModel的两个子类分别封装了两种不同数据源(二维嵌套列表与 Pandas 库的 DataFrame 格式数据)的表格数据,并用QTableView展示在窗口中。主窗口中使用垂直布局,将两个表格依次显示。

  • 数据模型类
  1. TableModel数据模型:将二维列表数据转换为 Qt 表格模型。data()方法根据给定的行列索引返回对应单元格的内容(DisplayRole下返回数据);rowCount()columnCount()分别返回数据行数和列数,数据来自传入的二维嵌套列表。
  2. TableMode_df数据模型:将 Pandas 的 DataFrame 数据格式转换为 Qt 表格墨西哥。data()方法利用 DataFrame 的iloc方法获取指定单元格的值,并转换为字符串显示;rowCount()columnCount()基于 DataFrame 的形状返回行数和列数;headerData()为横向(列)和纵向(行)提供表头数据(分别返回 DataFrame 的列名和索引值),方便在表格头部显示信息。
  3. 数据准备和模型创建。列表数据为生成一个 5×5 的二维列表(随机整数)作为数据源,构造TableModel对象,并用QTableView显示该数据;DataFrame 数据是利用 Pandas 和 Numpy 库随机生成一个 5×5 的 DataFrame,同时设置列名(Col_1 至 Col_5)和行索引(Row_1 至 Row_5),构造TableMode_df对象,再用另一个QTableView展示该数据。

该部分代码展示了如何在 QML 中使用TableView来显示数据,并通过 PySide6 提供的QAbstractTableModel和 QtQuick/QML 提供的TableModel构建数据模型作为数据源。整体设计包括表头、数据模型、布局、并支持水平和垂直表头同步滚动。

代码的核心点如表,

QML 代码 PySide6 代码
数据模型 TableModelInsideTableModel)(QML 端) TableModelQAbstractTableModel)(Python 端)
表格组件 TableView(QML) QML 绑定 QAbstractTableModelTableModel
表头组件 HorizontalHeaderViewVerticalHeaderView headerData() 方法提供列标题
数据绑定 model: tableModelmodel: tableModelInside engine.rootContext().setContextProperty("tableModel", model)

main.qml

这段 QML 代码展示了如何使用TableView显示数据,并通过HorizontalHeaderViewVerticalHeaderView实现表头的同步滚动。通过ColumnLayout管理布局,可以在同一窗口中显示多个TableView,每个TableView拥有独立的表头和数据视图。

  1. ApplicationWindow:创建应用窗口,设置窗口大小和标题。
  2. TableModel数据模型:定义了一个名为tableModelInside的模型,包含两列namecolor。数据模型是一个包含动物名称及颜色的数组。TableModelColumn定义了列的展示字段。rows数组包含了每行数据,如name: "cat", color: "black"
  3. ColumnLayout布局容器:包含两个嵌套的表格视图,每个表格占据一半的窗口空间,并且表头和数据视图之间通过Rectangle控制。每个TableView都有独立的水平表头和垂直表头。
  4. TableView:用于显示数据,每个TableView配置了model,绑定到一个数据模型,其一为tableModel,在 Python 代码中用QAbstractTableModel构建;另一个为tableModelInside,在 QML 代码中用TableModel构建。columnSpacingrowSpacing控制行和列之间的间距。columnWidthProvider定义每列的宽度。delegate定义表格单元格如何渲染,如使用Rectangle包裹每个单元格的显示内容(文本)。
  5. HorizontalHeaderViewVerticalHeaderView:分别用于显示水平和垂直表头,并且通过syncView属性与TableView进行同步,确保表格的滚动和表头一致。

main.py

该部分代码展示了通过 QtQuick/QML 和 PySide6/Python 结合来使用TableView组件显示表格数据的方法。在 PySide6/Python 中通过继承QAbstractTableModel构建数据模型TableModel,然后通过 QtQuick/QML 的TableView来显示数据。

  1. TableModel类数据模型:其构造函数__init__初始化表头(headers)和数据(data_list);rowCount返回表格的行数;columnCount返回表格的列数;data返回单元格的数据。通过角色(role)(如Qt.DisplayRole)来返回实际的内容。同时支持文本对齐和背景颜色的设置;headerData返回表头的数据,依据section参数确定当前列的表头。
  2. QtQuick/QML 和 PySide6/Python的集成:是通过engine.rootContext().setContextProperty("tableModel", model)TableModel的 Python 对象暴露给 QML,供 QML 中的TableView使用。

11.8 SQL 数据库

PySide6.QtSql模块提供了对 SQL 数据库的访问接口,允许在 PySide6 应用程序中管理和操作数据库。该模块基于 Qt 的 QtSQL 框架,提供了一套面向对象的数据库连接、查询执行及结果管理 API,支持 SQLite、MySQL、PostgreSQL 和 ODBC 等数据库。常用组件中QSqlDatabase类用于管理数据库连接,支持不同数据库驱动,如QSQLITE(SQLite)、QMYSQL(MySQL)、QPSQL(PostgreSQL)和QODBC)(ODBC (includes Microsoft SQL Server)等。并允许创建多个数据库连接。QSqlQuery类用于执行 SQL 语句(如SELECTINSERTUPDATEDELETE等)。支持参数绑定,提供查询效率并防止 SQL 注入。QSqlTableModel 允许将数据库数据直接绑定到 pySide6 的 UI 组件(如QTableView),方便在 GUI 中显示和编辑数据。

示例应用程序图11-13使用 PySide6 结合 SQLite 作为数据库,构建了一个图形化界面(GUI)来管理数据库。示例中所用数据库为chinook-database,是一个可用于 SQLserver、Oracle、MySQL等的示例数据库,适合于演示和测试针对单个和多个数据库服务的 ORM(Object-Relational Mapping) 工具。示例应用程序提供了数据查询、删选、编辑和 CRUD(create、read、update 和 delete) 操作,且通过QSqlRelationalTableModel关联多个表,实现外键映射。

PYC icon

图 11-13 用 Qt 模型查询 SQLite 数据库

为了方便和加快用户界面的设计开发,直接使用 Qt Designer 进行控件布局,如图11-14。从Object Inspector(对象观察器)可以查看所用到的控件及布局的层级关系。

PYC icon

图 11-14 项目 Qt Designer(Qt Creator) 用户界面布局

mainwindow.py

# This Python file uses the following encoding: utf-8
import sys, os
import re

from PySide6.QtWidgets import (
    QApplication,
    QMainWindow,
    QTableView,
    QHeaderView,
    QDataWidgetMapper,
)
from PySide6.QtSql import (
    QSqlDatabase,
    QSqlTableModel,
    QSqlRelation,
    QSqlRelationalTableModel,
    QSqlRelationalDelegate,
    QSqlQuery,
)
from PySide6.QtCore import Qt, Slot

from ui_form import Ui_MainWindow

basedir = os.path.dirname(__file__)


class MainWindow(QMainWindow):
    def __init__(self, parent=None):
        super().__init__(parent)
        self.ui = Ui_MainWindow()
        self.ui.setupUi(self)
        self.setWindowTitle("SQLite")

        db_path = os.path.join(basedir, "data/Chinook_Sqlite.sqlite")
        self.db = self.init_db(db_path)

        if self.db.isOpen():
            self.setup_model()
            self.setup_search()

    def init_db(self, db_path):
        db = QSqlDatabase.addDatabase("QSQLITE")
        db.setDatabaseName(db_path)
        if not db.open():
            error_message = db.lastError().text()
            raise RuntimeError(f"Database Error: Unable to open {db_path}. {error_message}")
            # print(f"Error: Unable to open database at {db_path}.")
            return None
        return db

    def setup_model(self):
        self.model_top = QSqlRelationalTableModel(self, self.db)

        self.model_top.setTable("Track")
        self.model_top.setEditStrategy(QSqlTableModel.OnFieldChange)

        self.remove_columns()
        self.set_sort()
        self.relational_table_add_columns()
        self.edit_related_fields()
        self.update_column_names()
        self.model_top.select()

        self.ui.tableView_top.setModel(self.model_top)
        self.ui.tableView_top.setColumnHidden(0, True)
        self.ui.tableView_top.setSortingEnabled(True)

        self.model_bottom = QSqlTableModel()
        self.ui.tableView_bottom.setModel(self.model_bottom)
        self.query = QSqlQuery(db=self.db)
        self.query.prepare(
            "SELECT Name, Composer, Album.Title FROM Track "
            "INNER JOIN Album ON Track.AlbumId=Album.AlbumId WHERE "
            "LOWER(Track.Name) LIKE '%' || LOWER(:track_name) || '%' AND "
            "LOWER(Track.Composer) LIKE '%' || LOWER(:track_composer) || '%' AND "
            "LOWER(Album.Title) LIKE '%' || LOWER(:album_title) || '%'"
        )
        self.ui.lineEdit_track.textChanged.connect(self.update_query)
        self.ui.lineEdit_composer.textChanged.connect(self.update_query)
        self.ui.lineEdit_album.textChanged.connect(self.update_query)
        self.update_query()

        self.tableView_stretch()

        self.model_mapper = QSqlTableModel()
        self.CRUD()

    def CRUD(self):
        self.mapper = QDataWidgetMapper()
        self.mapper.setModel(self.model_mapper)

        self.mapper.addMapping(self.ui.spinBox_trackid, 0)
        self.mapper.addMapping(self.ui.lineEdit_trackname, 1)
        self.mapper.addMapping(self.ui.lineEdit_composer_mapper, 5)
        self.mapper.addMapping(self.ui.spinBox_milliseconds, 6)
        self.mapper.addMapping(self.ui.spinBox_bytes, 7)
        self.mapper.addMapping(self.ui.doubleSpinBox_unitprice, 8)

        self.model_mapper.setTable("Track")
        self.model_mapper.select()
        self.mapper.toFirst()

        self.ui.pushButton_previous.clicked.connect(self.mapper.toPrevious)
        self.ui.pushButton_next.clicked.connect(self.mapper.toNext)
        self.ui.pushButton_Savechanges.clicked.connect(self.mapper.submit)

    def tableView_stretch(self):
        header_top = self.ui.tableView_top.horizontalHeader()
        header_top.setSectionResizeMode(QHeaderView.Stretch)
        header_bottom = self.ui.tableView_bottom.horizontalHeader()
        header_bottom.setSectionResizeMode(QHeaderView.Stretch)

    @Slot()
    def update_query(self):
        track_name = self.ui.lineEdit_track.text()
        track_composer = self.ui.lineEdit_composer.text()
        album_title = self.ui.lineEdit_album.text()

        self.query.bindValue(":track_name", track_name)
        self.query.bindValue(":track_composer", track_composer)
        self.query.bindValue(":album_title", album_title)

        self.query.exec()
        self.model_bottom.setQuery(self.query)

    def edit_related_fields(self):
        delegate = QSqlRelationalDelegate(self.ui.tableView_top)
        self.ui.tableView_top.setItemDelegate(delegate)

    def relational_table_add_columns(self):
        # relation=QSqlRelation('<related_table>', '<related_table_foreign_key_column>', '<column_to_display>')
        self.model_top.setRelation(2, QSqlRelation("Album", "AlbumId", "Title"))
        self.model_top.setRelation(3, QSqlRelation("MediaType", "MediaTypeId", "Name"))
        self.model_top.setRelation(4, QSqlRelation("Genre", "GenreId", "Name"))

    def update_column_names(self):
        column_titles_track = {
            "Name": "Track Name",
            # "AlbumId": "Album Title",
            "MediaTypeId": "Media Type Name",
            "GenreId": "Genre Name",
            "Composer": "Composer",
            "UnitPrice": "Unit Price",
        }

        for n, t in column_titles_track.items():
            idx = self.model_top.fieldIndex(n)
            self.model_top.setHeaderData(idx, Qt.Horizontal, t)

    def remove_columns(self):
        columns_to_remove_track = ["UnitPrice"]
        for cn in columns_to_remove_track:
            idx = self.model_top.fieldIndex(cn)
            self.model_top.removeColumns(idx, 1)

    def set_sort(self):
        idx = self.model_top.fieldIndex("Milliseconds")
        self.model_top.setSort(idx, Qt.DescendingOrder)

    @Slot()
    def setup_search(self):
        self.ui.lineEdit_search.textChanged.connect(self.filter_table)

    def filter_table(self):
        search_text = self.ui.lineEdit_search.text().strip()
        cleaned_text = re.sub(r"[^\w\s]", "", search_text)
        words = cleaned_text.split()

        if words:
            search_pattern = " AND ".join(
                [f"LOWER(Title) LIKE LOWER('%{word}%')" for word in words]
            )
            self.model_top.setFilter(search_pattern)
        else:
            self.model_top.setFilter("")
        self.model_top.select()


if __name__ == "__main__":
    app = QApplication(sys.argv)
    widget = MainWindow()
    widget.show()
    sys.exit(app.exec())

🤖 代码解读

  1. 数据库初始化(init_db()):连接 SQLite 数据库(Chinook_Sqlite.sqlite)。使用QSqlDatabase设置数据库驱动(QSQLITE)并打开数据库,返回QSqlDatabase实例。db.setDatabaseName(db_path)绑定数据库文件。db.open()尝试打开数据库,返回True表示打开成功,否则为Falsedb.lastError().text()可以获取具体的数据库错误信息。通过引发RuntimeError异常,查看错误信息。
  2. 数据模型(setup_model()):这里构建了三个数据模型,model_topmodel_bottommodel_mapper,第一个使用了QSqlRelationalTableModel方法,后两个使用了QSqlTableModel方法,分别用于操作数据库不同功能的演示。
  3. QSqlRelationalTableModel允许使用外键关联其它表的数据。这里setTable("Track")指定Track表作为数据源。setEditStrategy(QSqlTableModel.OnFieldChange)用于设置编辑策略,OnFieldChange表示字段发生变更时,数据立即更新数据库。
  4. 对于model_top数据模型,演示了删除不需要的列remove_columns();设置排序set_sort()(默认为Milliseconds降序排序);添加外键关联relational_table_add_columns(),让Track表可以显示AlbumMediaTypeGenre等表中的数据;edit_related_fields()配置 UI 代理,让QTableView处理外键字段的编辑;更新列名update_column_names(),使表头显示更友好的名称,如GenreId → Genre Name等;搜索功能setup_search()filter_table(),输入框监听(lineEdit_search.textChanged),根据输入内容动态筛选Track数据。self.model_top.select()用于刷新数据,加载选择的Track表内容。
  5. 表格 UI 绑定:model_topmodel_bottom数据模型通过setModel方法分别将其绑定到各自的视图QTableView上,显示数据库中表(Track)的数据。主表tableView_top绑定到model_to;搜索结果表tableView_bottom绑定到model_bottom,用于展示搜索结果。self.ui.tableView_top.setColumnHidden(0, True)隐藏了第一列。self.ui.tableView_top.setSortingEnabled(True)开启排序,即点击表头可排序。 6.复杂查询: model_bottom模型演示了自定义 SQL 查询,因为不涉及外键关联,使用了QSqlTableModel方法。用QSqlQuery进行高级 SQL 查询,查询TrackAlbum关联表,获取Track.NameTrack.ComposerAlbum.Title。并使用LOWER()进行不区分大小写的模糊搜索。绑定参数:track_name:track_composer:album_title以支持动态查询。用搜索框(文本框)绑定事件,当文本框内容发生改变时,自动调用update_query()执行搜索。
  6. self.tableView_stretch()是让tableView_toptableView_bottom自适应宽度。
  7. 数据编辑(CRUD):对于数据模型model_mapper,演示CRUD()处理单条数据映射编辑。通过QDataWidgetMapper绑定Track表字段到 UI 控件,包括TrackId → spinBox_trackidTrack Name → lineEdit_tracknameComposer → lineEdit_composer_mapperMilliseconds → spinBox_millisecondsBytes → spinBox_bytesUnit Price → doubleSpinBox_unitprice。并提供数据浏览与保存功能。pushButton_previous为上一条、pushButton_next为下一条和pushButton_Savechanges为提交修改。

11.9 图表

Python 编程语言的一大优势是用于数据科学领域,其具有丰富的数据分析统计工具,如数据结构库 NumPy、Pandas等,科学计算库 Scipy、SymPy 等,统计推断库 Statsmodels等,机器学习库 Scikit-learn 等,深度学习库 PyTorch 等,在数据可视化上有 MatplotlibPlotly、seaborn、bokeh、VTK、gradio 等。使用 PySide6 构建 GUI 应用程序时,可以从应用程序中访问所有这些 Python 库,从而实现复杂数据驱动应用程序和交互式仪表盘的构建。

图11-15展示的示例分别使用PyQtGraphMatplotlib打印一天中24小时的温度变化曲线(数据为模拟生成)。PyQtGraph 是基于 PyQt/PySide 和 NumPy 构建的纯 Python 图形和 GUI 库,旨在用于数学/科学/工程应用。尽管 PyQtGraph 完全由 Python 编写,但由于大量利用 Numpy 进行数值运算和 Qt 的 GraphicsView 框架进行快速显示,因此具有较好的计算效率, 为 Qt 应用程序的高性能绘图库,并支持实时数据绘制。Matplotlib 是一个用于在 Python 中创建静态、动画和交互式可视化的综合库。

PYC icon

图 11-15 分别用 PyQtGraph 和 Matplotlib 库打印一天温度变化曲线

# This Python file uses the following encoding: utf-8
import sys
import pandas as pd
import numpy as np

from PySide6.QtWidgets import (
    QApplication,
    QMainWindow,
    QHBoxLayout,
    QVBoxLayout,
    QWidget,
)
from PySide6 import QtWidgets
import pyqtgraph as pg
import matplotlib
from matplotlib.backends.backend_qtagg import FigureCanvasQTAgg, NavigationToolbar2QT
from matplotlib.figure import Figure
import matplotlib.pyplot as plt

from ui_form import Ui_MainWindow


class MplCanvas(FigureCanvasQTAgg):
    def __init__(self, parent=None, width=5, height=4, dpi=100):
        fig = Figure(figsize=(width, height), dpi=dpi)
        self.axes = fig.add_subplot(111)
        super().__init__(fig)


class MainWindow(QMainWindow):
    def __init__(self, parent=None):
        super().__init__(parent)
        self.ui = Ui_MainWindow()
        self.ui.setupUi(self)
        self.resize(680 * 2, 680)
        self.setWindowTitle("Plotting")

        data_site_temps = self.generate_sites_temps()
        self.graphWidget = pg.PlotWidget()
        self.layout = QHBoxLayout()
        self.layout.addWidget(self.graphWidget)
        central_widget = QWidget(self)
        self.setCentralWidget(central_widget)
        central_widget.setLayout(self.layout)

        self.chart_with_pyqtgraph(data_site_temps)
        self.chart_with_matplotlib(data_site_temps)

    def generate_sites_temps(self):
        # Generate 24-hour timestamps
        hours = np.arange(0, 24)

        # Generate reasonable temperature variations for two sites
        np.random.seed(42)  # For reproducibility
        site1_temps = (
            15 + 5 * np.sin((hours - 6) * np.pi / 12) + np.random.normal(0, 0.5, 24)
        )
        site2_temps = (
            18 + 4.5 * np.sin((hours - 5) * np.pi / 12) + np.random.normal(0, 0.5, 24)
        )

        # Create DataFrame
        data = pd.DataFrame(
            {
                "Hour": hours,
                "Site1_Temperature": site1_temps,
                "Site2_Temperature": site2_temps,
            }
        )

        return data

    def chart_with_pyqtgraph(self, data):
        self.graphWidget.setBackground("w")
        self.graphWidget.setTitle("Plotting with PyQtgraph", color="b", size="24pt")
        styles = {"color": "#f00", "font-size": "20px"}
        self.graphWidget.setLabel("left", "Temperature (°C)", **styles)
        self.graphWidget.setLabel("bottom", "Hour (H)", **styles)
        self.graphWidget.addLegend()
        self.graphWidget.showGrid(x=True, y=True)

        self.graphWidget.setXRange(0 - 1, 24 + 1, padding=0)
        min_val = data[["Site1_Temperature", "Site2_Temperature"]].min().min()
        max_val = data[["Site1_Temperature", "Site2_Temperature"]].max().max()
        self.graphWidget.setYRange(min_val - 1, max_val + 1, padding=0)

        self.plot(data["Hour"], data["Site1_Temperature"], "Sensor1", "r")
        self.plot(data["Hour"], data["Site2_Temperature"], "Sensor2", "b")

    def plot(self, x, y, plotname, color):
        pen = pg.mkPen(color=color)
        self.graphWidget.plot(
            x,
            y,
            name=plotname,
            pen=pen,
            symbol="+",
            symbolSize=30,
            symbolBrush=(color),
        )

    def chart_with_matplotlib(self, data):
        sc = MplCanvas(self, width=5, height=5, dpi=100)
        toolbar = NavigationToolbar2QT(sc, self)
        sc.axes.plot(data["Hour"], data["Site1_Temperature"], label="Site 1")
        sc.axes.plot(data["Hour"], data["Site2_Temperature"], label="Site 2")
        sc.axes.set_title(
            "Plotting with Matplotlib", fontsize=20
        )  # "Temperature Variations Over 24 Hours"
        sc.axes.grid(True)
        sc.axes.legend()
        sc.axes.set_xlabel("Hour of the Day")
        sc.axes.set_ylabel("Temperature (°C)")

        vLayout = QVBoxLayout()
        vLayout.addWidget(toolbar)
        vLayout.addWidget(sc)
        self.layout.addLayout(vLayout)
        self.show()


if __name__ == "__main__":
    app = QApplication(sys.argv)
    widget = MainWindow()
    widget.show()
    sys.exit(app.exec())

🤖 代码解读

该代码使用 PySide6 实现了一个 GUI 界面,并结合 PyQtGraph 和 Matplotlib 绘制并对比了两种不同的数据可视化方式。程序模拟了一天 24 小时的温度变化,并以折线图形式显示在 GUI 界面上。同一生成的数据,左图由 PyQtGraph 绘制,其交互性强,绘制速度快,适用于实时数据;右图由 Matplotlib 绘制,适合高质量出版级图表,有丰富的自定义选项。

该程序主要包括定义MplCanvas类,封装 Matplotlib 画布, 和MainWindow类。MainWindow类中定义了用于生成 24 小时温度数据的generate_sites_temps()方法;chart_with_pyqtgraph()plot()是使用 PyQtGraph 绘制折线图;chart_with_matplotlib()是使用 Matplotlib 绘制折线图。

  1. 生成 24 小时温度数据:site1_tempssite2_temps是通过正弦函数(np.sin())模拟温度变化,并加入随机扰动(np.random.normal())。生成的DataFrame存储两站点的温度数据。
  2. PyQtGraph 绘图:setBackground("w")设置背景颜色为白色;setTitle()设定标题,蓝色,字号24ptsetLabel()设置坐标轴标签,红色,字号20pxshowGrid(x=True, y=True)显示网格;addLegend()添加标签;setXRange(0, 24)使 X 轴覆盖0-24小时;setYRange()让 Y 轴适应数据范围;mkPen(color=color)创建绘图的画笔颜色;symbol="+"使用+作为数据点标记,symbolSize=30控制大小。
  3. Matplotlib 绘图:用MplCanvas(self, width=5, height=5, dpi=100)创建FigureCanvas画布;NavigationToolbar2QT(sc, self)添加 Matplotlib 的工具栏(含有放大、缩小和拖动等工具);sc.axes.plot()绘制折线;set_title("Plotting with Matplotlib", fontsize=20)设置标题;grid(True显示网格;legend()显示图例;set_xlabel()set_ylabel()设定坐标轴标签。

PyQtGraph 和 Matplotlib 绘图性能对比如表,

表:PyQtGraph 和 Matplotlib 绘图性能对比

特性 PyQtGraph Matplotlib
绘制速度 快,适合实时数据 慢,适合静态数据
交互性 高,可放缩拖拽 需要 NavigationToolbar2QT
美观度 适用于数据监控 适用于高质量出版
自定义能力 较弱 丰富
代码复杂度

11.10 打包和发布应用

PySide6 部署(deploy)/打包应用程序的过程通常涉及将 Python 脚本、QML 文件、资源文件和必要的依赖项打包成一个可以执行文件(.exe.appLinux ELF),以便可以在没有 Python 环境的计算机上运行。用于部署的工具有pyside6-deployfbsPyInstallercx_Freezepy2exepy2appbriefcase等,表展示了这些部署/打包工具的平台支持,

表 部署工具平台支持(引自 Deployment(Qt for Python),https://doc.qt.io/qtforpython-6/deployment/index.html)

Name(部署工具) License(许可) Qt 6 Qt 5 Linux macOS Windows
fbs GPL yes yes yes yes
PyInstaller GPL partial yes yes yes yes
cx_Freeze MIT yes yes yes yes yes
py2exe MIT partial partial no no yes
py2app MIT yes yes no yes no
briefcase BSD3 partial yes yes yes yes
Nuitka MIT yes yes yes yes yes

上述部署工具是将 Python 打包成可执行文件(.exe),但是要创建 Windows 安装程序(Installer),则还需要使用如 InstallForge工具创建完成。即如 PyInstaller 等部署工具是将.py文件作为输入,输出为独立可执行文件.exe;而 InstallForge 是将部署后的.exe可执行文件作为输入,输出为安装程序(.exe)。

  • 部署/打包
  1. 实验中使用了 PyInstaller 工具进行部署。在项目所在的 Python 环境下执行pip install pyinstaller(终端命令行下)安装工具。
  2. 试验以 11.7.1 [Q]ListView 中的待办事项应用/ToDo(基于 Qt Designer + PySide6)为例。执行pyinstaller --name ToDo --windowed --onefile --noconfirm mainwindow.py命令完成打包。参数--onefile为打包成单个.exe文件;--windowed为不显示终端窗口(适用于 GUI 应用);--name ToDo为生成可执行文件名为ToDo.exe。如果需要包含资源文件,如项目中的 data 数据文件夹,则可以增加参数--add-data "data;data"。如果需要调试模式,增加参数--debug=all,可以获取调试信息,有 Python 运行时错误、依赖加载和 Qt 相关问题等。如果在生成的dist目录下,执行生成的.exe可执行文件出错,可以尝试增加参数--collect-all PySide6,包含Qt6运行库。更多参数可以查看 PyInstaller 手册。
  3. 完成部署后,将在项目文件夹下生成build(PyInstaller 临时文件)和dist(PyInstaller 生成的最终可执行文件)两个文件夹,及ToDo.spec配置文件。当需要修改配置,可以在.spec中直接调整,并执行pyinstaller ToDo.spec完成部署,而无需重新执行第2步骤。而如何执行第2步骤时不能正常运行,则可以先生成.spec文件,执行pyinstaller --name ToDo --windowed --onefile --noconfirm mainwindow.py命令。自动生成的.spec文件由于参数不同会有差异,如是否包含--onefile参数,如果不包含,则增加有coll = COLLECT()部分。
执行`pyinstaller --name ToDoWithOnefile --windowed --onefile --noconfirm --add-data "data;data" mainwindow.py` 执行`pyinstaller --name ToDoWithoutOnefile --windowed --noconfirm --add-data "data;data" mainwindow.py`
# -*- mode: python ; coding: utf-8 -*-


a = Analysis(
    ['mainwindow.py'],
    pathex=[],
    binaries=[],
    datas=[('data', 'data')],
    hiddenimports=[],
    hookspath=[],
    hooksconfig={},
    runtime_hooks=[],
    excludes=[],
    noarchive=False,
    optimize=0,
)
pyz = PYZ(a.pure)

exe = EXE(
    pyz,
    a.scripts,
    a.binaries,
    a.datas,
    [],
    name='ToDoWithOnefile',
    debug=False,
    bootloader_ignore_signals=False,
    strip=False,
    upx=True,
    upx_exclude=[],
    runtime_tmpdir=None,
    console=False,
    disable_windowed_traceback=False,
    argv_emulation=False,
    target_arch=None,
    codesign_identity=None,
    entitlements_file=None,
)
# -*- mode: python ; coding: utf-8 -*-


# 1. `Analysis` 对象:分析要打包的脚本以及其依赖项
a = Analysis(
    ['mainwindow.py'],  # 要打包的主脚本文件
    pathex=[],  # 额外的路径,用于查找依赖模块(此处为空)
    binaries=[],  # 额外的二进制文件(如果有的话,此处为空)
    datas=[('data', 'data')],  # 要打包的数据文件,'data' 是源文件,'data' 是目标路径
    hiddenimports=[],  # 动态导入的模块(如果有的话,此处为空)
    hookspath=[],  # 自定义钩子文件的路径(此处为空)
    hooksconfig={},  # 钩子的配置字典(此处为空)
    runtime_hooks=[],  # 运行时钩子文件(此处为空)
    excludes=[],  # 要排除的模块(此处为空)
    noarchive=False,  # 是否将所有文件打包成一个归档文件(False 表示不打包成归档)
    optimize=0,  # 优化级别,0 表示不进行额外优化
)

# 2. `PYZ` 对象:将纯 Python 文件打包成 `.pyz` 文件
pyz = PYZ(a.pure)  # `a.pure` 是 `Analysis` 中收集的纯 Python 模块

# 3. `EXE` 对象:生成最终的可执行文件
exe = EXE(
    pyz,  # 生成的 `.pyz` 文件
    a.scripts,  # 在 `Analysis` 中指定的脚本(如 'mainwindow.py')
    [],  # 额外的运行时数据(此处为空)
    exclude_binaries=True,  # 不包括二进制文件
    name='ToDoWithoutOnefile',  # 可执行文件的名称
    debug=False,  # 不启用调试模式
    bootloader_ignore_signals=False,  # 是否忽略操作系统信号(False 表示不忽略)
    strip=False,  # 是否删除符号表和调试信息(此处为 False,表示不删除)
    upx=True,  # 启用 UPX 压缩,以减小可执行文件的体积
    console=False,  # 生成 GUI 程序(非控制台程序)
    disable_windowed_traceback=False,  # 禁用窗口模式下的错误追踪(False 表示启用)
    argv_emulation=False,  # 禁用 `argv` 模拟
    target_arch=None,  # 目标架构(如 x86_64),此处为 None
    codesign_identity=None,  # 用于 macOS 的代码签名标识
    entitlements_file=None,  # 用于 macOS 的权限文件
)

# 4. `COLLECT` 对象:收集所有组件并生成最终的输出
coll = COLLECT(
    exe,  # 生成的可执行文件
    a.binaries,  # 包含的二进制文件(如果有的话)
    a.datas,  # 包含的数据文件(如 'data')
    strip=False,  # 不对文件进行符号剥离
    upx=True,  # 启用 UPX 压缩
    upx_exclude=[],  # 不压缩的文件列表(此处为空)
    name='ToDoWithoutOnefile',  # 最终输出的文件名
)
  • 创建安装程序(Windows)

下载 InstallForge 应用程序并安装。InstallForge 是一个 Windows 平台上的安装包创建工具,可以通过图形化界面配置安装程序。图11-16列出了其中主要三个配置界面。其中在Files界面选择了使用参数--onefile打包成一个单独的.exe文件,因此不包含其它文件,如未使用--onefile参数时,也需要在该页面增加dist->项目文件夹->_internal,即_internal文件夹(包含应用程序依赖的所有文件)。Build界面中的Setup File为输出的安装程序(.exe),用于在 Windows 下安装。其它的配置说明可以从 InstallForge 官网文档中获取。

PYC icon

图 11-16 InstallForge 配置界面

试验的 ToDo 演示项目,未处理打包后数据本地磁盘保存问题,因此 ToDo 增加、删除、标记完成等工作尚不能保存。即关闭应用再打开后,将恢复到默认状态。

参考文献(Reference):

[1] Qt for Python, https://doc.qt.io/qtforpython-6/index.html.

[2] Martin Fitzpatrick, Create GUI Applications with Python & Qt6: The Hands-on Guide to Making Apps with Python (PySide6 Edition).Independently published.