Moving child widgets between QTabWidgets?

Maurizio Berti maurizio.berti at gmail.com
Thu Jan 26 02:15:41 GMT 2023


There are some issues in your code and misconceptions in your understanding.

First of all, setParent(None) does *not* delete anything, at least, not
explicitly.

In fact, if you think about it, top level windows normally have no parent
at all (except for dialogs).
For instance, when you create your MyWindow, it doesn't have any parent,
nor it could: widgets can only have other widgets (or None) as parents.
It's not a chicken/egg dilemma: at least one window or widget will always
have no parent in any QApplication.

Also, setting the parent to None is actually one way to make a child widget
to a top level one, with the exception of explicitly setting the Window
flag (which is implicitly done for QDialogs, since the Dialog flag also
includes Window).

What actually matters is that, if a widget *has* a parent, setting it to
None *may* result in deleting it, but that only happens as long as no
object has ownership of it and (in python) its reference count becomes 0.
This is exactly what happens with a common beginner mistake, when trying to
create new windows:

def createWindow(self):
    newWindow = QDialog() # or any QWidget() without a given parent
    newWindow.show()

The above creates a new widget without a parent (so, a top level window),
but since there's no parent nor any reference left after the function
returns, the widget is destroyed, just like it would happen with any local
variable in Python. The window will be shown only for a fraction of a
second (or not shown at all), and will be destroyed immediately.

Remember that PyQt (like PySide) is a *binding* to Qt. We use its API from
Python, but objects are created on the "C++ side". Python has a garbage
collection system that (theoretically) deletes the object whenever its
reference count is 0, but since the python reference of Qt objects is *not*
the object, deleting all references won't necessarily delete the Qt object.
Note that by "Qt object", I mean QObject, and, by extension, any Qt object
that inherits from QObject (including QWidget and all its subclasses).

In fact, the following will behave quite differently than the above:

def createWindow(self):
    newWindow = QDialog(self) # similar behavior also with QMainWindow
    newWindow.show()

The new window will not be destroyed, even if we didn't keep a persistent
reference to it.
Even the following won't change anything:

def createWindow(self):
    self.newWindow = QDialog(self)
    self.newWindow.show()
    self.newWindow = None
    # similarly:
    del self.newWindow

This, instead, will be practically the same as the first createWindow()
code above:

def createWindow(self):
    newWindow = QDialog(self)
    newWindow.show()
    QTimer.singleShot(0, lambda: newWindow.setParent(None))

Remember that setParent() resets the window flags and hides the widget. The
following will be *visually* the same, but will not delete the dialog:

def createWindow(self):
    self.newWindow = QDialog(self)
    self.newWindow.show()
    QTimer.singleShot(0, lambda: self.newWindow.setParent(None))

Note that there is no absolute guarantee about the order or timing of
QObject deletions in PyQt. The general rule is that a QObject will be
persistent as long as at least one python reference to it exists *or* it is
owned by another QObject.

There's a specific chapter in the PyQt documentation:
https://www.riverbankcomputing.com/static/Docs/PyQt5/gotchas.html#garbage-collection
Consider the following examples.

This will *not* delete the child, just its python reference:

parent = QObject()
child = QObject(parent)
del child

This will *not* delete the child, but just change its parent:

parent = QObject()
child = QObject(parent)
child.setParent(None)

This will not *immediately* delete the child:

parent = QObject()
child = QObject(parent)
child.deleteLater()
assert child in parent.children() # True

