stu:python_gui:pyqt_projet_biblio

Un formulaire d'entrées bibliographiques : Formulaires et layouts

On se propose ici de réaliser un formulaire (pour gérer le contenu d'une base de données par exemple). Le formulaire devra être esthétiquement réussi, et les objets devront être judicieusement disposés. La fenêtre sera redimensionnable. Pour atteindre cet objectif, nous allons nous intéresser aux Layouts de Qt.

Les layouts permettent d'indiquer à Qt comment disposer les objets, et comment ceux-ci réagiront au redimensionnement de la fenêtre. Ils sont à l'opposé du placement par position absolue des widgets.

- Mise en place des Widgets

Nous désirons réaliser l'application suivante :

Cette application permet d'entrer des renseignements au sujet d'un livre, et formate correctement la notice en bas de la fenêtre. Il ne s'agit que d'une ébauche et les informations à entrer ne sont pas assez complètes pour faire une notice correcte. Ceci nous permettra néanmoins, d'illustrer le principe des layouts.

Il existe plusieurs objets Qt correspondant à des layouts, parmi lesquels : QHBoxLayout, QVBoxLayout, QGridBoxLayout. Le premier permet de disposer des objets horizontalement, le second permet de les empiler et le troisième permet de les disposer sur une grille. Notre application utilise à la foix QVBoxLayout et QGridLayout. La figure suivante visualise les layouts. Nous voyons en bleu les 4 blocs empilés du QVBoxLayout. Le bloc le plus haut contient un QGridLayout de 4 lignes et trois colonnes dont les cellules sont visualisées en rouge. On constate que certaines cellules peuvent être vides et que certains widgets peuvent empiéter sur plusieurs cellules (voir la ligne du bas) :

Le principe d'utilisation des layouts est le suivant :

  1. création des layouts
  2. création des widgets
  3. ajout des widgets aux layouts, inclusion des layouts les uns dans les autres
  4. application du layout principal au widget principal
from PySide import QtGui,QtCore
import sys
 
class Fenetre(QtGui.QMainWindow):
    def __init__(self,parent=None) :
        super().__init__(parent)
        self.createInterface()
 
    def createInterface(self) :
        frame=QtGui.QWidget() # widget principal
        # widget du choix de l'année
        year=QtGui.QSpinBox() 
        year.setMaximum(3000)
        year.setMinimum(1400)
        year.setValue(2012))
        # widget d'édition des commentaires
        edit=QtGui.QTextEdit() 
 
        # Création du QGridLayout --------------
        fields=QtGui.QGridLayout()
        # Et ajout des objets (on précise numéro de ligne et colonne)
        # certains objets sont créés et ajoutés à la volée
 
        # première ligne
        fields.addWidget(QtGui.QLabel("Titre"),0,0)
        titre=QtGui.QLineEdit()
        fields.addWidget(titre,0,1)
 
        # seconde ligne
        fields.addWidget(QtGui.QLabel("Auteur"),1,0)
        auteur=QtGui.QLineEdit()
        fields.addWidget(auteur,1,1)
        fields.addWidget(QtGui.QCheckBox("n/c"),1,2)
 
        # troisième ligne
        fields.addWidget(QtGui.QLabel("Année"),2,0)
        fields.addWidget(year,2,1)
 
        # quatrième ligne (noter les paramères columnspan et rowspan
        # qui permettent au widget d'occuper 2 cases
        fields.addWidget(QtGui.QLabel("Éditeur"),3,0)
        fields.addWidget(QtGui.QLineEdit(),3,1,1,2)
 
        # Création de la zone d'affichage de la notice
        output=QtGui.QTextEdit()
        output.setReadOnly(True)
        output.setHtml("<i>Notice incomplète</i>")
 
        # Création du layout vertical
        vbox=QtGui.QVBoxLayout()
        # dans lequel on ajoute (de haut en bas) :
        # le QGridLayout : fields
        vbox.addLayout(fields)
        # le label "Commentaires "
        vbox.addWidget(QtGui.QLabel("Commentaires :"))
        # le widget d'édition des commentaires
        vbox.addWidget(edit,stretch=1)
        # le widget d'affichage de la notice
        vbox.addWidget(output,stretch=3)
 
        # Puis on affecte ce layout principal au widget frame
        frame.setLayout(vbox)
 
        # On définit frame comme étant le widget principal
        self.setCentralWidget(frame)
 
        # Réglages titre et taille de la fenêtre
        self.setWindowTitle('Livre')
        self.setGeometry(0,0,300,300)
 
app=QtGui.QApplication(sys.argv)
frame=Fenetre()
frame.show()
sys.exit(app.exec_())

