Looping over widgets inside widgets inside layouts - python

Similar question to what we have here Loop over widgets in PyQt Layout but a bit more complex...
I have
QVGridLayout
QGroupBox
QGridLayout
QLineEdit
I'd like to access QLineEdit but so far I dont know how to access children of QGroupBox
for i in range(self.GridLayout.count()):
item = self.GridLayout.itemAt(i)
for i in range(item.count()):
lay = item.itemAt(i)
edit = lay.findChildren(QLineEdit)
print edit.text()
Can any1 point me to right dirrection?

When a widget is added to a layout, it automatically becomes a child of the widget the layout it is set on. So the example reduces to a two-liner:
for group in self.GridLayout.parentWidget().findChildren(QGroupBox):
for edit in group.findChildren(QLineEdit):
# do stuff with edit
However, findChildren is recursive, so if all the line-edits are in group-boxes, this can be simplified to a one-liner:
for edit in self.GridLayout.parentWidget().findChildren(QLineEdit):
# do stuff with edit

Sorted :
for i in range(self.GridLayout.count()):
item = self.GridLayout.itemAt(i)
if type(item.widget()) == QGroupBox:
child = item.widget().children()
I had to use item.widget() to get access to GroupBox.
Hope this helps some1.

Related

Adding/Removing QSlider widgets on PyQt5

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)

Accessing multiple widgets

I have made a GUI using Qt Designer, I have like 20 or more lineEdit widgets, I am looking for the correct way to access all of them without having to access each one...I explain with an example in pseudocode:
This is what I DON'T want:
lineEdit1.setText("Value = 1")
lineEdit2.setText("Value = 2")
lineEdit3.setText("Value = 3")
and so on, up to
lineEdit20.setText("Value = 20")
I am looking for something like THIS:
for i in xrange(1,21):
lineEdit(i).setText("Value = whatever")
The problem is that, as far as I know, when dragging and dropping widgets, Qt Designer automatically adds a number to the end of the name...lineEdit1, lineEdit2,....so after that I do not know how to access all of them in the code.
Firstly, you don't have to accept the name that is automatically assigned in Qt Designer. Just set the objectName property to whatever is most suitable.
To iterate over groups of widgets, there are several QObject methods available.
The most powerful is findChildren which can search recursively for objects/widgets based on their class:
for child in self.findChildren(QtGui.QLineEdit):
child.setValue(value)
and also their objectName:
rgx = QtCore.QRegExp('lineEdit[0-9]+')
for child in self.findChildren(QtGui.QLineEdit, rgx):
child.setValue(value)
But a simpler technique would be to use Qt Designer to put all the widgets inside a container widget (such as a QWidget, QFrame, QGroupBox, etc), and then use the non-recursive children method to loop over them:
for index, child in enumerate(self.groupBox.children()):
child.setValue(value[index])
You might also want to consider using a QFormLayout, which provides a more structured way of doing things. This is available in Qt Designer: just drag and drop a Form Layout onto your main form and then use Add form layout row from the right-click menu to add widgets to it.
If you really want to use the QT designer, I guess your best solution is to simply add widgets to a list.
I know this is sub-optimal, but this way you will only have to do it once.
lineEditL = []
lineEditL.append(lineEdit1)
lineEditL.append(lineEdit2)
An other solution is to simply iterate the children. This is explained here, no reason to repeat.
QWidget *pWin = QApplication::activeWindow();
for( int i=1; i<=21; i++ ) {
QString strName ="LineEdit"+QString::number(i);
QLineEdit *le = pWin->findChild<QLineEdit *>(strName);
if( le ) le->setText("Value = "+Qstring::number(i) );
}

PySide: Removing a widget from a layout

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()

QScrollView with no scrollbars

I'm trying to make a QScrollView with around 100 different labels and buttons.
But after I add them, the scrollbars do not appear.
here is an example of the code I wrote:
self.btn = {}
self.scroll = QScrollView(self)
self.scroll.setGeometry(QRect(0,0,300,300))
self.scroll.enableClipper(True)
vp = self.scroll.viewport()
for i in range(0,100):
self.btn[i] = QPushButton(vp)
for i in range(0,100):
self.scroll.addChild(self.btn[i],1,50*i)
self.scroll.setVScrollBarMode(QScrollView.AlwaysOn)
make the scrollbar appear but not to work.
The buttons get added to the QScrollView but I can't scroll down to see them all
what am I doing wrong? I'm using qt3.
You don't add all your little items to the scrollview. You have to insert a single, large container (a QFrame derived class, for example) into the scrollview that contains all your smaller widgets.
Actually you just need to give the scroll-view a layout and add your widgets to this. Adding them as sub-widgets of one big widget within the scrollview will do this for you, but it's messier.

Clear all widgets in a layout in pyqt

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)

Categories

Resources