# Calling setCurrentIndex and scrollTo is inconsistent with a custom QListView and QAbstractListModel Original (deleted): https://stackoverflow.com/questions/79504092/calling-setcurrentindex-and-scrollto-is-inconsistent-with-a-custom-qlistview-and 2025-03-12 15:35:32Z 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()`. ```python 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](https://stackoverflow.com/questions/79499823/using-pyqt6-and-a-qlistview-backed-by-a-qabstractlistmodel-setcurrentindex-and). However, none of these code review style suggestions fixed the actual problem. 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**. This program takes the path as its first argument. Generating sample files can be done using the following: ``` #!/bin/sh mkdir -p /tmp/test1 for i in `seq 1 10`; do touch /tmp/test1/$i done mkdir -p /tmp/test2; for i in `seq 1 100`;do touch /tmp/test2/$i done ``` Then simply run `python example.py /tmp/test1` or `python example.py /tmp/test2`. You will see `test1` will always maintain the currently selected index on reshuffles reliably, but `test2` will not. ``` 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, path): super().__init__() browser = MediaBrowser(path) self.setCentralWidget(browser) def main(): app = QApplication(sys.argv) window = MainWindow(sys.argv[1]) window.show() sys.exit(app.exec()) if __name__ == '__main__': main() ``` **EDIT**: Here is another example with the background thumbnail thread (which was already gutted but still active) fully removed. In this example. the `setSelectedIndex()` does select the previous index reliably, but `scrollTo()` will not move the view to the selected item. So there are two issues here, why is `scrollTo()` not working in this case? (It does not work at all. In the previous version, if `setSelectedIndex()` worked, `scrollTo()` would also worked if called from a `QTimer.singleShot`. In this example it does not work, with or without a QTimer delay). `setSelectedIndex()` does work in this case. So why does it not work with the background thread that's needed to update the preview image? Is there a better way to do preview images that will allow `setSelectedIndex()` to work? ``` import sys from os import listdir from os.path import isfile, join, basename from random import shuffle from typing import Optional from PyQt6 import QtCore from PyQt6.QtCore import QSize, Qt, pyqtSlot, QModelIndex, QAbstractListModel, QVariant, pyqtSignal from PyQt6.QtGui import QImage, QPixmap from PyQt6.QtWidgets import QApplication, QListView, QAbstractItemView, QListWidget, QWidget, QStyle, QMainWindow class FileListModel(QAbstractListModel): numberPopulated = pyqtSignal(int) def __init__(self, dir_path: str): super().__init__() 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}) 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'] if role == Qt.ItemDataRole.DisplayRole: return QVariant(basename(filename)) if role == Qt.ItemDataRole.DecorationRole: 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().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, path): super().__init__() browser = MediaBrowser(path) self.setCentralWidget(browser) def main(): app = QApplication(sys.argv) window = MainWindow(sys.argv[1]) window.show() sys.exit(app.exec()) if __name__ == '__main__': main() ``` ### Comments Is this really the **smallest possible** program that reproduces your problem? I think there's a lot that could be removed - I don't think we need any `QImage` or `os.listdir` - the latter shows this example is not **self-contained**. Please create a real [minimal reproducible example](https://stackoverflow.com/help/minimal-reproducible-example) with simple values so we can all generate the same results. – [Toby Speight](https://stackoverflow.com/users/4850040/toby-speight) Commented Mar 13 at 11:02 If I remove any more, it may not solve the actual bug on my actual program. The model is a complex one because it's used as an image thumbnailer. This example is 230 lines and only has one dependency. I've cut out a lot and the program is still clearly reproducible. I think the model is important because the issue could be outside the `mousePressEvent()` block and I want to give people full context. I need a solution to a real bug I have in [mediahug](https://gitlab.com/djsumdog/mediahug/), not a trivial problem that may not solve the actual issue in my open source application. – [djsumdog](https://stackoverflow.com/users/696836/djsumdog) Commented Mar 13 at 20:28 I added additional scripts to produce a series of test files. Everything needed to reproduce this is contained in this question. I also see two more request to close a question with a complete reproducable example! Please stop this. If you don't have a solution, that's fine, but it doesn't help anyone to shut down a question with fully reproducable code! – [djsumdog](https://stackoverflow.com/users/696836/djsumdog) Commented Mar 13 at 20:34 @djsumdog You should keep removing more and more until the problem is no longer reproducible. This is a very simple and effective way to narrow down the problem, and will often lead directly to a solution. If you don't make the effort to do this yourself, it means someone else will have to, which is not helpful. This is why the guidelines ask for a **minimal** reproducible example. Saying that you "think" it's this or it "may be" that is not constructive. You can easily eliminate such guesswork with some basic trial and error debugging. – [ekhumoro](https://stackoverflow.com/users/984421/ekhumoro) Commented Mar 13 at 23:14 @ekhumoro I didn't include the most minimal example in the PyQT documentation work fine. But it doesn't cover things like creating the previews asynchronously. I've added another example where I remove the thread entirely. setSelectedIndex works here, but scrollTo no longer works. I've literally refactored this a few times, tried to emit an event after the model refresh, and a few dozen other things I don't want to bloat this post with. There is an issue within nuances of Qt here and I'm hoping an expert can point it out. I have spent days debugging this. – [djsumdog](https://stackoverflow.com/users/696836/djsumdog) Commented Mar 14 at 2:18 I also edited the question to ask about the potential issue with the background thread. If you have a solution or a suggestion, please let me know and I will try it. Complaining about the questions, or trying to close it, isn't going to help anybody. – [djsumdog](https://stackoverflow.com/users/696836/djsumdog) Commented Mar 14 at 2:24 You're doing a wrong assumption with `QModelIndex.isValid()`; as the [docs explain](https://doc.qt.io/qt-6/qmodelindex.html): "A valid index belongs to a model, and has non-negative row and column numbers". There is no mention about the model also **having** that index. `scrollTo()` doesn't work because at that point the index has not been fetched yet, therefore it doesn't exist. What you need to use is [hasIndex()](https://doc.qt.io/qt-6/qabstractitemmodel.html#hasIndex), and eventually call `fetchMore()` until either `hasIndex()` or `canFetchMore()` return True. Then do a delayed `scrollTo`. – [musicamante](https://stackoverflow.com/users/2001654/musicamante) Commented Mar 14 at 17:56 We don't close questions whimsically. We may vote to if we find they don't follow SO guidelines. Note that your code is still far from being minimal: for example, the file listing is irrelevant (just generate a list of strings), the decoration and size hints are not part of the issue (which you could've found out if you tried to remove them). But even without considering this, if we didn't previously complain about your code, we would have never got to the point in which we **finally** got a more accurate code that better allows us (including you) to isolate and find the issue. – [musicamante](https://stackoverflow.com/users/2001654/musicamante) Commented Mar 14 at 18:14 Finally: the main issue here is about standard model/view aspects, the part about the threading should be asked in a separate question. Still, consider what already suggested in your previous post: UI elements should **never** be created or accessed in external threads: `__load_thumb` is directly called from the thread and tries to create a QWidget, which is completely inappropriate: you are just lucky that your OS configuration allows that, as in many cases it could cause a crash. Also, creating a QWidget to get the style is just wrong: use `QApplication.style()`. – [musicamante](https://stackoverflow.com/users/2001654/musicamante) Commented Mar 14 at 18:27