mins
mins

Reputation: 7524

Why doesn't QTreeView.scrollTo() work initially

The code below just displays a tree view of computer drives. Each time a new file/folder is selected, the view scrolls to make this new selection visible.

Question 1: While this works, the initial selection after the application is launched doesn't trigger the scroll. Why?

Question 2: If the instructions:

self.my_view.scrollTo(index, QAbstractItemView.EnsureVisible)
self.my_view.resizeColumnToContents(0)

are inverted:

self.my_view.resizeColumnToContents(0)
self.my_view.scrollTo(index, QAbstractItemView.EnsureVisible)

the first column size is not adjusted either on the initial display, only after. Why?

import sys
from PyQt5.QtCore import Qt, QModelIndex, QDir
from PyQt5.QtWidgets import QApplication, QTreeView, QMainWindow, QFileSystemModel, QAbstractItemView


class MyWindow(QMainWindow):
    def __init__(self):
        super().__init__()

        # Instance variables
        self.my_view = QTreeView()
        self.my_model = QFileSystemModel()

        # Init FS model to show all computer drives
        model_root_path = str(self.my_model.myComputer())
        self.my_model.setRootPath(model_root_path)

        # Init tree view
        self.my_view.setModel(self.my_model)
        self.my_view.setRootIndex(self.my_model.index(model_root_path))
        self.my_view.setSelectionMode(QAbstractItemView.SingleSelection)
        self.my_view.setSelectionBehavior(QAbstractItemView.SelectRows)

        # Connect selection change events to custom slot
        select_model = self.my_view.selectionModel()
        select_model.currentRowChanged.connect(self.current_row_changed)

        # Main window
        self.setCentralWidget(self.my_view)
        self.setGeometry(200, 200, 800, 600)

        # Select initial row on view
        focus_path = QDir.currentPath()
        focus_index = self.my_model.index(focus_path)
        self.my_view.setCurrentIndex(focus_index)

    def current_row_changed(self):
        """Current row of the model has changed"""

        # Scroll view to new row
        index = self.my_view.selectionModel().currentIndex()
        self.my_view.scrollTo(index, QAbstractItemView.EnsureVisible)
        self.my_view.resizeColumnToContents(0)

        # Show path of current row in window title
        absolute_path = self.my_model.filePath(index)
        self.setWindowTitle(absolute_path)


def main():
    a = QApplication(sys.argv)
    mw = MyWindow()
    mw.show()
    sys.exit(a.exec_())

if __name__ == '__main__':
    main()

`


Edit: After using the good solution provided by @ekhumoro, my sample code above worked. However this other piece of code still didn't:

import os
import sys

from PyQt5.QtCore import pyqtSignal, QTimer, QDir, Qt
from PyQt5.QtWidgets import QMainWindow, QGridLayout, QWidget, QTreeView, QAbstractItemView, QFileSystemModel, \
    QApplication


class AppWindow(QMainWindow):

    default_folder_path = "."

    def __init__(self):
        super().__init__()
        self.folder_view = FolderTreeView()
        self.folder_view.folder_has_changed.connect(self.folder_changed)
        self.build_ui()
        self.show()

        # Select initial folder
        self.select_initial_folder()

    def build_ui(self):
        main_widget = QWidget()
        layout = QGridLayout(main_widget)
        layout.addWidget(self.folder_view)
        self.setCentralWidget(main_widget)
        self.setGeometry(200, 100, 800, 600)

    def select_initial_folder(self):
        folder_index = self.folder_view.get_index(AppWindow.default_folder_path)
        if folder_index.isValid():
            self.folder_view.select_folder(folder_index)

    def folder_changed(self, folder_path):
        if not os.path.isdir(folder_path):
            print("Non existing folder:", folder_path)
            return


class FolderTreeView(QTreeView):
    folder_has_changed = pyqtSignal(str)

    def __init__(self):
        super().__init__()
        self.folder_tree_model = FolderTreeModel()
        self.setModel(self.folder_tree_model)
        self.setSelectionMode(QAbstractItemView.SingleSelection)
        self.setSelectionBehavior(QAbstractItemView.SelectRows)

    def select_folder(self, folder_index):
        self.setCurrentIndex(folder_index)

    def currentChanged(self, current, previous):
        super(FolderTreeView, self).currentChanged(current, previous)

        # Scroll the view to current item and resize folder name column
        QTimer.singleShot(50, lambda: self.delayed_scroll(current))

        # Emit signal for other uses
        self.folder_has_changed.emit(self.folder_tree_model.filePath(current))

    def delayed_scroll(self, index):
        self.scrollTo(index, QAbstractItemView.EnsureVisible)
        self.resizeColumnToContents(0)

    def get_index(self, folder_path):
        return self.folder_tree_model.index(folder_path)


class FolderTreeModel(QFileSystemModel):
    def __init__(self):
        super().__init__()
        self.setFilter(QDir.AllDirs | QDir.NoDotAndDotDot)
        self.setRootPath("")


def main():
    app = QApplication(sys.argv)
    window = AppWindow()
    sys.exit(app.exec_())

if __name__ == '__main__':
    main()

Upvotes: 3

Views: 1549

Answers (1)

ekhumoro
ekhumoro

Reputation: 120798

The first problem may be caused if, by default, the model initialises its current index to the current directory. This would mean that if you set it again to the same index, the row-change signal will not be emitted (because nothing changed). This can be fixed by calling the row-change handler directly:

class MyWindow(QMainWindow):
    def __init__(self):
        super().__init__()
        ...
        focus_path = QDir.currentPath()
        focus_index = self.my_model.index(focus_path)
        self.my_view.setCurrentIndex(focus_index)
        self.current_row_changed()

    def current_row_changed(self):
        index = self.my_view.currentIndex()
        self.my_view.scrollTo(index, QAbstractItemView.EnsureVisible)
        self.my_view.resizeColumnToContents(0)
        ...

As to the second problem: when you call scrollTo, it may have to expand several directories in order to select the required index. This could obviously change the width of the first column, so you should always call resizeColumnToContents afterwards in order to get the correct width.

UPDATE:

I think there is also another problem caused by timing issues. The QFileSystemModel must work asynchronously to some extent, because it has to request resources from the operating system and then wait for the response. Also, before it gets the response, it cannot know in advance exactly how much data it is going to receive, because the file-system may have been updated while it was waiting. Potentially, the response could include data from a huge directory containing thousands of files. So in order to keep the GUI responsive, the data is processed in batches which are of a sufficient size to fill the current view. If the current index is set before the window has been shown and all its widgets fully laid out, there is no guarantee that the view will be able to resize its columns correctly.

This can be fixed by explicitly re-calling the row-change handler via a one-shot timer with a small delay. This should allow the view to recalculate its column widths correctly:

    ...    
    focus_path = QDir.currentPath()
    focus_index = self.my_model.index(focus_path)
    self.my_view.setCurrentIndex(focus_index)
    QTimer.singleShot(50, self.current_row_changed)

def current_row_changed(self):
    index = self.my_view.currentIndex()
    self.my_view.scrollTo(index, QAbstractItemView.EnsureVisible)
    self.my_view.resizeColumnToContents(0)

Upvotes: 3

Related Questions