PyQt QRadioButton toggled singal callback execute after delete [duplicate] - python

There are some examples of removing specific items from a layout, but I can not find anything on simply deleting everything from a frame.
Using pyqt designer, I have created a frame. Then using pyuic4 the file is converted to python. In the main program, some layouts, items, and widgets are dynamically inserted to the frame. However, I dont actually keep track of all the items. On a button refresh, I want to delete everything contained in the frame and populate it again.
My question is, is there a simple way to delete everything that is contained in a frame, including layouts, widgets, and items.
As of now, I can do:
for i in range(len(MyResourceFrame.children())):
MyResourceFrame.children()[i].deleteLater()
However, I have code directly under that, and after the first qframe population, clicking on repopulate give the error that a frame is already there, which then removes all items. The second click on repopulate works. Does this have something to do with "Later" wanting to be out of scope first or is that just a name?

The deleteLater slot will just schedule the object for deletion. That is, the object won't be deleted until control returns to the event loop (which will usually mean after the currently executing function has returned).
If you want to delete an object immediately, use the sip module. This should allow you delete a layout and all its contained widgets like this:
PyQt5:
from PyQt5 import sip
...
class Window(QtWidgets.QMainWindow):
...
def populateFrame(self):
self.deleteLayout(self.frame.layout())
layout = QtWidgets.QVBoxLayout(self.frame)
...
def deleteLayout(self, layout):
if layout is not None:
while layout.count():
item = layout.takeAt(0)
widget = item.widget()
if widget is not None:
widget.deleteLater()
else:
self.deleteLayout(item.layout())
sip.delete(layout)
PyQt4:
import sip
...
class Window(QtGui.QMainWindow):
...
def populateFrame(self):
self.deleteLayout(self.frame.layout())
layout = QtGui.QVBoxLayout(self.frame)
...
def deleteLayout(self, layout):
if layout is not None:
while layout.count():
item = layout.takeAt(0)
widget = item.widget()
if widget is not None:
widget.deleteLater()
else:
self.deleteLayout(item.layout())
sip.delete(layout)

The problem was the layout predefined inside your QFrame. If you remove it in QtDesigner your frame will appear correctly with the first click.

Related

Can I get a signal from a QGroupbox when one of the radiobuttons in it is changed?

I'm making a user-interface in PYQT 5. It includes a QGroupBox containing several QRadioButton. When there is a toggled event I check the buttons and do some stuff. The problem is that there are two toggled events because one button is toggled on and one off so my code is always running twice.
I have been looking for an event from the QGroupBox instead. That should only happen once instead of twice when i toggle a radiobutton.
def __init__(self):
self.radioButton1.toggled.connect(self.update_stimulus)
self.radioButton2.toggled.connect(self.update_stimulus)
self.radioButton3.toggled.connect(self.update_stimulus)
def update_stimulus(self):
if self.radioButton1.isChecked():
print('1')
if self.radioButton2.isChecked():
print('2')
if self.radioButton3.isChecked():
print('3')
# Do stuff based on button positions
I tried using
self.groupBox.toggled.connect(self.update_stimulus)
But that only works if the groupbox itself is toggled. Is there any way to get a signal from the groupbox when one of the radiobuttons changes or do I have to find some way of doing it with the indivdual signals from the radiobuttons?
As #ekhumoro explains, you can add the checked parameter to the function and do your processing only if it's True:
def update_stimulus(self, checked):
if not checked:
return
if self.radioButton1.isChecked():
print('1')
if self.radioButton2.isChecked():
print('2')
if self.radioButton3.isChecked():
print('3')
Keep in mind, though, that if you want to have different radio button "groups" within the same groupbox, none of this will work properly as they will all be considered as part of the same single group: all Qt buttons (widgets that inherit QAbstractButton: QPushButton, QToolButton, QCheckBox and QRadioButton) have an autoExclusive property which is off by default except from radio buttons. This property makes all button that belong to the same parent widget automatically exclusive.
If you need different groups within the same parent, the solution is to use a QButtonGroup, which extends the exclusive functionality by limiting the membership of each button to a specific group.
def __init__(self):
# ...
self.buttonGroupA = QtWidgets.QButtonGroup()
self.buttonGroupA.addButton(self.radioButton1)
self.buttonGroupA.addButton(self.radioButton2)
self.buttonGroupA.addButton(self.radioButton3)
self.buttonGroupA.buttonToggled[QtWidgets.QAbstractButton, bool].connect(self.update_stimulusA)
self.buttonGroupB = QtWidgets.QButtonGroup()
self.buttonGroupB.addButton(self.radioButton4)
self.buttonGroupB.addButton(self.radioButton5)
self.buttonGroupB.addButton(self.radioButton6)
self.buttonGroupB.buttonToggled[QtWidgets.QAbstractButton, bool].connect(self.update_stimulusB)
def update_stimulusA(self, btn, checked):
if not checked:
return
# do something with group A
def update_stimulusB(self, btn, checked):
if not checked:
return
# do something with group B
Creation of a button group is also possible from Designer: just select at least two buttons that will be members of the same group, right click on one of them, go to the "Assign to button group" sub menu and select "New button group". To add a button to an existing group, just use the same context menu and choose the group you want to add that button to.

