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

