PySide6 Exclude Folders from QTreeView - python

I have an application where i need to display 2 different TreeViews. One for showing the folders (folderView) and the other will display the Files (fileView) inside the selected folder from the folderView. The following Code works fine but i am having a strange issue:
in the screen shot below, if i click on the bin folder for example, then switch back to VBoxGuestAdd.., the fileView will display the bin folder in the fileView.
p.s.: using an ubuntu 22.04 machine
and here my code:
import sys
from PySide6.QtCore import QDir
from PySide6.QtWidgets import QApplication, QWidget, QHBoxLayout, QTreeView, QFileSystemModel
def folderView_selectionchanged():
current_index = folderView.currentIndex()
selected_folder_path = folderModel.fileInfo(current_index).absoluteFilePath()
fileView.setRootIndex(fileModel.setRootPath(selected_folder_path))
app = QApplication(sys.argv)
window = QWidget()
layout = QHBoxLayout()
folderView = QTreeView()
folderModel = QFileSystemModel()
folderModel.setRootPath("/")
folderModel.setFilter(QDir.NoDotAndDotDot | QDir.AllDirs)
folderView.setModel(folderModel)
folderView.selectionModel().selectionChanged.connect(folderView_selectionchanged)
fileView = QTreeView()
fileModel = QFileSystemModel()
fileModel.setRootPath("/")
fileModel.setFilter(QDir.NoDotAndDotDot | QDir.Files)
fileView.setModel(fileModel)
layout.addWidget(folderView)
layout.addWidget(fileView)
window.setLayout(layout)
window.show()
app.exec()

This is a "bug" probably caused by the asynchronous nature of QFileSystemModel, which uses threading to fill the model and delays calls for the model structure updates.
It seems that it's also been already reported as QTBUG-93634, but it has got no attention yet.
A possible workaround is to "reset" the filter and set it again:
def folderView_selectionchanged():
current_index = folderView.currentIndex()
selected_folder_path = folderModel.fileInfo(current_index).absoluteFilePath()
fileView.setRootIndex(fileModel.setRootPath(selected_folder_path))
fileModel.setFilter(QDir.AllDirs)
fileModel.setFilter(QDir.NoDotAndDotDot | QDir.Files)
But the above might not work for big/slow file systems. The only possible solution I can think of is to use a QSortFilterProxyModel and override the filterAcceptsRow() function. It will not be as fast as the basic model, but it will work as expected.
Note that:
filterAcceptsRow() is always checked against the model hierarchy, so always allowing the filter to pass anything outside the filtered directory is mandatory, otherwise it will be filtered out (if the parent directory is filtered out, there is no child to show);
since setRootPath() invalidates the layout and checks the filters again, we must clear the validation until the new root path is set, and then restore it and reset the filters again; this is done by temporarily replacing the actual filtering function with a dummy one that always returns True;
class FileProxy(QSortFilterProxyModel):
validParent = None
def __init__(self):
super().__init__()
self.fsModel = QFileSystemModel()
self.setSourceModel(self.fsModel)
def filterAcceptsRow(self, row, parent):
return (
self.validParent != parent
or not self.fsModel.isDir(
self.fsModel.index(row, 0, parent))
)
def setRootPath(self, path):
func = self.filterAcceptsRow
self.filterAcceptsRow = lambda *args: True
self.validParent = self.fsModel.setRootPath(path)
self.filterAcceptsRow = func
self.invalidateFilter()
return self.mapFromSource(self.validParent)

Related

Why does layoutChanged.emit() not update the QListView when using a QSortFilterProxyModel?

