Using PyQt6 and a QListView, backed by a QAbstractListModel, setCurrentIndex and scrollTo only work sometimes Original (deleted): https://stackoverflow.com/questions/79499823/using-pyqt6-and-a-qlistview-backed-by-a-qabstractlistmodel-setcurrentindex-and 2025-03-11 06:25:29Z If you find this question and are stuck with the same issue, I [re-asked it here with the changes mentioned in the comments](https://stackoverflow.com/questions/79504092/with-pyqt6-calling-setcurrentindex-and-scrollto-is-inconsistant-with-a-custom-ql). In my PyQt6 application, there is a situation where I am updating the underlying `QAbstractListModel` for a `QListView`. After the update, I'm trying to select a specific index. The `QListView` has `self.setSelectionMode(QAbstractItemView.SelectionMode.SingleSelection)` in its constructor. This is the code that's called after a model update. ```python idx = self._media_browser.get_file_index(self.current_file) q_idx = self._media_browser.model().createIndex(idx, 0) if not q_idx.isValid(): self.log.error('Index is invalid') self._media_browser.scrollTo(q_idx) self._media_browser.setCurrentIndex(q_idx) ``` `self.current_file` stores the file name of the item I want to select. I never get the `Index is invalid` message in my logs. I've tried to remove the `scrollTo` and change the order of it and the setting of the Index. Sometimes, the correct file will get selected. Sometimes it won't. It's completely arbitrary. I've tried adding a `QApplication.processEvents()` before calling this and it doesn't make a difference. I've also tried wrapping the logic in a `QTimer.singleShot(100...` and that didn't improve the reliability either. The exact revision and location of where I've placed this function can be found here: https://gitlab.com/djsumdog/mediahug/-/blob/be7ff2fded26bcd8643035c588942983b7b1b4ff/mediahug/gui/viewtab/viewtab.py#L73 The `get_file_index()` looks like the following: ```python def get_file_index(self, filename: str) -> Optional[int]: for i, file in enumerate(self.all_files()): if file['filename'] == filename: return i ``` I'm guessing this is some kind of race condition. Is there a way to wait and ensure the `QListView` is ready to select/scroll to the given index? The index is valid, but sometimes the `setCurrentIndex` selects the correct item and sometimes it does not. ### Minimal Example I've been asked in the comments to provide a minimal example. This is a very complex program with a lot of moving components. This is the smallest example I could trim down into a single Python file. It reads files from `/tmp/media`. Left click selects an element, right click will shuffle the order. On a directory with only 3 or 4 files, the currently selected file will be re-selected after a shuffle. Add in enough files to require pagination and the re-selection becomes unreliable. The location where I am doing this is in the `def mousePressEvent(self, event):...` ```python 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 QIcon, QPixmap from PyQt6.QtWidgets import QApplication, QListView, QAbstractItemView, QListWidget, QWidget, QStyle, QMainWindow def video_thumb(filename: str, threshold=10, thumb_width=250, thumb_height=250): try: import cv2 vcap = cv2.VideoCapture(filename) # Jump to Middle of file total_frames = vcap.get(cv2.CAP_PROP_FRAME_COUNT) vcap.set(cv2.CAP_PROP_POS_FRAMES, total_frames / 2) # Make sure we don't get a blank frame res, im_ar = vcap.read() while im_ar.mean() < threshold and res: res, im_ar = vcap.read() # Create Thumbnail im_ar = cv2.resize(im_ar, (thumb_width, thumb_height), 0, 0, cv2.INTER_LINEAR) res, thumb_buf = cv2.imencode('.jpeg', im_ar) bt = thumb_buf.tobytes() return bt # noqa except Exception as e: print(f'Could not generate thumbnail for {filename}. Error {e}') return None class ThumbLoaderThread(QThread): thumbnail_loaded = pyqtSignal(QModelIndex, QIcon) def __init__(self): QThread.__init__(self) self.thumbnail_queue = Queue() self.error_icon = QWidget().style().standardIcon(QStyle.StandardPixmap.SP_DialogCancelButton) def add_thumbnail(self, index: QModelIndex, filename: str): self.thumbnail_queue.put({'index': index, 'filename': filename}) def run(self) -> None: print('Starting Thumbnailer Thread') while not self.isInterruptionRequested(): try: item = self.thumbnail_queue.get(timeout=1) thumb = self.__load_thumb(item['filename']) if thumb: self.thumbnail_loaded.emit(item['index'], thumb) else: self.thumbnail_loaded.emit(item['index'], self.error_icon) except Empty: ... @lru_cache(maxsize=5000) def __load_thumb(self, filename): print(f'Loading Thumbnail For {filename}') thumb = video_thumb(filename, 10, 250, 250) img = QPixmap() img.loadFromData(thumb, 'JPEG') return QIcon(img) 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, index: QModelIndex, thumbnail: QIcon): self.files[index.row()]['thumbnail'] = thumbnail self.dataChanged.emit(index, index) 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(index, 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): QListView.__init__(self) 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.scrollTo(q_idx) self.setCurrentIndex(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): QMainWindow.__init__(self) 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() ``` Please provide an appropriate [minimal reproducible example](https://stackoverflow.com/help/minimal-reproducible-example), don't put the link to external resources, as they may not be accessible to some, or may change in the future, thus making the question invalid. – [musicamante](https://stackoverflow.com/users/2001654/musicamante) Commented Mar 11 at 12:29 @musicamante This is a very complicated program and it's very difficult to distill a minimal example. I added the smallest example I could trim down that does reproduce the issue. – [djsumdog](https://stackoverflow.com/users/696836/djsumdog) Commented Mar 11 at 18:39 With all due respect, I sincerely doubt that the `video_thumb` function (and its cv requirement) is actually necessary for the reproducibility of the issue: you could just put a `time.sleep()` there and return a value. Besides, as with most UI elements, QPixmap and QIcon are not considered thread safe and should never be created in threads outside the main one. The usage of QModelIndex in a thread is also a possible issue: as the docs explain, they should only be used immediately and then discarded, which may be a huge problem if the model is being updated in the meantime. – [musicamante](https://stackoverflow.com/users/2001654/musicamante) Commented Mar 12 at 2:46 You may try to queue a QPersistentModelIndex instead, but it's quite likely that it would still be invalid anyway, and that's because QAbstractItemModel is not thread safe. Since you're using a mono dimensional model dealing with file names, using the file name alone should be enough, then in the main thread look up for the name in the model and update its data by creating the appropriate object: QImage is thread safe, so just create/emit that in the thread, and then convert it to a QIcon with `QPixmap.fromImage()` in the main thread. – [musicamante](https://stackoverflow.com/users/2001654/musicamante) Commented Mar 12 at 2:53 Please take your time to improve your code and make it more minimal, as right now it contains lots of things that are quite distracting, which will make most possible answerers decide to ignore your post even if they would've been able to help you. 1. ensure that the thread is part of the issue (and fix it considering what suggested above); 2. if it is, replace the cv function with a dummy one that just "needs time"; 3. remove other irrelevant aspects: for example, checking an index parent (or row boundaries if used in a listview) in a list model is pointless, as it is returning QVariants. – [musicamante](https://stackoverflow.com/users/2001654/musicamante) Commented Mar 12 at 3:03 Finally, make your "super calls" more consistent: you're indiscriminately using `.function(self)`, `super(Class, self).function()` and `super().function()`. While it's obviously not relevant for the question, writing more consistent (and, therefore, _good_) code improves your chances of people keeping reading it, instead of giving up because of its syntax: everybody appreciates and possibly upvote a question with well written code (including people not able to answer) considering your efforts, thus increasing the visibility of your post and, therefore, the possibility of receiving help. – [musicamante](https://stackoverflow.com/users/2001654/musicamante) Commented Mar 12 at 3:13