Accessing multiple widgets - python

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) );
}

Related

How to apply a QGraphicsEffect to child widgets with a viewport?

I'm trying to apply a QGraphicsEffect to all the child widgets of a given widget, let's just say a QDialog. To do this, I set the graphics effect of the dialog, and it works as expected for everything except child widgets with a viewport (QScrollArea, QAbstractItemView, etc). These will render correctly (with the effect applied) when resizing the window frame, but on any internal updates (re-rendering a newly selected item in a QListView, say) it re-renders without the effect applied.
I know you can assign a QGraphicsEffect to the viewport of a QAbstractScrollArea, but this doesn't work for me since I want to be able to use the one effect across all of the child widgets (effect properties will be dynamic/animated), and you can't apply the same effect to multiple sources.
Here is an example that shows the peculiar rendering using a QListView:
from PySide2 import QtWidgets
d = QtWidgets.QDialog()
d.show()
layout = QtWidgets.QVBoxLayout()
d.setLayout(layout)
lw = QtWidgets.QListWidget()
lw.addItems(["dog", "cat", "fish", "platypus"])
layout.addWidget(lw)
btn = QtWidgets.QPushButton("so fuzzy!")
layout.addWidget(btn)
ge = QtWidgets.QGraphicsBlurEffect()
lw.viewport().setGraphicsEffect(ge) # assign effect to viewport
d.setGraphicsEffect(ge) # AND to whole dialog?
ge.setBlurRadius(20)
and a screenshot:
and you can't apply the same [instanced] effect to multiple sources.
In the last lines of your code, you're attempting to set the graphics effect for two widgets using the same instanced class. Alas, you cannot! From the Qt Documentation, QGraphicsItem.setGraphicsEffect()
If effect is the installed effect on a different item, will remove the
effect from the item and install it on this item.
The approach to take here is to create a QGraphicsEffect instance - for each widget. Then, for QScrollArea, QAbstractItemView, run their find children methods (.findChildren(QtWidgets.QWidget) ) to iterate & set their graphics effects:
ge = QtWidgets.QGraphicsBlurEffect
d.setGraphicsEffect(ge())
lw.viewport().setGraphicsEffect(ge())
# Find List Widget's child widgets. Assign their graphics effect.
lw_children = lw.findChildren(QtWidgets.QWidget)
for c in lw_children:
c.setGraphicsEffect(ge())

PyQT - QlistWidget with infinite scroll

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.

Looping over widgets inside widgets inside layouts

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.

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

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