Le code qui précède est un peu long. Mais nous avons créé beaucoup d'objets. Les commentaires doivent suffire à comprendre ce qui se passe. Signalons toutefois :

  • le layout principal (vbox) n'est pas appliqué directement à la fenêtre (ça ne fonctionnerait pas). Il doit être appliqué sur un widget (généralement de type QFrame) qui est définit comme étant le widget principal de la fenêtre (méthode setCentralWidget).
  • le paramètre stretch, utilisé lors de l'ajout de la zone de commentaire et de notice permet d'affecter des poids différents à ces deux zones. Plutôt que se partager équitablement l'espace, la zone de notice en occupera trois fois plus que la zone de commentaires (si possible).

Voici quelques pointeurs vers des fonctions qui peuvent vous êtres utiles lors de la mise en place de layouts :

  • La méthode obj.sizePolicy() renvoie la tactique de redimensionnement des objets (type QSizePolicy)
  • La méthode setVerticalPolicy() d'un objet QSizePolicy permet de modifier cette tactique : taille d'objet fixe, occupant tout l'espace etc…

Le programme qui précède peut être exécuté, mais il ne fait encore rien. Il nous reste à coder la partie traitement (récupération des informations dans les différents champs et formatage de la notice), et à connecter les signaux et les slots pour que l'application soit réactive.

- Connexions des signaux et des slots

En ce qui concerne la connexion des signaux aux slots, la simplicité de l'application permet de formater la notice au fur et à mesure des changements dans les champs, c'est à dire que toute modification, même d'un seul caractère, dans un des champs provoquera la mise à jour de la notice.

Le signal correspondant à la mise à jour des champs est textChanged. Le signal correspondant à la mise à jour du widget QSpinBox est valueChanged et celui correspondant au changement d'état du widget QCheckBox est stateChanged.

Tous ces signaux vont être connectés au slot updateNotice que nous allons créer. Cette dernière méthode va relever le contenu de tous les champs, formater une chaîne de caractères contenant la notice, et provoquer son affichage dans la zone de notice (output).

Pour relever le contenu des champs (méthode displayText), nous avons besoin d'avoir une référence vers les widgets en question. De plus, cette référence doit être accessible à toute la classe. Au moment de la création des widgets, nous devons donc conserver une référence sur eux. Plutôt que de multiplier les attributs (il peut y en avoir un très grand nombre s'il y a beaucoup de widgets), nous allons ajouter un seul attribut de type dictionnaire qui contiendra les références vers tous les widgets :

class Fenetre(QtGui.QMainWindow):
    def __init__(self,parent=None) :
        super().__init__(parent)
        # Dictionnaire qui contiendra les widgets
        self.widgets={}
        self.createInterface()
 
    ....

Puis, nous ajoutons les références aux objets créés dans ce dictionnaire :

class Fenetre(QtGui.QMainWindow) :
    ...
    def createInterface(self) :
        frame=QtGui.QWidget()
        self.widgets['year']=QtGui.QSpinBox()
        self.widgets['year'].setMaximum(3000)
        self.widgets['year'].setMinimum(1400)
        self.widgets['year'].setValue(2012)
        self.widgets['comm']=QtGui.QTextEdit() 
 
        fields=QtGui.QGridLayout()
        fields.addWidget(QtGui.QLabel("Titre"),0,0)
        self.widgets["f_titre"]=QtGui.QLineEdit()
        fields.addWidget(self.widgets["f_titre"],0,1)
 
        fields.addWidget(QtGui.QLabel("Auteur"),1,0)
        self.widgets["f_auteur"]=QtGui.QLineEdit()
        fields.addWidget(self.widgets["f_auteur"],1,1)
        self.widgets["auteurnc"]=QtGui.QCheckBox("n/c")
        fields.addWidget(self.widgets["auteurnc"],1,2)
 
        fields.addWidget(QtGui.QLabel("Année"),2,0)
        fields.addWidget(self.widgets["year"],2,1)
        fields.addWidget(QtGui.QLabel("Éditeur"),3,0)
        self.widgets["f_editeur"]=QtGui.QLineEdit()
        fields.addWidget(self.widgets["f_editeur"],3,1,1,2)
 
        self.widgets["output"]=QtGui.QTextEdit()
        self.widgets["output"].setReadOnly(True)
 
        vbox=QtGui.QVBoxLayout()
        vbox.addLayout(fields)
        vbox.addWidget(QtGui.QLabel("Commentaires :"))
        vbox.addWidget(self.widgets['comm'])
        vbox.addWidget(self.widgets["output"])
        frame.setLayout(vbox)
 
        self.setCentralWidget(frame)
        self.setWindowTitle('Livre')
        self.setGeometry(0,0,300,300)

