-->

Up or down lines of widgets

2019-09-19 13:32发布

问题:

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.

回答1:

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



回答2:

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.