The above will only work within the same call; it will not work with the
interactive shell, if the input hook is active and a QCoreApplication
exists (see
https://www.riverbankcomputing.com/static/Docs/PyQt5/python_shell.html ).
As the documentation explains, deleteLater() doesn't immediately delete the
object, but will *schedule its deletion*.
This will, in fact, behave differently instead (assuming a Q*Application
instance was previously created):

parent = QObject()
child = QObject(parent)
child.deleteLater()
QCoreApplication.processEvents() # process the current event queue,
including scheduled deletions
assert child in parent.children() # False


Now, back to your case.
As the documentation explains, removeTab() just removes the tab ("The page
widget itself is not deleted"), but it doesn't change the ownership (the
documentation always notes it when that happens), meaning that removing the
tab will keep the widget as a child of the tab widget.

In your code, you reparent all children of QSplitter to None, including the
frame used as parent of the tab widget. That frame was declared as a local
variable in the __init__, so the only thing that keeps it alive is its
parent (the splitter): setting the parent to None clears that, and since
there's no other reference left in python, it's destroyed along with any of
its children.

At the same time, the tab widget will still exist, because self.tab_widget
keeps it alive. Unfortunately, you destroy that reference right after
that (self.tab_widget
= None), and all the "removed" tab widgets along with it, since they are
still owned by it.

Depending on your needs, there are various possibilities. If you want to
delete all children of QSplitter before adding a new one, you could do that
in three steps:

- get a list of the current children of QSplitter;
- add the new one;
- reparent the previously collected widgets to None;

But, actually, all the above can be done much more easily.
Note that using a reverse list of indexes to transfer Qt items isn't really
effective, as you'd need to reverse again the resulting list (in fact,
after using the change above, your code will add tabs in reverse order).
Just always call removeTab(0) for every iteration of the loop (or use a
while loop until the tab widget is empty).
Even better, directly add the pages to the new tab widget in the same
order; since reparenting causes a child remove event for the original
parent, they will be automatically moved while you're getting them, meaning
that always using the 0 index is perfectly safe, because the removal order
is the same as the insertion one.

    def switch(self):
        old_tab_widget = self.tab_widget
        self.tab_widget = QTabWidget()
        while old_tab_widget.count():
            # this will automatically reparent the page, thus removing it
from
            # the "old" tab widget; unlike deleteLater, reparenting happens
immediately
            self.tab_widget.addTab(old_tab_widget.widget(0),
old_tab_widget.tabText(0))

        splitter = self.centralWidget()
        for i in range(splitter.count()):
            # schedule widgets for removal, they will still exist within
this function call
            splitter.widget(i).deleteLater()

        splitter.addWidget(self.tab_widget)

Note that the second part of the code above (the one including the for
loop) can even be put at the very beginning of the function: since
deleteLater() is only scheduling the deletion, the widgets will still be
accessible within the lifetime of the function (as long as
QApplication.processEvents() is not called, obviously)

Hope this helps you to clarify things.

Best regards,
Maurizio


Il giorno mer 18 gen 2023 alle ore 14:49 Matic Kukovec <
kukovecmatic at hotmail.com> ha scritto:

