PyQt4 快速入门:用 Python 编写跨平台 Qt GUI 应用(learnxinyminutes-docs it/pyqt 指南详解)

原创2026-10-08 21:06:581,375 阅读
文章标签:文档教程

PyQt4 快速入门:用 Python 编写跨平台 Qt GUI 应用(learnxinyminutes-docs it/pyqt 指南详解)

本指南基于 learnxinyminutes-docs 仓库中意大利语版 it/pyqt.md(及其英文原版 pyqt.md)展开,以两个可直接运行的完整示例为主线,带你从零掌握 PyQt4 的核心编程模型:应用对象与事件循环、控件(Widget)的创建与布局、信号与槽(Signal/Slot)连接,以及模态对话框的弹出与退出流程。读完本文,你将能够独立写出一个带按钮交互、对话框弹窗的最小可运行 PyQt 桌面应用,并理解它与 C++ 版 Qt 教程(qt.md)之间的对应关系。

一、Qt 与 PyQt:为什么用 Python 写 Qt

Qt 是一个广泛使用的跨平台软件开发框架,用同一套代码即可在多种软件与硬件平台上运行,几乎无需改动,同时保留原生应用的性能与响应速度。Qt 最初使用 C++ 编写,但社区为它提供了多种语言的绑定,其中 PyQt 就是面向 Python 的官方绑定之一。

仓库中 pyqt.md 明确指出,本文内容是对 Aleksey Kholovchuk 所写 C++ 版 Qt 入门教程(见 qt.md)的改编:代码示例保持相同的功能语义,只是改用 PyQt 实现。因此,阅读本指南时你完全可以对照 qt.md 中的 C++ 写法,观察同一种 GUI 思想在两种语言下的不同表达。

需要特别说明的是,it/pyqt.md 与 pyqt.md 均基于 PyQt4(即 Qt4 时代的绑定)编写,示例中使用的 PyQt4.QtGui、QtGui.QApplication、app.exec_() 等 API 均属于 PyQt4 命名空间;这与现代 PyQt5/PyQt6 中 PyQt5.QtWidgets、app.exec() 的写法不同,运行示例前请确认你的 Python 与 PyQt 版本与示例匹配(详见下文"版本与运行注意事项")。

二、第一个程序:创建"Hello World"窗口

it/pyqt.md 的第一个示例完整展示了 PyQt 应用的标准骨架:创建应用对象 → 创建控件 → 设置属性 → 显示 → 启动事件循环。以下是完整代码(注释为中文讲解):

import sys
from PyQt4 import QtGui

def window():
    # 创建一个应用对象:管理整个应用的资源,接收命令行参数
    app = QtGui.QApplication(sys.argv)
    # 创建一个 widget,作为后续子控件的父容器
    w = QtGui.QWidget()
    # 在 widget 中创建一个标签(label)子控件
    b = QtGui.QLabel(w)
    # 设置标签的文本内容
    b.setText("Hello World!")
    # 设置窗口的几何信息:x、y 坐标与宽、高
    w.setGeometry(100, 100, 200, 50)
    # 将标签移动到窗口内的指定位置(相对父控件的偏移)
    b.move(50, 20)
    # 设置窗口标题栏显示的标题
    w.setWindowTitle("PyQt")
    # 显示窗口及其所有子控件(Qt 中控件默认是隐藏的,必须调用 show)
    w.show()
    # 启动事件循环:程序在此阻塞运行,直到窗口被关闭
    sys.exit(app.exec_())

if __name__ == '__main__':
    window()

这段代码虽然简短,却包含了 PyQt 程序必须理解的全部核心概念:

  • QApplication(sys.argv):每个 PyQt GUI 程序有且只有一个应用对象,它负责管理事件循环、全局资源与窗口系统交互。传入 sys.argv 是为了让 Qt 能够解析命令行参数(例如某些平台的高 DPI 选项)。
  • QWidget():控件是所有界面元素的基类。这里 w 既是顶层窗口,又是标签的父控件。
  • QLabel(w):把父控件作为构造参数传入,即"把标签挂到 widget 上",这是 Qt 中建立控件父子关系的惯用方式。父子关系决定了子控件随父控件一起显示、移动和销毁。
  • setGeometry(100, 100, 200, 50):一次设置窗口在屏幕上的位置(100, 100)和尺寸(宽 200、高 50)。
  • b.move(50, 20):将标签移动到窗口客户区内的相对位置。
  • show() 与 exec_():在 Qt 中控件默认不可见,show() 才使其显示;exec_() 启动事件循环,程序在此阻塞,持续响应窗口事件(重绘、点击、关闭等),直到退出。PyQt4 中该方法写作 exec_()(带下划线,因为 exec 是 Python 2 的关键字),这正是"PyQt4 时代"API 的典型特征。

