In a application I create lines of widgets like this (there is a button to create the line and another to remove the widget):
Code for creation of the widgets lines :
def ajouter_ref_artistique(self) :
'''
Gestion des widgets (AJOUTER widgets) partie Références artistiques
'''
# Nombre d'éléments présents
try :
# ...
r = len(self.l_t_ref_art)
except :
# ...
r = len(self.liste_ref_artistiques)
# Création des QTextEdit
self.dico_chem_ref_art[r] = QTextEdit()
self.dico_com_ref_art[r] = QTextEdit()
self.fonte(self.dico_com_ref_art[r]) # Fontion pout l'attribution de la fonte
self.dico_chem_ref_art[r].setMaximumWidth(150)
self.dico_chem_ref_art[r].setMinimumWidth(150)
self.dico_chem_ref_art[r].setMaximumHeight(84)
self.dico_chem_ref_art[r].setMinimumHeight(84)
self.dico_com_ref_art[r].setMaximumWidth(430)
self.dico_com_ref_art[r].setMinimumWidth(430)
self.dico_com_ref_art[r].setMaximumHeight(84)
self.dico_com_ref_art[r].setMinimumHeight(84)
# Création des boutons de chargement
self.dico_bout_charg_ref_art[r] = QPushButton("Ouvrir référence art. {}".format(r+1))
self.dico_bout_charg_ref_art[r].setMaximumWidth(180)
self.dico_bout_charg_ref_art[r].setStyleSheet("background-color: qlineargradient(x1: 0, y1: 0, x2: 0, y2: 1, stop: 0 #fdfbf7, stop: 1 #6190F2);border-style: solid;border-width: 2px;border-radius: 8px;border-color: #9BB7F0;padding: 2px")
# Répartition dans la grille
self.grille_3_stack_6.addWidget(self.dico_chem_ref_art[r], r, 0)
self.grille_3_stack_6.addWidget(self.dico_com_ref_art[r], r, 1)
self.grille_3_stack_6.addWidget(self.dico_bout_charg_ref_art[r], r, 2)
# Ecriture des n°s de lignes
self.dico_chem_ref_art[r].setText(str(r+1)+'. ')
# Placement en lecture seule (vignette de l'oeuvre et chemin)
self.dico_chem_ref_art[r].setReadOnly(True)
try :
# ...
self.l_t_ref_art.append([self.dico_chem_ref_art[r], self.dico_com_ref_art[r], r])
except :
# ...
# id 0 --> self.dico_chem_ref_art[r] (QTextEdit pour récup des données et affichage vignette image)
# id 1 --> self.dico_com_ref_art[r] (QTextEdit pour récup des données commentaires oeuvres)
# id 2 --> r (Chiffre correspondant au numéro du bouton)
self.liste_ref_artistiques.append([self.dico_chem_ref_art[r], self.dico_com_ref_art[r], r])
# =====================================================
# Signaux
# ---------- Récup des données textuelles
self.dico_chem_ref_art[r].textChanged.connect(self.changements_phase_6)
self.dico_com_ref_art[r].textChanged.connect(self.changements_phase_6)
# ---------- Récup du libellé du bouton sélectionné par l'utilisateur
self.dico_bout_charg_ref_art[r].released.connect(self.libelle_bouton_ref_art)
# =====================================================
Code for removing widget lines :
def supprimer_ref_artistique(self) :
'''
Gestion des widgets (SUPPRIMER widgets) partie Références artistiques
'''
try :
# Dans le cas du chargement d'une séquence prof
row = len(self.l_t_ref_art) - 1
del self.l_t_ref_art[row]
# ...
self.row_ref_art = row
except :
# Dans le cas de la 1ère création d'une séquence prof
row = len(self.liste_ref_artistiques) - 1
del self.liste_ref_artistiques[row]
# ...
self.row_ref_art = row
if row >= 0:
for column in range(self.grille_3_stack_6.columnCount()):
item = self.grille_3_stack_6.itemAtPosition(row, column)
#item = self.grille_3_stack_6.itemAtPosition(1, column)
if item is not None:
item.widget().deleteLater()
del self.dico_chem_ref_art[row]
del self.dico_com_ref_art[row]
del self.dico_bout_charg_ref_art[row]
I would also like to change the position of the widgets lines (up or down) with 'up' and 'down' buttons, I don't know how to do it.
You can use a QListWidget to store your rows.
Qt has some difficult syntax for putting a QWidget as an item in a QListWidget.
For simple text, you can follow this post to move items up:
currentRow = self.listWidget.currentRow()
currentItem = self.listWidget.takeItem(currentRow)
self.listWidget.insertItem(currentRow - 1, currentItem)
And a similar process to move items down. When you have widgets in your rows, it gets a lot more involved. See complete example below.
import sys
import random
from PyQt5.QtWidgets import (QWidget, QPushButton, QLabel, QWidget,
QHBoxLayout, QVBoxLayout, QApplication, QListWidget, QTextEdit,
QListWidgetItem, QLayout, QLineEdit)
class Example(QWidget):
def __init__(self):
super().__init__()
self.initUI()
def initUI(self):
addButton = QPushButton('+')
addButton.clicked.connect(self.addrow)
delButton = QPushButton('-')
delButton.clicked.connect(self.delrow)
upButton = QPushButton('▲', parent = self)
upButton.clicked.connect(self.rowup)
downButton = QPushButton('▼', parent = self)
downButton.clicked.connect(self.rowdown)
hbox = QHBoxLayout()
hbox.addWidget(addButton)
hbox.addWidget(delButton)
hbox.addWidget(upButton)
hbox.addWidget(downButton)
self.listbox = QListWidget()
vbox = QVBoxLayout()
vbox.addLayout(hbox)
vbox.addWidget(self.listbox)
vbox.setStretch(0,1)
vbox.setStretch(1,4)
self.setLayout(vbox)
self.setGeometry(300, 300, 600, 400)
self.setWindowTitle('Test')
self.show()
def rowup(self):
row_num = self.listbox.currentRow()
if row_num > 0:
row = self.listbox.itemWidget(self.listbox.currentItem())
itemN = self.listbox.currentItem().clone()
self.listbox.insertItem(row_num -1, itemN)
self.listbox.setItemWidget(itemN, row)
self.listbox.takeItem(row_num+1)
self.listbox.setCurrentRow(row_num-1)
def rowdown(self):
row_num = self.listbox.currentRow()
if row_num == -1:
# no selection. abort
return
elif row_num < self.listbox.count():
row = self.listbox.itemWidget(self.listbox.currentItem())
itemN = self.listbox.currentItem().clone()
self.listbox.insertItem(row_num + 2, itemN)
self.listbox.setItemWidget(itemN, row)
self.listbox.takeItem(row_num)
self.listbox.setCurrentRow(row_num+1)
def addrow(self):
row = self.makerow()
itemN = QListWidgetItem()
itemN.setSizeHint(row.sizeHint())
self.listbox.addItem(itemN) # add itemN to end of list. use insertItem
# to insert in specific location
self.listbox.setItemWidget(itemN, row)
def delrow(self):
if self.listbox.currentRow() == -1:
# no selection. delete last row
row_num = self.listbox.count() - 1
else:
row_num = self.listbox.currentRow()
item = self.listbox.takeItem(row_num)
del item
def makerow(self):
widget = QWidget()
hbox = QHBoxLayout()
r = random.random()
r = '%f' % r
print(r)
label = QLabel(r)
textedit = QLineEdit()
button = QPushButton('Ouvrir reference art %s' % r)
hbox.addWidget(label)
hbox.addWidget(textedit)
hbox.addWidget(button)
hbox.addStretch()
hbox.setSizeConstraint(QLayout.SetFixedSize)
widget.setLayout(hbox)
return widget
if __name__ == '__main__':
app = QApplication(sys.argv)
ex = Example()
sys.exit(app.exec_())
Originally my application classifies and formats the data given by the user, saves the data in a file (managed by the pickle module) and generates files in pdf format. I want to do a little facelift for a greater user-friendliness. In my application, widget lines can be added and removed with buttons, but here bfris and S. Nick allowed me to discover and understand the usefulness of QListWidget.
I bring a small participation by connecting the widgets to the data provided by the user (data displayed and put back in order in the terminal, ... with update after adding, deleting, up, down). There is some of the code (which I adapted here) that comes from my application.
Apologies for my bad English in the comments of the code. The code (be careful you have to install the pillow module) :
import sys, os
# Pillow
try : import Image
except ImportError : from PIL import Image
from PyQt5.QtWidgets import (QWidget, QPushButton, QLabel, QWidget,
QHBoxLayout, QVBoxLayout, QApplication, QListWidget, QTextEdit,
QListWidgetItem, QLayout, QLineEdit, QFileDialog)
class ExampleQListWidgetAndData(QWidget):
'''
Here lines of widgets are displayed, deleted, reordered. In these
widget lines you can load and enter data that can then be processed.
'''
def __init__(self):
super().__init__()
self.initUI()
# -----------------------
# Dictionary of the path of artistic references
# -----------------------
self.dico_chem_ref_art = {}
# -----------------------
# Dictionary of comments of artistic references
# -----------------------
self.dico_com_ref_art = {}
# -----------------------
# QPushButton dictionary of loading artistic references
# -----------------------
self.dico_bout_charg_ref_art = {}
# -----------------------
# List for loading data
# -----------------------
self.liste_ref_artistiques = []
def initUI(self):
addButton = QPushButton('+')
addButton.clicked.connect(self.ajouter_ref_artistique)
delButton = QPushButton('-')
delButton.clicked.connect(self.supprimer_ref_artistique)
upButton = QPushButton('▲', parent = self)
upButton.clicked.connect(self.monter_ligne_widgets_references_artistiques)
downButton = QPushButton('▼', parent = self)
downButton.clicked.connect(self.descendre_ligne_widgets_references_artistiques)
hbox = QHBoxLayout()
hbox.addWidget(addButton)
hbox.addWidget(delButton)
hbox.addWidget(upButton)
hbox.addWidget(downButton)
self.listbox = QListWidget()
vbox = QVBoxLayout()
vbox.addLayout(hbox)
vbox.addWidget(self.listbox)
vbox.setStretch(0,1)
vbox.setStretch(1,4)
self.setLayout(vbox)
self.setGeometry(50, 50, 900, 700)
self.setWindowTitle('Test')
self.show()
def monter_ligne_widgets_references_artistiques(self) :
### UP ###
row_num = self.listbox.currentRow()
if row_num > 0 :
row = self.listbox.itemWidget(self.listbox.currentItem())
itemN = self.listbox.currentItem().clone()
self.listbox.insertItem(row_num -1, itemN)
self.listbox.setItemWidget(itemN, row)
self.listbox.takeItem(row_num+1)
self.listbox.setCurrentRow(row_num-1)
# Index of the widgets line (0, 1, 2, ....)
id_row = row_num - 1
# Exchange of indexes (permutation of data) so that values are placed in the right place (in
# full correlation with the placement of widget lines ... such as those on the screen)
self.liste_ref_artistiques[id_row], self.liste_ref_artistiques[row_num] = self.liste_ref_artistiques[row_num], self.liste_ref_artistiques[id_row]
# TEST
self.changements_phase_6()
def descendre_ligne_widgets_references_artistiques(self) :
### DOWN ###
row_num = self.listbox.currentRow()
if row_num == -1 : return
elif row_num < self.listbox.count():
row = self.listbox.itemWidget(self.listbox.currentItem())
itemN = self.listbox.currentItem().clone()
self.listbox.insertItem(row_num + 2, itemN)
self.listbox.setItemWidget(itemN, row)
self.listbox.takeItem(row_num)
self.listbox.setCurrentRow(row_num+1)
# Index of the widgets line (0, 1, 2, ....)
id_row = row_num + 1
# Exchange of indexes (permutation of data) so that values are placed in the right place (in
# full correlation with the placement of widget lines ... such as those on the screen)
if self.listbox.count() >= row_num+2 :
self.liste_ref_artistiques[row_num], self.liste_ref_artistiques[id_row] = self.liste_ref_artistiques[id_row], self.liste_ref_artistiques[row_num]
# TEST
self.changements_phase_6()
def ajouter_ref_artistique(self):
### + (add) ###
widget = QWidget()
hbox = QHBoxLayout()
# Number of elements
r = len(self.liste_ref_artistiques)
#
self.dico_chem_ref_art[r] = QTextEdit()
self.dico_com_ref_art[r] = QTextEdit()
self.dico_chem_ref_art[r].setMaximumWidth(150)
self.dico_chem_ref_art[r].setMinimumWidth(150)
self.dico_chem_ref_art[r].setMaximumHeight(84)
self.dico_chem_ref_art[r].setMinimumHeight(84)
self.dico_com_ref_art[r].setMaximumWidth(430)
self.dico_com_ref_art[r].setMinimumWidth(430)
self.dico_com_ref_art[r].setMaximumHeight(84)
self.dico_com_ref_art[r].setMinimumHeight(84)
self.dico_bout_charg_ref_art[r] = QPushButton("Ouvrir référence art. {}".format(r+1))
self.dico_bout_charg_ref_art[r].setStyleSheet("background-color: qlineargradient(x1: 0, y1: 0, x2: 0, y2: 1, stop: 0 #fdfbf7, stop: 1 #6190F2);border-style: solid;border-width: 2px;border-radius: 8px;border-color: #9BB7F0;padding: 2px")
hbox.addWidget(self.dico_chem_ref_art[r])
hbox.addWidget(self.dico_com_ref_art[r])
hbox.addWidget(self.dico_bout_charg_ref_art[r])
hbox.addStretch()
hbox.setSizeConstraint(QLayout.SetFixedSize)
# Writing the number of lines in the buttons
self.dico_chem_ref_art[r].setText(str(r+1)+'. ')
# Placement in read-only (thumbnail of the work and path)
self.dico_chem_ref_art[r].setReadOnly(True)
widget.setLayout(hbox)
#
row = widget
itemN = QListWidgetItem()
itemN.setSizeHint(row.sizeHint())
self.listbox.addItem(itemN)
self.listbox.setItemWidget(itemN, row)
# The data are entered in a list ...
# id 0 --> self.dico_chem_ref_art[r] (QTextEdit for data recovery and image thumbnail display)
# id 1 --> self.dico_com_ref_art[r] (QTextEdit to retrieve data comments works)
# id 2 --> r (Number corresponding to the number of the button)
self.liste_ref_artistiques.append([self.dico_chem_ref_art[r], self.dico_com_ref_art[r], r])
# =====================================================
# Signals
# ---------- Retrieving textual data
self.dico_chem_ref_art[r].textChanged.connect(self.changements_phase_6)
self.dico_com_ref_art[r].textChanged.connect(self.changements_phase_6)
# ---------- Retrieving the label of the button selected by the user
self.dico_bout_charg_ref_art[r].released.connect(self.libelle_bouton_ref_art)
# =====================================================
def supprimer_ref_artistique(self):
### - (remove) ###
if self.listbox.currentRow() == -1 :
row_num = self.listbox.count() - 1
else :
row_num = self.listbox.currentRow()
item = self.listbox.takeItem(row_num)
del item
# We refresh the widgets in the dedicated list
del self.liste_ref_artistiques[row_num]
# TEST
self.changements_phase_6()
def changements_phase_6(self) :
"""
DATA ...
"""
# [["Path of the image 1 --> artwork", "Comment on the artwork 1", Button index 1 (int)], ...]
# The: \ufffc\n is removed from the path of the image, by hashing [2:]
if str(type(self.liste_ref_artistiques[0][0])) != "<class 'str'>" :
# The list retrieves the textual data
self.liste_ref_artistiques_finale = [[str(refart[0].toPlainText())[2:], str(refart[1].toPlainText()), refart[2]] for refart in self.liste_ref_artistiques if str(type(refart[0])) and str(type(refart[1])) and str(type(refart[2])) != "<class 'str'>"]
# Reordering the index of buttons after changes
self.liste_ref_artistiques_finale = [[ref[0], ref[1], n] for n, ref in enumerate(self.liste_ref_artistiques_finale)]
print('CHANGEMENTS : ', self.liste_ref_artistiques_finale)
def redim_img(self, chemin, nouv_w) :
'''
Image resizing function (works with Pillow)
'''
# Resizing the image for display in the QTextEdit
# Width of the future thumbnail
self.nouv_w = nouv_w
# Opening the image
obImg = Image.open(chemin)
# Recover image dimensions
w, h = obImg.size
# Calculation of the ratio of the image
ratio = float(w)/float(h)
# Calculation of future height
self.calcHauteur_img = int(float(self.nouv_w)/ratio)
# Returns the tuple: (width, height) of each image
self.tuple_dim_img_vignette = (self.nouv_w, self.calcHauteur_img)
return self.tuple_dim_img_vignette
def libelle_bouton_ref_art(self) :
'''
Select the id pressed button in order to
displaying the path of the selected image
in the dedicated QTextEdit
'''
# Retrieve the action of the button
message_bouton_ref_art = self.sender()
# The button text (eg for button n°2) is
# in the form : Ouvrir référence art 2
# This retrieves the text of the button action
texte_bouton = message_bouton_ref_art.text()
# Only the text including the number is selected
numero_bouton = texte_bouton[-2:]
# Si il y a un espace ds la sélection, c'est à dire,
# par exemple, pour le 3ème bouton on obtiendra " 3",
# ... si il y 10 boutons, on aura "10" (on se laisse
# la possibilité de pouvoir sélectionner de 1 à 99
# boutons)
# If there is a space in the selection, that is,
# for example, for the 3rd button we will get "3",
# ... if there are 10 buttons, we will have "10"
# (ability to select from 1 to 99 buttons)
if numero_bouton[0:1] in [" ", "&"] :
numero_bouton = numero_bouton[1:2]
numero_bouton = int(numero_bouton)
# Index of the button
i = numero_bouton - 1
# =====================================================
# Signal
# ---------- Display images/thumbnails and image paths
self.dico_bout_charg_ref_art[i].clicked.connect(lambda: self.ouvrir_image_boite_ref_art(i))
# =====================================================
def ouvrir_image_boite_ref_art(self, n) :
'''
Function for the opening dialog to load
the different works (artistic references)
'''
rep_defaut_chargement = os.path.expanduser('~')
ouv_fichier = QFileDialog.getOpenFileName(self, 'Ouvrir une image', rep_defaut_chargement, 'Images (*.jpg *.jpeg *.JPG *.JPEG *.png *.gif)')[0]
# Recover path and file name
chemin_fichier_ref_art = str(ouv_fichier)
# Call the resize image function for artistic
# references (thumbnailing). The width dimension
# is 100 pixels (self.nouv_w)
self.redim_img(chemin_fichier_ref_art, 100)
# Showing the thumbnail in the QTextEdit
self.dico_chem_ref_art[n].setHtml('<center><img src="{}" width="{}" height="{}" title="{}" /></center><h6><b>{}</b></h6>'.format(chemin_fichier_ref_art, 100, self.calcHauteur_img, chemin_fichier_ref_art, chemin_fichier_ref_art))
# The final list of data is updated with the
# new data (the path and name of the loaded image)
self.liste_ref_artistiques_finale[n][0] = chemin_fichier_ref_art
if __name__ == '__main__':
app = QApplication(sys.argv)
ex = ExampleQListWidgetAndData()
sys.exit(app.exec_())
Screenshots with the display of data from the terminal:
I hope this can help someday.