I'm trying to remove a Qt widget from a layout in a PySide application.
Here is a minimal example. It is a widget with 5 buttons in it, and the middle one is supposed to remove itself when clicked:
import sys
from PySide import QtGui
app = QtGui.QApplication(sys.argv)
widget = QtGui.QWidget()
layout = QtGui.QVBoxLayout()
buttons = [QtGui.QPushButton(str(x)) for x in xrange(5)]
def deleteButton():
b = layout.takeAt(2)
buttons.pop(2)
del b
buttons[2].clicked.connect(deleteButton)
map(layout.addWidget, buttons)
widget.setLayout(layout)
widget.show()
app.exec_()
What actually happens is this:
The button is unclickable and clearly isn't taken into consideration for the layout computations, but its image stays in place.
According to the Qt documentation, the correct way of deleting all objects from a layout is:
while ((child = layout->takeAt(0)) != 0) {
delete child;
}
Here I just want to delete the third button, so I just call takeAt(2), and then del b to call the destructor on that item. The button object is also .pop'd from the buttons list to make sure there is no leftover reference to the object. How does my code differ from the one in the Qt docs that would cause such a behavior?
Super simple fix:
def deleteButton():
b = layout.takeAt(2)
buttons.pop(2)
b.widget().deleteLater()
You first have to make sure you are addressing the actual button and not the QWidgetItem that is returned from the layout, and then call deleteLater() which will tell Qt to destroy the widget after this slot ends and control returns to the event loop.
Another example illustrates why the problem is occurring. Even though you take the layout item, the underlying widget is still parented to the original layouts widget.
def deleteButton():
b = layout.takeAt(2)
buttons.pop(2)
w = b.widget()
w.setParent(None)
This is not the preferred way, as it still leaves the cleanup of the object ambiguous. But it shows that clearing the parent allows it to leave the visual display. Use deleteLater() though. It properly cleans everything up.
The answer that 'jdi' provided is valid, although If anyone is interested, I tried implementing what is suggested in the Qt Documentation with the loop of every child Widget, and I got the following code working in Python PySide6:
def delete():
while ((child := layout.takeAt(0)) != None):
child.widget().deleteLater()
Related
I have a QlistWidget and I need to implement on this an Infinite Scroll, something like this HTML example:
https://scrollmagic.io/examples/advanced/infinite_scrolling.html
Basically, when the user scrolls to the last item of the list, I need to load more items and dynamically append it in the QlistWidget.
Is it possible? I didn't find any example yet.
There are likely many ways to achieve this task, but the easiest I found is to watch for changes in the scroll bar, and detect if we're at the bottom before adding more items to the list widget.
import sys, random
from PyQt5.QtWidgets import QApplication, QListWidget
class infinite_scroll_area(QListWidget): #https://doc.qt.io/qt-5/qlistwidget.html
def __init__(self):
super().__init__() #call the parent constructor if you're overriding it.
#connect our own function the valueChanged event
self.verticalScrollBar().valueChanged.connect(self.valueChanged)
self.add_lines(15)
self.show()
def valueChanged(self, value): #https://doc.qt.io/qt-5/qabstractslider.html#valueChanged
if value == self.verticalScrollBar().maximum(): #if we're at the end
self.add_lines(5)
def add_lines(self, n):
for _ in range(n): #add random lines
line_text = str(random.randint(0,100)) + ' some data'
self.addItem(line_text)
if __name__ == "__main__":
app = QApplication(sys.argv)
widget = infinite_scroll_area()
sys.exit(app.exec_())
You can directly grab scroll wheel events by overriding the wheelEvent method of QListWidget, then do the logic there which solves the potential problem of not starting out with enough list items for the scrollbar to appear. If it's not there, it can't change value, and the event can't fire. It introduces a new problem however as scrolling with the mouse wheel is not the only way to scroll the view (arrow keys, page up/down keys, etc). With the number of classes and subclasses in any gui library, it becomes imperative to get really familiar with the documentation. It's a little inconvenient that it isn't as comprehensive for python specifically, but I think the c++ docs are second to none as far as gui library documentation goes.
I want to dynamically change the number of sliders on my application window, in dependence of the number of checked items in a QStandardItemModel structure.
My application window has an instance of QVBoxLayout called sliders, which I update when a button is pressed:
first removing all sliders eventually in there:
self.sliders.removeWidget(slider)
And then creating a new set.
The relevant code:
def create_sliders(self):
if len(self.sliders_list):
for sl in self.sliders_list:
self.sliders.removeWidget(sl)
self.sliders_list = []
for index in range(self.model.rowCount()):
if self.model.item(index).checkState():
slid = QSlider(Qt.Horizontal)
self.sliders.addWidget(slid)
self.sliders_list.append(slid)
The principle seems to work, however what happens is weird as the deleted sliders do not really disappear but it is as they were 'disconnected' from the underlying layout.
When created, the sliders keep their position among other elements while I resize the main window.
However, once they've been removed, they occupy a fixed position and can for instance disappear if I reduce the size of the window.
Unfortunately I'm having difficulties in linking images (it says the format is not supported when I try to link from pasteboard), so I hope this description is enough to highlight the issue.
Do I have to remove the sliders using a different procedure?
EDIT
Thanks to #eyllansec for his reply, it condenses a bunch of other replies around the topic, which I wasn't able to find as I did not know the method deleteLater() which is the key to get rid of widgets inside a QLayout.
I am marking it as my chosen (hey, it's the only one and it works, after all!), however I want to propose my own code which also works with minimal changes w.r.t. to what I proposed in the beginning.
The key point here is that I was using the metod QLayout.removeWidget(QWidget) which I was wrongly thinking, it would..er..Remove the widget! But actually what it does is (if I understood it right) remove it from the layout instance.
That is why it was still hanging in my window, although it seemed disconnected
Manual reference
Also, the proposed code is far more general than what I need, as it is a recursion over layout contents, which could in principle be both other QLayout objects or QWidgets (or even Qspacer), and be organized in a hierarchy (i.e., a QWidget QLayout within a QLayout and so on).
check this other answer
From there, the use of recursion and the series of if-then constructs.
My case is much simpler though, as I use this QVLayout instance to just place my QSliders and this will be all. So, for me, I stick to my list for now as I do not like the formalism of QLayout.TakeAt(n) and I don't need it. I was glad that the references I build in a list are absolutely fine to work with.
In the end, this is the slightly changed code that works for me in this case:
def create_sliders(self):
if len(self.sliders_list):
for sl in self.sliders_list:
sl.deleteLater()
self.sliders_list = []
for index in range(self.model.rowCount()):
if self.model.item(index).checkState():
slid = QSlider(Qt.Horizontal)
self.sliders.addWidget(slid)
self.sliders_list.append(slid)
It is not necessary to save the sliders in a list, they can be accessed through the layout where it is contained. I have implemented a function that deletes the elements within a layout. The solution is as follows:
def create_sliders(self):
self.clearLayout(self.sliders)
for index in range(self.model.rowCount()):
if self.model.item(index).checkState():
slid = QSlider(Qt.Horizontal)
self.sliders.addWidget(slid)
def clearLayout(self, layout):
if layout:
while layout.count():
item = layout.takeAt(0)
widget = item.widget()
if widget:
widget.deleteLater()
else :
self.clearLayout(item.layout())
layout.removeItem(item)
I use python2.7 and PyQt4. I created a simple app with a button-box and a table-widget. If I edit a table cell and press the Ok button, the cell editor always disappears. But after I add the app.setStyleSheet(s) line, the cell editor does not disappear after the OK button is pressed. What is going on?
import sys
from PyQt4 import QtGui
class Widget(QtGui.QWidget):
def __init__(self):
QtGui.QWidget.__init__(self)
l = QtGui.QVBoxLayout(self)
table = QtGui.QTableWidget()
table.setColumnCount(3)
table.setRowCount(5)
l.addWidget(table)
l.addWidget(QtGui.QDialogButtonBox(QtGui.QDialogButtonBox.Ok | QtGui.QDialogButtonBox.Cancel))
if __name__ == "__main__":
app = QtGui.QApplication(sys.argv)
s = "QWidget{background:red;}"
# app.setStyleSheet(s)
app.setStyleSheet(s)
mw = QtGui.QMainWindow()
w = Widget()
mw.setCentralWidget(w)
mw.show()
sys.exit(app.exec_())
This seems to be a bug in Qt4, but I can't find a specific report for it. The same code works as expected when using PyQt5, so it somehow got fixed at some point.
The bug is actually in QDialogButtonBox rather than QTableWidget. If you add a QLineEdit to the example and set focus on it, you will see that clicking on the Ok button does not switch focus. Or to be more precise, clicking on the first button does not switch focus. All the other buttons in the button-box work normally.
I thought that this might have something to do with the default and/or autoDefault properties, because they usually change the way buttons are styled (e.g. a highlighted border). But setting the properties doesn't have any effect - the bug really does seem to affect just the first button.
i use python 2.7 + qt4.8
how to dynamically change the number of widgets in the window? I need to remove all the widgets and create new ones in the right quantity. testarovaniya made for a simple script:
import sys
from PyQt4 import QtCore, QtGui, uic
class MainForm(QtGui.QDialog):
def __init__(self):
super(MainForm, self).__init__()
uic.loadUi("testQTwindow.ui", self)
self.connect(self.addBtn, QtCore.SIGNAL("clicked()"), self.addBtnClick)
self.connect(self.delBtn, QtCore.SIGNAL("clicked()"), self.delBtnClick)
def addBtnClick(self):
self.tempBtn = QtGui.QPushButton('button')
self.gridLayout_2.addWidget(self.tempBtn)
def delBtnClick(self):
while True:
item = self.gridLayout_2.takeAt(0)
if not item:
break
self.gridLayout_2.removeWidget(item.widget())
app = QtGui.QApplication(sys.argv)
form = MainForm()
form.show()
sys.exit(app.exec_())
and load this UI: https://yadi.sk/d/jBOmSubYhqbjm
I have two buttons. One for adding buttons to QScrollArea with gridLayout. And the second to remove all the widgets in the QScrollArea. Adding works. I can see how there are new buttons. But when you press the cleaning button does not disappear, and new ones continue to appear over the old ones. The old button can also be pressed, which suggests that they work, and not just the ghosts that are cleaned redrawing window.
I try repaint() and update() functions - but it has no effect...
This is a simple example, but even he is not working. And I do not need to add a button in the future, and whole blocks with a bunch of elements.
How to add and remove widgets dynamically?
This part of the loop should be enough:
while True:
item = self.gridLayout_2.takeAt(0)
I suspect you are attempting to delete widgets you already removed, and so prematurely ending your loop. There may have been an error message written somewhere.
Is there a way to clear (delete) all the widgets in a layout?
self.plot_layout = QtGui.QGridLayout()
self.plot_layout.setGeometry(QtCore.QRect(200,200,200,200))
self.root_layout.addLayout(self.plot_layout)
self.plot_layout.addWidget(MyWidget())
Now I want to replace the widget in plot_layout with a new widget. Is there an easy way to clear all the widgets in plot_layout? I don't see any method such.
After a lot of research (and this one took quite time, so I add it here for future reference), this is the way I found to really clear and delete the widgets in a layout:
for i in reversed(range(layout.count())):
layout.itemAt(i).widget().setParent(None)
What the documentation says about the QWidget is that:
The new widget is deleted when its parent is deleted.
Important note: You need to loop backwards because removing things from the beginning shifts items and changes the order of items in the layout.
To test and confirm that the layout is empty:
for i in range(layout.count()): print i
There seems to be another way to do it. Instead of using the setParent function, use the deleteLater() function like this:
for i in reversed(range(layout.count())):
layout.itemAt(i).widget().deleteLater()
The documentation says that QObject.deleteLater (self)
Schedules this object for deletion.
However, if you run the test code specified above, it prints some values. This indicates that the layout still has items, as opposed to the code with setParent.
This may be a bit too late but just wanted to add this for future reference:
def clearLayout(layout):
while layout.count():
child = layout.takeAt(0)
if child.widget():
child.widget().deleteLater()
Adapted from Qt docs http://doc.qt.io/qt-5/qlayout.html#takeAt. Remember that when you are removing children from the layout in a while or for loop, you are effectively modifying the index # of each child item in the layout. That's why you'll run into problems using a for i in range() loop.
The answer from PALEN works well if you do not need to put new widgets to your layout.
for i in reversed(range(layout.count())):
layout.itemAt(i).widget().setParent(None)
But you will get a "Segmentation fault (core dumped)" at some point if you empty and fill the layout many times or with many widgets. It seems that the layout keeps a list of widget and that this list is limited in size.
If you remove the widgets that way:
for i in reversed(range(layout.count())):
widgetToRemove = layout.itemAt(i).widget()
# remove it from the layout list
layout.removeWidget(widgetToRemove)
# remove it from the gui
widgetToRemove.setParent(None)
You won't get that problem.
That's how I clear a layout :
def clearLayout(layout):
if layout is not None:
while layout.count():
child = layout.takeAt(0)
if child.widget() is not None:
child.widget().deleteLater()
elif child.layout() is not None:
clearLayout(child.layout())
You can use the close() method of widget:
for i in range(layout.count()): layout.itemAt(i).widget().close()
I use:
while layout.count() > 0:
layout.itemAt(0).setParent(None)
My solution to this problem is to override the setLayout method of QWidget. The following code updates the layout to the new layout which may or may not contain items that are already displayed. You can simply create a new layout object, add whatever you want to it, then call setLayout. Of course, you can also just call clearLayout to remove everything.
def setLayout(self, layout):
self.clearLayout()
QWidget.setLayout(self, layout)
def clearLayout(self):
if self.layout() is not None:
old_layout = self.layout()
for i in reversed(range(old_layout.count())):
old_layout.itemAt(i).widget().setParent(None)
import sip
sip.delete(old_layout)
Most of the existing answers don't account for nested layouts, so I made a recursive function, that given a layout it will recursively delete everything inside it, and all the layouts inside of it. here it is:
def clearLayout(layout):
print("-- -- input layout: "+str(layout))
for i in reversed(range(layout.count())):
layoutItem = layout.itemAt(i)
if layoutItem.widget() is not None:
widgetToRemove = layoutItem.widget()
print("found widget: " + str(widgetToRemove))
widgetToRemove.setParent(None)
layout.removeWidget(widgetToRemove)
elif layoutItem.spacerItem() is not None:
print("found spacer: " + str(layoutItem.spacerItem()))
else:
layoutToRemove = layout.itemAt(i)
print("-- found Layout: "+str(layoutToRemove))
clearLayout(layoutToRemove)
I might not have accounted for all UI types, not sure. Hope this helps!
From the docs:
To remove a widget from a layout, call removeWidget(). Calling QWidget.hide() on a widget also effectively removes the widget from the layout until QWidget.show() is called.
removeWidget is inherited from QLayout, that's why it's not listed among the QGridLayout methods.
I had issues with solutions previously mentioned. There were lingering widgets that were causing problems; I suspect deletion was scheduled, but not finihsed. I also had to set the widgets parent to None.
this was my solution:
def clearLayout(layout):
while layout.count():
child = layout.takeAt(0)
childWidget = child.widget()
if childWidget:
childWidget.setParent(None)
childWidget.deleteLater()
A couple of solutions, if you are swapping between known views using a stacked widget and just flipping the shown index might be a lot easier than adding and removing single widgets from a layout.
If you want to replace all the children of a widget then the QObject functions findChildren should get you there e.g. I don't know how the template functions are wrapped in pyqt though. But you could also search for the widgets by name if you know them.
for i in reversed(range(layout.count())):
if layout.itemAt(i).widget():
layout.itemAt(i).widget().setParent(None)
else:
layout.removeItem(layout.itemAt(i))
for i in reversed (range(layout.count())):
layout.itemAt(i).widget().close()
layout.takeAt(i)
or
for i in range(layout.count()):
layout.itemAt(0).widget().close()
layout.takeAt(0)