I am having trouble getting QListView to work with proxy models.
I started using QListViews with models without any issues, and changed data during runtime using datachanged.emit() signals of that model.
However, when I try to change data in a proxy-model based QListView, that data is not getting updated in the interface.
I boiled it down to a minimal example, but still cannot see the root cause of this behaviour:
from PyQt5.QtWidgets import (QWidget, QHBoxLayout, QApplication, QListView)
from PyQt5 import QtCore
# model for QListView
class ListModel(QtCore.QAbstractListModel):
def __init__(self, items):
super().__init__()
self.items = items
def data(self, index, role):
if role == QtCore.Qt.DisplayRole:
return self.items[index.row()]
def rowCount(self, index):
return len(self.items)
class MainWindow(QWidget):
def __init__(self):
super().__init__()
# creating list with "standard" model and an initial dataset => this works
list_view_1 = QListView()
list_view_1.setModel(ListModel(['original', 'model']))
# creating list with proxy based on source model and an initial dataset => this works
list_view_2 = QListView()
proxy = QtCore.QSortFilterProxyModel()
proxy.setSourceModel(ListModel(['original', 'proxy+model']))
list_view_2.setModel(proxy)
# changing data in model and emitting layout change => this works
list_view_1.model().items = ['changed', 'model']
list_view_1.model().layoutChanged.emit()
# changing data in proxy and emitting layout change => this does not work?
list_view_2.model().items = ['changed', 'proxy+model']
list_view_2.model().layoutChanged.emit()
# adding layout to the interface
hbox = QHBoxLayout()
hbox.addWidget(list_view_1)
hbox.addWidget(list_view_2)
self.setLayout(hbox)
self.show()
app = QApplication([])
window = MainWindow()
app.exec_()
None of my searches led to an explanation of this behaviour, a related stack thread only mentioned the order of the models, which I already implemented that way.
Can anyone point me to a solution on how to update data in a "proxy-model" correctly?
The problem is that you are creating an attribute (items) in the proxy model instead of updating the source model items, the solution is to update the source model items:
# ...
# changing data in model and emitting layout change => this works
list_view_1.model().items = ["changed", "model"]
list_view_1.model().layoutChanged.emit()
# changing data in proxy and emitting layout change => this does not work?
list_view_2.model().sourceModel().items = ["changed", "proxy+model"]
list_view_2.model().layoutChanged.emit()
# ...

Different QFileSystemModel rowcount values of in different parts of the code [duplicate]

I am learn about Model/View architecture in pyqt, but when i follow the Using model indexes instruction and try to write a demo in pyqt5 style.The QModelIndex couldn't get child node information?
The code:
class DemoB(QPushButton):
def __init__(self):
super().__init__()
self.clicked.connect(self.on_clicked)
def on_clicked(self, checked):
model = QFileSystemModel()
model.setRootPath(QDir.homePath())
parentIndex = model.index(QDir.homePath())
print(parentIndex.data() )
print(parentIndex, model.rowCount(parentIndex), QDir.homePath())
for row in range(model.rowCount(parentIndex)):
index = model.index(row, 0, parentIndex)
print(index, index.data())
The result:
My folder:
Explanation:
As the docs(1, 2) points out:
Caching and Performance QFileSystemModel will not fetch any
files or directories until setRootPath() is called. This will prevent
any unnecessary querying on the file system until that point such as
listing the drives on Windows.
Unlike QDirModel, QFileSystemModel uses a separate thread to populate
itself so it will not cause the main thread to hang as the file system
is being queried. Calls to rowCount() will return 0 until the model
populates a directory.
QFileSystemModel keeps a cache with file information. The cache is
automatically kept up to date using the QFileSystemWatcher.
QModelIndex QFileSystemModel::setRootPath(const QString &newPath) Sets
the directory that is being watched by the model to newPath by
installing a file system watcher on it. Any changes to files and
directories within this directory will be reflected in the model.
If the path is changed, the rootPathChanged() signal will be emitted.
Note: This function does not change the structure of the model or
modify the data available to views. In other words, the "root" of the
model is not changed to include only files and directories within the
directory specified by newPath in the file system.
emphasis mine
The loading process is executed in a different thread and the loading is done asynchronously, so at the time you make the request the model is not yet loaded.
Solution:
The solution is to request the information after it has been loaded that will be notified through the directoryLoaded signal of QFileSystemModel:
from PyQt5.QtCore import pyqtSlot, QDir
from PyQt5.QtWidgets import QApplication, QFileSystemModel, QPushButton
class DemoB(QPushButton):
def __init__(self, parent=None):
super().__init__(parent)
self.clicked.connect(self.on_clicked)
self.model = QFileSystemModel(self)
self.model.directoryLoaded.connect(self.on_directoryLoaded)
#pyqtSlot()
def on_clicked(self):
self.model.setRootPath(QDir.homePath())
#pyqtSlot(str)
def on_directoryLoaded(self, directory):
parentIndex = self.model.index(directory)
for row in range(self.model.rowCount(parentIndex)):
index = self.model.index(row, 0, parentIndex)
print(index, index.data())
if __name__ == "__main__":
import sys
app = QApplication(sys.argv)
w = DemoB()
w.show()
sys.exit(app.exec_())

Why my QFileSystemModel QModelIndex couldn't get child node infomation?

