I created a simple MediaBrowser using a QListWidget
in PyQT5:
class MediaBrowser(QListWidget):
def __init__(self, database: Database, viewtab, dir_path):
QListWidget.__init__(self)
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.current_file_widgets = []
self.thumb_loader_thread = None
self.itemSelectionChanged.connect(self.selection_change)
# Should theoretically speed things up but it does not
# self.setSizeAdjustPolicy(QListWidget.AdjustToContents)
self.setSelectionMode(QAbstractItemView.SingleSelection)
self.setViewMode(QListWidget.IconMode)
self.setResizeMode(QListWidget.Adjust)
self.setIconSize(QSize(THUMB_WIDTH, THUMB_HEIGHT))
self.load_files(dir_path)
...
...
def all_files(self):
return self.findItems('*', Qt.MatchWildcard)
def load_files(self, dir_path):
if self.thumb_loader_thread and self.thumb_loader_thread.isRunning():
self.log.info('Killing Previous Thumbnail Loading Thread')
self.thumb_loader_thread.requestInterruption()
self.thumb_loader_thread.wait(sys.maxsize)
self.log.info('Previous Thumbnail Thread Done')
self.clear()
# Load New File widgets
onlyfiles = [f for f in listdir(dir_path) if isfile(join(dir_path, f))]
for f in onlyfiles:
vid = join(dir_path, f)
self.log.debug(f"Creating File/Thumb Widget {vid}")
self.addItem(ThumbWidget(vid, self.database))
self.thumb_loader_thread = ThumbLoaderThread(self.all_files(), dir_path)
self.thumb_loader_thread.start()
The MediaBrowser
which is a QListWidget
, adds a bunch of ThumbWidget
items (which are QListWidgetItem
objects) when it starts:
class ThumbWidget(QListWidgetItem):
def __init__(self, filename: str, database):
QListWidgetItem.__init__(self)
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):
metadata = self.database.file_metadata(self.filename)
img_thumb = metadata['thumb']
if img_thumb:
img = QPixmap()
img.loadFromData(img_thumb, 'JPEG')
self.setIcon(QIcon(img))
This takes a lot of time at startup. I'd like to only load a thumbnail for the item when it is scrolled into view. Elsewhere within my code, the MediaBrowser
is within a QScrollArea
.
self.media_scroller = QScrollArea()
self.media_scroller.setWidget(self._media_browser)
self.media_scroller.setWidgetResizable(True)
Is there any way to get events to know when a particular QWidgetItem
is current scrolled in/out of view? That way I can load and unload thumbnails, making for more efficient startup times.
The full code for this project can be found here:
https://gitlab.com/djsumdog/mediahug/-/tree/master/mediahug/gui/viewtab
Thanks for all the comments on the question. It's interesting the linked documentation was to the Python section of QT's website, but the code on it was C++. I've used it to create a model for handling files in PyQT5. There are some things like database
and a thumbnailer I implement elsewhere, but it should be easy to adapt for anyone looking for a Python implementation of a QAbstractListModel
that handles files:
import logging
from functools import lru_cache
from os import listdir
from os.path import isfile, join, basename
from PyQt5 import QtCore
from PyQt5.QtCore import QAbstractListModel, QModelIndex, QVariant, Qt, QSize, pyqtSignal
from PyQt5.QtGui import QIcon, QPixmap
from PyQt5.QtWidgets import QStyle, QWidget
class FileListModel(QAbstractListModel):
numberPopulated = pyqtSignal(int)
def __init__(self, database: Database, dir_path: str):
super().__init__()
self.log = logging.getLogger('<your app name>')
self.database = database
self.files = []
self.loaded_file_count = 0
self.set_dir_path(dir_path)
def rowCount(self, parent: QModelIndex = QtCore.QModelIndex()) -> int:
return 0 if parent.isValid() else self.loaded_file_count
def set_dir_path(self, dir_path: str):
self.beginResetModel()
self.files = []
self.loaded_file_count = 0
only_files = [f for f in listdir(dir_path) if isfile(join(dir_path, f))]
for f in only_files:
vid = join(dir_path, f)
self.files.append(vid)
self.endResetModel()
def data(self, index: QModelIndex, role: int = Qt.DisplayRole):
if not index.isValid():
return QVariant()
if index.row() >= len(self.files) or index.row() < 0:
return QVariant()
if role == Qt.DisplayRole:
filename = basename(self.files[index.row()])
return QVariant(filename)
if role == Qt.DecorationRole:
thumb = self.__load_thumb(self.files[index.row()])
if thumb:
return thumb
else:
return QWidget().style().standardIcon(QStyle.SP_FileIcon)
if role == Qt.SizeHintRole:
return QSize(THUMB_WIDTH, THUMB_HEIGHT + FILENAME_MARGIN)
return QVariant()
def fetchMore(self, parent: QModelIndex) -> None:
if parent.isValid():
return
remainder = len(self.files) - self.loaded_file_count
items_to_fetch = min(100, remainder)
if items_to_fetch <= 0:
self.log.debug("No More Items to Fetch")
return
self.log.debug(f'Loaded Items: {self.loaded_file_count} / Items to Fetch: {items_to_fetch}')
self.beginInsertRows(QModelIndex(), self.loaded_file_count, self.loaded_file_count + items_to_fetch - 1)
self.loaded_file_count += items_to_fetch
self.endInsertRows()
self.numberPopulated.emit(items_to_fetch)
def canFetchMore(self, parent: QModelIndex) -> bool:
if parent.isValid():
return False
can_fetch = self.loaded_file_count < len(self.files)
self.log.debug(f'Can Fetch More? {can_fetch}')
return can_fetch
@lru_cache(maxsize=5000)
def __load_thumb(self, filename):
self.log.debug(f'Loading Thumbnail For {filename}')
metadata = self.database.file_metadata(filename)
img_thumb = metadata['thumb']
if img_thumb:
img = QPixmap()
img.loadFromData(img_thumb, 'JPEG')
return QIcon(img)
In the above example, I'm loading thumbnails from my database layer and added an lru_cache
annotation around it. This works for now, but there will be a delay on large folders while scrolling, if thumbnails haven't been generated/cached yet.
A future improvement and speed up would involve changing the if role == Qt.DecorationRole
section of data()
to always return a loading icon if the thumbnail is not immediately available in memory. Then have a background tasks, thread or delegate that then loads or generates the thumbnail in the background, and use the dataChanged()
call on the model to indicate the thumbnail is now ready.
I also changed the QListWidget
to a QListView
and change the following in the constructor:
self.setLayoutMode(QListView.Batched)
self.setBatchSize(10)
self.setUniformItemSizes(True)
self.file_list_model = FileListModel(database, dir_path)
self.setModel(self.file_list_model)
self.selectionModel().selectionChanged.connect(self.selection_change)