deleate last two widgets from Grid layout in pyqt - python

i have this function that deleates the last QLineEdit widget from the QGridLayout
it checks if the index of the widget is the last one and if the widget is a instance of QLineEdit
---> deleates the widget
def deleate_lastlineedit(self):
widgets = (self.main_layout.itemAt(i).widget() for i in range(self.main_layout.count()))
for index, widget in enumerate(widgets):
if index == (self.main_layout.count()-1) and isinstance(widget, (qtw.QLineEdit,qtw.QLabel)):
widget.deleteLater()
break
I have added a Qlabel widget to the same row and want that the function deleates the last Qlabel and Qlinedit widget at the same time after pushing a button, so far its deleates one at a time, need to click the button two times.
I tried to insert an counter so the iteration stops not at one iteration but at two iterrations so it gets the two widgets but didnt had an effekt.
also inserted two versions of the function
one that deleates the qline edit and the other that deleates the qlabel
and connected them to the same button but didnt work either
self.getlistof_button.clicked.connect(self.deleate_lastlineedit)
self.getlistof_button.clicked.connect(self.deleate_lastqlabel)
so how can I deleate the two widgets at the same time ?
fullcode
#!/usr/bin/env python
"""
Creates an linedit when button pushed
dleates last linedit
"""
import sys
import sqlite3
from PyQt5 import QtWidgets as qtw
from PyQt5 import QtCore as qtc
from PyQt5 import QtGui as qtg
from PyQt5 import QtSql as qsql
from PyQt5 import sip
class AddWidget(qtw.QWidget):
'''
Interface
'''
# Attribut Signal
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
# your code will go here
# interface
# position
qtRectangle = self.frameGeometry()
centerPoint = qtw.QDesktopWidget().availableGeometry().center()
qtRectangle.moveCenter(centerPoint)
self.move(qtRectangle.topLeft())
# size
self.resize(700, 410)
# frame title
self.setWindowTitle("add Widget")
# heading
heading_label = qtw.QLabel('add Widget')
heading_label.setAlignment(qtc.Qt.AlignHCenter | qtc.Qt.AlignTop)
# add Button
self.addwidget_button = qtw.QPushButton("add Widget")
self.getlistof_button = qtw.QPushButton("deleate")
self.linedittext_button = qtw.QPushButton("linedit text")
self.main_layout = qtw.QGridLayout()
self.main_layout.addWidget(self.getlistof_button,0,0)
self.main_layout.addWidget(self.addwidget_button, 1, 0)
self.main_layout.addWidget(self.linedittext_button, 2, 0)
self.setLayout(self.main_layout)
self.show()
# functionality
self.addwidget_button.clicked.connect(self.add_widget)
self.getlistof_button.clicked.connect(self.deleate_lastlineedit)
self.getlistof_button.clicked.connect(self.deleate_lastqlabel)
self.linedittext_button.clicked.connect(self.count)
def count(self):
x = self.main_layout.rowCount()
print(self.main_layout.rowCount()+1)
print(type(x))
def add_widget(self):
my_lineedit = qtw.QLineEdit()
x1 = (self.main_layout.rowCount()+1)
my_dynmic_label = qtw.QLabel("Dynamic")
self.main_layout.addWidget(my_dynmic_label,x1,0)
self.main_layout.addWidget(my_lineedit,x1,1)
def deleate_lastqlabel(self):
widgets = (self.main_layout.itemAt(i).widget() for i in range(self.main_layout.count()))
for index, widget in enumerate(widgets):
if index == (self.main_layout.count()-1) and isinstance(widget, qtw.QLabel):
# print("yes")
widget.deleteLater()
break
def deleate_lastlineedit(self):
widgets = (self.main_layout.itemAt(i).widget() for i in range(self.main_layout.count()))
for index, widget in enumerate(widgets):
if index == (self.main_layout.count()-1) and isinstance(widget, qtw.QLineEdit):
widget.deleteLater()
break
if __name__ == '__main__':
app = qtw.QApplication(sys.argv)
w = AddWidget()
sys.exit(app.exec_())