À la fin de la méthode, nous connectons les signaux qui correspondent à un changement opéré par l'utiisateur (case cochée, texte entré, année modifiée) au slot updateNotice :

    def createInterface(self) :
        ....
        # Slots connexion
        self.widgets["year"].valueChanged.connect(self.updateNotice)
        self.widgets["f_titre"].textChanged.connect(self.updateNotice)
        self.widgets["f_auteur"].textChanged.connect(self.updateNotice)
        self.widgets["f_editeur"].textChanged.connect(self.updateNotice)
        self.widgets["auteurnc"].stateChanged.connect(self.updateNotice)
        self.widgets["comm"].textChanged.connect(self.updateNotice)

- Traitement

Enfin, il ne nous reste plus qu'à écire la méthode updateNotice, que nous appelons au passage dans ''__init__. Cette méthode récupère le contenu de chaque champ, formate une chaîne Html en conséquence et affiche cette chaîne dans la zone de sortie de la notice. <file python livre.py> from PySide import QtGui,QtCore import sys import datetime def createAndName(obj,name) : obj.setObjectName(name) return obj class Fenetre(QtGui.QMainWindow): def init(self,parent=None) : super().init(parent) # Dictionnaire qui contiendra les widgets self.widgets={} self.createInterface() self.updateNotice() def updateNotice(self) : s=“<o>” if self.widgets[“auteurnc”].checkState() : s+=“[<b>”+self.widgets[“f_auteur”].displayText()+“</b>], ” else : s+=“<b>”+self.widgets[“f_auteur”].displayText()+“</b>, ” s+=self.widgets[“f_titre”].displayText()+“, ” s+=“<i>”+self.widgets[“f_editeur”].displayText()+“</i>, ” s+=str(self.widgets[“year”].value()) s+=“</p>” s+=“<hr/><font size='-1'>”+self.widgets[“comm”].toPlainText()+“</font>” self.widgets[“output”].setHtml(s) def createInterface(self) : frame=QtGui.QWidget() self.widgets['year']=QtGui.QSpinBox() self.widgets['year'].setMaximum(3000) self.widgets['year'].setMinimum(1400) self.widgets['year'].setValue(datetime.date.today().isocalendar()[0]) self.widgets['comm']=QtGui.QTextEdit() fields=QtGui.QGridLayout() fields.addWidget(QtGui.QLabel(“Titre”),0,0) self.widgets[“f_titre”]=QtGui.QLineEdit() fields.addWidget(self.widgets[“f_titre”],0,1) fields.addWidget(QtGui.QLabel(“Auteur”),1,0) self.widgets[“f_auteur”]=QtGui.QLineEdit() fields.addWidget(self.widgets[“f_auteur”],1,1) self.widgets[“auteurnc”]=QtGui.QCheckBox(“n/c”) fields.addWidget(self.widgets[“auteurnc”],1,2) fields.addWidget(QtGui.QLabel(“Année”),2,0) fields.addWidget(self.widgets[“year”],2,1) fields.addWidget(QtGui.QLabel(“Éditeur”),3,0) self.widgets[“f_editeur”]=QtGui.QLineEdit() fields.addWidget(self.widgets[“f_editeur”],3,1,1,2) self.widgets[“output”]=QtGui.QTextEdit() self.widgets[“output”].setReadOnly(True) vbox=QtGui.QVBoxLayout() vbox.addLayout(fields) vbox.addWidget(QtGui.QLabel(“Commentaires :”)) vbox.addWidget(self.widgets['comm']) vbox.addWidget(self.widgets[“output”]) frame.setLayout(vbox) self.setCentralWidget(frame) self.setWindowTitle('Livre') self.setGeometry(0,0,300,300) # Slots connexion self.widgets[“year”].valueChanged.connect(self.updateNotice) self.widgets[“f_titre”].textChanged.connect(self.updateNotice) self.widgets[“f_auteur”].textChanged.connect(self.updateNotice) self.widgets[“f_editeur”].textChanged.connect(self.updateNotice) self.widgets[“auteurnc”].stateChanged.connect(self.updateNotice) self.widgets[“comm”].textChanged.connect(self.updateNotice) app=QtGui.QApplication(sys.argv) frame=Fenetre() frame.show() sys.exit(app.exec_()) </file> Résultat : ===== - Pistes d'améliorations ===== Les possibilités d'amélioration sont nombreuses pour ce type de programme. La plus évidente est l'ajout de champs à la notice, comme le numéro de l'édition, l'URL de l'ouvrage s'il est en ligne… Une autre amélioration est l'enregistrement de plusieurs ouvrages dans une base de données. Cette base peut être gérée par le module QtSql ou de manière plus standard par le module sqlite3 de la bibliothèque standard Python.

Pour obtenir un layout dynamique (une barre horizontale ou verticale qui permet à l'utilisateur de diminuer une zone en en augmentant une autre), on utilise un objet de type ''QSPlitter'', auquel on peut ajouter des Widgets, comme on le fait dans un Layout.

stu/python_gui/pyqt_projet_biblio.txt · Dernière modification: 2014/03/31 16:45 (modification externe)