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.
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 :
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 :
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
).Voici quelques pointeurs vers des fonctions qui peuvent vous êtres utiles lors de la mise en place de layouts :
obj.sizePolicy()
renvoie la tactique de redimensionnement des objets (type QSizePolicy
)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.
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)
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 :
QtSql
===== - 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
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.