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_())
Related
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)
Is it possible to add another .py file (via import) and use it as an initialisation and event handler for a PyQt5 Multiwindow project other window & main .py files?
I have a project that consists of the following files:
main.py - the main app which imports the Ui...py files below & the ui_init_events.py file also
main.py - the main program
Ui_main_window_ui.py - a complied Qt UI to display the main window
Ui_frmConsoleLog_ui.py - a complied Qt UI to display a window with a textEdit & comboBox objects
ui_init_events.py - I want this file to have functions for setting each windows Ui object fields as well as contain the Ui windows object events such as btn.clicked.connect(self.SomeotherFunc)
I have found both these posts helpful but am stuck as I do not know how to reference objects other than the self object and can't find where this is explained. See: PyQt: Modifying Widget Object from another Function
Multiple Windows in PyQt4
In the 2nd post they are using multi-windows (via the QDialog) object also, however they are only using a single .py file. I am using Visual Studio Code & have built the Ui files then complied them. They are works in progress so I expect to make more changes meaning they wil be overwritten so I do not want to edit these files.
I cannot work out how to reference and change the properties in another window for initialisation purposes. The thread is:
Here is the main bits. Currently I call a function to return some data using self.combobox.additems (see #1) but I think calling this over and over again from main.py somewhat decreases code readability.
(#2) Therefore I would like advice how to move all the initialisation parts of an existing PyQt window (being an imported .py file used to generate the window + controls) into a separate .py file (eg named ui_init_events.py).
However in trying & researching this I do not know nor can find an example how to reference the objects using their full hierarchical naming convention. I have tried Application. QWindow., etc. I only know how to use self.... and it isn't working (of course as that would be the referring to the function itself I understand that its being called in, not the PyQt window I'm wanting to reference). See (#3) for what is not working.
Any ideas please on resources available to assist with understanding how PyQt labels its objects from the root of the object tree is appreciated as well as an example of how to set the comboBox_Selector in the Ui_frmConsoleLog window from app.py or another .py file other than from the Ui_frmConsoleLog_ui.py or within the class definition under --init--(self, parent=None): ?
The desired file structure is like this:
+-- main.py (or app.py)
|
+--- Ui_main_window_ui.py (has setupui & adds objects for main window)
|
+--- Ui_main_window_init.py (has init code for Ui_main_window_ui.py)
|
+--- Ui_main_window_events.py (has event code for Ui_main_window_ui.py)
|
+--- Ui_frmConsoleLog_ui.py (has setupui & adds objects for 2nd window)
|
+--- Ui_frmConsoleLog_init.py (has init code for Ui_frmConsoleLog_ui.py)
|
+--- Ui_frmConsoleLog_events.py (has event code for Ui_frmConsoleLog_ui.py)
main.py file contents
# Default imports
import sys, os, io, sched, time
import ui_resources_rc
from PyQt5.QtCore import QObject, QThread, pyqtSignal
from configparser import ConfigParser
...a portion of code here uses a ini config file for retrieving/saving some settings of the UI & I want to set the properties for each object accordingly...
from PyQt5.QtWidgets import (QApplication, QDialog, QMainWindow, QMessageBox, QWidget)
from PyQt5.uic import loadUi
from Ui_main_window_ui import Ui_MainWindow
from Ui_frmConsoleLog_ui import Ui_frmConsoleLog
from ui_init_events import (funcUpdateFormCombos)
...todo... open a network connection to the system (its an inverter)
# This window will be used for filtering network poll event types
class ConsoleLogWindow(QWidget, Ui_frmConsoleLog):
def __init__(self):
super().__init__()
self.setWindowTitle('Console Log')
class AppWindow(QMainWindow, Ui_MainWindow):
def __init__(self, parent=None):
super().__init__(parent)
self.win = None # No Console Log window visible yet.
self.setupUi(self)
#(1) Here is where I will have to call this a lot of times to
# populate the vaious window objects which I dont want to do
self.comboBox_Selector.addItems(funcUpdateFormCombos())
self.action_Console.triggered.connect(self.OpenConsoleLogWindow)
self.action_About.triggered.connect(self.about)
# Populate the controls as per the config file
#initialise() <-- (2) Here is where I want to call a
# function in ui_init_events.py to setup
# the form's objects as created in QtDesigner
# & exported as a .py file. I want to continue
# to edit each PyQt window later so I dont want
# to add to the Ui_... files themselves but in
# main.py call a function that has the code to
# set the object fields like checked, combobox
# list contents, etc.
def OpenConsoleLogWindow(self, checked):
if self.win is None:
self.win = ConsoleLogWindow()
self.win.show()
else:
self.win.close() # Close window.
self.win = None # Discard reference.
if __name__ == "__main__":
app = QApplication(sys.argv)
win = AppWindow()
win.show()
print('HERE')
sys.exit(app.exec())
ui_init_events.py file contents (this is the file I want to house all window object setup without editing the PyQt UI converted .ui to .py files such as Ui_main_window_u.py & Ui_frmConsoleLog_ui.py which I don't want to edit at all.
# Contains the initialisation of widgets fields
def funcUpdateFormCombos():
LIST = ['Amps', 'Volts', 'Watts']
LIST.sort()
return tuple(LIST)
# Would prefer to have a single file per Ui_...py file that houses
# all the events that get triggered for each Ui windows.
# How is this done? eg: <not working> (3)
def initialise()
frmConsoleLog.self.comboBox_Selector.addItems('Watts','Amps','Volts')
Contents of Ui_frmConsoleLog_ui.py (has the combobox I want to populate)
from PyQt5 import QtCore, QtGui, QtWidgets
class Ui_frmConsoleLog(object):
def setupUi(self, frmConsoleLog):
frmConsoleLog.setObjectName("frmConsoleLog")
frmConsoleLog.resize(640, 468)
frmConsoleLog.setToolTip("")
frmConsoleLog.setStatusTip("")
frmConsoleLog.setAccessibleName("")
self.horizontalLayout = QtWidgets.QHBoxLayout(frmConsoleLog)
self.horizontalLayout.setContentsMargins(0, 1, 0, 0)
self.horizontalLayout.setObjectName("horizontalLayout")
self.comboBox_Selector = QtWidgets.QComboBox(frmConsoleLog)
self.comboBox_Selector.setObjectName("comboBox_Selector")
self.horizontalLayout.addWidget(self.comboBox_Selector)
self.textEdit_ConsoleLog = QtWidgets.QTextEdit(frmConsoleLog)
self.textEdit_ConsoleLog.setEnabled(True)
sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Fixed, QtWidgets.QSizePolicy.Fixed)
sizePolicy.setHorizontalStretch(0)
sizePolicy.setVerticalStretch(0)
sizePolicy.setHeightForWidth(self.textEdit_ConsoleLog.sizePolicy().hasHeightForWidth())
self.textEdit_ConsoleLog.setSizePolicy(sizePolicy)
self.textEdit_ConsoleLog.setObjectName("textEdit_ConsoleLog")
self.horizontalLayout.addWidget(self.textEdit_ConsoleLog)
self.retranslateUi(frmConsoleLog)
QtCore.QMetaObject.connectSlotsByName(frmConsoleLog)
def retranslateUi(self, frmConsoleLog):
_translate = QtCore.QCoreApplication.translate
frmConsoleLog.setWindowTitle(_translate("frmConsoleLog", "Console Log"))
Worked it out, thanks to an answer in my previous post (that the poster removed?), here's the snippet that works and allows your functions in another .py file to set MainWindow pyQt object control properties in the parent main.py file for a much tidier project. I honestly don't care that professional devs think that is not good practice (if they do they are very closed minded).
In app.py (or main.py - I had two files and was playing with names)
class AppWindow(QMainWindow, Ui_MainWindow):
def __init__(self, parent=None):
super().__init__(parent)
self.win = None # No Console Log window visible yet.
self.setupUi(self)
from init_Ui_main_window import (initMainWindow)
initMainWindow(self)
...
In init_Ui_main_window.py:
def initMainWindow(window):
funcLoadConfig()
# Populate the controls in the main window
window.line_Settings_Simple_Default_Selection.setText(str(SETTINGS_SIMPLE_DEFAULT_SELECTOR))
Note: SETTINGS_SIMPLE_DEFAULT_SELECTOR is a globally defined static variable that wont ever be changed as its read from a config ini file. The user will be able to change the line value if they want to override the default, but it's nice to have a default value populated.
Was that easy as when initMainWindow(self) is called self from the previous object context is passed across to the function and set in function variable window, so to reference the main windows objects its just a case of referencing window.[object name as one would in main.py]
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_())
This is the code i have to display a tree view of a directory called "C:\Myfolder".
import sys
from PyQt4 import QtGui,QtCore
class Myview(QtGui.QMainWindow):
def __init__(self,parent=None):
QtGui.QMainWindow.__init__(self)
model = QtGui.QFileSystemModel()
model.setRootPath('C:\Myfolder')
view = QtGui.QTreeView()
view.setModel(model)
self.setCentralWidget(view)
if __name__ == '__main__':
app = QtGui.QApplication(sys.argv)
myview = Myview()
myview.show()
sys.exit(app.exec_())
Even though i set the RootPath to "C:\Myfolder" , the tree view display all the drives and folder.
How can i limit the QFileSystemModel so that TreeView only display items inside "C:\Myfolder" directory?
You would need to add view.setRootIndex(model.index("C:\Myfolder")) according to the QFileSystemModel documentation.
It is extremely important that you write "C:/Myfolder" and NOT "C:\Myfolder". Otherwise, it thinks the directory does not exist and will always show you all drives and all folders.
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