QLayout Additem - prevent transfer of ownership

I am having problems with the object references when using the QLayout to arrange my widgets in on bigger window next to each other.
I have the following situation
class MyClass(QObject):
widgetCollection = []
def spawn(self):
widget = MyQWidget() #containing a QLineWidget called "nameEdit"
self.widgetCollection.append(widget)
self._window = QtGui.QWidget()
layout = QtGui.QHBoxLayout()
listView = QtGui.QListWidget()
for equation in self.wigdetCollection:
equationName = equation.nameEdit.text()
item = QtGui.QListWidgetItem(equationName)
listView.addItem(item)
layout.addWidget(listView)
layout.addWidget(widget)
self._window.setWindowTitle("Equation Editor")
self._window.setLayout(layout)
self._window.show()
def respawn(self):
self.spawn()
Each time I call spawn(), I want to add a new widget to the collection. Addionally, I want to open a new window where there is a ListView with all widget names on the left and the newly created widget on the right.
Now the first call to the spawn()-method works like expected. But the second call throws an exeption:
equationName = widget.nameEdit.text()
RuntimeError: wrapped C/C++ object of type QLineEdit has been deleted
I think it has something to do with the line
layout.addWidget(widget)
I have read somewhere that the layout takes ownership of the widget when added as an item. With that I loose the widget as soon as I am out of the scope of the layout-reference (which is local in this case). So it seems that the widget-item in my collection gets deleted too.
Can someone help? How do I prevent that.
The problem is that self._window is replaced every time spawn() is closed: Python discards the old window widget, and self._window is made to reference a new QWidget instance. Since the old window widget is parent to a list view and qlineedit widget (called -- confusingly -- just "widget" in the code), these will be destroyed in the Qt sense of the term -- i.e. their C++ portion will be destroyed. The list view is not referenced anywhere else in the shown code so the Python portion of the list view will be destroyed too, and this is the way it should be.
HOWEVER, the qlineedit is referenced in the class-wide registry (widgetCollection) and so the Python portion of the qlineedit will NOT be destroyed. This means that every time spawn() is called, the previous instance of QLineEdit (via MyQWidget in class-wide widgetCollection) becomes a zombie: it is dead at the C++ Qt level, but it is still alive at the Python level. A zombie's methods can still be accessed, but in many cases you will see the Qt error about C/C++ having been deleted, and you may get a crash or other undefined behavior.
It is not clear from the code posted what is the intent of the widgetCollection, but since the previous item appended will become a zombie during a spawn(), widgetCollection is useless as shown. The solution to the problem depends on details about how the collection is used, but since one can presume that the collection has a purpose, then the issue is the replacement of the window widget: perhaps the widget collection should be a window collection, then when self._window is replaced, the previous window stays alive so the child widgets stay alive too.
Solved it by myself :-)
My initial call to the spawn()-method was like that:
mc = MyClass()
mc.spawn()
First one okay. Now I wanted to spawn another from within inst1. For that I have used
self.spawn()
in the respawn()-method throwing the above mentioned error.
It has to be the following
def respawn(self):
mc = MyClass()
mc.spawn()
I had to create another instance of MyClass that shares the widgetCollection with all other instances. As desired....

Trying to make a collapsible widget: How to hide/unhide all child widgets?

Im making a simplish widget that can act as a container for other widgets. One of the features of the widget is that you can expand/collapse it by clicking on it. My current method is basically looking up all child widgets of the layout and hiding them. I'm looking for any help on how to handle this properly - my current implementation has at least one serious caveat: that you can't add widgets while it's collapsed (they're added in an 'unhidden' state)
heres the setCollapsed method that is run when the widget is clicked
def collapsed(self):
return self._isCollapsed
def setCollapsed(self, collapseBool):
self._isCollapsed = collapseBool
if self.layout()!=None:
childWidgets = [self.layout().itemAt(i).widget() for i in range(self.layout().count())]
for w in childWidgets:
if isinstance(w,QtGui.QWidget):
w.setHidden(collapseBool)
if collapseBool:
self._cachedMargin = self.layout().margin()
self.layout().setMargin(0)
else:
self.layout().setMargin(self._cachedMargin)
Rather than hiding all child widgets individually, I would just hide a single parent item.