运行该脚本后,屏幕上会出现一个标题为 "PyQt"、大小 200×50 的小窗口,窗口内显示 "Hello World!" 文本。

三、信号与槽:点击按钮弹出对话框

第二个示例引入 PyQt 最核心的交互机制——信号与槽(Signals and Slots):点击主窗口中的按钮,弹出一个模态对话框;点击对话框中的按钮,整个程序退出。

import sys
from PyQt4.QtGui import *
from PyQt4.QtCore import *


def window():
    app = QApplication(sys.argv)
    w = QWidget()
    # 创建一个按钮并挂载到 widget w 上
    b = QPushButton(w)
    b.setText("Premimi")          # 设置按钮文本
    b.move(50, 50)                # 移动按钮到指定位置
    # 将按钮的 clicked 信号连接到 showdialog 函数
    # 注意:连接的是函数对象本身,不能带 "()",
    # 否则会立即执行函数并把返回值传给 connect
    b.clicked.connect(showdialog)
    w.setWindowTitle("PyQt Dialog")
    w.show()
    sys.exit(app.exec_())

# 该函数创建带一个按钮的对话框;
# 点击对话框按钮后,整个进程结束
def showdialog():
    d = QDialog()
    b1 = QPushButton("ok", d)
    b1.move(50, 50)
    d.setWindowTitle("Dialog")
    # 设置模态方式:对话框激活期间阻塞父窗口的交互
    d.setWindowModality(Qt.ApplicationModal)
    # 点击 "ok" 按钮时直接退出整个进程
    b1.clicked.connect(sys.exit)
    d.exec_()

if __name__ == '__main__':
    window()

本例相比第一个示例,在三个层面做了进阶:

1. 通配导入两个核心模块 from PyQt4.QtGui import * 与 from PyQt4.QtCore import * 引入了全部控件类与核心常量。这样 QApplication、QPushButton、QDialog 等类无需再写 QtGui. 前缀;Qt.ApplicationModal 这类枚举常量则来自 QtCore。

2. 信号连接时"不加括号" b.clicked.connect(showdialog) 中 showdialog 不带 ()。这是信号槽机制的关键约定:connect 接收的是可调用对象本身,Python 会在按钮被点击时再去调用它。如果写成 connect(showdialog()),函数会在连接阶段被立即执行一次,返回值(这里是 None)被当作槽传入,点击按钮时将不会产生任何效果。中文翻译版 zh-cn/pyqt.md 对此有同样的强调。

3. 模态对话框的三种要素

  • QDialog():对话框是一个独立于主窗口的顶层窗口,常用于要求用户确认决策或提供信息——这正是 it/pyqt.md 所说"询问用户确认决定或提供信息"的典型场景;
  • setWindowModality(Qt.ApplicationModal):Qt.ApplicationModal 是应用级模态,对话框弹出期间会阻塞父窗口乃至整个应用的交互,用户必须先处理对话框;
  • d.exec_():与 QApplication.exec_() 类似,启动对话框自己的事件循环,直到对话框被关闭才返回,从而在代码中形成"等待用户操作"的同步语义。

值得留意的是,showdialog() 是在 window() 内部通过信号槽注册的,而它内部又调用了 sys.exit 作为 b1.clicked 的槽——也就是说点击"ok"按钮会直接终止整个 Python 进程,这是一种简单粗暴但有效的"退出确认"实现。

四、深入理解信号与槽:从 C++ 原版看本质

