Issue
So I have the main window. When I click a button in the main window, a new widget is created (in a new window):
self.doorButton.clicked.connect(self.open_door)
def open_door(self):
self.doorwin = QtWidgets.QWidget()
self.doorui = doorwinActions(self.doors)
self.doorui.setupUi(self.doorwin)
self.doorwin.show()
The new QWidget or doorwin has only one widget - tableWidget
I use the object self.doors to populate the table. Now since I have a worker thread (QThread) updating the said object (self.doors), I use QTimer to repopulate the table every 1 second.
class doorwinActions(Ui_doorWin):
def __init__(self,doors):
self.doors = doors
# update setupUi
def setupUi(self, Widget):
super().setupUi(Widget)
Widget.move(self.left, self.top) # set location for window
Widget.setWindowTitle(self.title) # change title
self.timer = QTimer()
self.timer.timeout.connect(lambda:self.popTable())
self.timer.start(1000)
def popTable(self):
mutex.lock()
entries = len(self.doors)
self.tableWidget.setRowCount(entries)
for i in range(entries):
self.tableWidget.setItem(i,0,QtWidgets.QTableWidgetItem(str(self.doors[i][0])))
self.tableWidget.setItem(i,1,QtWidgets.QTableWidgetItem(self.doors[i][1].get_id()))
self.tableWidget.setItem(i,2,QtWidgets.QTableWidgetItem(self.doors[i][1].get_name()))
self.tableWidget.setItem(i,3,QtWidgets.QTableWidgetItem(str(self.doors[i][2])))
mutex.unlock()
When I run the program, it runs smoothly. It opens a new window. If the self.doors object is updated while the window is open, the GUI reflects the change. BUT, the problem occurs if I reopen the window. If I close the window and then click on the button again, the program crashes with the error:
RuntimeError: wrapped C/C++ object of type QTableWidget has been deleted
From what I understand about my code, when I close the window, the whole widget window (and the table) is deleted. And when I click on the doorButton, new Qwidget/table are created. So, the question is, why would it delete something it just created?
What (sort of) works? - If I move the setup of the door window to the main window's setup, it works. So the open_door function would just be:
def open_door(self):
self.doorwin.show()
The rest would be in the main window setup. But the problem is, then even when I close the window, the QTimer is still going in the background, just eating up processing power.
So, either,
- How do I stop the event when the window is closed OR
- How do I stop the tableWidget from being deleted?
Solution
Your main problem is garbage collection.
When you do this:
def open_door(self):
self.doorwin = QtWidgets.QWidget()
self.doorui = doorwinActions(self.doors)
self.doorui.setupUi(self.doorwin)
self.doorwin.show()
You are creating a new QWidget. It has absolutely no other reference but self.doorwin
. This means that if you call open_door
again, you will be overwriting self.doorwin
, and since the previous widget was not referenced anywhere else, it will get deleted along with all its contents.
Now, QTimers are tricky. You created a QTimer in doorwinActions
and QTimers can be persistent even if they have no parent: they keep going on until they're stopped or deleted, and they can only be deleted explicitly or when their parent is deleted (with the exception of timers created with the static QTimer.singleShot()
function).
Finally, you must remember that PyQt (like PySide) is a binding. It creates "connections" with the objects created in Qt (let's call them "C++ objects"), and through those bindings we can access those objects, their functions and so on, through python references.
But, and this is of foremost importance, both objects can have a different lifespan:
- the python reference can be deleted, and the Qt object can still exist;
- the Qt object can be destroyed, yet we still have the python object that referenced (past tense) it;
This is exactly what happens in your case: the self.doorui
object is overwritten, but it has an object (the QTimer, self.timer
) that is still alive, so the Python garbage collector will not delete it, and the timer is still able to call popTable
. But, at that point, the widget (self.doorwin
) and its contents have been destroyed on the "C++ side", which causes your crash: while self.tableWidget
still exists as a python reference, the actual widget has been destroyed along with its parent widget, and calling its functions causes a fatal error as the binding cannot find the actual object.
Now, how to solve that?
There are various options, and it depends on what you need to do with that window. But, before that, there is something much more important.
You have been manually editing a file generated by pyuic
, but those files are not intended for that. They are to be considered like "resource" files, used only for their purpose (the setupUi
method), and never, EVER be manually edited. Doing that is considered bad practice and is conceptually wrong for many reasons - and their header clearly warns about that. To read more about the commonly accepted approaches for those files, read the official guidelines about using Designer.
One of those reasons is exactly related to the garbage collection issue explained above.
Note that subclassing the pyuic form class alone is also discouraged (and pointless if you want to extend the widget behavior); the most common, accepted and suggested practice is to create a class that inherits both from the Qt widget class and the UI class. The following code assumes that you have recreated the file with pyuic
and named ui_doorwin.py
:
# ...
from ui_doorwin import Ui_DoorWin
# ...
class DoorWin(QtWidgets.QWidget, Ui_DoorWin):
def __init__(self, doors):
super().__init__()
self.doors = doors
self.setupUi(self)
self.timer = QTimer(self) # <-- IMPORTANT! Note the "self" argument
self.timer.timeout.connect(self.popTable)
self.timer.start(1000)
def popTable(self):
# ...
With the above code you can be sure that whenever the widget gets deleted for any reason, the timer will be destroyed along with it, so the function will not be called trying to access objects that don't exist anymore.
If you need to keep using an existing instance of the window, the solution is pretty simple: create a None
instance (or class) attribute and check if it already exists before creating a new one:
class SomeParent(QtWidgets.QWidget):
doorwin = None
# ...
def open_door(self):
if not self.doorwin:
self.doorwin = DoorWin()
self.doorwin.show()
The above code will not stop the table from updating, which is something you might not want, so you might choose to start and stop the timer depending on when the window is actually shown:
class DoorWin(QtWidgets.QWidget, Ui_DoorWin):
def __init__(self, doors):
super().__init__()
self.doors = doors
self.setupUi(self)
self.timer = QTimer(self)
self.timer.timeout.connect(self.popTable)
def showEvent(self, event):
if not event.spontaneous():
self.timer.start()
def hideEvent(self, event):
if not event.spontaneous():
self.timer.stop()
The event.spontaneous()
check above is to prevent stopping the timer if the show/hide event is caused by system calls, like minimizing the window or changing desktop. It's up to you to decide if you want to keep the timer going on and process all data, even if the window is not shown.
Then, if you want to completely destroy the window when it's closed and when a new one is opened, do the following:
class DoorWin(QtWidgets.QWidget, Ui_DoorWin):
def __init__(self, doors):
# ... (as above)
self.setAttribute(QtCore.Qt.WA_DeleteOnClose)
and then ensure that the widget exists (note that if it's closed by the user the reference still exists):
class SomeParent(QtWidgets.QWidget):
doorwin = None
# ...
def open_door(self):
if self.doorwin:
try:
self.doorwin.close()
except RuntimeError:
pass
self.doorwin = DoorWin()
self.doorwin.show()
Answered By - musicamante
0 comments:
Post a Comment
Note: Only a member of this blog may post a comment.