replacing layout on a QWidget with another layout

I have a widget which changes when an option is toggled. This invalidates all layouts and widgets. I keep list of all layouts, so I can delete them using something similar to this answer:
class MyWidget(QFrame):
# ...
def reLayout(self):
def deleteLayoutChilds(l):
while l.count():
item=l.takeAt(0)
widget=item.widget()
if widget: widget.deleteLater()
else: deleteLayoutChilds(item.layout())
for l in self.allLayouts: deleteLayoutChilds(l)
# now install the new layout
##
## how to delete the old layout first?
l=self.layout(); del l # no effect
#
layout=QGridLayout(self)
## warning: QLayout: Attempting to add QLayout "" to MyWidget "", which already has a layout.
How can I get rid of the old layout and set the new one?
The documentation is quite terse and apparently not directly applicable to python:
QWidget.setLayout (self, QLayout)
The QLayout argument has it's ownership transferred to Qt.
Sets the layout manager for this widget to layout.
If there already is a layout manager installed on this widget, QWidget
won't let you install another. You must first delete the existing
layout manager (returned by layout()) before you can call setLayout()
with the new layout.
If layout is the layout manger on a different widget, setLayout() will
reparent the layout and make it the layout manager for this widget.
Example:
QVBoxLayout *layout = new QVBoxLayout;
layout->addWidget(formWidget);
setLayout(layout);
An alternative to calling this function is to pass this widget to the
layout's constructor.
The QWidget will take ownership of layout.
See also layout() and Layout Management.
You can simply reparent the layout to a temporary widget:
def reLayout(self):
QWidget().setLayout(self.layout())
layout = QGridLayout(self)
...
That will reparent all the child widgets to that temporary object, and that object is deleted immediately along with its new children because we don't keep a reference to it.
But the typical way to have multiple layouts for a single widget and to be able to switch between them is to use a QStackedWidget or QStackedLayout.
And if you still need the answer to that secondary question:
How to delete the old layout first?
It seems you can't delete directly a QObject which has a parent, because the parent is keeping a reference to that object. But you can add the object to a temporary QObjectCleanupHandler which, like the above solution, will be deleted with the object(s) it contains immediately:
QObjectCleanupHandler().add(self.layout())

Big number of pushbuttons and smart checking which is checked

I have dialog:
It contains many flat QPushButtons, QTextEdit and another QPushButton. After click on 'Get list' we can see list of checked buttons in QTextEdit.
My question is how to get this functionality in some smart way. Right now I'm checking every button:
if self.ui.bq6o.isChecked():
cards.append("Q6o")
if self.ui.bk2o.isChecked():
cards.append("K2o")
if self.ui.bq3o.isChecked():
cards.append("Q3o")
if self.ui.bt7s.isChecked():
cards.append("T7s")
if self.ui.bq4o.isChecked():
cards.append("Q4o")
if self.ui.bt4s.isChecked():
cards.append("T4s")
if self.ui.b98o.isChecked():
cards.append("98o")
if self.ui.bjto.isChecked():
cards.append("JTo")
if self.ui.btt.isChecked():
cards.append("TT")
if self.ui.bq7o.isChecked():
cards.append("Q7o")
[...]
Obviously I can't like code like that. I was looking for some widget "button matrix" like, but without luck. I will be grateful for advises.
All the buttons should be children of the same widget, probably the dialog itself. Just get a handle to that widget to get all the child buttons, then loop through them and if they're checked, included their text.
parent = dialog # or whatever
cards = [widget.text() for widget in parent.children() if isinstance(widget, QPushButton) and widget.isChecked()]
You may need to include some code in the if statement to exclude the "Get List" button, or any other pushbuttons in your dialog that could be set to "checked" but shouldn't be included in cards list.
As #Brendan suggested in the other question, you could loop through them in a single list comprehension. But one other approach is to connect each buttons toggled signal to a slot that allows them to register when they are checked.
# somewhere in your class
self.checkedList = set()
def buttonChecked(self, checked):
button = self.sender()
if checked:
self.checkedList.add(button)
else:
if button in self.checkedList:
self.checkedList.remove(button)
# when you create a new button
button.toggled.connect(self.buttonChecked)
This would let you always have a set of just the checked buttons, which are self reporting. Otherwise, you would have to track them under their parent and loop to find out which are checked each time.
Update
Here is a another version that combines #Brendans loop and my signal suggestion. This might help in a situation where your buttons are a bit more spread out across your UI as opposed to be all under a single parent... but first assuming them all under a parent...
parent = dialog
for widget in parent.children():
if isinstance(widget, QPushButton):
widget.toggled.connect(self.buttonChecked)
You could repeat this in your __init__() for all the locations of your buttons and get them all registered to the slot.

Categories