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

如何在PyQt5中监听QListWidgetItem可见性变化并触发回调

实现QListWidget项的可视区域检测与缩略图按需加载

核心思路

QListWidget本身没有直接提供项进入/离开视图的信号,但可以通过监听滚动事件、结合可视区域判断来实现按需加载。具体步骤如下:


步骤1:修改MediaBrowser初始化与加载逻辑

取消启动时的全量缩略图加载线程,改用滚动停止后检测可视项的方式,同时引入线程池异步加载缩略图,避免阻塞UI。

from PyQt5.QtCore import QTimer, Qt, QRect, QMetaObject
from concurrent.futures import ThreadPoolExecutor
import logging
from os import listdir, path
from PyQt5.QtWidgets import QListWidget, QAbstractItemView, QStyle, QWidget

# 假设已定义THUMB_WIDTH、THUMB_HEIGHT、FILENAME_MARGIN常量
# 假设Database类已实现file_metadata方法

class MediaBrowser(QListWidget):
    def __init__(self, database, viewtab, dir_path):
        super().__init__()
        self.log = logging.getLogger('mediahug')
        self.database = database
        self.viewtab = viewtab

        self.setLayoutMode(QListView.Batched)
        self.setBatchSize(10000)
        self.setUniformItemSizes(True)

        self.current_directory = dir_path
        self.loaded_thumbs = set()  # 记录已加载缩略图的项
        self.itemSelectionChanged.connect(self.selection_change)

        self.setSelectionMode(QAbstractItemView.SingleSelection)
        self.setViewMode(QListWidget.IconMode)
        self.setResizeMode(QListWidget.Adjust)
        self.setIconSize(QSize(THUMB_WIDTH, THUMB_HEIGHT))

        # 滚动停止后延迟检测可视项,避免频繁触发
        self.thumb_check_timer = QTimer(self)
        self.thumb_check_timer.setSingleShot(True)
        self.thumb_check_timer.setInterval(100)  # 100ms延迟,可按需调整
        self.thumb_check_timer.timeout.connect(self.check_visible_items)

        # 线程池,控制并发加载数量,避免资源占用过高
        self.thread_pool = ThreadPoolExecutor(max_workers=4)

        self.load_files(dir_path)

    def scrollEvent(self, event):
        super().scrollEvent(event)
        # 滚动时重启定时器,确保只有滚动停止后才执行检测
        self.thumb_check_timer.start()

    def check_visible_items(self):
        viewport_rect = self.viewport().rect()
        visible_items = []

        # 遍历所有项,判断是否在可视区域内
        for row in range(self.count()):
            item = self.item(row)
            item_rect = self.visualRect(self.indexFromItem(item))
            
            # 判断项与可视区域是否相交
            if viewport_rect.intersects(item_rect):
                visible_items.append(item)
            else:
                # 对离开可视区域的项,可选恢复占位图标以释放内存
                if item in self.loaded_thumbs:
                    standard_file_icon = QWidget().style().standardIcon(QStyle.SP_FileIcon)
                    item.setIcon(standard_file_icon)
                    self.loaded_thumbs.remove(item)
        
        # 加载可视项的缩略图
        self.load_visible_thumbs(visible_items)

    def load_visible_thumbs(self, visible_items):
        for item in visible_items:
            if item not in self.loaded_thumbs:
                # 异步加载缩略图,避免阻塞UI
                future = self.thread_pool.submit(item.load_thumb)
                future.add_done_callback(lambda fut, item=item: self.on_thumb_loaded(fut, item))

    def on_thumb_loaded(self, future, item):
        # 线程任务完成后,在主线程更新UI
        img = future.result()
        if img:
            # 使用QMetaObject确保在主线程执行UI操作
            QMetaObject.invokeMethod(self, "_update_item_icon", 
                                    Qt.QueuedConnection,
                                    args=(item, img))

    def _update_item_icon(self, item, img):
        item.setIcon(QIcon(img))
        self.loaded_thumbs.add(item)

    def load_files(self, dir_path):
        # 重置线程池与已加载记录
        self.thread_pool.shutdown(wait=False)
        self.thread_pool = ThreadPoolExecutor(max_workers=4)
        self.loaded_thumbs.clear()

        self.clear()
        # 仅加载项的占位信息,不加载缩略图
        onlyfiles = [f for f in listdir(dir_path) if path.isfile(path.join(dir_path, f))]
        for f in onlyfiles:
            vid = path.join(dir_path, f)
            self.log.debug(f"Creating File/Thumb Widget {vid}")
            self.addItem(ThumbWidget(vid, self.database))
        
        # 初始化时加载当前可视区域的缩略图
        self.check_visible_items()

    # 保留原有的selection_change方法
    def selection_change(self):
        # 你的原有逻辑
        pass

步骤2:修改ThumbWidget的加载逻辑

让load_thumb仅负责获取图片数据,不直接操作UI(UI操作必须在主线程):

from os.path import basename
from PyQt5.QtWidgets import QListWidgetItem, QStyle, QWidget
from PyQt5.QtGui import QPixmap, QIcon

class ThumbWidget(QListWidgetItem):
    def __init__(self, filename: str, database):
        super().__init__()
        self.filename = filename
        self.database = database
        self.setText(basename(self.filename))
        # 初始设置占位图标
        standard_file_icon = QWidget().style().standardIcon(QStyle.SP_FileIcon)
        self.setIcon(standard_file_icon)
        self.setSizeHint(QSize(THUMB_WIDTH, THUMB_HEIGHT + FILENAME_MARGIN))

    def __str__(self):
        return f'Thumbnail for {self.filename}'

    def load_thumb(self):
        # 仅在子线程中获取图片数据,不操作UI
        metadata = self.database.file_metadata(self.filename)
        img_thumb = metadata['thumb']
        if img_thumb:
            img = QPixmap()
            img.loadFromData(img_thumb, 'JPEG')
            return img
        return None

关键说明

  1. 滚动事件处理:通过scrollEvent监听滚动动作,配合定时器实现“滚动停止后再检测”,避免滚动过程中频繁触发加载逻辑,提升性能。
  2. 可视区域判断:利用viewport().rect()获取当前可视区域,通过visualRect(item.index)获取项的显示区域,使用intersects方法判断项是否进入可视范围。
  3. 异步加载:使用ThreadPoolExecutor实现多线程异步加载缩略图,避免阻塞UI线程;加载完成后通过QMetaObject.invokeMethod确保在主线程更新图标,符合PyQt的线程安全要求。
  4. 内存优化:对离开可视区域的项,可选择恢复占位图标,释放缩略图占用的内存,尤其在处理大量媒体文件时效果明显。

内容的提问来源于stack exchange,提问作者djsumdog

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.17 21:57:18