PySide2使用后台工作者异步加载QListView缩略图实现方案咨询
QListView 异步加载缩略图实现方案
思路可行性确认
你提出的5点实现逻辑是Qt框架下列表控件异步加载缩略图的标准最优方案,完全可行:既不会阻塞主线程导致界面卡顿,也能避免重复下载、按需加载可见区域的条目资源。
完整实现代码
import os import sys from PySide2 import QtCore, QtGui, QtWidgets try: # python 2 from urllib import urlretrieve from urllib2 import urlopen except Exception as e: # python 3 from urllib.request import urlretrieve, urlopen from urllib.parse import urlparse # 自定义信号类,QRunnable不能直接继承QObject,单独定义信号载体 class DownloadSignals(QtCore.QObject): download_finished = QtCore.Signal(QtCore.QModelIndex, str) # 对应索引、缓存路径 class ThumbnailDownloader(QtCore.QRunnable): def __init__(self, index, url, output_path): super().__init__() self.index = index self.url = url self.output_path = output_path self.signals = DownloadSignals() def run(self): # 后台线程执行下载逻辑,不阻塞主界面 if os.path.exists(self.output_path): self.signals.download_finished.emit(self.index, self.output_path) return try: response = urlopen(self.url, timeout=5) with open(self.output_path, "wb") as f: f.write(response.read()) self.signals.download_finished.emit(self.index, self.output_path) except Exception as e: # 下载失败可自行补充降级逻辑,这里默认忽略 pass class ThumbnailDelegate(QtWidgets.QStyledItemDelegate): def __init__(self, parent=None): super().__init__(parent) self.downloading_set = set() # 记录正在下载的条目,避免重复请求 self.thread_pool = QtCore.QThreadPool.globalInstance() self.placeholder_icon = QtGui.QIcon() # 占位空图标,可替换为自定义加载状态图 def initStyleOption(self, option, index): super().initStyleOption(option, index) # 检查当前条目是否已经有图标 current_icon = option.icon if current_icon.isNull(): option.icon = self.placeholder_icon # 获取存储的url和缓存路径 url = index.data(QtCore.Qt.UserRole + 1) output_path = index.data(QtCore.Qt.UserRole) # 已经在下载就跳过 cache_key = f"{index.row()}_{output_path}" if cache_key not in self.downloading_set: self.downloading_set.add(cache_key) downloader = ThumbnailDownloader(index, url, output_path) downloader.signals.download_finished.connect(self.on_download_finished) self.thread_pool.start(downloader) @QtCore.Slot(QtCore.QModelIndex, str) def on_download_finished(self, index, output_path): # 从下载队列移除 cache_key = f"{index.row()}_{output_path}" if cache_key in self.downloading_set: self.downloading_set.remove(cache_key) # 通知主窗口更新model数据 self.parent().update_item_icon(index, output_path) class ExampleDialog(QtWidgets.QDialog): def __init__(self): super(ExampleDialog, self).__init__() self.itemModel = QtGui.QStandardItemModel() self.uiListView = QtWidgets.QListView() self.uiListView.setIconSize(QtCore.QSize(80, 60)) self.uiListView.setGridSize(QtCore.QSize(90, 70)) self.uiListView.setModel(self.itemModel) # 绑定自定义delegate self.delegate = ThumbnailDelegate(self) self.uiListView.setItemDelegate(self.delegate) self.mainLayout = QtWidgets.QVBoxLayout(self) self.mainLayout.addWidget(self.uiListView) self.populateImages() def populateImages(self): root = os.path.join(os.getenv('APPDATA'), 'MyApp\\cache') if not os.path.exists(root): os.makedirs(root) for x in range(20): url = 'https://picsum.photos/id/{}/80/60.jpg'.format(x) p = urlparse(url).path ext = os.path.splitext(p)[-1] output = os.path.join(root, '{}{}'.format(x, ext)) # 初始化item时只存数据,不执行同步下载 item = QtGui.QStandardItem('{}'.format(x)) item.setData(output, QtCore.Qt.UserRole) # 存储缓存路径 item.setData(url, QtCore.Qt.UserRole + 1) # 存储下载url self.itemModel.appendRow(item) @QtCore.Slot(QtCore.QModelIndex, str) def update_item_icon(self, index, output_path): # 更新model中对应条目的图标,QListView会自动刷新对应条目展示 if os.path.exists(output_path): pixmap = QtGui.QPixmap(output_path) self.itemModel.setData(index, pixmap, QtCore.Qt.DecorationRole) if __name__ == '__main__': app = QtWidgets.QApplication(sys.argv) window = ExampleDialog() window.resize(600, 400) window.show() window.raise_() sys.exit(app.exec_())
核心逻辑说明
- 用
QThreadPool+QRunnable实现异步下载,避免阻塞主界面线程,默认支持多任务并行下载 - 自定义delegate中维护下载中的条目集合,避免同一个条目重复发起下载请求
- 只有条目进入可见区域、触发
initStyleOption时才会发起下载,实现按需加载,减少不必要的资源消耗 - 下载完成后通过信号通知主线程更新model对应角色的数据,无需手动调用视图刷新接口
内容的提问来源于stack exchange,提问作者JokerMartini
相关产品推荐
相关产品推荐