As the name suggests, deleteLater() deletes the object later.
Schedules this object for deletion.
The object will be deleted when control returns to the event loop.
If you add a print(self.main_layout.count()) after each deleteLater call in the cycle, you'll see that the count is still the same, and that's because the control is not yet returned to the event loop.
You should use layout.removeWidget() instead.
Besides that, it will not be enough anyway.
You are cycling through the whole list until you find the last element, but this means that the next-to-last will not be checked. A possible solution would be to do the for cycle twice, but doing it wouldn't be the smartest thing to do.
So, as I already suggested, you should use reversed().
Also, you need some form of control since you're going to remove two widgets, otherwise the cycle will break as soon as it finds the first match for isinstance.
def deleate_lastlineedit(self):
labelRemoved = editRemoved = False
widgets = [self.main_layout.itemAt(i).widget() for i in range(self.main_layout.count())]
for widget in reversed(widgets):
if isinstance(widget, qtw.QLineEdit):
editRemoved = True
elif isinstance(widget, qtw.QLabel):
labelRemoved = True
else:
continue
# in this case, removeWidget is not necessary, since we're not
# checking the count, but I'll leave it anyway for completeness;
self.main_layout.removeWidget(widget)
widget.deleteLater()
if editRemoved and labelRemoved:
break
Since you only need to remove the last widgets, creating a generator for the whole widgets is unnecessary. As long as you always insert only QLabels and QLineEdits at the end of the layout, you can just use a while loop.
def deleate_lastlineedit(self):
labelRemoved = editRemoved = False
while not (labelRemoved and editRemoved):
widget = self.main_layout.itemAt(self.main_layout.count() - 1).widget()
# now here removeWidget *IS* required, otherwise the while loop will
# never exit
self.main_layout.removeWidget(widget)
widget.deleteLater()
if isinstance(widget, qtw.QLineEdit):
editRemoved = True
elif isinstance(widget, qtw.QLabel):
labelRemoved = True
PS: I've already suggested you to better study Python's control flows, please follow my advice: this is your FOURTH question with almost the same issue, and I only answered because I wanted to clarify the deleteLater() problem, but you wouldn't even have needed to ask it, if you'd follow my previous suggestions and answers. Please, do study and practice, you can't expect to code a GUI if you don't even understand the most elementary basics of its language.

Related

How to modify single column QVBoxLayout to be multi-column?

