维护层,核查于 2026-09-01。 本指南面向现有 PyQt5 应用,以及明确选择 Qt 5 的团队。2019 年原始检查清单作为独立存档完整保留在文末。
这份实用指南聚焦容易引发 PyQt5 故障的小决策:在隔离环境中安装,选择生成或运行时加载 Qt Designer 表单,正确构建 QScrollArea,稳定解析资源路径,精确连接和断开信号处理函数,保持 GUI 线程响应,清理 QThread worker,以及在不丢失元数据的情况下移动标签页。
Table of Contents
2026 年的新项目还应选 PyQt5 吗?
Riverbank 仍同时发布 PyQt5 和 PyQt6。PyQt5 绑定 Qt 5,PyQt6 绑定 Qt 6。维护经过验证的 Qt 5 应用,或依赖仅支持 Qt 5 的组件时,保留 PyQt5 是合理的。对新应用,在锁定 Qt 5 之前先评估 PyQt6;然后在依赖文件中明确绑定和版本范围,不要依赖全局环境里碰巧已安装的包。
许可证是发布条件,不是上线前一天才处理的细节。Riverbank 以 GPL v3 和商业许可双重发布 PyQt;PyQt 不是 LGPL。如果应用将按与 GPL 不兼容的条款分发,应在发布前阅读 Riverbank 官方许可资料,并获得合适的专业意见。
在可复现环境中安装 PyQt5
创建可随时重建的虚拟环境,并用该解释器的 pip 安装:
python -m venv .venv
# POSIX shells
source .venv/bin/activate
# Windows PowerShell
.venvScriptsActivate.ps1
python -m pip install --upgrade pip
python -m pip install PyQt5
核对应用实际使用的 Python、PyQt 和 Qt:
python -c "import sys; from PyQt5.QtCore import PYQT_VERSION_STR, QT_VERSION_STR; print(sys.executable); print('PyQt', PYQT_VERSION_STR, 'Qt', QT_VERSION_STR)"
Riverbank 提醒,没有兼容 wheel 时,pip 可能退回下载源码分发包。因此,难懂的编译器或 qmake 错误并不说明应该去安装另一个随机 Qt 包;先检查 Python 版本、平台、pip 版本,以及是否选中了兼容 wheel。
从 .ui 文件生成 .py 文件
Qt Designer 以 XML .ui 文件保存表单。PyQt5 官方支持两种工作流:用 pyuic5 生成 Python,或用 PyQt5.uic 在运行时加载 .ui。生成代码时运行:
pyuic5 -x main_window.ui -o ui_main_window.py
-x 选项会添加一小段可直接运行的测试代码。如果生成的文件只会被其他模块导入,则省略 -x:
pyuic5 main_window.ui -o ui_main_window.py
一个实用的项目结构如下:
project/
├── main.py
├── ui_main.py # 由 .ui 生成
├── workers.py # QThread/Worker 代码
└── resources/
└── icon.png
将生成的 UI 文件与应用逻辑分开。如果 .ui 文件发生变化,重新生成 ui_main.py,不要手动编辑它。
如果希望表单可直接修改,不经生成步骤就生效,可在运行时加载:
from pathlib import Path
from PyQt5 import uic
from PyQt5.QtWidgets import QMainWindow
class MainWindow(QMainWindow):
def __init__(self):
super().__init__()
ui_file = Path(__file__).resolve().parent / "main_window.ui"
uic.loadUi(str(ui_file), self)
每个表单选定一种工作流。生成模块应是可复现的构建产物;运行时加载的表单则必须包含在安装包中。Riverbank 还警告,pyuic5 生成的代码不保证与更早的 PyQt5 版本兼容,因此应用项目锁定的工具链生成。
让控件可滚动
要让大型表单或结果面板可滚动,可以把内容控件放进 QScrollArea:
from PyQt5.QtWidgets import QWidget, QVBoxLayout, QScrollArea, QLabel
content = QWidget()
layout = QVBoxLayout(content)
for i in range(100):
layout.addWidget(QLabel(f"Row {i}"))
scroll_area = QScrollArea()
scroll_area.setWidgetResizable(True)
scroll_area.setWidget(content)
如果使用 Qt Designer,添加一个 QScrollArea,在其中放置一个子控件,并把实际布局设置在这个子控件上。滚动区域本身通常应启用 widgetResizable。
修改外观并设置任务栏图标
在主窗口上使用 setWindowTitle() 和 setWindowIcon()。在显示窗口之前设置应用图标:
import sys
from pathlib import Path
from PyQt5.QtGui import QIcon
from PyQt5.QtWidgets import QApplication, QMainWindow
base_dir = Path(__file__).resolve().parent
icon = QIcon(str(base_dir / "resources" / "icon.png"))
app = QApplication(sys.argv)
app.setApplicationName("PyQt5 Application")
app.setWindowIcon(icon)
window = QMainWindow()
window.setWindowTitle("PyQt5 Application")
window.setWindowIcon(icon)
window.show()
sys.exit(app.exec_())
简单样式可以使用样式表:
window.setStyleSheet("""
QPushButton {
padding: 6px 10px;
}
QLineEdit {
padding: 4px;
}
""")
除非项目有专门的主题文件,否则样式表应保持精简。资源文件的路径应相对于模块或安装包解析,不要依赖进程的当前工作目录。如果图标和翻译需要嵌入,Qt 提供资源系统,PyQt5 通过 .qrc 文件和 pyrcc5 支持它。任务栏或 Dock 中的最终显示方式仍取决于平台和桌面环境。
连接信号
信号通过 .connect() 连接到槽:
self.button.clicked.connect(self.run_task)
槽可以是任何可调用对象:
def run_task(self):
print("Button clicked")
要传递参数,可以使用 lambda 或 functools.partial。记得处理信号自带的参数;QAbstractButton.clicked 可能传入 checked 状态:
self.button.clicked.connect(
lambda _checked=False: self.open_file("data.txt")
)
优先只连接一次,在槽函数里根据状态分支。如果确实必须动态替换连接,保留 connect() 返回的 Connection,并断开这一个精确连接:
self._open_connection = self.button.clicked.connect(
lambda _checked=False: self.open_file("data.txt")
)
# Later, before installing a replacement:
self.button.clicked.disconnect(self._open_connection)
self.button.clicked.connect(self.new_handler)
不带参数调用 disconnect() 会移除该绑定信号上的所有槽。断开一个不存在的连接会引发异常。保留返回的连接可避免无意移除其他组件的处理函数,也是官方文档中断开 lambda 的方法。
使用 pyqtSignal
自定义信号声明为类属性:
from PyQt5.QtCore import QObject, pyqtSignal
class Worker(QObject):
progress = pyqtSignal(int)
result = pyqtSignal(str)
failed = pyqtSignal(str)
一个信号可以发出多个值:
class Worker(QObject):
finished = pyqtSignal(str, int)
类型列表遵循以下模式:
pyqtSignal(type1, type2, ...)
对于多个值,优先直接发出结构化值,而不是依赖全局变量:
self.finished.emit("done", 100)
元组也可以作为一个 Python 对象发出:
summary = pyqtSignal(tuple)
self.summary.emit((filename, count, elapsed_seconds))
使用 QThread 运行耗时任务
不要在按钮处理函数中直接运行缓慢任务。这会阻塞事件循环并冻结 GUI。Qt 文档中的 worker-object 模式会把 QObject 移到独立 QThread,再通过信号把结果传回 GUI。永远不要从 worker 直接更新控件。
from PyQt5.QtCore import QObject, QThread, pyqtSignal, pyqtSlot
class Worker(QObject):
progress = pyqtSignal(int)
result = pyqtSignal(str)
failed = pyqtSignal(str)
finished = pyqtSignal()
@pyqtSlot()
def run(self):
try:
for i in range(101):
if QThread.currentThread().isInterruptionRequested():
return
# Do part of the long-running task here.
self.progress.emit(i)
self.result.emit("Task complete")
except Exception as exc:
self.failed.emit(str(exc))
finally:
self.finished.emit()
def start_worker(self):
if getattr(self, "thread", None) is not None:
return
self.thread = QThread()
self.worker = Worker()
self.worker.moveToThread(self.thread)
self.thread.started.connect(self.worker.run)
self.worker.progress.connect(self.progress_bar.setValue)
self.worker.result.connect(self.on_worker_finished)
self.worker.failed.connect(self.on_worker_failed)
self.worker.finished.connect(self.thread.quit)
self.thread.finished.connect(self.worker.deleteLater)
self.thread.finished.connect(self.thread.deleteLater)
self.thread.finished.connect(self.clear_worker_references)
self.thread.start()
def cancel_worker(self):
if self.thread is not None:
self.thread.requestInterruption()
def clear_worker_references(self):
self.worker = None
self.thread = None
在 finished 前保留两个对象的引用;只存在于局部变量的 Python wrapper 可能在工作仍运行时被回收。取消必须是协作式的:requestInterruption() 只设置标志,worker 必须在安全边界检查它。确保成功、失败和取消每条路径都会发出 finished、退出事件循环,并安排 Qt 对象删除。
QThread 可以改善响应性,适合阻塞 I/O,也适合会释放 Python 全局解释器锁的原生代码。在普通的启用 GIL 的 CPython 中,它不会自动让纯 Python CPU 密集任务并行。这种情况应先测量,再考虑基于进程的 worker。无 GIL 的 CPython 构建已存在,但它是独立的部署选择,需要另行验证扩展兼容性。
修改标签页顺序
如果用户应该自行拖动标签页,启用内置行为:
self.tabs.setMovable(True)
如果程序需要可控地移动,先验证索引,再用 removeTab() 和 insertTab() 移动,同时保留标签元数据:
index = self.tabs.indexOf(self.settings_tab)
if index < 0:
raise ValueError("settings_tab is not in the tab widget")
widget = self.tabs.widget(index)
label = self.tabs.tabText(index)
icon = self.tabs.tabIcon(index)
tooltip = self.tabs.tabToolTip(index)
enabled = self.tabs.isTabEnabled(index)
self.tabs.removeTab(index)
self.tabs.insertTab(0, widget, icon, label)
self.tabs.setTabToolTip(0, tooltip)
self.tabs.setTabEnabled(0, enabled)
self.tabs.setCurrentWidget(widget)
如果标签页是在 Qt Designer 中创建的,通常在那里重新排序会更简单。当顺序取决于用户设置或运行时状态时,再使用代码处理。
模块化预处理和控制逻辑
一个可维护的 PyQt 项目会分离 UI 设置、预处理和控制逻辑:
class MainWindow(QMainWindow):
def __init__(self):
super().__init__()
self.ui = Ui_MainWindow()
self.ui.setupUi(self)
self.connect_signals()
def connect_signals(self):
self.ui.runButton.clicked.connect(self.run_preprocessing)
def run_preprocessing(self):
options = self.collect_options()
self.start_worker(options)
def collect_options(self):
return {
"input": self.ui.inputLineEdit.text(),
"enabled": self.ui.enableCheckBox.isChecked(),
}
这样可以让生成的 UI 代码随时重建,并让业务逻辑保持可测试。主窗口协调界面;worker 类处理耗时编排;普通辅助模块保存可复用的解析、预处理或计算函数。
小型项目也可以把边界写进目录结构:
project/
├── pyproject.toml
├── src/app/
│ ├── main.py
│ ├── window.py
│ ├── ui_main_window.py # generated; do not hand-edit
│ ├── workers.py
│ ├── services.py # no widget access
│ └── resources/
└── tests/
故障检查清单
- 窗口卡住: 某个槽仍在 GUI 线程做耗时工作。
QThread: Destroyed while thread is still running: 控制器没有保留 thread/worker,或应用关闭时没有停止并等待线程。- worker 触碰控件: 发出数据,在 GUI 线程的槽里更新控件。
- 滚动区空白: 先把布局放到唯一子控件上,再把子控件传给
setWidget()。 - 图标只在项目目录启动时出现: 相对
__file__解析,或使用 Qt 资源系统。 - 一次点击执行两次: 同一信号被重复连接;集中管理连接初始化。
disconnect()异常: 跟踪精确 callable 或返回的Connection,不要猜测连接状态。- 标签页丢失 tooltip 或启用状态: remove/insert 前后保留全部元数据,或用
setMovable(True)让用户移动。
一手文档
- Riverbank:PyQt 简介、绑定代际与许可
- Riverbank:安装 PyQt5
- Riverbank:使用 Qt Designer 和 `pyuic5`
- Riverbank:PyQt5 信号、槽、连接对象与 `pyqtSignal`
- Riverbank:PyQt5 资源系统
- Qt:`QThread` 与 worker-object 模式
- Qt:`QScrollArea`
- Qt:`QTabWidget`
- Python:用 `venv` 创建虚拟环境
- Python:GIL 与线程性能注意事项
---
2019 年原始存档(英文原文保留)
以下是 WordPress 原始导出的完整可见用词与顺序:发布于 2019-04-15,最后修改于 2019-04-20。仅为通过仓库格式检查而规范化了不可见的行末空格。
Make it scrollable
Generating .py file from .ui
python -m PyQt5.uic.pyuic -x [FILENAME].ui -o [FILENAME].py
Modularized
Change appearance & setup taskbar icon
Signal connection
Parallel processing
pyqtSignal, QThread, Worker
pyqtSignal[type1, type2, …]
using global variable, tuple
using disconnect to clear outdated signal
button.clicked.disconnect()
Change the order of tab
modularized the pre-processing and control part
