如何加速含大量子部件的PySide窗口创建速度?
问题描述
我遇到创建PySide窗口耗时过长(可达数秒)的问题:该窗口包含大量子部件,每个子部件基于QWidget且由多个小部件组成。测试显示,创建含100个子部件的窗口约需0.6秒,1000个则约6秒,耗时随子部件数量线性增长。请问有哪些可大幅或小幅降低窗口创建时间的优化技巧?能否通过pickle序列化部件,实现一次创建后多次复用?
复现代码
import sys import time from PySide2 import QtWidgets from PySide2 import QtCore # Adjust this number to increase/ decrease the number of sub widgets being # created. NUMBER_ELEMENTS = 1000 class Main(QtWidgets.QWidget): def __init__(self): super(Main, self).__init__() self.setMinimumSize(QtCore.QSize(1000, 700)) self.table = QtWidgets.QTableWidget() self.layout = QtWidgets.QVBoxLayout() self.layout.addWidget(self.table) self.setLayout(self.layout) self.table.setColumnCount(1) self.table.setRowCount(NUMBER_ELEMENTS) for row in range(NUMBER_ELEMENTS): widget = SubWidget() self.table.setCellWidget(row, 0, widget) self.table.resizeColumnsToContents() class SubWidget(QtWidgets.QWidget): def __init__(self): super(SubWidget, self).__init__() self.label_name = QtWidgets.QLabel("My name") self.label_format = QtWidgets.QLabel("my format") self.label_frames = QtWidgets.QLabel("XXX") self.label_type = QtWidgets.QLabel("some type") self.label_size = QtWidgets.QLabel("some size") self.push_favorite = QtWidgets.QPushButton("hello") self.label_comment = QtWidgets.QTextEdit("something") self.label_comment.setMaximumHeight(80) self.label_comment.setProperty("style", "comment") self.video = QtWidgets.QLabel("my video") self.layout_main = QtWidgets.QHBoxLayout() self.layout_main.addWidget(self.video, 17) self.layout_main.addWidget(self.label_name, 20) self.layout_main.addWidget(self.label_format, 10) self.layout_main.addWidget(self.label_frames, 10) self.layout_main.addWidget(self.label_type, 10) self.layout_main.addWidget(self.label_size, 10) self.layout_main.addWidget(self.push_favorite, 15) self.layout_main.addWidget(self.label_comment, 50) self.setLayout(self.layout_main) if __name__ == "__main__": start = time.time() app = QtWidgets.QApplication(sys.argv) main_window = Main() main_window.raise_() main_window.show() print "Duration to build window: ", time.time() - start app.exec_()
解决方案
先聊pickle的问题——这条路走不通
很明确地说:Qt的QWidget对象不能用pickle序列化。因为每个QWidget都绑定了系统底层的窗口资源(比如窗口句柄、绘图上下文),这些资源是无法被序列化存储的。哪怕你强行尝试,要么直接报错,要么反序列化出来的部件完全没法正常工作,甚至会搞崩程序。
如果想复用部件的配置,可以写个工厂函数,提前定义好SubWidget的所有属性,每次需要的时候调用函数生成新实例,这比序列化靠谱多了。
优化技巧:从大幅到小幅分等级来
1. 用QStyledItemDelegate替代CellWidget(大幅提速)
你现在用的setCellWidget其实是性能最差的方式——每个单元格都是一个独立的QWidget,1000行就是1000个完整的Widget实例,开销拉满。换成QStyledItemDelegate自定义绘制单元格内容,能把创建时间砍到原来的1/10甚至更低。
原理是Delegate复用一套绘制逻辑,直接把内容画到单元格上,而不是为每个cell创建独立部件。你可以在Delegate的paint方法里绘制所有标签、按钮的样式,通过editorEvent处理按钮点击、文本编辑这些交互。虽然需要写点自定义绘制的代码,但性能提升是质的飞跃。
2. 禁用批量创建时的自动更新(小幅但见效快)
Qt在创建和添加部件时,会自动触发布局计算和界面重绘,批量操作时这些重复计算会浪费很多时间。你可以临时关掉表格的自动更新,等所有部件都创建好再打开:
# 在循环创建子部件前加上 self.table.setUpdatesEnabled(False) # 循环结束后恢复 self.table.setUpdatesEnabled(True) self.table.resizeColumnsToContents()
这个小改动能帮你把1000行的创建时间从6秒降到4-5秒左右,几乎不需要改太多代码。
3. 延迟加载/懒加载(初始耗时直接砍到接近0)
如果用户不需要一开始就看到所有1000行,可以只创建当前可见区域的部件,滚动时再动态加载:
- 监听表格的滚动事件,计算当前可见的行范围
- 只给可见行设置CellWidget,不可见的行把之前的部件销毁掉(或者缓存起来)
- 这种方式下,初始窗口创建时间会从几秒变成几十毫秒,用户体验直接拉满
4. 优化SubWidget本身的开销(小幅累加)
- 减少部件数量:比如把几个静态标签合并成一个QLabel,用HTML格式排版(比如
<b>My name</b> | my format),少创建几个QLabel实例 - 固定部件尺寸:给SubWidget和内部的所有部件设置固定大小(比如
self.setFixedHeight(100)),避免Qt反复计算布局 - 延迟非必要操作:把一些非初始化必须的逻辑(比如设置样式、加载非默认内容)延迟到部件第一次显示时再执行,不要堆在
__init__里
优化后的代码示例
这里给你改了Main类,加上了禁用自动更新的优化,同时给SubWidget加了固定高度:
import sys import time from PySide2 import QtWidgets from PySide2 import QtCore NUMBER_ELEMENTS = 1000 class Main(QtWidgets.QWidget): def __init__(self): super(Main, self).__init__() self.setMinimumSize(QtCore.QSize(1000, 700)) self.table = QtWidgets.QTableWidget() self.layout = QtWidgets.QVBoxLayout() self.layout.addWidget(self.table) self.setLayout(self.layout) self.table.setColumnCount(1) self.table.setRowCount(NUMBER_ELEMENTS) # 优化:禁用表格自动更新,避免重复重绘 self.table.setUpdatesEnabled(False) for row in range(NUMBER_ELEMENTS): widget = SubWidget() self.table.setCellWidget(row, 0, widget) # 恢复更新并调整列宽 self.table.setUpdatesEnabled(True) self.table.resizeColumnsToContents() class SubWidget(QtWidgets.QWidget): def __init__(self): super(SubWidget, self).__init__() # 优化:固定高度,减少布局计算 self.setFixedHeight(100) self.label_name = QtWidgets.QLabel("My name") self.label_format = QtWidgets.QLabel("my format") self.label_frames = QtWidgets.QLabel("XXX") self.label_type = QtWidgets.QLabel("some type") self.label_size = QtWidgets.QLabel("some size") self.push_favorite = QtWidgets.QPushButton("hello") self.label_comment = QtWidgets.QTextEdit("something") self.label_comment.setMaximumHeight(80) self.label_comment.setProperty("style", "comment") self.video = QtWidgets.QLabel("my video") self.layout_main = QtWidgets.QHBoxLayout() self.layout_main.addWidget(self.video, 17) self.layout_main.addWidget(self.label_name, 20) self.layout_main.addWidget(self.label_format, 10) self.layout_main.addWidget(self.label_frames, 10) self.layout_main.addWidget(self.label_type, 10) self.layout_main.addWidget(self.label_size, 10) self.layout_main.addWidget(self.push_favorite, 15) self.layout_main.addWidget(self.label_comment, 50) self.setLayout(self.layout_main) if __name__ == "__main__": start = time.time() app = QtWidgets.QApplication(sys.argv) main_window = Main() main_window.raise_() main_window.show() print("Duration to build window: ", time.time() - start) app.exec_()
内容的提问来源于stack exchange,提问作者Simon

