I am here to ask your help as my last hope to get out of this hell.
Just to be clear, I am not here for reading comments like: there is a builtin in PyQt for what you are doing etc... I know it, its name is QFileSystemModel and it does what I'm trying to do on its own. I just want to make my personal File Browser for my application to customize everything. With that being said let's dive into this problem:
With the following code I was just trying to make a Custom File Browser that takes some base folders and generates the others on demand. For example: my base folders are 'C:' and 'E:', when i click on the Expand icon I trigger the 'expand signal' and remove the dummy item i added inside the ItemFactory.item() method and simply look for subfolders and append them to the current item.
Here is the code:
`from PyQt5.QtCore import QModelIndex
from PyQt5.QtGui import QStandardItemModel, QStandardItem
from PyQt5.QtWidgets import QAbstractItemView, QTreeView, QSizePolicy
from src.graphics.components.externalwindows.pathtreesupport.Item import ItemFactory
from src.tools.Tools import find_path, get_available_disks, ls, isWindows
from src.tools.documents.Document import Document
from src.tools.Variables import DataBase as db
from src.tools.documents.FileTypes import FT, Filetypes
class PathTree(QTreeView):
def __init__(self, parent, is_file: bool = False):
super().__init__(parent)
self.paths: list\[list\[str\]\] = \[\]
self.configurations(is_file)
self.starting_point(is_file)
self.expanded.connect(self.add_subdirectories)
self.destroyed.connect(self.emergency)
#self.doubleClicked.connect(lambda index: self.setPath(index))
self.setAnimated(True)
def configurations(self, is_file: bool):
self.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
with open(find_path("tree.qss")) as f:
self.setStyleSheet(f.read())
self.setEditTriggers(QAbstractItemView.NoEditTriggers)
self.setHeaderHidden(True)
model = QStandardItemModel(self)
self.setModel(model)
def starting_point(self, is_file: bool):
model: QStandardItemModel = self.model()
items = []
if not is_file:
items += [ItemFactory.item(i, FT.FFOLD) for i in get_available_disks()]
else:
items.append(ItemFactory.item(db.FOLDER.getValue(), FT.FFOLD))
model.appendColumn(items)
def add_subdirectories(self, index: QModelIndex):
if not index.isValid():
return
model: QStandardItemModel = self.model()
item: QStandardItem = model.itemFromIndex(index)
dummy = item.child(0, 0)
if dummy.text() == "dummy":
item_names = ls(self.genPathFromItem(item))
items = [ItemFactory.item(i, FT.FFOLD) for i in item_names]
if items:
print("DONE")
item.appendRows(items)
print("COMPLETED")
item.removeRow(0)
def genPathFromItem(self, item: QStandardItem):
copy = item
items: list[str] = [copy.text()]
while copy.parent() is not None:
parent = copy.parent()
items.append(parent.text())
copy = parent
if not isWindows():
items[-1] = "" # For fixing main directory issue
return Document.SEP.join(items[::-1]) + Document.SEP
def emergency(self, o):
self.expanded.disconnect()
self.destroyed.disconnect()
self.model().clear()`
Just to be clear: imports like FT, Filetypes, Database and Document are not really important for the solution because my problem is only about this class and the way items are being handled.
When I start my program and show the window of the QTreeView everything works, but, if I do it for a second time my program randomly crashes after expanding a variable number of items with the following exit code: -1073740791 (0xC0000409).
I have already found the source of the error: 'item.appendRows(items)' inside the 'add_subdirectories' method.
And I have already found out the low-level exception that is raised and that leads to this exit-code: RuntimeError: wrapped C/C++ object of type QStandardItem has been deleted.
I was just wondering if there is a way to check if an item is "not deleted" because I tried everything.
I have tried to:
- Subclass the model and the item class looking for something to check for the item validity but I didn't find anything;
- Handle the exception in the aforementioned line of code but this didn't work and still caused the crash of the entire application.
- Check if it was possible to add items after an event that is fired after clicking the expand icon but I didn't find any event available for this.
- Validate the item from the model but apparently they are not related except for a positional (row-column) relationship which is not useful to take into account.
- Validate the index of the item, but it is always valid and this does not make any sense because if the item is deleted how is it possible that the index is valid.
- Manage a possible row-deletion event with a custom model but no row-deletion was being done on my items.
These are all attempts I made or I am able to remember.
I want to mention, but I don't know if it could be useful, that the QTreeView is being put into a QFrame that is inside a secondary QMainWindow that works as Tool for my main application, if this could be an useful information I will provide the code of the QMainWindow.
Thank you in advance for the help.
This is the minimal reproducible code as requested:
import os
import re
import sys
import psutil
from PyQt5.QtCore import QModelIndex, QEvent, Qt
from PyQt5.QtGui import QStandardItemModel, QStandardItem
from PyQt5.QtWidgets import QSizePolicy, QAbstractItemView, QTreeView, QMainWindow, QPushButton, QDesktopWidget, QLabel, \
QVBoxLayout, QHBoxLayout, QFrame, QApplication, QLayout
if os.name == "nt":
import win32api
import win32con
def isWindows():
return os.name == "nt"
def is_file_hidden(file: str, path: str) -> bool:
if isWindows():
try:
attribute = win32api.GetFileAttributes(path)
return attribute & (win32con.FILE_ATTRIBUTE_HIDDEN | win32con.FILE_ATTRIBUTE_SYSTEM) != 0
except Exception as e:
del e
return False
return file.startswith(".")
def ls(path: str, ext: tuple = ()):
results: list[str] = []
try:
dirs = os.listdir(path)
for i in dirs:
valid = ext != () and re.match(fr"\b\w+\.({'|'.join(ext)})\b", i) is not None
if not valid:
fp = os.path.join(path, i) # full path
valid = os.path.isdir(fp) and not is_file_hidden(i, fp)
if valid:
results.append(i)
except:
pass
return results
def get_available_disks():
partitions = psutil.disk_partitions(all=False)
disks = [partition.device[:-1] for partition in partitions if "cdrom" not in partition.opts]
return disks
class ItemFactory:
fts = None
@staticmethod
def item(text: str):
item = QStandardItem(text)
item.appendRow(QStandardItem("dummy"))
return item
class PathTree(QTreeView):
SEP = "\\" if os.name == "nt" else "/"
def __init__(self, parent, is_file: bool = False):
super().__init__(parent)
self.paths: list[list[str]] = []
self.configurations(is_file)
self.starting_point(is_file)
self.expanded.connect(self.add_subdirectories)
self.destroyed.connect(self.emergency)
self.setAnimated(True)
def configurations(self, is_file: bool):
self.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
self.setEditTriggers(QAbstractItemView.NoEditTriggers)
self.setHeaderHidden(True)
model = QStandardItemModel(self)
self.setModel(model)
def starting_point(self, is_file: bool):
model: QStandardItemModel = self.model()
items = []
if not is_file:
items += [ItemFactory.item(i) for i in get_available_disks()]
model.appendColumn(items)
def add_subdirectories(self, index: QModelIndex):
if not index.isValid():
return
model: QStandardItemModel = self.model()
item: QStandardItem = model.itemFromIndex(index)
dummy = item.child(0, 0)
if dummy.text() == "dummy":
item_names = ls(self.genPathFromItem(item))
items = [ItemFactory.item(i) for i in item_names]
if items:
print("DONE")
item.appendRows(items)
print("COMPLETED")
item.removeRow(0)
def genPathFromItem(self, item: QStandardItem):
copy = item
items: list[str] = [copy.text()]
while copy.parent() is not None:
parent = copy.parent()
items.append(parent.text())
copy = parent
if not isWindows():
items[-1] = "" # For fixing main directory issue
return PathTree.SEP.join(items[::-1]) + PathTree.SEP
def emergency(self, o):
self.expanded.disconnect()
self.destroyed.disconnect()
self.model().clear()
def wOpen(anywidget=None, size: tuple = (400, 450)):
tw = TrashWindow(anywidget, size) # Error
tw.show() # Error
class TrashWindow(QMainWindow):
def __init__(self, widget, size: tuple = (300, 300)):
super(QMainWindow, self).__init__()
self.setObjectName("Trash")
self.setConfigurations(size)
self.___widget = widget
self.frame = QFrame(self)
self.setMainWidget(widget)
self.setCentralWidget(self.frame)
def setConfigurations(self, size: tuple):
self.setWindowFlag(Qt.FramelessWindowHint, True)
self.setWindowFlag(Qt.Tool, True)
self.setMinimumWidth(size[0])
self.setMinimumHeight(size[1])
self.centerOnScreen()
def setMainWidget(self, widget):
widget.setParent(self.frame)
ml: QVBoxLayout = QVBoxLayout(self.frame)
tl: QHBoxLayout = QHBoxLayout(self.frame)
ml.setContentsMargins(0, 0, 0, 0)
ml.setSpacing(0)
ml.setSizeConstraint(QLayout.SetNoConstraint)
tl.setContentsMargins(0, 0, 0, 0)
tl.setSpacing(0)
tl.setSizeConstraint(QLayout.SetNoConstraint)
ml.addLayout(tl)
ml.addWidget(widget)
tl.addWidget(QLabel(self))
tl.addWidget(TrashButton(self.frame, self))
tl.setStretch(0, 5)
tl.setStretch(1, 1)
def centerOnScreen(self):
desktop = QDesktopWidget()
screen_geometry = desktop.screenGeometry()
x = (screen_geometry.width() - self.width()) // 2
y = (screen_geometry.height() - self.height()) // 2
self.move(x, y)
class TrashButton(QPushButton):
def __init__(self, parent, mw: QMainWindow):
super(QPushButton, self).__init__(parent)
self.mw: QMainWindow = mw
self.setFixedHeight(30)
self.setText("x")
self.setObjectName("Button")
self.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.MinimumExpanding)
def mousePressEvent(self, e: QEvent):
self.mw.destroy(True, True) # Error
self.mw.close() # Error
class MainWindow(QMainWindow):
def __init__(self):
super().__init__()
# Set window title
self.setWindowTitle("Main Window")
# Create a central widget
central_widget = QPushButton("Click Me!")
central_widget.clicked.connect(self.button_clicked)
self.setCentralWidget(central_widget)
def button_clicked(self):
wOpen(PathTree(None)) # Unuseful
if __name__ == "__main__":
app = QApplication(sys.argv)
window = MainWindow()
window.show()
sys.exit(app.exec_())
The previous code has been fixed with the following changes:
- Removing wOpen function and moving its body to the button clicked Slot
button_clicked(self). - Inside the
MainWindowclass create an attribute and set it to None. - In the
button_clicked(self)check the value of the attribute, if it is None create the TrashWindow assigning its reference to the attribute. - Replace the
destroy(True, True)andclose()methods inside theTrashButtonclass with thedeleteLater()QObject's method.
Again, thanks for the help!