I am learn about Model/View architecture in pyqt, but when i follow the Using model indexes instruction and try to write a demo in pyqt5 style.The QModelIndex couldn't get child node information?
The code:
class DemoB(QPushButton):
def __init__(self):
super().__init__()
self.clicked.connect(self.on_clicked)
def on_clicked(self, checked):
model = QFileSystemModel()
model.setRootPath(QDir.homePath())
parentIndex = model.index(QDir.homePath())
print(parentIndex.data() )
print(parentIndex, model.rowCount(parentIndex), QDir.homePath())
for row in range(model.rowCount(parentIndex)):
index = model.index(row, 0, parentIndex)
print(index, index.data())
The result:
My folder:
Explanation:
As the docs(1, 2) points out:
Caching and Performance QFileSystemModel will not fetch any
files or directories until setRootPath() is called. This will prevent
any unnecessary querying on the file system until that point such as
listing the drives on Windows.
Unlike QDirModel, QFileSystemModel uses a separate thread to populate
itself so it will not cause the main thread to hang as the file system
is being queried. Calls to rowCount() will return 0 until the model
populates a directory.
QFileSystemModel keeps a cache with file information. The cache is
automatically kept up to date using the QFileSystemWatcher.
QModelIndex QFileSystemModel::setRootPath(const QString &newPath) Sets
the directory that is being watched by the model to newPath by
installing a file system watcher on it. Any changes to files and
directories within this directory will be reflected in the model.
If the path is changed, the rootPathChanged() signal will be emitted.
Note: This function does not change the structure of the model or
modify the data available to views. In other words, the "root" of the
model is not changed to include only files and directories within the
directory specified by newPath in the file system.
emphasis mine
The loading process is executed in a different thread and the loading is done asynchronously, so at the time you make the request the model is not yet loaded.
Solution:
The solution is to request the information after it has been loaded that will be notified through the directoryLoaded signal of QFileSystemModel:
from PyQt5.QtCore import pyqtSlot, QDir
from PyQt5.QtWidgets import QApplication, QFileSystemModel, QPushButton
class DemoB(QPushButton):
def __init__(self, parent=None):
super().__init__(parent)
self.clicked.connect(self.on_clicked)
self.model = QFileSystemModel(self)
self.model.directoryLoaded.connect(self.on_directoryLoaded)
#pyqtSlot()
def on_clicked(self):
self.model.setRootPath(QDir.homePath())
#pyqtSlot(str)
def on_directoryLoaded(self, directory):
parentIndex = self.model.index(directory)
for row in range(self.model.rowCount(parentIndex)):
index = self.model.index(row, 0, parentIndex)
print(index, index.data())
if __name__ == "__main__":
import sys
app = QApplication(sys.argv)
w = DemoB()
w.show()
sys.exit(app.exec_())

Python3 PyQt5 setEnabled of a QAction causing crashes

