PyQt5 2026 实用指南:Qt Designer、信号槽、QThread、滚动区、图标与标签页

维护层,核查于 2026-09-01。 本指南面向现有 PyQt5 应用,以及明确选择 Qt 5 的团队。2019 年原始检查清单作为独立存档完整保留在文末。

这份实用指南聚焦容易引发 PyQt5 故障的小决策:在隔离环境中安装,选择生成或运行时加载 Qt Designer 表单,正确构建 QScrollArea,稳定解析资源路径,精确连接和断开信号处理函数,保持 GUI 线程响应,清理 QThread worker,以及在不丢失元数据的情况下移动标签页。

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")

要传递参数,可以使用 lambdafunctools.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) 让用户移动。

一手文档

---

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

Leave a Reply