信号与槽不是 PyQt 的发明,而是整个 Qt 框架的架构基石。C++ 原版教程 qt.md 中有精确定义:

  • 信号(Signal):当对象发生特定事件时被发射。例如用户按下 QPushButton 时,pressed/clicked 信号被发射;
  • 槽(Slot):响应信号而执行的动作,可以是一个函数、一个方法,甚至是一个内置操作(如本例中的 sys.exit)。

在 C++ 中,连接写作 QObject::connect(&button, &QPushButton::pressed, &textLabel, &QLabel::show)(见 qt.md 第二个示例);而在 PyQt 中,等价写法就是本指南第二例的 b.clicked.connect(showdialog)。PyQt 的信号槽比 C++ 更灵活:槽可以是任何 Python 可调用对象——普通函数、绑定方法、lambda,甚至内置函数 sys.exit 都可以直接作为槽使用,这正是第二例 b1.clicked.connect(sys.exit) 能成立的原因。

qt.md 还展示了另一个重要能力:通过继承扩展控件行为。它定义了一个继承 QLabel 的 CounterLabel 类(内部维护计数器,槽函数 increaseCounter() 每次被调用就把计数写入文本),并在主程序中用 QObject::connect(&button, &QPushButton::pressed, &counterLabel, &CounterLabel::increaseCounter) 把按钮按下信号接到自定义槽上。在 PyQt 中,同样的思路可以用一个普通的 Python 类继承 QtGui.QLabel 并定义一个方法作为槽来实现,无需 C++ 版所要求的 Q_OBJECT 宏与元对象编译器(moc)预处理——这正是 Python 绑定的便利之处。

五、从 PyQt4 到 PyQt5/PyQt6:版本与运行注意事项

it/pyqt.md 与英文原版 pyqt.md 均以 PyQt4 为例。由于 PyQt4 基于 Qt4 时代,而如今主流发行版普遍使用 PyQt5/PyQt6,从源码结构可以推断(对比 PyQt4 的 from PyQt4.QtGui import QtGui 与现代绑定文档),迁移时主要有两点变化:

  1. 模块拆分:PyQt5 起 GUI 控件类从 QtGui 拆分到 QtWidgets 模块,QApplication、QWidget、QLabel、QPushButton、QDialog 均需从 PyQt5.QtWidgets(或 PyQt6 的 PyQt6.QtWidgets)导入;
  2. 方法改名:app.exec_() 在 PyQt5/PyQt6 中写作 app.exec()(Python 3 中 exec 不再是保留字)。

因此,在 PyQt5/PyQt6 环境下运行本文示例时,可把第一行导入改为:

from PyQt5.QtWidgets import QApplication, QWidget, QLabel  # 或 PyQt6

并将 sys.exit(app.exec_()) 改为 sys.exit(app.exec())。若你仍使用 Python 2 + PyQt4 环境,则按原文示例原样运行即可。这也是仓库采用"以真实可运行代码教学"这一理念的体现——每个文档都是经过注释的完整代码,可读、可改、可运行。

六、进一步学习与仓库相关资源

本指南只是 Qt/PyQt 庞大体系的一小部分。仓库中还提供了以下可直接对照阅读的资源:

  • pyqt.md:英文原版 PyQt 指南,与本篇内容一一对应;
  • qt.md:C++ 版 Qt 入门原文,包含信号槽、Q_OBJECT 宏与自定义控件继承的完整讲解,是理解 PyQt 底层语义的最佳对照;
  • zh-cn/pyqt.md:中文翻译版,注释更详尽;
  • es/pyqt.md、pt-br/pyqt.md、fr/pyqt.md、ru/pyqt.md、de/pyqt.md 等:更多语言的 PyQt 指南;
  • README.md:了解 Learn X in Y Minutes 项目"以代码形式写文档"的总体理念。

继续深入的方向包括:布局管理器(QVBoxLayout 等,见 qt.md 中的 layout.addWidget 用法)、自定义控件继承、信号槽的参数传递、菜单/工具栏、以及使用 Qt Designer 可视化设计界面。记住 Qt 的 GUI 编程精髓只有两条:一切皆控件(Widget),控件之间靠信号与槽沟通。掌握本文的两个示例,你就已经站上了这条主线的起点。

登录后查看全文
learnxinyminutes-docs