For a project I'm creating a GUI using Python 3 and PyQt5. Because it has to be usable by people outside of my immediate team, I want to disable actions on the menu until they've already filled out some forms in other parts of the program (e.g. disabling the final solution view when they haven't set up the initial data connection). The issue is that when I try to call the QAction's setEnabled function outside of the function that created it (but still inside the overall class), it's causing my script to crash with no error code, so I'm having trouble understanding the issue. In the snipit below, I'm trying to set the "View Solution" menu option as true. There are some more options in that menu, but I deleted them here to make it more easy to read.
The code is structured something like this:
import sys
from PyQt5.QtWidgets import QMainWindow, QAction, qApp, QApplication, QMessageBox, QStackedLayout
class MediaPlanner(QMainWindow):
def __init__(self):
super().__init__()
self.initUI()
def initUI(self):
# Menu bar example from: zetcode.com/gui/pyqt5/
exitAction = QAction('&Exit', self)
exitAction.setShortcut('Ctrl+Q')
exitAction.setStatusTip('Exit application')
exitAction.triggered.connect(self.close)
newProject = QAction('&New Project', self)
newProject.setShortcut('Ctrl+N')
newProject.setStatusTip('Start A New Project')
newProject.triggered.connect(self.createNewProject)
openProject = QAction('&Open Project',self)
openProject.setShortcut('Ctrl+O')
openProject.setStatusTip('Open A Saved Project')
openProject.setEnabled(False)
viewSolution = QAction('&View Solution',self)
viewSolution.setStatusTip('View the Current Solution (If Built)')
viewSolution.setEnabled(False)
self.statusBar()
menubar = self.menuBar()
filemenu = menubar.addMenu('&File')
filemenu.addAction(newProject)
filemenu.addAction(openProject)
filemenu.addAction(exitAction)
viewmenu = menubar.addMenu('&View')
viewmenu.addAction(viewSolution)
self.setGeometry(300,300,700,300)
self.setWindowTitle('Menubar')
self.show()
def createNewProject(self):
print('Project Created')
self.viewSolution.setEnabled(True)
if __name__ == '__main__':
app = QApplication(sys.argv)
gui = MediaPlanner()
sys.exit(app.exec_())
The problem is that viewSolution is a variable, but it is not a member of the class so you will not be able to access it through the self instance. One possible solution is to make viewSolution member of the class as shown below:
self.viewSolution = QAction('&View Solution',self)
self.viewSolution.setStatusTip('View the Current Solution (If Built)')
self.viewSolution.setEnabled(False)
...
viewmenu.addAction(self.viewSolution)
Another possible solution is to use the sender() function, this function returns the object that emits the signal, using the following:
def createNewProject(self):
print('Project Created')
self.sender().setEnabled(True)

My QFileSystemModel doesn't work as expected in PyQt

EDIT2: model.hasChildren(parentIndex) returns True, but model.rowCount(parentIndex) returns 0. Is QFileSystemModel just fubar in PyQt?
EDIT: With a bit of adaptation this all works exactly as it should if I use QDirModel. This is deprecated, but maybe QFileSystemModel hasn't been fully implemented in PyQt?
I'm learning the Qt Model/View architecture at the moment, and I've found something that doesn't work as I'd expect it to. I've got the following code (adapted from Qt Model Classes):
from PyQt4 import QtCore, QtGui
model = QtGui.QFileSystemModel()
parentIndex = model.index(QtCore.QDir.currentPath())
print model.isDir(parentIndex) #prints True
print model.data(parentIndex).toString() #prints name of current directory
rows = model.rowCount(parentIndex)
print rows #prints 0 (even though the current directory has directory and file children)
The question:
Is this a problem with PyQt, have I just done something wrong, or am I completely misunderstanding QFileSystemModel? According to the documentation, model.rowCount(parentIndex) should return the number of children in the current directory. (I'm running this under Ubuntu with Python 2.6)
The QFileSystemModel docs say that it needs an instance of a Gui application, so I've also placed the above code in a QWidget as follows, but with the same result:
import sys
from PyQt4 import QtCore, QtGui
class Widget(QtGui.QWidget):
def __init__(self, parent=None):
QtGui.QWidget.__init__(self, parent)
model = QtGui.QFileSystemModel()
parentIndex = model.index(QtCore.QDir.currentPath())
print model.isDir(parentIndex)
print model.data(parentIndex).toString()
rows = model.rowCount(parentIndex)
print rows
def main():
app = QtGui.QApplication(sys.argv)
widget = Widget()
widget.show()
sys.exit(app.exec_())
if __name__ == '__main__':
main()
I've solved it.
The reason to use QFileSystemModel as opposed to QDirModel is because QFileSystemModel loads the data from the filesystem in a separate thread. The problem with that is that if you try to print the number of children just after it's been constructed is that it won't have loaded the children yet. The way to fix the above code is to add the following:
self.timer = QtCore.QTimer(self)
self.timer.singleShot(1, self.printRowCount)
to the end of the constructor, and add a printRowCount method which will print the correct number of children. Phew.
Since you've already figured it out, just a couple of extra thoughts on what was going on with your model: QFileSystemModel::rowCount returns rows from the visibleChildren collection; I guess you're correctly identified the problem: at the time when you're checking row count it was not yet populated. I've changed your example without using timers; pls, check if it works for you:
class Widget(QtGui.QWidget):
def __init__(self, parent=None):
QtGui.QWidget.__init__(self, parent)
self.model = QtGui.QFileSystemModel()
self.model.setRootPath(QtCore.QDir.currentPath())
def checkParent(self):
parentIndex = self.model.index(QtCore.QDir.currentPath())
print self.model.isDir(parentIndex)
print self.model.data(parentIndex).toString()
rows = self.model.rowCount(parentIndex)
print "row count:", rows
def main():
app = QtGui.QApplication(sys.argv)
widget = Widget()
widget.show()
app.processEvents(QtCore.QEventLoop.AllEvents)
widget.checkParent()
sys.exit(app.exec_())
if __name__ == '__main__':
main()
I believe your code should work correctly on any UI event after widget constructed is shown on the screen
regards

Categories

Resources