Reputation: 4626
I'm trying to add a label to show the number of files in a folder. Seems easy enough, but recently I noticed that the numbers were not right (probably been wrong forever, I just never noticed). I've tried this in pyqt5 and pyqt6 on linux.
For demonstration purposes, I create a folder with 20 files in it, but filtered to show only 10 files. I then set the filters via timer to show only 2 of the files and then again with all files. You'll see from the output below that it takes a second for the counts to update.
import os.path
import sys
import tempfile
# from PyQt6 import QtCore, QtGui, QtWidgets
from PyQt5 import QtCore, QtGui, QtWidgets
def test(path):
view = QtWidgets.QTreeView()
# model = QtGui.QFileSystemModel(view) # pyqt6
model = QtWidgets.QFileSystemModel(view) # pyqt5
view.setModel(model)
model.setRootPath(path)
index = model.index(model.rootPath())
view.setRootIndex(index)
model.setNameFilterDisables(False) # True shows filtered items disabled...False removes them
model.setNameFilters(['*.foo'])
def loaded():
print(f'loaded {model.rowCount(index)=}')
def update_filter2():
model.setNameFilters(['1.*'])
print(f'update_filter {model.rowCount(index)=} expected=2') # TODO: how to get this to report 2
def update_filter20():
model.setNameFilters([])
print(f'update_filter {model.rowCount(index)=} expected=20') # TODO: how to get this to report 20
model.directoryLoaded.connect(loaded)
QtCore.QTimer.singleShot(2000, update_filter2)
QtCore.QTimer.singleShot(3000, update_filter2)
QtCore.QTimer.singleShot(4000, update_filter20)
QtCore.QTimer.singleShot(5000, update_filter20)
return view
if __name__ == '__main__':
app = QtWidgets.QApplication([])
# create test data and clean up afterwards
with tempfile.TemporaryDirectory() as temp_dir_name:
print(temp_dir_name)
for i in range(10):
with open(os.path.join(temp_dir_name, f'{i}.foo'), 'w') as file:
print(i, file=file)
with open(os.path.join(temp_dir_name, f'{i}.bar'), 'w') as file:
print(i, file=file)
w = test(temp_dir_name)
w.show()
sys.exit(app.exec())
If I wait a second, I do see the expected row count. So I guess the question is, short of timer logic. How do you know when it is safe to call rowCount()? I was hoping to see directoryLoaded signaled again. Is there some other signal?
/tmp/tmplwv8nxi4
loaded model.rowCount(index)=10
loaded model.rowCount(index)=10
update_filter model.rowCount(index)=10 expected=2
update_filter model.rowCount(index)=2 expected=2
update_filter model.rowCount(index)=2 expected=20
update_filter model.rowCount(index)=20 expected=20
Upvotes: 0
Views: 155
Reputation: 48300
As the documentation explains:
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.
By default, models have a maximum default number of items that are fetched, and QFileSystemModel delays fetchMore()
on sorting and filtering using the same threading aspects. This makes instantaneous row counting unreliable.
Since you only need to display a "static" count of the visible items, the solution is to properly connect to the signals that actually change the layout of the model:
def updateCount():
print(model.rowCount(view.rootIndex()))
model.layoutChanged.connect(updateCount)
model.rowsRemoved.connect(updateCount)
model.rowsInserted.connect(updateCount)
layoutChanged
is usually enough for basic sorting/filtering, but since you might also be interested in directory changes when an item is created or removed (including renaming), the other two signals should be connected too.
For performance reasons, it's usually better to create a QTimer instance with the singleShot
flag set and a very low interval (more than 0, but less than 10-20ms), and then connect those signals to the start
slot of the timer. In this way the timer is eventually restarted if more than one signal is fired in a short period of time.
Do not use the static QTimer.singleShot
for that.
timer = QTimer(interval=10, timeout=updateCount)
model.layoutChanged.connect(timer.start)
model.rowsRemoved.connect(timer.start)
model.rowsInserted.connect(timer.start)
Upvotes: 3