> Hi Maurizio,
>
> You are correct, I apologize for the mistakes.
> I managed to get a self-contained example which throws the same error as
> my development code:
>
>
>     from PyQt5.QtCore import *
>     from PyQt5.QtGui import *
>     from PyQt5.QtWidgets import *
>
>     class MyWindow(QMainWindow):
>         def __init__(self):
>             super().__init__()
>             splitter = QSplitter(self)
>             self.setCentralWidget(splitter)
>
>             # Frame & frame
>             frame = QFrame(self)
>             splitter.addWidget(frame)
>             layout = QVBoxLayout()
>             frame.setLayout(layout)
>
>             # Button
>             button = QPushButton("Switch")
>             button.clicked.connect(self.switch)
>             layout.addWidget(button)
>
>             # Tab widget
>             tab_widget = QTabWidget()
>             layout.addWidget(tab_widget)
>             self.tab_widget = tab_widget
>
>             # Red
>             w = QWidget(tab_widget)
>             w.setStyleSheet("background: red;")
>             tab_widget.addTab(w, "Red")
>
>             # Green
>             w = QWidget(tab_widget)
>             w.setStyleSheet("background: green;")
>             tab_widget.addTab(w, "Green")
>
>             # TreeView
>             tv = QTreeView(tab_widget)
>             tv.horizontalScrollbarAction(1)
>
> tv.setSelectionBehavior(QAbstractItemView.SelectionBehavior.SelectRows)
>             tree_model = QStandardItemModel()
>             tree_model.setHorizontalHeaderLabels(["TREE EXPLORER"])
>             tv.header().hide()
>             tv.setModel(tree_model)
>             for i in range(100):
>                 item = QStandardItem("ITEM {}".format(i))
>
>             tv.setUniformRowHeights(True)
>
>             tab_widget.addTab(tv, "TreeView")
>
>         def switch(self, *args):
>             widgets = []
>             old_tab_widget = self.tab_widget
>             for i in reversed(range(old_tab_widget.count())):
>                 widgets.append((old_tab_widget.widget(i),
> old_tab_widget.tabText(i)))
>                 old_tab_widget.removeTab(i)
>
>             for i in reversed(range(self.centralWidget().count())):
>                 self.centralWidget().widget(i).setParent(None)
>             old_tab_widget = None
>             self.tab_widget = None
>
>             new_tab_widget = QTabWidget()
>             for widget, tab_text in widgets:
>                 new_tab_widget.addTab(widget, tab_text) # <- ERROR THROWN
> HERE
>
>             self.centralWidget().addWidget(new_tab_widget)
>             self.tab_widget = new_tab_widget
>
>
>     if __name__ == '__main__':
>         import sys
>
>         app = QApplication(sys.argv)
>         w = MyWindow()
>
>         w.resize(640, 480)
>         w.show()
>
>         sys.exit(app.exec_())
>
>
> Matic
>
> ------------------------------
> *From:* Maurizio Berti <maurizio.berti at gmail.com>
> *Sent:* Tuesday, January 17, 2023 10:25 PM
> *To:* Matic Kukovec <kukovecmatic at hotmail.com>
> *Cc:* PyQt at riverbankcomputing.com <pyqt at riverbankcomputing.com>
> *Subject:* Re: Moving child widgets between QTabWidgets?
>
> If it throws that error, it's because the widgets (or their parent) have
> been deleted.
> Unfortunately, your code is insufficient to actually understand where the
> issue is, and, by the way, it also has important issues: first of all the
> second for loop is wrong (new_tab_widget is a widget, so that will throw an
> exception), and using removeTab with the number of a counter is wrong, as
> you'll always get even numbered items, if you want to remove all widgets,
> always use removeTab(0).
> I suggest you to provide a valid minimal reproducible example.
>
> Maurizio
>
> Il giorno mar 17 gen 2023 alle ore 14:11 Matic Kukovec <
> kukovecmatic at hotmail.com> ha scritto:
>
> HI,
>
> What is the PyQt idiomatic way of moving a widget from one QTabWidget to
> another?
> I have tried this:
>
>
> widgets = []
> for i in range(old_tab_widget.count()):
>     widgets.append((old_tab_widget.widget(i), old_tab_widget.tabText(i)))
>     old_tab_widget.removeTab(i)
>
> for widget, tab_text in new_tab_widget:
>     new_tab_widget.addTab(widget, tab_text)
>
>
> but this throws errors for example for QTreeView widgets like this:
>
> ...
>     new_tab_widget.addTab(widget, tab_text)
> RuntimeError: wrapped C/C++ object of type TreeExplorer has been deleted
>
> (TreeExplorer is my subclassed QTreeView)
>
> Thanks
> Matic
>
>
>
> --
> È difficile avere una convinzione precisa quando si parla delle ragioni
> del cuore. - "Sostiene Pereira", Antonio Tabucchi
> http://www.jidesk.net
>


-- 
È difficile avere una convinzione precisa quando si parla delle ragioni del
cuore. - "Sostiene Pereira", Antonio Tabucchi
http://www.jidesk.net
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <https://www.riverbankcomputing.com/pipermail/pyqt/attachments/20230126/52580323/attachment-0001.htm>


More information about the PyQt mailing list