I have a column of auto-generated buttons which, if there are too many of, can squash UI elements in the window. Therefore, I want to automatically convert the single column of buttons - nominally inside of a QVBoxLayout referred to as self.main_layout - into a multi-column affair by:
Removing the buttons from self.main_layout
Adding them to alternating new columns represented by QVBoxLayouts
Changing self.main_layout to a QHBoxLayout
Adding the new columns to this layout
My attempt simply results in the buttons staying in a single column but now don't even resize to fill the QSplitter frame they occupy:
app = QApplication(sys.argv)
window = TestCase()
app.exec_()
class TestCase(QMainWindow):
def __init__(self):
super().__init__()
test = QWidget()
self.layout = QVBoxLayout()
test.setLayout(self.layout)
for i in range(10):
temp_btn = QPushButton(str(i))
temp_btn.pressed.connect(self.multi_col)
self.layout.addWidget(temp_btn)
self.setCentralWidget(test)
#pyqtSlot()
def multi_col(self):
cols = [QVBoxLayout(), QVBoxLayout()]
while self.layout.count():
child = self.layout.takeAt(0)
if child.widget():
self.layout.removeItem(child)
cols[0].addItem(child)
cols[1], cols[0] = cols[0], cols[1]
self.layout = QHBoxLayout()
self.layout.addLayout(cols[0])
self.layout.addLayout(cols[1])
Any glaringly obvious thing I'm doing wrong here?
Replacing a layout of a QWidget is not so simple with assigning another object to the variable that stored the reference of the other layout. In a few lines of code you are doing:
self.layout = Foo()
widget.setLayout(self.layout)
self.layout = Bar()
An object is not the same as a variable, the object itself is the entity that performs the actions but the variable is only a place where the reference of the object is stored. For example, objects could be people and variables our names, so if they change our name it does not imply that they change us as a person.
The solution is to remove the QLayout using sip.delete and then set the new layout:
import sys
from PyQt5.QtCore import pyqtSlot
from PyQt5.QtWidgets import (
QApplication,
QHBoxLayout,
QMainWindow,
QPushButton,
QVBoxLayout,
QWidget,
)
import sip
class TestCase(QMainWindow):
def __init__(self):
super().__init__()
test = QWidget()
self.setCentralWidget(test)
layout = QVBoxLayout(test)
for i in range(10):
temp_btn = QPushButton(str(i))
temp_btn.pressed.connect(self.multi_col)
layout.addWidget(temp_btn)
#pyqtSlot()
def multi_col(self):
cols = [QVBoxLayout(), QVBoxLayout()]
old_layout = self.centralWidget().layout()
while old_layout.count():
child = old_layout.takeAt(0)
widget = child.widget()
if widget is not None:
old_layout.removeItem(child)
cols[0].addWidget(widget)
cols[1], cols[0] = cols[0], cols[1]
sip.delete(old_layout)
lay = QHBoxLayout(self.centralWidget())
lay.addLayout(cols[0])
lay.addLayout(cols[1])
def main():
app = QApplication(sys.argv)
window = TestCase()
window.show()
app.exec_()
if __name__ == "__main__":
main()
I'd like to propose an alternative solution, which is to use a QGridLayout and just change the column of the widgets instead of setting a new layout everytime. The "trick" is that addWidget() always adds the widget at the specified position, even if it was already part of the layout, so you don't need to remove layout items.
Obviously, the drawback of this approach is that if the widgets have different heights, every row depends on the minimum required height of all widgets in that row, but since the OP was about using buttons, that shouldn't be the case.
This has the major benefit that the switch can be done automatically with one function, possibly by setting a maximum column number to provide further implementation.
In the following example the multi_col function actually increases the column count until the maximum number is reached, then it resets to one column again.
class TestCase(QMainWindow):
def __init__(self):
super().__init__()
test = QWidget()
self.layout = QGridLayout()
test.setLayout(self.layout)
for i in range(10):
temp_btn = QPushButton(str(i))
temp_btn.clicked.connect(self.multi_col)
self.layout.addWidget(temp_btn)
self.setCentralWidget(test)
self.multiColumns = 3
self.columns = 1
def multi_col(self):
maxCol = 0
widgets = []
for i in range(self.layout.count()):
item = self.layout.itemAt(i)
if item.widget():
widgets.append(item.widget())
row, col, rSpan, cSpan = self.layout.getItemPosition(i)
maxCol = max(col, maxCol)
if maxCol < self.multiColumns - 1:
self.columns += 1
else:
self.columns = 1
row = col = 0
for widget in widgets:
self.layout.addWidget(widget, row, col)
col += 1
if col > self.columns - 1:
col = 0
row += 1
Note: I changed to the clicked signal, as it's usually preferred against pressed due to the standard convention of buttons (which "accept" a click only if the mouse button is released inside it), and in your case it also creates two issues:
visually, the UI creates confusion, with the pressed button becoming unpressed in a different position (since the mouse button is released outside its actual and, at that point, different geometry);
conceptually, because if the user moves the mouse again inside the previously pressed button before releasing the mouse, it will trigger pressed once again;

PyQt5 - How to change specific item background color in List Widget

This program has a simple list widget. How do I change the background color of any specific item in the list without it being clicked?
from PyQt5 import QtWidgets, uic, QtGui
import threading
app = QtWidgets.QApplication([])
ui = uic.loadUi("app.ui")
update_ui = True
ui.lst_positions.addItem("Item 1")
ui.lst_positions.addItem("Item 2")
ui.lst_positions.addItem("Item 3")
def ui_update():
while update_ui == True:
# Updating labels works fine with threading
ui.lbl_in_position.setText("Random text")
# This does not work with threading
item = ui.lst_positions.item(1)
item.setBackground(QtGui.QColor("red"))
ui_thread = threading.Thread(target=ui_update)
ui_thread.daemon = True
ui_thread.start()
ui.show()
app.exec()
The task is trivial:
Get the QListWidgetItem associated with the index.
Use the setBackground method of the QListWidgetItem.
item = ui.lst_positions.item(item_number_to_change)
item.setBackground(QtGui.QColor("red"))
Update:
The problem is that you should not update the GUI from another thread since the elements of the GUI are not thread-safe, so you get that warning that you point out in the comments. One solution is to create a QObject that has signals (which are thread-safe) to exchange information between threads.
# ...
class Signaller(QtCore.QObject):
textChanged = QtCore.pyqtSignal(str)
itemChanged = QtCore.pyqtSignal(int, QtGui.QColor)
def ui_update(qobject):
while update_ui == True:
qobject.textChanged.emit("Random text")
qobject.itemChanged.emit(i, QtGui.QColor("red"))
def foo(i, color):
item = ui.lst_positions.item(i)
if item:
item.setBackground(color)
signaller = Signaller()
signaller.textChanged.connect(ui.lbl_in_position.setText)
signaller.itemChanged.connect(foo)
ui_thread = threading.Thread(target=ui_update, args=(signaller,))
ui_thread.daemon = True
ui_thread.start()
# ...
Note: Just because the text is displayed correctly does not imply that it is correct. Qt does not guarantee (that implies that it can fail) the correct operation if the GUI is updated from another thread directly.

