pythonqtselectionpyside6qcolumnview

How to deselect columns in PySide/Qt QColumnView


I am creating a simple File Browser and trying to implement Miller Columns like found in the macOS Finder. Qt provides both QColumnView and a QFileSystemModel which should make it easy to combine and get the functionality I'm after.

However, if you click on several levels of directories, then click on an empty space a couple levels up from the current directory, the view doesn't change. The highlight is removed from the folder you're clicking in, but that is the only change to the visual.

As an example of what I am trying to do, on top is the Finder and on bottom is my current app:

Incorrect column functionality example

I have tried intercepting as many Signals and Slots as I can think of, including pressed, clicked, entered, selectionModel().currentRowChanged, and UpdateRequest to override Qt's behavior and set the correct currentIndex, but have not found the state information in the Model or View useful for setting the correct Index.

I have also tried logging every event (with a bare def event() override) and there doesn't even seem to be an event triggered when I make the deselection click in the root folder.

MCVE:

from pathlib import Path

from PySide6.QtWidgets import (
    QApplication,
    QFileSystemModel,
    QColumnView,
    QMainWindow,
)


class CustomColumns(QColumnView):
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)


class BrowserWindow(QMainWindow):
    def __init__(self, location=None):
        super().__init__()
        if location is None:
            location = Path.home()
        location = Path(location)
        self.setGeometry(625, 333, 1395, 795)
        self.setWindowTitle(location.name)

        self._root = str(location)
        self._files = QFileSystemModel()
        self._files.setRootPath(self._root)

        self._view = CustomColumns()
        self._view.setModel(self._files)

        self.setCentralWidget(self._view)

    def show(self):
        super().show()
        self._view.setRootIndex(self._files.index(self._root))


class FileBrowser(QApplication):
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.setQuitOnLastWindowClosed(False)
        self.window = BrowserWindow(location=Path.home())
        self.window.show()


def main():
    app = FileBrowser()
    app.exec()


if __name__ == "__main__":
    main()

Solution

  • The issue here is that a QColumnView is actually composed of a series of QListView columns. So the column-view itself isn't much help when trying to handle most signals and events. One way to work-around this is to reimplement createColumn and install an event-filter on the viewport of each view so that the relevant mouse-events can be tracked. This is necessary because the built-in signals of QListView won't fire when clicking on a blank area.

    Below is a basic demo that shows one way to implement this. A separate QObject class is used to watch events so as not to interfere with the existing event-filter of the column-view. Clicking on a blank area of any column will move the current-index to the clicked column:

    from pathlib import Path
    from PySide6.QtWidgets import (
        QApplication, QFileSystemModel, QColumnView, QMainWindow, QListView,
        )
    from PySide6.QtCore import (
        QEvent, QObject, Signal, QModelIndex,
        )
    
    class ColumnWatcher(QObject):
        columnClicked = Signal(QModelIndex, bool)
    
        def eventFilter(self, source, event):
            try:
                if (isinstance(view := source.parent(), QListView) and
                    event.type() == QEvent.MouseButtonPress):
                    index = view.indexAt(event.position().toPoint())
                    self.columnClicked.emit(view.rootIndex(), not index.isValid())
                return super().eventFilter(source, event)
            except RuntimeError:
                return False
    
    class CustomColumns(QColumnView):
        def __init__(self, *args, **kwargs):
            super().__init__(*args, **kwargs)
            self._watcher = ColumnWatcher(self)
            self._watcher.columnClicked.connect(self.handleClicked)
    
        def createColumn(self, index):
            view = super().createColumn(index)
            view.viewport().installEventFilter(self._watcher)
            return view
    
        def handleClicked(self, root, blank):
            if blank:
                if root != self.rootIndex():
                    self.setCurrentIndex(root)
                else:
                    self.setRootIndex(root)
                    self.setCurrentIndex(QModelIndex())
    
    class BrowserWindow(QMainWindow):
        def __init__(self, location=None):
            super().__init__()
            if location is None:
                location = Path.home()
            location = Path(location)
            self.setGeometry(625, 333, 1395, 795)
            self.setWindowTitle(location.name)
    
            self._root = str(location)
            self._files = QFileSystemModel()
            self._files.setRootPath(self._root)
    
            self._view = CustomColumns()
            self._view.setModel(self._files)
    
            self.setCentralWidget(self._view)
    
        def showEvent(self, event):
            self._view.setRootIndex(self._files.index(self._root))
    
    class FileBrowser(QApplication):
        def __init__(self, *args, **kwargs):
            super().__init__(*args, **kwargs)
            self.setQuitOnLastWindowClosed(False)
            self.window = BrowserWindow(location=Path.home())
            self.window.show()
    
    def main():
        app = FileBrowser()
        app.exec()
    
    if __name__ == "__main__":
        main()