[Python] With PyQt6 calling setCurrentIndex and scrollTo is inconsistant with a custom...

Discussão em 'Python' iniciado por Stack, Março 12, 2025.

  1. Stack

    Stack Membro Participativo

    Within a QListView, I'm attempting to maintain the currently selected element after the the list changes its sort order. I've tried using createIndex() and index(). In the following block createIndex() always returns a valid index, but it only selects/scrolls to the correct element some of the time. Specifically if I only have a couple of elements currently being displayed.

    The moment I switch to a large number of elements (30+) where scrolling is required, I still get a valid Index, but the QListView does not select the element with setCurrentIndex().

    def mousePressEvent(self, event):
    """Prevent context menu from also selecting a file"""
    if event.type() == QtCore.QEvent.Type.MouseButtonPress:
    if event.button() == Qt.MouseButton.RightButton:
    # In our minimalistic example, right click
    # Means we will shuffle
    self.chdir(self.current_directory)

    if self.current_file:
    idx = self.model().get_file_index(self.current_file)
    print(f'Attempting to select and scroll to {self.current_file} at index {idx}')
    q_idx = self.model().createIndex(idx, 0)
    if not q_idx.isValid():
    print('Index is invalid')
    self.setCurrentIndex(q_idx)
    self.scrollTo(q_idx)
    else:
    super(MediaBrowser, self).mousePressEvent(event)


    I load thumbnails in the background for this particular model. I've created a minimal example that removes the thumbnail loading, and is only dependent on PyQt6. It had a hard coded path to load files from /tmp/media at the bottom. Left click selects and right click shuffles.

    I have fixed the thread safety and consistency issues the last time I asked this question. Tell me what you want and I'll edit it, but this is a real bug I have picked up a few times and have struggled for days on. This is a basic example that is replicable.

    import sys
    from os import listdir
    from os.path import isfile, join, basename
    from functools import lru_cache
    from queue import Queue, Empty
    from random import shuffle
    from typing import Optional

    from PyQt6 import QtCore
    from PyQt6.QtCore import QSize, Qt, pyqtSlot, QModelIndex, QAbstractListModel, QVariant, QThread, pyqtSignal
    from PyQt6.QtGui import QImage, QPixmap
    from PyQt6.QtWidgets import QApplication, QListView, QAbstractItemView, QListWidget, QWidget, QStyle, QMainWindow


    class ThumbLoaderThread(QThread):

    thumbnail_loaded = pyqtSignal(str, QImage)

    def __init__(self):
    super().__init__()
    self.thumbnail_queue = Queue()
    self.error_icon = QWidget().style().standardIcon(QStyle.StandardPixmap.SP_DialogCancelButton).pixmap(250, 250).toImage()

    def add_thumbnail(self, filename: str):
    self.thumbnail_queue.put(filename)

    def run(self) -> None:
    print('Starting Thumbnailer Thread')
    while not self.isInterruptionRequested():
    try:
    filename = self.thumbnail_queue.get(timeout=1)
    thumb = self.__load_thumb(filename)
    if thumb:
    self.thumbnail_loaded.emit(filename, thumb)
    else:
    self.thumbnail_loaded.emit(filename, self.error_icon)
    except Empty:
    ...

    @lru_cache(maxsize=5000)
    def __load_thumb(self, filename):
    print(f'Loading Thumbnail For {filename}')
    # In the real application, I use openCV to create a thumbnail here
    # For right now, we're just using a standard image
    return QWidget().style().standardIcon(QStyle.StandardPixmap.SP_FileIcon).pixmap(250, 250).toImage()


    class FileListModel(QAbstractListModel):

    numberPopulated = pyqtSignal(int)

    def __init__(self, dir_path: str):
    super().__init__()
    self.thumbnail_thread = ThumbLoaderThread()
    self.thumbnail_thread.thumbnail_loaded.connect(self.thumbnail_generated)
    self.thumbnail_thread.start()
    self.files = []
    self.loaded_file_count = 0
    self.set_dir_path(dir_path)

    def thumbnail_generated(self, filename: str, thumbnail: QImage):
    idx = self.index_for_filename(filename)
    if idx >= 0:
    q_idx = self.createIndex(idx, 0)
    self.files[idx]['thumbnail'] = QPixmap.fromImage(thumbnail)
    self.dataChanged.emit(q_idx, q_idx)

    def index_for_filename(self, filename) -> int:
    for index, item in enumerate(self.files):
    if item.get('filename') == filename:
    return index
    return -1

    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))]

    # The full program has sorting
    # In this minimal example, we'll just shuffle the order
    shuffle(only_files)

    for f in only_files:
    vid = join(dir_path, f)
    self.files.append({'filename': vid, 'thumbnail': None})
    self.endResetModel()

    def data(self, index: QModelIndex, role: int = Qt.ItemDataRole.DisplayRole):
    if not index.isValid():
    return QVariant()

    if index.row() >= len(self.files) or index.row() < 0:
    return QVariant()

    filename = self.files[index.row()]['filename']
    thumbnail = self.files[index.row()]['thumbnail']

    if role == Qt.ItemDataRole.DisplayRole:
    return QVariant(basename(filename))

    if role == Qt.ItemDataRole.DecorationRole:
    if thumbnail:
    return thumbnail
    else:
    self.thumbnail_thread.add_thumbnail(filename)
    return QWidget().style().standardIcon(QStyle.StandardPixmap.SP_BrowserReload)

    if role == Qt.ItemDataRole.SizeHintRole:
    return QSize(250, 250 + 25)

    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:
    print("No More Items to Fetch")
    return

    print(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 get_file_index(self, filename: str) -> Optional[int]:
    for i, file in enumerate(self.files):
    if file['filename'] == filename:
    return i

    def canFetchMore(self, parent: QModelIndex) -> bool:
    if parent.isValid():
    return False
    can_fetch = self.loaded_file_count < len(self.files)
    return can_fetch


    class MediaBrowser(QListView):

    def __init__(self, dir_path):
    super().__init__()
    self.setLayoutMode(QListView.LayoutMode.Batched)
    self.setBatchSize(10)
    self.setUniformItemSizes(True)
    self.current_directory = dir_path

    self.setSelectionMode(QAbstractItemView.SelectionMode.SingleSelection)
    self.setViewMode(QListWidget.ViewMode.IconMode)
    self.setResizeMode(QListWidget.ResizeMode.Adjust)
    self.setIconSize(QSize(250, 250))

    self.file_list_model = FileListModel(dir_path)
    self.setModel(self.file_list_model)
    self.selectionModel().selectionChanged.connect(self.selection_change)
    self.current_file = None

    def mousePressEvent(self, event):
    """Prevent context menu from also selecting a file"""
    if event.type() == QtCore.QEvent.Type.MouseButtonPress:
    if event.button() == Qt.MouseButton.RightButton:
    # In our minimalistic example, right click
    # Means we will shuffle
    self.chdir(self.current_directory)

    if self.current_file:
    idx = self.model().get_file_index(self.current_file)
    print(f'Attempting to select and scroll to {self.current_file} at index {idx}')
    q_idx = self.model().createIndex(idx, 0)
    if not q_idx.isValid():
    print('Index is invalid')
    self.setCurrentIndex(q_idx)
    self.scrollTo(q_idx)
    else:
    super(MediaBrowser, self).mousePressEvent(event)

    def chdir(self, directory: str):
    print(f'Change Directory {directory}.')
    self.current_directory = directory
    self.load_files(directory)

    @pyqtSlot()
    def selection_change(self):
    selected = self.selectionModel().selectedIndexes()
    if len(selected) != 1:
    print(f'Invalid Selection {selected}')
    else:
    s = selected[0]
    print(f'Item Selection {s}')
    self.current_file = self.get_model_filename(s.row())

    def showEvent(self, event):
    super().showEvent(event)
    QApplication.processEvents()

    def all_files(self):
    return self.file_list_model.files

    def get_model_filename(self, index):
    return self.all_files()[index]['filename']

    def load_files(self, dir_path):
    try:
    self.file_list_model.set_dir_path(dir_path)
    except PermissionError as e:
    print(f'{e.strerror}')


    class MainWindow(QMainWindow):

    def __init__(self):
    super().__init__()
    browser = MediaBrowser("/tmp/media")
    self.setCentralWidget(browser)


    def main():
    app = QApplication(sys.argv)
    window = MainWindow()
    window.show()
    sys.exit(app.exec())


    if __name__ == '__main__':
    main()

    Continue reading...

Compartilhe esta Página