You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.09.30 15:39:02