Do I ever need to manually destroy objects (like pixmaps)?

I am writing a PySide2-based applicatioon, which includes a QScrollArea that holds a lot of QPixmap images (or better: a list of QLabel's that in turn contain the pixmaps). That list of images can grow quite large over time, so when a certain number is reached I periodically remove some of these images from the scroll area - which works fine.
I do have the impression, however, that even after removing some of the images the memory consumption of my application is still the same. So removing the label widgets might not be sufficient. From the PySide2 docs on QLayout.removeWidget():
Removes the widget widget from the layout. After this call, it is the caller’s responsibility to give the widget a reasonable geometry or to put the widget back into a layout or to explicitly hide it if necessary.
In order to remove the widget I do the following:
while self.images_scroll_layout.count() > MAX_IMAGES:
to_remove = self.images_scroll_layout.itemAt(self.images_scroll_layout.count() - 1)
self.images_scroll_layout.removeItem(to_remove)
to_remove.widget().deleteLater()
So my question is: Do I need to manually destroy the labels/pixmaps I removed from the layout, or should they be garbage-collected automatically?
To understand the operation you have to have the following clear concepts:
A QObject will not be removed by the GC if it has a parent.
When a widget is added to a layout, then the widget is set as a child of the widget where the layout was established.
When using removeWidget() then only the widget is removed from the widget list that handles the layout, so the parent of the widget is still the widget that handles the layout.
To verify you can use the following code where the destroyed signal that indicates when a QObject is deleted will not be emitted.
from PySide2 import QtWidgets
class Widget(QtWidgets.QWidget):
def __init__(self, parent=None):
super().__init__(parent)
self.add_button = QtWidgets.QPushButton(self.tr("Add"), clicked=self.add_widget)
self.remove_button = QtWidgets.QPushButton(
self.tr("Remove"), clicked=self.remove_widget
)
scrollarea = QtWidgets.QScrollArea(widgetResizable=True)
widget = QtWidgets.QWidget()
scrollarea.setWidget(widget)
lay = QtWidgets.QVBoxLayout(self)
lay.addWidget(self.add_button)
lay.addWidget(self.remove_button)
lay.addWidget(scrollarea)
self.resize(640, 480)
self.label_layouts = QtWidgets.QVBoxLayout(widget)
self.counter = 0
def add_widget(self):
label = QtWidgets.QLabel(f"label {self.counter}")
self.label_layouts.addWidget(label)
self.counter += 1
def remove_widget(self):
item = self.label_layouts.itemAt(0)
if item is None:
return
widget = item.widget()
if widget is None:
return
widget.destroyed.connect(print)
print(f"widget: {widget} Parent: {widget.parentWidget()}")
if __name__ == "__main__":
import sys
app = QtWidgets.QApplication(sys.argv)
w = Widget()
w.show()
sys.exit(app.exec_())
In conclusion: removeWidget() is not used to remove the widget from memory but only makes the layout not handle that widget, if you want to remove a widget you must use deleteLater().
def remove_widget(self):
item = self.label_layouts.itemAt(0)
if item is None:
return
widget = item.widget()
if widget is None:
return
widget.destroyed.connect(print)
widget.deleteLater()

Delete last widget from gridlayout

I have a Grid layout in which I add Qlineedits at runtime.
while pushing the button I want to delete the last qline edit from the gridlaout
Why does this function delete all qlinedits at the same time ?
def deleate_widgets(self):
widgets = (self.main_layout.itemAt(i).widget() for i in range(self.main_layout.count()))
for widget in widgets:
if isinstance(widget, qtw.QLineEdit):
print("linedit: %s - %s" %(widget.objectName(), widget.text()))
widget.deleteLater() # all objects
How to change the code to only delete one widget at a time, preferably the last added widget ?
full code
#!/usr/bin/env python
"""
Interface to get the specific weight of each of the 5 containers
start_measurment_button starts thread /thread_worker
transfer_data button start query and send data to database
"""
import sys
import sqlite3
from PyQt5 import QtWidgets as qtw
from PyQt5 import QtCore as qtc
from PyQt5 import QtGui as qtg
from PyQt5 import QtSql as qsql
from PyQt5 import sip
class AddWidget(qtw.QWidget):
'''
Interface with embedded SQL functions
'''
# Attribut Signal
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
# your code will go here
self.mylist = []
# interface
# position
qtRectangle = self.frameGeometry()
centerPoint = qtw.QDesktopWidget().availableGeometry().center()
qtRectangle.moveCenter(centerPoint)
self.move(qtRectangle.topLeft())
# size
self.resize(700, 410)
# frame title
self.setWindowTitle("add Widget")
# heading
heading_label = qtw.QLabel('add Widget')
heading_label.setAlignment(qtc.Qt.AlignHCenter | qtc.Qt.AlignTop)
# add Button
self.addwidget_button = qtw.QPushButton("add Widget")
self.getlistof_button = qtw.QPushButton("deleate")
self.main_layout = qtw.QGridLayout()
self.main_layout.addWidget(self.getlistof_button,0,0)
self.main_layout.addWidget(self.addwidget_button, 1, 0)
self.setLayout(self.main_layout)
self.show()
# functionality
self.addwidget_button.clicked.connect(self.add_widget)
# self.getlistof_button.clicked.connect(self.deleate_widgets_try)
def add_widget(self):
self.my_lineedit = qtw.QLineEdit()
self.mylist.append(self.my_lineedit)
self.main_layout.addWidget(self.my_lineedit)
def deleate_widgets(self):
widgets = (self.main_layout.itemAt(i).widget() for i in range(self.main_layout.count()))
for widget in widgets:
if isinstance(widget, qtw.QLineEdit):
print(widget)
# print("linedit: %s - %s" %(widget.objectName(), widget.text()))
# widget.deleteLater() # alle objects
#
# def deleate_widgets_try(self):
# widgets = (self.main_layout.itemAt(i).widget() for i in range(self.main_layout.count()))
# my_iter = iter(widgets)
#
# if isinstance(my_iter, qtw.QLineEdit):
# next(my_iter.deleteLater()) # alle objects)
if __name__ == '__main__':
app = qtw.QApplication(sys.argv)
w = AddWidget()
sys.exit(app.exec_())
Your function removes all widgets because you are cycling through all the widgets, from the first to the last.
Also, there is really no need to go through the whole layout, since you already keep a list of widgets that always appends the last one at the end.
Just pop out the last item from the list. Removing it from the layout shouldn't be necessary, as deleteLater() would take care of it, but that's just for demonstration purposes.
def deleate_widgets(self):
# remove the last item from the list
lastWidget = self.mylist.pop(-1)
self.main_layout.removeWidget(lastWidget)
lastWidget.deleteLater()
For the sake of completeness, your function should have done the following:
cycle the widgets through the layout backwards;
break the cycle as soon as the first (as in last) item is found;
def deleate_widgets(self):
widgets = [self.main_layout.itemAt(i).widget() for i in range(self.main_layout.count())]
# use reversed() to cycle the list backwards
for widget in reversed(widgets):
if isinstance(widget, qtw.QLineEdit):
print("linedit: %s - %s" %(widget.objectName(), widget.text()))
widget.deleteLater()
# the line edit has been found, exit the cycle with break to avoid
# deleting further widgets
break
Also, there's really no use in creating instance attributes (self.someobject = ...) for objects that don't need a persistent reference, especially if you are creating those objects repeatedly (which will result in a constant overwrite of that attribute, making it useless) and you already are keeping them in an persistent data model object (usually a list, a tuple, a dictionary) like self.mylist in your case (and that has to be an instance attribute):
def add_widget(self):
# no need to create a "self.my_lineedit"
my_lineedit = qtw.QLineEdit()
self.mylist.append(my_lineedit)
self.main_layout.addWidget(my_lineedit)
Having seen your previous questions and comments, I strongly suggest you to better study and experiment with the Python data models, control flows and classes, as they are basic concepts (of Python and programming in general) that must be understood and internalized before attempting to do anything else.

Python+Qt, QScrollArea problem: what's wrong with this code?

I have the following code:
#!/usr/bin/env python
import sys
from PyQt4 import QtGui, QtCore
class SimfilePanel(QtGui.QWidget):
'''This class provides the simfile panel shown on the right side of the main window.'''
def __init__(self, parent=None):
'''Load song info here.'''
QtGui.QWidget.__init__(self, parent)
## Make widgets.
# Pane with simfile information.
simfileInfoPane = QtGui.QWidget()
simfileInfoPane.setSizePolicy(QtGui.QSizePolicy.Expanding, QtGui.QSizePolicy.Expanding)
simfileInfoGrid = QtGui.QGridLayout()
simfileInfoPane.setLayout(simfileInfoGrid)
simfileInfoScrollArea = QtGui.QScrollArea()
simfileInfoScrollArea.setWidget(simfileInfoPane)
#if DEBUG: simfileInfoScrollArea.setBackgroundRole(QtGui.QPalette.Dark);
# This will change
labels = []
textfields = []
for i in range(0,20):
labels.append( QtGui.QLabel("Label "+str(i)) )
textfields.append( QtGui.QLineEdit() )
labels[i].setBuddy(textfields[i])
simfileInfoGrid.addWidget(labels[i], i, 0)
simfileInfoGrid.addWidget(textfields[i], i, 1)
## Put widgets in a grid layout.
mainvbox = QtGui.QVBoxLayout()
mainvbox.addWidget(simfileInfoScrollArea)
self.setLayout(mainvbox)
# Standalone testing
if __name__ == "__main__":
app = QtGui.QApplication(sys.argv)
panel = SimfilePanel()
panel.show()
sys.exit(app.exec_())
I can't get anything that I'm putting into the simfileInfoGrid to display! They'll display if I leave out the scroll area, but I need the scroll area as I will have a lot of fields to edit in the final version and I don't want to stretch the entire window over the screen.
As you see I've tried to add a size policy to simfileInfoPane, but it doesn't seem to affect anything. The area that's supposed to contain my pane stays empty!
Add the pane to the scroll area after you've added all the grid's contents. In particular you need to call QScrollArea.setWidget after you have finished creating the widget you add.
I don't know exactly why this is the problem, but I do know that I tend to initialize widgets "bottom-up": I finish adding all the contents of a sub-layout before I ever add it to a parent layout. I believe this is Qt optimizing order of rendering but I could be wrong about that.
The code below is a patch, mostly so you can see where the one-line change is.
diff -u 1848547.py tmp2.py
--- 1848547.py 2009-12-04 11:19:09.000000000 -0800
+++ tmp2.py 2009-12-04 11:34:58.000000000 -0800
## -19,7 +19,6 ##
simfileInfoPane.setLayout(simfileInfoGrid)
simfileInfoScrollArea = QtGui.QScrollArea()
- simfileInfoScrollArea.setWidget(simfileInfoPane)
#if DEBUG:
simfileInfoScrollArea.setBackgroundRole(QtGui.QPalette.Dark)
## -33,6 +32,8 ##
simfileInfoGrid.addWidget(labels[i], i, 0)
simfileInfoGrid.addWidget(textfields[i], i, 1)
+ simfileInfoScrollArea.setWidget(simfileInfoPane)
+
## Put widgets in a grid layout.
mainvbox = QtGui.QVBoxLayout()
mainvbox.addWidget(simfileInfoScrollArea)
I have recently been struggling with the same thing, and I believe I have found the solution you are looking for.
The problem seems to be that when you add an empty widget to the scroll area, it has dimensions of zero by zero (since there is nothing inside of it).
The reason it doesn't get bigger is because there is a flag called widgetResizable that is False by default.
By simply calling setWidgetResizable(True) on the scroll area, the widget gets bigger as new items are added.
I hope this helps.

Categories