Nous avons vu jusqu'à présent comment concevoir des applications en mode console, à savoir, n'utilisant que le mode texte comme interface avec l'utilisateur. Or, la plupart des applications utilisées par le grand public offrent une interface graphique : une fenêtre comportant des boutons, des zones de texte, des cases à cocher, … Il est temps pour nous d'aborder comment parer nos applications d'une interface graphique et ainsi rendre leur utilisation beaucoup plus aisée.

Il existe plusieurs bibliothèques graphiques en Python telles que Tkinter qui offre un choix limité d'éléments graphiques et son aspect est assez austère. Nous allons utiliser la librairie PySide depuis le module éponyme qui offre la plupart des composants courants et qui est assez simple d'utilisation. De plus, elle s'adapte au thème configuré sur le système d'exploitation.

Application : un générateur de mot de passe

Nous allons travailler depuis un script permettant de générer des mots de passe aléatoirement pouvant comporter des minuscules, des majuscules, des chiffres et des symboles. La longueur du mot de passe est variable.

from random import choice
def genererMotDePasse(tailleMotDePasse=8, minuscules=True, majuscules=True, chiffres=True, symboles=True):
	caracteres = ""
	if minuscules:
		caracteres += "abcdefghijklmnopqrstuvwxyz"
	if majuscules:
		caracteres += "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
	if chiffres:
		caracteres += "0123456789"
	if symboles:
		caracteres += "&~#{([-|_\^@)=+$]}*%!/:.;?,"
	motDePasse = ""
	for i in range(tailleMotDePasse):
		motDePasse += choice(caracteres)
	return(motDePasse)

Nous allons réaliser l'interface suivante.

schema-gui

Notre interface permet de choisir quels jeux de caractères utiliser pour notre mot de passe à l'aide des cases à cocher. La glissière permet de faire varier la taille du mot de passe. Enfin, après avoir cliqué sur le bouton Générer, le mot de passe apparaît dans la zone de texte. Le bouton Vers le presse-papier copie le mot de passe généré dans le presse-papier et le bouton Quitter ferme l'application.

Les composants graphiques utilisés

Nous allons utiliser divers composants graphiques aussi nommés widgets (pour Window Gadgets). Nous utiliserons donc les classes suivantes :

La boîte de dialogue
QDialog
Les cases à cocher
QCheckBox
L'étiquette "Taille du mot de passe"
QLabel
Le champ de texte
QLineEdit
La glissière
QSlider
Les boutons
QPushButton

Nous allons donc écrire une classe correspondant à notre fenêtre en héritant la classe QDialog et y décrire l'ensemble des widgets comme des attributs de la classe dans le constructeur :

import sys
from PySide import QtCore, QtGui
class MaFenetre(QtGui.QDialog):
	def __init__(self, parent=None):
		QtGui.QDialog.__init__(self,parent)
		# Les cases à cocher
		self.__caseMinuscules = QtGui.QCheckBox("Minuscules")
		self.__caseMajuscules = QtGui.QCheckBox("Majuscules")
		self.__caseChiffres = QtGui.QCheckBox("Chiffres")
		self.__caseSymboles = QtGui.QCheckBox("Symboles")
		# Les boutons
		self.__boutonQuitter = QtGui.QPushButton("Quitter")
		self.__boutonCopier = QtGui.QPushButton("Vers le presse-papier")
		self.__boutonGenerer = QtGui.QPushButton("Générer")
		# Le champ de texte
		self.__champTexte = QtGui.QLineEdit("")
		# La glissière
		self.__glissiereTaille = QtGui.QSlider(QtCore.Qt.Horizontal)
		# Le label
		self.__labelTaille = QtGui.QLabel("Taille du mot de passe : ")

Nous allons à présent aborder le placement des widgets dans la boîte de dialogue. PySide nous propose plusieurs méthodes pour placer les widgets. La solution la plus simple est le placement sur une grille à l'aide de la classe QGridLayout : chaque widget occupe une case dans une grille. Il est cependant possible de faire en sorte qu'un widget occupe plusieurs lignes ou colonnes.

Voici notre maquette d'interface dont les widgets ont été répartis sur une grille.

schema-gui-grid

Pour implémenter cela, nous allons créer un objet de la classe QGridLayout, puis ajouter les widgets créés précédemment avec la méthode addWidget(widget, ligne, colonne) avec ligne et colonne, le numéro de la ligne et de la colonne souhaitées. Enfin, nous définirons le layout comme étant l'élément central de la fenêtre avec self.setLayout(layout).

Nous ajoutons donc à notre constructeur la portion de code suivante :

class MaFenetre(QtGui.QDialog):
	def __init__(self, parent=None):
		QtGui.QDialog.__init__(self,parent)
		# ...
		layout = QtGui.QGridLayout()
		layout.addWidget(self.__caseMajuscules, 0, 0)
		layout.addWidget(self.__labelTaille, 0, 1)
		layout.addWidget(self.__caseMinuscules, 0, 2)
		layout.addWidget(self.__caseChiffres, 1, 0)
		layout.addWidget(self.__glissiereTaille, 1, 1)
		layout.addWidget(self.__caseSymboles, 1, 2)
		layout.addWidget(self.__champTexte, 2, 1)
		layout.addWidget(self.__boutonQuitter, 3, 0)
		layout.addWidget(self.__boutonCopier, 3, 1)
		layout.addWidget(self.__boutonGenerer, 3, 2)
		self.setLayout(layout)

Nous allons terminer la préparation de notre fenêtre en modifiant le titre de la boîte de dialogue avec la méthode self.setWindowTitle(titre). Nous allons également définir le minimum et le maximum de la glissière avec respectivement les méthodes setMinimum et setMaximum. Nous allons cocher par défaut la case minuscules et chiffres avec la méthode setChecked. Nous ajoutons les lignes suivantes à notre constructeur :

class MaFenetre(QtGui.QDialog):
	def __init__(self, parent=None):
		QtGui.QDialog.__init__(self,parent)
		# ...
		self.setWindowTitle("Générateur de mot de passe")
		self.__caseMinuscules.setChecked(True)
		self.__caseChiffres.setChecked(True)
		self.__glissiereTaille.setMinimum(8)
		self.__glissiereTaille.setMaximum(30)

Nous allons enfin ajouter une icône à notre application pour que celle-ci soit reconnaissable dans la barre des tâches. Nous ajoutons les trois lignes suivantes au constructeur de notre boîte de dialogue :

class MaFenetre(QtGui.QDialog):
	def __init__(self, parent=None):
		# ...
		icone = QtGui.QIcon()
		icone.addPixmap(QtGui.QPixmap("cadenas.svg"), QtGui.QIcon.Normal, QtGui.QIcon.Off)
		self.setWindowIcon(icone)

Pour exécuter notre fenêtre, on écrit les lignes suivantes dans le programme principal qui permettent de créer une application Qt en fournissant les arguments de la ligne de commande (ligne 1), instancie notre fenêtre (ligne 2) et l'affiche (ligne 3) :

app = QtGui.QApplication(sys.argv)
dialog = MaFenetre()
dialog.exec_()

Voici le code complet :

import sys
from PySide import QtCore, QtGui
class MaFenetre(QtGui.QDialog):
	def __init__(self, parent=None):
		QtGui.QDialog.__init__(self,parent)
		# Les cases à cocher
		self.__caseMinuscules = QtGui.QCheckBox("Minuscules")
		self.__caseMajuscules = QtGui.QCheckBox("Majuscules")
		self.__caseChiffres = QtGui.QCheckBox("Chiffres")
		self.__caseSymboles = QtGui.QCheckBox("Symboles")
		# Les boutons
		self.__boutonQuitter = QtGui.QPushButton("Quitter")
		self.__boutonCopier = QtGui.QPushButton("Vers le presse-papier")
		self.__boutonGenerer = QtGui.QPushButton("Générer")
		# Le champ de texte
		self.__champTexte = QtGui.QLineEdit("")
		# La glissière
		self.__glissiereTaille = QtGui.QSlider(QtCore.Qt.Horizontal)
		self.__glissiereTaille.setMinimum(8)
		self.__glissiereTaille.setMaximum(30)
		# Le label
		self.__labelTaille = QtGui.QLabel("Taille du mot de passe : ")
		# Agencement des widgets
		layout = QtGui.QGridLayout()
		layout.addWidget(self.__caseMajuscules, 0, 0)
		layout.addWidget(self.__labelTaille, 0, 1)
		layout.addWidget(self.__caseMinuscules, 0, 2)
		layout.addWidget(self.__caseChiffres, 1, 0)
		layout.addWidget(self.__glissiereTaille, 1, 1)
		layout.addWidget(self.__caseSymboles, 1, 2)
		layout.addWidget(self.__champTexte, 2, 1)
		layout.addWidget(self.__boutonQuitter, 3, 0)
		layout.addWidget(self.__boutonCopier, 3, 1)
		layout.addWidget(self.__boutonGenerer, 3, 2)
		self.setLayout(layout)
		# Configuration des éléments
		self.setWindowTitle("Générateur de mot de passe")
		icone = QtGui.QIcon()
		icone.addPixmap(QtGui.QPixmap("cadenas.svg"), QtGui.QIcon.Normal, QtGui.QIcon.Off)
		self.setWindowIcon(icone)
		self.__caseMinuscules.setChecked(True)
		self.__caseChiffres.setChecked(True)
# Exécution du programme
app = QtGui.QApplication(sys.argv)
dialog = MaFenetre()
dialog.exec_()

Voici le résultat obtenu.

fenetre-gen-mdp

Les signaux

Nous avons obtenu une interface graphique mais celle-ci ne fonctionne pas : il est temps de relier les composants graphiques au code que nous avons écrit en début de chapitre.

Chaque widget de la fenêtre produit des signaux lorsqu'on l'utilise. Chaque signal peut être relié à un slot avec la méthode connect en fournissant en argument quelle fonction appeler lors de la réception du signal.

Pour illustrer cela, nous allons créer une méthode à notre objet MaFenetre, nommée quitter, et contenant la ligne self.accept() qui ferme la boîte de dialogue. Nous allons connecter le signal clicked (généré quand le bouton est cliqué) émis par le bouton Quitter à cette fonction :

class MaFenetre(QtGui.QDialog):
	def __init__(self, parent=None):
		# ...
		self.__boutonQuitter.clicked.connect(self.quitter)
	def quitter(self):
		self.accept()

Nous allons faire de même pour les boutons Vers le presse-papier et Générer. La copie vers le presse-papier nécessite la création d'un objet QtGui.QApplication.clipboard() et on y affecte, avec la méthode setText(texte) le contenu du champ de texte, lu avec la méthode text().

class MaFenetre(QtGui.QDialog):
	def __init__(self, parent=None):
		# ...
		self.__boutonCopier.clicked.connect(self.copier)
	def copier(self):
		pressePapier = QtGui.QApplication.clipboard()
		pressePapier.setText(self.__champTexte.text())

De manière analogue, nous allons créer une fonction generer permettant d'appeler notre fonction genererMotDePasse en lui fournissant comme arguments la taille souhaitée, lue depuis la glissière avec la méthode value() et en choisissant les caractères à partir des cases à cocher avec la méthode isChecked() qui retourne True lorsqu'elles sont cochées. Enfin, la valeur retournée par la fonction sera définie comme texte du champ de texte avec la méthode setText(texte).

class MaFenetre(QtGui.QDialog):
	def __init__(self, parent=None):
		# ...
		self.__boutonGenerer.clicked.connect(self.generer)
	def generer(self):
		tailleMotDePasse = self.__glissiereTaille.value()
		minuscules = self.__caseMinuscules.isChecked()
		majuscules = self.__caseMajuscules.isChecked()
		chiffres = self.__caseChiffres.isChecked()
		symboles = self.__caseSymboles.isChecked()
		self.__champTexte.setText(genererMotDePasse(tailleMotDePasse, minuscules, majuscules, chiffres, symboles))

Nous allons améliorer notre programme en modifiant la valeur du label Taille du mot de passe : en ajoutant la valeur actuelle de la glissière. Pour cela, nous modifions la ligne créant notre label et ajouter une fonction déclenchée par la modification de la valeur de la glissière, à savoir le signal valueChanged().

class MaFenetre(QtGui.QDialog):
	def __init__(self, parent=None):
		# ...
		self.__labelTaille = QtGui.QLabel("Taille du mot de passe : " + str(self.__glissiereTaille.value()))
		# ...
		self.__glissiereTaille.valueChanged.connect(self.changerTailleMotDePasse)
	def changerTailleMotDePasse(self):
		self.__labelTaille.setText("Taille du mot de passe : " + str(self.__glissiereTaille.value()))

Voici le code source complet de notre application :

import sys
from PySide import QtCore, QtGui
from random import choice
def genererMotDePasse(tailleMotDePasse=8, minuscules=True, majuscules=True, chiffres=True, symboles=True):
	caracteres = ""
	if minuscules:
		caracteres += "abcdefghijklmnopqrstuvwxyz"
	if majuscules:
		caracteres += "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
	if chiffres:
		caracteres += "0123456789"
	if symboles:
		caracteres += "&~#{([-|_\^@)=+$]}*%!/:.;?,"
	motDePasse = ""
	for i in range(tailleMotDePasse):
		motDePasse += choice(caracteres)
	return(motDePasse)
class MaFenetre(QtGui.QDialog):
	def __init__(self, parent=None):
		QtGui.QDialog.__init__(self,parent)
		# Les cases à cocher
		self.__caseMinuscules = QtGui.QCheckBox("Minuscules")
		self.__caseMajuscules = QtGui.QCheckBox("Majuscules")
		self.__caseChiffres = QtGui.QCheckBox("Chiffres")
		self.__caseSymboles = QtGui.QCheckBox("Symboles")
		# Les boutons
		self.__boutonQuitter = QtGui.QPushButton("Quitter")
		self.__boutonCopier = QtGui.QPushButton("Vers le presse-papier")
		self.__boutonGenerer = QtGui.QPushButton("Générer")
		# Le champ de texte
		self.__champTexte = QtGui.QLineEdit("")
		# La glissière
		self.__glissiereTaille = QtGui.QSlider(QtCore.Qt.Horizontal)
		self.__glissiereTaille.setMinimum(8)
		self.__glissiereTaille.setMaximum(30)
		# Le label
		self.__labelTaille = QtGui.QLabel("Taille du mot de passe : " + str(self.__glissiereTaille.value()))
		layout = QtGui.QGridLayout()
		layout.addWidget(self.__caseMajuscules, 0, 0)
		layout.addWidget(self.__labelTaille, 0, 1)
		layout.addWidget(self.__caseMinuscules, 0, 2)
		layout.addWidget(self.__caseChiffres, 1, 0)
		layout.addWidget(self.__glissiereTaille, 1, 1)
		layout.addWidget(self.__caseSymboles, 1, 2)
		layout.addWidget(self.__champTexte, 2, 1)
		layout.addWidget(self.__boutonQuitter, 3, 0)
		layout.addWidget(self.__boutonCopier, 3, 1)
		layout.addWidget(self.__boutonGenerer, 3, 2)
		self.setLayout(layout)
		self.setWindowTitle("Générateur de mot de passe")
		icone = QtGui.QIcon()
		icone.addPixmap(QtGui.QPixmap("cadenas.svg"), QtGui.QIcon.Normal, QtGui.QIcon.Off)
		self.setWindowIcon(icone)
		self.__caseMinuscules.setChecked(True)
		self.__caseChiffres.setChecked(True)
		self.__boutonQuitter.clicked.connect(self.quitter)
		self.__boutonCopier.clicked.connect(self.copier)
		self.__boutonGenerer.clicked.connect(self.generer)
		self.__glissiereTaille.valueChanged.connect(self.changerTailleMotDePasse)
	def quitter(self):
		self.accept()
	def copier(self):
		pressePapier = QtGui.QApplication.clipboard()
		pressePapier.setText(self.__champTexte.text())
	def generer(self):
		tailleMotDePasse = self.__glissiereTaille.value()
		minuscules = self.__caseMinuscules.isChecked()
		majuscules = self.__caseMajuscules.isChecked()
		chiffres = self.__caseChiffres.isChecked()
		symboles = self.__caseSymboles.isChecked()
		self.__champTexte.setText(genererMotDePasse(tailleMotDePasse, minuscules, majuscules, chiffres, symboles))
	def changerTailleMotDePasse(self):
		self.__labelTaille.setText("Taille du mot de passe : " + str(self.__glissiereTaille.value()))
app = QtGui.QApplication(sys.argv)
dialog = MaFenetre()
dialog.exec_()

Les widgets courants PySide

Présentation

L'ensemble des widgets présentés ici héritent de la classe QWidget qui offre une méthode setEnabled, qui permet d'activer ou de désactiver le widget. Nous allons découvrir dans cette section plusieurs widgets courants proposés par PySide.

Le champ de texte QLineEdit

Le champ de texte QLineEdit permet à l'utilisateur de lire et de modifier une chaîne de caractères. En voici les méthodes courantes :

MéthodeDescription
text()Retourne le texte contenu dans le champ de texte.
setText(texte)Modifie le contenu du champ de texte par le texte fourni en argument.
clear()Efface le contenu du champ de texte.
setMaxLength(taille)Définit la taille maximale du champ de texte.
maxLength(taille)Retourne la taille maximale du champ de texte.
copy()Copie le contenu du champ de texte dans le presse-papier.
paste()Colle le contenu du presse-papier dans le champ de texte.
setEchoMode(mode)Modifie l'affichage du contenu du champ de texte sans modifier son contenu :
  • QLineEdit.Normal : Affiche les caractères normalement (par défaut).
  • QLineEdit.NoEcho : Rien n'est affiché.
  • QLineEdit.Password : Affiche des étoiles à la place des caractères (pour les mots de passe)
  • QLineEdit.PasswordEchoOnEdit : Affiche les caractères normalement tant que le champ est sélectionné. Sinon, affiche des étoiles.
setCompleter(completer)Permet de définir une instance de QCompleter pour fournir de l'auto-complétion.
setInputMask(masque)Permet de configurer le format de données attendu avec une chaîne de caractères. Par exemple :
  • 000.000.000.000 : Adresse IPv4
  • HH:HH:HH:HH:HH:HH : Adresse MAC
  • 00-00-0000 : Date au format JJ-MM-AAAA.

Il est possible de rendre le masque visible en lui ajoutant ";" suivi d'un caractère de remplacement. Par exemple, "00-00-0000;_" affichera "__-__-____" tant qu'il ne sera pas rempli.

Voici quelques signaux proposés par la classe QLineEdit :

SignalDéclencheur
textChangedLorsque le texte change (par l'utilisateur ou par le programme).
textEditedLorsque le texte est changé par l'utilisateur.
cursorPositionChangedLorsque le curseur est déplacé.
returnPressedLorsque la touche Entrée est pressée.
editingFinishedLorsque le champ perd le focus ou la touche Entrée est pressée.

La classe QAbstractButton

La classe QAbstractButton est une classe regroupant différents boutons (comme QPushButton, QCheckBox …). Il est cependant impossible de créer un objet de cette classe.

MéthodeDescription
setIcon(icone)Permet d'ajouter une icône avec une instance de la classe QtGui.QIcon au bouton.
setShortcut(raccourci)Associe au bouton un raccourci clavier sous la forme d'une chaîne de caractères (exemple : CTRL + C).
setCheckable()Permet de rendre le bouton bistable (il maintient l'état après le clic). Les QRadioButton et QCheckBox sont bistables par défaut.
setChecked()Permet de valider un bouton.
isChecked()Retourne l'état du bouton.

Voici quelques signaux proposés par la classe QAbstractButton :

SignalDéclencheur
clickedLorsque le bouton est cliqué.
pressedLorsque le bouton est appuyé.
releasedLorsque le bouton est relâché.
toogledLorsque le bouton change d'état comme par exemple les boutons de barre d'outils.

La case à cocher QCheckBox

La case à cocher permet de sélectionner entre 0 et plusieurs choix parmi l'ensemble de cases disponibles. On y retrouve les méthodes et signaux héritées de QAbstractButton. Par défaut les cases à cocher sont bistables.

Le bouton radio QRadioButton

Les boutons radios permettent un choix exclusif parmi plusieurs options présentes dans le même conteneur (layout ou widget). Un seul peut être coché à la fois. Ils héritent également des méthodes et signaux de la classe QAbstractButton. Le signal toggled permet de vérifier qu'un bouton radio change d'état.

Le bouton poussoir QPushButton

Le bouton poussoir QPushButton permet de déclencher une action lors de son clic. Ce widget hérite également de la classe QAbstractButton. Il est possible d'associer un menu (instance de la classe QMenu) avec la méthode setMenu(menu).

La boîte de sélection QComboBox

La boîte de sélection QComboBox permet de choisir un élément parmi une liste. Voici les méthodes principales de cette classe :

MéthodeDescription
addItem(chaine)Permet d'ajouter une chaîne de caractères (avec ou sans icône) à la liste de choix.
addItem(icone, chaine)
addItems(liste)Permet d'ajouter une liste de chaîne de caractères à la liste de choix.
currentIndex()Retourne l'index de l'élément actuellement sélectionné.
currentText()Retourne la chaîne de caractères actuellement sélectionnée.
setEditable()Permet de modifier ou non les éléments de la boîte de sélection. Par défaut, cela n'est pas possible.
insertItem(l, chaine)Permet d'ajouter un ou plusieurs éléments pendant l'exécution du programme à l'index l.
insertItems(l, liste)
insertSeparator()Permet de grouper les éléments en ajoutant un séparateur entre ceux-ci.
clear()Efface les éléments contenus.

Voici la liste des signaux proposés par QComboBox :

SignalDéclencheur
activatedLorsque l'utilisateur interagit avec.
currentIndexChangedLorsque l'élément sélectionné change (par le programme ou l'utilisateur). Retourne l'index de l'élément sélectionné.
highlightedRetourne l'index de l'élément surligné.
editTextChangedLorsque l'utilisateur modifie le contenu de la boîte de dialogue.

Les champs numériques QSpinBox et QDoubleSpinBox

Les champs numériques QSpinBox et QDoubleSpinBox forcent l'utilisateur à saisir des données numériques. Le champ QSpinBox n'accepte que les valeurs entières, et le champ QDoubleSpinBox les valeurs décimales. Voici les méthodes de ces champs :

MéthodeDescription
setMinimum(minimum)Définissent le minimum et le maximum autorisés par le champ.
setMaximum(maximum)
setRange(min, max)
setSingleStepFixe le pas d'incrémentation.
setSuffix(chaine)Ajoutent un suffixe ou un préfixe au champ pour plus de lisibilité (exemple : €, £, litres, km, …).
setPrefix(chaine)
setValue(valeur)Définit la valeur du champ.
value()Retourne la valeur du champ.
setDecimals(nbDecimales)Pour QDoubleSpinBox. Permet de définir le nombre de décimales à l'affichage.

Ces deux champs numériques offrent un seul signal : valueChanged qui est émit quand l'utilisateur change la valeur contenue. La valeur retournée est la valeur du champ. Pour QDoubleSpinBox, la chaîne de caractères est codée en fonction de la langue (en France, on utilise une virgule (,) comme séparateur de décimale).

Les champs horodateurs QDateEdit, QTimeEdit et QDateTimeEdit

Ces champs sont conçus pour saisir des données temporelles. Le nom des méthodes et signaux dépendent du type de données (QDateEdit pour la date, QTimeEdit pour l'heure et QDateTimeEdit pour la date et l'heure). Voici les méthodes de ces classes :

MéthodeDescription
date()Retournent la valeur du champ sous la forme d'un objet QDate, QTime ou QDateTime.
time()
dateTime()
setDate()On remplit le champ avec les objets QDate, QTime ou QDateTime.
setTime()
setDateTime()
setMinimumDate()Définissent le minimum du champ avec les objets QDate, QTime ou QDateTime.
setMinimumTime()
setMinimumDateTime()
setMaximumDate()Définissent le maximum du champ avec les objets QDate, QTime ou QDateTime.
setMaximumTime()
setMaximumDateTime()
setCalendarPopup()Permet d'afficher un calendrier pour les objets manipulant des dates.

Les signaux émis dépendent des valeurs qui ont changés :

SignalDéclencheur
dateChangedDéclenchés lors de la modification de la valeur.
timeChanged
dateTimeChanged

La zone de texte QTextEdit

À l'instar de la classe QLineEdit, ce widget permet d'éditer du texte, mais offre une zone d'édition plus grande et permet la mise en forme du contenu au format HTML. Voici les méthodes usuelles :

MéthodeDescription
toPlainText()Retourne le texte contenu dans le champ de texte.
setText(texte)Modifie le texte du champ par celui en argument.
toHtml()Retourne le code HTML contenu dans le champ de texte.
setHtml(texte)Modifie le contenu du champ de texte par le code HTML fourni en argument.
clear()Efface le contenu du champ de texte.
copy()Copie le contenu du champ de texte dans le presse-papier.
paste()Colle le contenu du presse-papier dans le champ de texte.
undo()Annule la dernière opération.
redo()Refait la dernière opération annulée.

Voici quelques signaux proposés par la classe QLineEdit :

SignalDéclencheur
textChangedLorsque le texte change (par l'utilisateur ou par le programme).
cursorPositionChangedLorsque le curseur s'est déplacé.

La boîte à onglets QTabWidget

La classe QTabWidget permet de regrouper des widgets dans différents onglets. Voici les différentes méthodes offertes par cette classe :

MéthodeDescription
addTab(widget, nom)Permet d'ajouter un onglet contenant un widget et dont le nom et le widget sont passés en argument.
insertTab(widget, nom)Permet d'ajouter un onglet contenant un widget et dont le nom et le widget sont passés en argument pendant l'exécution du programme.
tabPosition()Retourne l'indice de l'onglet actuellement sélectionné.

Le signal currentChanged est émis lorsque l'utilisateur change d'onglet. Voici un exemple de mise en œuvre de la boîte à onglets.

fenetre-tab1
fenetre-tab2

Voici le code source de l'exemple ci-dessous :

import sys
from PySide import QtCore, QtGui
class Dialog(QtGui.QDialog):
	def __init__(self, parent=None):
		QtGui.QDialog.__init__(self,parent)
		# Les champs
		self.__champTexteNomAuteur = QtGui.QLineEdit("")
		self.__champTextePrenomAuteur = QtGui.QLineEdit("")
		self.__champDateNaissanceAuteur = QtGui.QDateEdit()
		self.__champDateNaissanceAuteur.setCalendarPopup(True)
		self.__champTexteTitreLivre = QtGui.QLineEdit("")
		self.__champDatePublication = QtGui.QDateEdit()
		self.__champDatePublication.setCalendarPopup(True)
		# Les widgets
		self.__widgetAuteur = QtGui.QWidget()
		self.__widgetLivre = QtGui.QWidget()
		# Les layouts des onglets
		self.__layoutAuteur = QtGui.QFormLayout()
		self.__layoutAuteur.addRow("Nom : ", self.__champTexteNomAuteur)
		self.__layoutAuteur.addRow("Prénom : ", self.__champTextePrenomAuteur)
		self.__layoutAuteur.addRow("Date de naissance : ", self.__champDateNaissanceAuteur)
		self.__widgetAuteur.setLayout(self.__layoutAuteur)
		self.__layoutLivre = QtGui.QFormLayout()
		self.__layoutLivre.addRow("Titre : ", self.__champTexteTitreLivre)
		self.__layoutLivre.addRow("Date de publication : ", self.__champDatePublication)
		self.__widgetLivre.setLayout(self.__layoutLivre)
		# La boîte à onglets
		self.__tabWidget = QtGui.QTabWidget()
		self.__tabWidget.addTab(self.__widgetAuteur, "Auteur")
		self.__tabWidget.addTab(self.__widgetLivre, "Livre")
		# Le layout final
		self.__mainLayout = QtGui.QVBoxLayout()
		self.__mainLayout.addWidget(self.__tabWidget)
		self.setLayout(self.__mainLayout)
app = QtGui.QApplication(sys.argv)
dialog = Dialog()
dialog.exec_()

La boîte à regroupement QGroupBox

Cette classe permet de regrouper des widgets dans une boîte avec un titre. Elles sont souvent utilisées pour organiser les choix proposés. Voici les méthodes offertes par cette classe :

MéthodeDescription
setLayout(layout)Définit le layout passé en argument comme le layout utilisé pour cette instance.
setChecked(bool)Permet de créer une boîte de regroupement optionnelle.
isChecked()Pour les boîtes de regroupement optionnelles, retourne si le groupe a été coché.

La zone de défilement QScrollArea

Les zones de défilement sont utilisées pour l'affichage de widgets de grande taille tels que des images, des tableaux ou des zones de texte. Elles font apparaître des ascenseurs pour pouvoir faire défiler les zones non visibles. Cette classe ne contient qu'un seul widget placé avec la méthode setWidget(widget).

Le panneau séparé QSplitter

Le panneau séparé permet de placer plusieurs widgets côte à côte séparés par un séparateur pouvant être déplacé par l'utilisateur. La géométrie des widgets dépend donc de la position de ce séparateur. Il est possible d'ajouter plusieurs composants. Voici les méthodes de cette classe :

MéthodeDescription
addWidget(widget)Ajoute un widget aux panneaux.
setStretchFactor(index, entier)Permet de définir un cœfficient de la taille occupée par chaque widget.
setOrientation(arg)Permet de modifier l'orientation du panneau séparé. Voici les arguments possibles :
  • Qt.Vertical : Empilement vertical (par défaut)
  • Qt.Horizontal : Empilement horizontal.

L'affichage en liste QListWidget

Cette classe permet l'affichage d'éléments sous forme d'une liste. Elle permet la vue en liste (par défaut) ou par icônes. Voici les méthodes offertes par la classe QListWidget :

MéthodeDescription
addItem(chaine)Ajoute un élément à la liste (texte seul).
addItem(item)Ajoute un objet QListWidgetItem à la liste (texte et icône).
insertItem(l, chaine)Permet d'ajouter un ou plusieurs éléments à la position pendant l'exécution du programme.
insertItems(l, liste)
setViewMode(arg)Permet de modifier le mode d'affichage de la liste :
  • QtListView.ListMode : Vue en liste (par défaut)
  • QtListView.IconMode : Vue en icônes
currentRow()Retourne l'index de la ligne sélectionnée.
currentItem()Retourne l'objet QListWidgetItem correspondant à la ligne sélectionnée.
clear()Efface les entrées présentes.

Cette classe génère de nombreux signaux dont en voici un extrait :

SignalDéclencheur
currentItemCangedDéclenché lors du changement d'éléments sélectionnés. Retourne l'élément précédemment sélectionné et l'élément nouvellement sélectionné.
itemActivatedDéclenché lors de la sélection d'un élément. Retourne l'élément sélectionné.
itemClickedDéclenché lors du clic d'un élément. Retourne l'élément cliqué.
itemDoubleClickedDéclenché lors du double-clic d'un élément. Retourne l'élément double-cliqué.

L'affichage en tableau QTableWidget

Il est possible d'afficher des données sous la forme d'une table. Chaque cellule contient une instance de la classe QtGui.QTableWidgetItem. Lors de sa création, on passe le nombre de lignes et de colonnes en argument du constructeur. Voici les méthodes usuelles :

MéthodeDescription
setItem(ligne, colonne, item)Définit la cellule spécifiée par sa ligne et sa colonne. L'item passé en argument est un objet QTableWidgetItem(chaine).
setHorizontalHeaderLabels (liste)Modifie les en-têtes des colonnes de la table.
setVerticalHeaderLabels (liste)Modifie les en-têtes des lignes de la table.
setRowCount(nombre)Définit le nombre de lignes passées en argument.
setColumnCount(nombre)Définit le nombre de colonnes passées en argument.
rowCount()Retourne le nombre de lignes de la table.
columnCount()Retourne le nombre de colonnes de la table.

Voici quelques signaux proposés :

SignalDéclencheur
currentItemCangedDéclenché lors du changement d'éléments sélectionnés. Retourne l'élément précédemment sélectionné et l'élément nouvellement sélectionné.
itemActivatedDéclenché lors de la sélection d'un élément. Retourne l'élément sélectionné.
itemClickedDéclenché lors du clic d'un élément. Retourne l'élément cliqué.
itemDoubleClickedDéclenché lors du double-clic d'un élément. Retourne l'élément double-cliqué.
currentCellCangedDéclenché lors du changement d'éléments sélectionnés. Retourne les coordonnées de l'élément précédemment sélectionné et les coordonnées de l'élément nouvellement sélectionné.
cellActivatedDéclenché lors de la sélection d'un élément. Retourne les coordonnées de l'élément sélectionné.
cellClickedDéclenché lors du clic d'un élément. Retourne les coordonnées de l'élément cliqué.
cellDoubleClickedDéclenché lors du double-clic d'un élément. Retourne les coordonnées de l'élément double-cliqué.

L'affichage en arbre QTreeWidget

Il est possible de représenter les données sous la forme d'un arbre. Chaque élément de l'arbre est une instance de la classe QtGui.QTreeWidgetItem avec en argument la chaîne de caractères de l'élément. Pour ajouter un élément enfant, on utilise la méthode addChild(item) avec comme argument l'item enfant implémentant QTreeWidgetItem. On définit l'élément racine de l'arbre avec la méthode addTopLevelItem(item). Les signaux sont identiques à la classe QListWidget.

La boîte de dialogue QInputDialog

Pour simplifier nos programmes, il existe la classe QInputDialog qui permet de demander une donnée à l'utilisateur. Cette boîte de dialogue comporte le champ à saisir, un titre, un message, un bouton pour valider et un bouton pour annuler. Cette classe s'utilise comme suit :

age = QtGui.QInputDialog.getInt(parent, "Votre âge", "Entrez votre âge : ")

Les méthodes offertes permettent de déterminer le type de données à demander :

MéthodeDescription
getInt(parent, titre, message, valeur)Demande un entier à l'utilisateur.
getDouble(parent, titre, message, valeur)Demande un réel à l'utilisateur.
getItem(parent, titre, message, listeValeurs, editable)Demande un élément parmi la liste à l'utilisateur. Peut être modifiable si editable=True.
getText(parent, titre, message)Demande une chaîne à l'utilisateur. Peut être caché si echo=QtGui.QLineEdit.Password.

Le sélectionneur de couleur QColorDialog

Cette boîte de dialogue permet de choisir une couleur parmi un nuancier. Voici les méthodes offertes par cette classe :

MéthodeDescription
selectedColor()Retourne la couleur choisie par l'utilisateur (classe QColor).
setCurrentColor(couleur)Définit la couleur de la boîte de dialogue.
getColor()Ouvre la boîte de dialogue pour choisir la couleur.

Voici les signaux proposés par cette classe :

SignalDéclencheur
colorSelectedDéclenché lors de la sélection d'une couleur. Retourne la couleur sélectionnée.
currentColorChangedDéclenché lors du changement de couleur choisie. Retourne la couleur sélectionnée.

Le sélectionneur de fontes QFontDialog

Cette boîte de dialogue permet de choisir une fonte parmi les polices installées sur le système. Voici les méthodes offertes par cette classe :

MéthodeDescription
selectedFont()Retourne la fonte choisie par l'utilisateur (classe QFont).
setCurrentFont(fonte)Définit la fonte de la boîte de dialogue.
getFont()Ouvre la boîte de dialogue pour choisir la fonte.

Voici les signaux proposés par cette classe :

SignalDéclencheur
fontSelectedDéclenché lors de la sélection d'une fonte. Retourne la fonte sélectionnée.
currentFontChangedDéclenché lors du changement de fonte choisie. Retourne la fonte sélectionnée.

Le sélectionneur de fichier QFileDialog

La boîte de dialogue QFileDialog permet de choisir un fichier ou un répertoire. Voici les méthodes permettant de créer ou choisir un fichier ou un répertoire. Toutes les méthodes présentées retournent le chemin du fichier et le filtre choisis dans le cas des fichiers :

MéthodeDescription
getExistingDirectory()Permet de sélectionner un répertoire.
getOpenFileName()Permet de sélectionner un fichier à ouvrir.
getOpenFileNames()Permet de sélectionner un ou plusieurs fichiers à ouvrir.
getSaveFileName()Permet de sauvegarder un fichier.

Les layouts

Les layouts permettent de placer les widgets dans les conteneurs (fenêtre, QTabWidget, …). Voici les layouts proposés par PySide.

Le placement sur une ligne QHBoxLayout et sur une colonne QVBoxLayout

Ces layouts simplifient la mise en place des widgets en les juxtaposant (verticalement avec QVBoxLayout et horizontalement avec QHBoxLayout) avec la méthode addWidget(widget). Il est cependant possible d'ajouter un layout au sein du layout actuel avec la méthode addLayout(layout). Il est enfin possible d'ajouter un espace élastique qui occupe tout l'espace restant lors du redimensionnement de la fenêtre avec addStretch.

Voici un exemple de mise en œuvre de ces layouts.

fenetre-layout

Voici le code source permettant d'obtenir ce résultat :

import sys
from PySide import QtCore, QtGui
class Dialog(QtGui.QDialog):
	def __init__(self, parent=None):
		QtGui.QDialog.__init__(self,parent)
		self.setWindowTitle("Saisie de tarif")
		self.__labelLibelle = QtGui.QLabel("Libellé : ")
		self.__champLibelle = QtGui.QLineEdit("")
		self.__layoutLibelle = QtGui.QHBoxLayout()
		self.__layoutLibelle.addWidget(self.__labelLibelle)
		self.__layoutLibelle.addWidget(self.__champLibelle)
		self.__labelPrixHT = QtGui.QLabel("Prix HT : ")
		self.__champPrixHT = QtGui.QDoubleSpinBox()
		self.__champPrixHT.setSuffix("€")
		self.__labelTauxTVA = QtGui.QLabel("TVA : ")
		self.__champTauxTVA = QtGui.QDoubleSpinBox()
		self.__champTauxTVA.setSuffix("%")
		self.__layoutPrix = QtGui.QHBoxLayout()
		self.__layoutPrix.addWidget(self.__labelPrixHT)
		self.__layoutPrix.addWidget(self.__champPrixHT)
		self.__layoutPrix.addWidget(self.__labelTauxTVA)
		self.__layoutPrix.addWidget(self.__champTauxTVA)
		self.__boutonAnnuler = QtGui.QPushButton("Annuler")
		self.__boutonValider = QtGui.QPushButton("Valider")
		self.__layoutBoutons = QtGui.QHBoxLayout()
		self.__layoutBoutons.addWidget(self.__boutonAnnuler)
		self.__layoutBoutons.addStretch()
		self.__layoutBoutons.addWidget(self.__boutonValider)
		self.__layoutPrincipal = QtGui.QVBoxLayout()
		self.__layoutPrincipal.addLayout(self.__layoutLibelle)
		self.__layoutPrincipal.addLayout(self.__layoutPrix)
		self.__layoutPrincipal.addLayout(self.__layoutBoutons)
		self.setLayout(self.__layoutPrincipal)
app = QtGui.QApplication(sys.argv)
dialog = Dialog()
dialog.exec_()

Le placement en formulaire QFormLayout

Le placement en formulaire permet de simplifier le code source de votre application en proposant un layout mettant en forme les widgets en formulaire. Cette mise en forme est divisée en deux colonnes, avec à gauche les labels associés aux widgets situés à droite.

Le layout possède la méthode addRow(chaine, widget) qui crée le label associé aux widgets avec comme texte la chaîne passée en argument.

Le placement en grille QGridLayout

Nous avons déjà vu ce type de layout au début de ce chapitre. Nous ajouterons comment faire en sorte qu'un widget ou un layout occupent plusieurs lignes ou colonnes. Pour cela, il faut ajouter deux arguments permettant de spécifier le nombres de lignes et de colonnes occupées.

Voici un exemple de mise en œuvre de cette fusion de cellules.

layout = QtGui.QGridLayout()
layout.addWidget(widget0, 0 ,0, 1, 2)
layout.addWidget(widget1, 0 ,2)
layout.addWidget(widget2, 1 ,0, 2, 1)
layout.addWidget(widget3, 1 ,1)
layout.addWidget(widget4, 1 ,2)
layout.addWidget(widget5, 2 ,1)
layout.addWidget(widget6, 2 ,2)

Code source

Widget 0Widget 1
Widget 2Widget 3Widget 4
Widget 5Widget 6

Rendu

Les fenêtres principales

Les fenêtres des applications sont généralement construites avec la classe QMainWindow qui gère automatiquement les barres d'outils, les menus, les barres d'états …

Application : le bloc-notes

Nous allons étudier cette classe en créant un bloc-notes permettant d'ouvrir, d'éditer et d'enregistrer un fichier. Notre application s'articulera autour d'une zone de texte. Voici un schéma de la fenêtre à concevoir.

schema-gui-blocnotes

Nous allons instancier la classe QMainWindow qui est affichée en appelant la méthode show() ou la méthode setVisible(bool). Dans cette partie, nous aborderons uniquement la construction de la fenêtre avec la barre d'outils et de menu.

import sys
from PySide import QtCore, QtGui
class BlocNotes(QtGui.QMainWindow):
	def __init__(self, parent=None):
		QtGui.QMainWindow.__init__(self, parent)
		# La fenêtre sera décrite ici
		self.show()
app = QtGui.QApplication(sys.argv)
fenetre = BlocNotes()
app.exec_()

Nous allons créer la zone de texte centrale et la définir comme widget central de la fenêtre :

class BlocNotes(QtGui.QMainWindow):
	def __init__(self, parent=None):
		QtGui.QMainWindow.__init__(self, parent)
		self.setWindowTitle("Bloc-notes")
		self.__zoneTexte = QtGui.QTextEdit()
		self.setCentralWidget(self.__zoneTexte)
		# ...

Pour définir un layout comme widget central, créez une instance QtGui.QWidget, affectez votre layout à ce widget avec la méthode setLayout et définissez ce widget comme widget central.

Les actions QAction

Dans notre application, nous avons la barre de menu et la barre d'outils qui comportent les mêmes actions. La classe QAction permet de regrouper tous ces éléments graphiques et les associer à la même méthode, en y ajoutant une icône et un raccourci clavier. Cette classe peut être utilisée de trois manières différentes :

Voici quelques méthodes offertes par cette classe :

MéthodeDescription
setStatusTip(chaine)Définit le texte affiché dans la barre d'actions des fenêtres.
setShortcuts(raccourci)Définit le raccourci clavier. L'argument est une instance de la classe QKeySequence.

Les actions génèrent le signal triggered lorsqu'elles sont activées. Voici les actions utilisées pour notre application :

class BlocNotes(QtGui.QMainWindow):
	def __init__(self, parent=None):
		QtGui.QMainWindow.__init__(self, parent)
		# ...
		self.__actionNew = QtGui.QAction(QtGui.QIcon("document-new.svg"), "Nouveau", self)
		self.__actionNew.setShortcuts(QtGui.QKeySequence.New)
		self.__actionNew.setStatusTip("Nouveau document")
		self.__actionOpen = QtGui.QAction(QtGui.QIcon("document-open.svg"), "Ouvrir", self)
		self.__actionOpen.setShortcuts(QtGui.QKeySequence.Open)
		self.__actionOpen.setStatusTip("Ouvrir un document existant")
		self.__actionSave = QtGui.QAction(QtGui.QIcon("document-save.svg"), "Enregistrer", self)
		self.__actionSave.setShortcuts(QtGui.QKeySequence.Save)
		self.__actionSave.setStatusTip("Enregistrer le document")
		self.__actionSaveAs = QtGui.QAction(QtGui.QIcon("document-save-as.svg"), "Enregistrer sous", self)
		self.__actionSaveAs.setShortcuts(QtGui.QKeySequence.SaveAs)
		self.__actionSaveAs.setStatusTip("Enregistrer le document sous")
		self.__actionQuit = QtGui.QAction(QtGui.QIcon("exit.svg"), "Quitter", self)
		self.__actionQuit.setShortcuts(QtGui.QKeySequence.Quit)
		self.__actionQuit.setStatusTip("Quitter l'application")
		self.__actionUndo = QtGui.QAction(QtGui.QIcon("undo.svg"), "Annuler", self)
		self.__actionUndo.setShortcuts(QtGui.QKeySequence.Undo)
		self.__actionUndo.setStatusTip("Annuler la dernière opération")
		self.__actionRedo = QtGui.QAction(QtGui.QIcon("redo.svg"), "Refaire", self)
		self.__actionRedo.setShortcuts(QtGui.QKeySequence.Redo)
		self.__actionRedo.setStatusTip("Refaire la dernière opération")
		self.__actionCut = QtGui.QAction(QtGui.QIcon("edit-cut.svg"), "Couper", self)
		self.__actionCut.setShortcuts(QtGui.QKeySequence.Cut)
		self.__actionCut.setStatusTip("Couper le texte vers le presse-papier")
		self.__actionCopy = QtGui.QAction(QtGui.QIcon("edit-copy.svg"), "Copier", self)
		self.__actionCopy.setShortcuts(QtGui.QKeySequence.Copy)
		self.__actionCopy.setStatusTip("Copier le texte vers le presse-papier")
		self.__actionPaste = QtGui.QAction(QtGui.QIcon("edit-paste.svg"), "Coller", self)
		self.__actionPaste.setShortcuts(QtGui.QKeySequence.Paste)
		self.__actionPaste.setStatusTip("Coller le texte depuis le presse-papier")
		# ...

Nous allons associer les actions aux méthodes créées (non décrites ici) :

class BlocNotes(QtGui.QMainWindow):
	def __init__(self, parent=None):
		QtGui.QMainWindow.__init__(self, parent)
		# ...
		self.__actionNew.triggered.connect(self.newDocument)
		self.__actionOpen.triggered.connect(self.openDocument)
		self.__actionSave.triggered.connect(self.saveDocument)
		self.__actionSaveAs.triggered.connect(self.saveAsDocument)
		self.__actionQuit.triggered.connect(self.quit)
		self.__actionUndo.triggered.connect(self.undo)
		self.__actionRedo.triggered.connect(self.redo)
		self.__actionCut.triggered.connect(self.cut)
		self.__actionCopy.triggered.connect(self.copy)
		self.__actionPaste.triggered.connect(self.paste)
		# ...

Les barres de menu QMenu

Nous allons à présent utiliser les actions précédemment créées pour les insérer dans des menus. Pour cela, nous allons créer deux menus : le menu Fichier et le menu Édition. Pour créer un menu, on appelle la méthode addMenu(nom) de la fenêtre QMainWindow. On passe en argument le nom du menu.

Ce nouveau menu accepte deux méthodes, addAction(action) qui ajoute une action au menu, et la méthode addSeparator() qui ajoute un séparateur.

Voici la création de nos deux menus :

class BlocNotes(QtGui.QMainWindow):
	def __init__(self, parent=None):
		QtGui.QMainWindow.__init__(self, parent)
		# ...
		self.__menuFile = self.menuBar().addMenu("Fichier")
		self.__menuFile.addAction(self.__actionNew)
		self.__menuFile.addAction(self.__actionOpen)
		self.__menuFile.addAction(self.__actionSave)
		self.__menuFile.addAction(self.__actionSaveAs)
		self.__menuFile.addSeparator()
		self.__menuFile.addAction(self.__actionQuit)
		self.__menuEdit = self.menuBar().addMenu("Édition")
		self.__menuEdit.addAction(self.__actionUndo)
		self.__menuEdit.addAction(self.__actionRedo)
		self.__menuEdit.addSeparator()
		self.__menuEdit.addAction(self.__actionCut)
		self.__menuEdit.addAction(self.__actionCopy)
		self.__menuEdit.addAction(self.__actionPaste)
		# ...

Les barres d'outils

À l'instar des barres de menu, la classe QMainWindow possède une méthode addToolBar(nom) avec le nom passé en argument. Ces barres d'outils possèdent deux méthodes addAction(action) qui ajoute une action au menu et la méthode addSeparator() qui ajoute un séparateur.

Voici la création de la barre d'outils :

class BlocNotes(QtGui.QMainWindow):
	def __init__(self, parent=None):
		QtGui.QMainWindow.__init__(self, parent)
		# ...
		self.__barreFile = self.addToolBar("Fichier")
		self.__barreFile.addAction(self.__actionNew)
		self.__barreFile.addAction(self.__actionOpen)
		self.__barreFile.addAction(self.__actionSave)
		self.__barreEdit = self.addToolBar("Édition")
		self.__barreEdit.addAction(self.__actionUndo)
		self.__barreEdit.addAction(self.__actionRedo)
		self.__barreEdit.addAction(self.__actionCut)
		self.__barreEdit.addAction(self.__actionCopy)
		self.__barreEdit.addAction(self.__actionPaste)
		# ...
Voici le code source complet de notre application :
#!/usr/bin/env python3
import sys
from PySide import QtCore, QtGui
class BlocNotes(QtGui.QMainWindow):
	def __init__(self, parent=None):
		QtGui.QMainWindow.__init__(self, parent)
		self.setWindowTitle("Bloc-notes")
		self.__zoneTexte = QtGui.QTextEdit()
		self.setCentralWidget(self.__zoneTexte)
		self.__actionNew = QtGui.QAction(QtGui.QIcon("document-new.svg"), "Nouveau", self)
		self.__actionNew.setShortcuts(QtGui.QKeySequence.New)
		self.__actionNew.setStatusTip("Nouveau document")
		self.__actionOpen = QtGui.QAction(QtGui.QIcon("document-open.svg"), "Ouvrir", self)
		self.__actionOpen.setShortcuts(QtGui.QKeySequence.Open)
		self.__actionOpen.setStatusTip("Ouvrir un document existant")
		self.__actionSave = QtGui.QAction(QtGui.QIcon("document-save.svg"), "Enregistrer", self)
		self.__actionSave.setShortcuts(QtGui.QKeySequence.Save)
		self.__actionSave.setStatusTip("Enregistrer le document")
		self.__actionSaveAs = QtGui.QAction(QtGui.QIcon("document-save-as.svg"), "Enregistrer sous", self)
		self.__actionSaveAs.setShortcuts(QtGui.QKeySequence.SaveAs)
		self.__actionSaveAs.setStatusTip("Enregistrer le document sous")
		self.__actionQuit = QtGui.QAction(QtGui.QIcon("exit.svg"), "Quitter", self)
		self.__actionQuit.setShortcuts(QtGui.QKeySequence.Quit)
		self.__actionQuit.setStatusTip("Quitter l'application")
		self.__actionUndo = QtGui.QAction(QtGui.QIcon("undo.svg"), "Annuler", self)
		self.__actionUndo.setShortcuts(QtGui.QKeySequence.Undo)
		self.__actionUndo.setStatusTip("Annuler la dernière opération")
		self.__actionRedo = QtGui.QAction(QtGui.QIcon("redo.svg"), "Refaire", self)
		self.__actionRedo.setShortcuts(QtGui.QKeySequence.Redo)
		self.__actionRedo.setStatusTip("Refaire la dernière opération")
		self.__actionCut = QtGui.QAction(QtGui.QIcon("edit-cut.svg"), "Couper", self)
		self.__actionCut.setShortcuts(QtGui.QKeySequence.Cut)
		self.__actionCut.setStatusTip("Couper le texte vers le presse-papier")
		self.__actionCopy = QtGui.QAction(QtGui.QIcon("edit-copy.svg"), "Copier", self)
		self.__actionCopy.setShortcuts(QtGui.QKeySequence.Copy)
		self.__actionCopy.setStatusTip("Copier le texte vers le presse-papier")
		self.__actionPaste = QtGui.QAction(QtGui.QIcon("edit-paste.svg"), "Coller", self)
		self.__actionPaste.setShortcuts(QtGui.QKeySequence.Paste)
		self.__actionPaste.setStatusTip("Coller le texte depuis le presse-papier")
		self.__actionNew.triggered.connect(self.newDocument)
		self.__actionOpen.triggered.connect(self.openDocument)
		self.__actionSave.triggered.connect(self.saveDocument)
		self.__actionSaveAs.triggered.connect(self.saveAsDocument)
		self.__actionQuit.triggered.connect(self.quit)
		self.__actionUndo.triggered.connect(self.undo)
		self.__actionRedo.triggered.connect(self.redo)
		self.__actionCut.triggered.connect(self.cut)
		self.__actionCopy.triggered.connect(self.copy)
		self.__actionPaste.triggered.connect(self.paste)
		self.__menuFile = self.menuBar().addMenu("Fichier")
		self.__menuFile.addAction(self.__actionNew)
		self.__menuFile.addAction(self.__actionOpen)
		self.__menuFile.addAction(self.__actionSave)
		self.__menuFile.addAction(self.__actionSaveAs)
		self.__menuFile.addSeparator()
		self.__menuFile.addAction(self.__actionQuit)
		self.__menuEdit = self.menuBar().addMenu("Édition")
		self.__menuEdit.addAction(self.__actionUndo)
		self.__menuEdit.addAction(self.__actionRedo)
		self.__menuEdit.addSeparator()
		self.__menuEdit.addAction(self.__actionCut)
		self.__menuEdit.addAction(self.__actionCopy)
		self.__menuEdit.addAction(self.__actionPaste)
		self.__barreFile = self.addToolBar("Fichier")
		self.__barreFile.addAction(self.__actionNew)
		self.__barreFile.addAction(self.__actionOpen)
		self.__barreFile.addAction(self.__actionSave)
		self.__barreEdit = self.addToolBar("Édition")
		self.__barreEdit.addAction(self.__actionUndo)
		self.__barreEdit.addAction(self.__actionRedo)
		self.__barreEdit.addAction(self.__actionCut)
		self.__barreEdit.addAction(self.__actionCopy)
		self.__barreEdit.addAction(self.__actionPaste)
		self.show()
app = QtGui.QApplication(sys.argv)
fenetre = BlocNotes()
app.exec_()

Voici le rendu de notre fenêtre.

fenetre-blocnotes

Exercices

Vous êtes nouvellement embauché dans une entreprise en bâtiment pour créer un programme permettant au secrétariat de saisir les estimations de travaux de l'entreprise. Votre application devra s'interfacer avec une base de données SQLite qui stockera le catalogue des prestations proposées par l'entreprise et leurs tarifs. Cette base de données contiendra également les estimations déjà réalisées.

Pour créer une estimation, il faut d'abord saisir les informations relatives au client (nom, prénom, adresse, code postal, ville, téléphone, courriel), le titre du chantier, puis choisir dans le catalogue les prestations à ajouter. Chaque prestation est dans une catégorie et comporte un texte la décrivant, un prix unitaire et une unité (définissant le prix unitaire). Voici un exemple :

PrestationPrix unitaire (€/unité)Unité
Cloison sur ossature métallique. 45

Si la prestation n'existe pas, une boîte de dialogue permet d'en ajouter, de même pour les catégories. Lors de l'ajout d'une prestation à l'estimation, l'utilisateur doit choisir un taux de TVA (exprimé en %). Une fois la saisie de la prestation terminée, les totaux hors-taxes, de TVA et le total TTC (hors-taxes + TVA) sont automatiquement mis à jour.

Enfin, il sera possible d'exporter l'estimation au format texte suivant l'exemple ci-dessous :

Société Bati Plus
52 rue de Clairecombe
74930 Moulincourbe
							Lionel Paulin
							48 Ruelle de Locvaux
							74019 Mivran
							01 98 74 30 52
							lionel.paulin@exemple.com
Estimation numéro 524 réalisée le 10 avril 2017. 
Chantier de plâtrerie
Prestation			Prix unitaire	Quantité	Total HT	TVA		Total TTC
Cloison sur ossature métallique	45 €/m²		17 m²		765€		153€ (20%)	918€
Pose d'une porte		78 €/porte	1 porte		78€		15,6€ (20%)	93,6€

Total HT : 843€
Total TVA : 168,6€
Total TTC : 1011,6€

Tous les totaux seront arrondis à deux décimales.

#!/usr/bin/env python3
import sys
from PySide import QtCore, QtGui
import sqlite3
import os
import time
fichierBaseDeDonnees = "tarification.db"
if os.path.isfile(fichierBaseDeDonnees) == False:
	baseDeDonnees = sqlite3.connect(fichierBaseDeDonnees)
	curseur = baseDeDonnees.cursor()
	curseur.execute("CREATE TABLE Prestations (id INTEGER PRIMARY KEY AUTOINCREMENT, categorie TEXT NOT NULL, libelle TEXT NOT NULL, prixUnitaire REAL NOT NULL, unite TEXT NOT NULL)")
	baseDeDonnees.commit()
	curseur.execute("CREATE TABLE Estimations (id INTEGER PRIMARY KEY AUTOINCREMENT, nomClient TEXT NOT NULL, prenomClient TEXT NOT NULL, adresse TEXT NOT NULL, codePostal TEXT NOT NULL, ville TEXT NOT NULL, telephone TEXT, courriel TEXT, nomChantier TEXT)")
	baseDeDonnees.commit()
	curseur.execute("CREATE TABLE LignesEstimations (id INTEGER PRIMARY KEY AUTOINCREMENT, idEstimation INTEGER NOT NULL, idPrestation INTEGER NOT NULL, ordre INTEGER, quantite REAL NOT NULL, tauxTVA REAL)")
	baseDeDonnees.commit()
baseDeDonnees = sqlite3.connect(fichierBaseDeDonnees)
curseur = baseDeDonnees.cursor()
class EditionPrestation(QtGui.QDialog):
	def __init__(self,idPrestation=None,parent=None):
		QtGui.QDialog.__init__(self,parent)
		self.__idPrestation = idPrestation
		self.setWindowTitle("Éditer une prestation")
		self.setWindowIcon(QtGui.QIcon("/usr/share/icons/Humanity/actions/24/stock_edit.svg"))
		self.__categorie = QtGui.QComboBox()
		requete = """SELECT categorie FROM Prestations GROUP BY categorie ORDER BY categorie;"""
		curseur.execute(requete)
		self.__categorie.setEditable(True)
		self.__categorie.addItems([resultat[0] for resultat in curseur.fetchall()])
		self.__texteLibelle = QtGui.QLineEdit("")
		self.__prixUnitaire = QtGui.QDoubleSpinBox()
		self.__prixUnitaire.setMaximum(99999.99)
		self.__prixUnitaire.setSuffix("€")
		self.__unite = QtGui.QLineEdit("")
		self.__valider = QtGui.QPushButton("Valider")
		self.__valider.setIcon(QtGui.QIcon("/usr/share/icons/Humanity/actions/24/dialog-apply.svg"))
		self.__annuler = QtGui.QPushButton("Annuler")
		self.__annuler.setIcon(QtGui.QIcon("/usr/share/icons/Humanity/actions/24/dialog-cancel.svg"))
		self.__layoutPrincipal = QtGui.QVBoxLayout()
		self.__layoutLigne1 = QtGui.QFormLayout()
		self.__layoutLigne1.addRow("Categorie :", self.__categorie)
		self.__layoutLigne2 = QtGui.QFormLayout()
		self.__layoutLigne2.addRow("Prestation :", self.__texteLibelle)
		self.__layoutLigne3 = QtGui.QFormLayout()
		self.__layoutLigne3.addRow("Prix unitaire :", self.__prixUnitaire)
		self.__layoutLigne4 = QtGui.QFormLayout()
		self.__layoutLigne4.addRow("Unité :", self.__unite)
		self.__layoutBoutons = QtGui.QHBoxLayout()
		self.__layoutBoutons.addWidget(self.__valider)
		self.__layoutBoutons.addWidget(self.__annuler)
		self.__layoutPrincipal.addLayout(self.__layoutLigne1)
		self.__layoutPrincipal.addLayout(self.__layoutLigne2)
		self.__layoutPrincipal.addLayout(self.__layoutLigne3)
		self.__layoutPrincipal.addLayout(self.__layoutLigne4)
		self.__layoutPrincipal.addLayout(self.__layoutBoutons)
		self.setLayout(self.__layoutPrincipal)
		self.__unite.textChanged.connect(self.modifierUnite)
		self.__valider.clicked.connect(self.valider)
		self.__annuler.clicked.connect(self.accept)
		if self.__idPrestation != None:
			requete = """SELECT categorie, libelle, prixUnitaire, unite FROM Prestations WHERE id = ?;"""
			curseur.execute(requete, (self.__idPrestation, ))
			lignePrestation = curseur.fetchone()
			self.__categorie.setEditText(lignePrestation[0])
			self.__texteLibelle.setText(lignePrestation[1])
			self.__prixUnitaire.setValue(lignePrestation[2])
			self.__unite.setText(lignePrestation[3])
	def valider(self):
		categorie = self.__categorie.currentText()
		texteLibelle = self.__texteLibelle.text()
		prixUnitaire = self.__prixUnitaire.value()
		unite = self.__unite.text()
		if self.__idPrestation == None:
			requete = """INSERT INTO Prestations (categorie, libelle, prixUnitaire, unite) VALUES (?, ?, ?, ?);"""
			curseur.execute(requete, (categorie, texteLibelle, prixUnitaire, unite))
			baseDeDonnees.commit()
		else:
			requete = """UPDATE Prestations SET categorie = ?, libelle = ?, prixUnitaire = ?, unite = ? WHERE id = ?;"""
			curseur.execute(requete, (categorie, texteLibelle, prixUnitaire, unite, self.__idPrestation))
			baseDeDonnees.commit()
		self.accept()
	def modifierUnite(self):
		suffixe = "€"
		if self.__unite.text() != "":
			suffixe += "/" + self.__unite.text()
		self.__prixUnitaire.setSuffix(suffixe)
class OuvrirPrestation(QtGui.QDialog):
	def __init__(self,parent=None):
		QtGui.QDialog.__init__(self,parent)
		self.__donneesRetournees = None
		self.setWindowTitle("Tarification")
		self.setWindowIcon(QtGui.QIcon("/usr/share/icons/Humanity/actions/24/document-open.svg"))
		self.__listeChiffrages = QtGui.QListWidget()
		self.__chiffrages = []
		self.listerChiffrages()
		self.__ouvrir = QtGui.QPushButton("Ouvrir")
		self.__ouvrir.setIcon(QtGui.QIcon("/usr/share/icons/Humanity/actions/24/document-open.svg"))
		self.__supprimer = QtGui.QPushButton("Supprimer")
		self.__supprimer.setIcon(QtGui.QIcon("/usr/share/icons/Humanity/actions/24/stock_delete.svg"))
		self.__annuler = QtGui.QPushButton("Annuler")
		self.__annuler.setIcon(QtGui.QIcon("/usr/share/icons/Humanity/actions/24/dialog-cancel.svg"))
		self.__layoutPrincipal = QtGui.QVBoxLayout()
		self.__layoutPrincipal.addWidget(self.__listeChiffrages)
		self.__layoutBouton = QtGui.QHBoxLayout()
		self.__layoutBouton.addWidget(self.__annuler)
		self.__layoutBouton.addWidget(self.__supprimer)
		self.__layoutBouton.addWidget(self.__ouvrir)
		self.__layoutPrincipal.addLayout(self.__layoutBouton)
		self.setLayout(self.__layoutPrincipal)
		self.__annuler.clicked.connect(self.accept)
		self.__supprimer.clicked.connect(self.supprimerChiffrage)
		self.__ouvrir.clicked.connect(self.ouvrirChiffrage)
	def getDonneesRetournees(self):
		return(self.__donneesRetournees)
	def listerChiffrages(self):
		self.__listeChiffrages.clear()
		self.__chiffrages = []
		requete = """SELECT id, nomClient, prenomClient, nomChantier FROM Estimations;"""
		curseur.execute(requete)
		for noLigne, ligne in enumerate(curseur.fetchall()):
			self.__chiffrages.append({"id":ligne[0], "nomClient":ligne[1], "prenomClient":ligne[2], "nomChantier":ligne[3]})
			self.__listeChiffrages.insertItem(noLigne, ligne[2] + " " + ligne[1] + " - " + ligne[3])
	def supprimerChiffrage(self):
		index = self.__listeChiffrages.currentRow()
		idChiffrage = self.__chiffrages[index]["id"]
		requete = """DELETE FROM Estimations WHERE id = ?;"""
		curseur.execute(requete, (idChiffrage, ))
		baseDeDonnees.commit()
		self.listerChiffrages()
	def ouvrirChiffrage(self):
		index = self.__listeChiffrages.currentRow()
		idChiffrage = self.__chiffrages[index]["id"]
		lignesChiffrage = []
		donneesClient = {}
		requete = """SELECT nomClient, prenomClient, adresse, codePostal, ville, telephone, courriel, nomChantier FROM Estimations WHERE id = ?;"""
		curseur.execute(requete, (idChiffrage, ))
		resultat = curseur.fetchone()
		donneesClient["nomClient"] = resultat[0]
		donneesClient["prenomClient"] = resultat[1]
		donneesClient["adresse"] = resultat[2]
		donneesClient["codePostal"] = resultat[3]
		donneesClient["ville"] = resultat[4]
		donneesClient["telephone"] = resultat[5]
		donneesClient["courriel"] = resultat[6]
		donneesClient["nomChantier"] = resultat[7]
		requete = """SELECT idPrestation, libelle, prixUnitaire, unite, quantite, tauxTVA FROM LignesEstimations JOIN Prestations ON LignesEstimations.idPrestation = Prestations.id WHERE idEstimation = ? ORDER BY ordre;"""
		curseur.execute(requete, (idChiffrage, ))
		for ligne in curseur.fetchall():
			ligneChiffrage = {}
			ligneChiffrage["idPrestation"] = ligne[0]
			ligneChiffrage["libelle"] = ligne[1]
			ligneChiffrage["prixUnitaire"] = float(ligne[2])
			ligneChiffrage["unite"] = ligne[3]
			ligneChiffrage["quantite"] = float(ligne[4])
			ligneChiffrage["tva"] = float(ligne[5])
			ligneChiffrage["prixHT"] = round(ligneChiffrage["prixUnitaire"] * ligneChiffrage["quantite"], 2)
			ligneChiffrage["prixTVA"] = round(ligneChiffrage["prixHT"] * ligneChiffrage["tva"] / 100.0, 2)
			ligneChiffrage["prixTTC"] = round(ligneChiffrage["prixHT"] + ligneChiffrage["prixTVA"], 2)
			lignesChiffrage.append(ligneChiffrage)
		self.__donneesRetournees = {"idChiffrage":idChiffrage, "lignesChiffrage":lignesChiffrage, "donneesClient":donneesClient}
		self.accept()
class Tarification(QtGui.QMainWindow):
	def __init__(self, parent=None):
		QtGui.QMainWindow.__init__(self, parent)
		self.setWindowTitle("Tarification")
		self.setWindowIcon(QtGui.QIcon("/usr/share/icons/Humanity/emblems/32/emblem-money.svg"))
		self.__listeCategories = QtGui.QComboBox()
		self.__idChiffrage = None
		self.__prestations = []
		self.__lignesChiffrage = []
		self.__listePrestations = QtGui.QListWidget()
		self.__barreRecherche = QtGui.QLineEdit("")
		self.__creerPrestation = QtGui.QPushButton("Créer une prestation")
		self.__creerPrestation.setIcon(QtGui.QIcon("/usr/share/icons/Humanity/actions/24/edit-add.svg"))
		self.__modifierPrestation = QtGui.QPushButton("Modifier une prestation")
		self.__modifierPrestation.setIcon(QtGui.QIcon("/usr/share/icons/Humanity/actions/24/stock_edit.svg"))
		self.__supprimerPrestation = QtGui.QPushButton("Supprimer une prestation")
		self.__supprimerPrestation.setIcon(QtGui.QIcon("/usr/share/icons/Humanity/actions/24/stock_delete.svg"))
		self.__prixUnitaire = 0.0
		self.__unite = ""
		self.__prixHT = 0.0
		self.__prixTVA = 0.0
		self.__prixTTC = 0.0
		self.__prixUnitaireLabel = QtGui.QLabel("Prix unitaire : 0,00€")
		self.__quantiteLabel = QtGui.QLabel("Quantité : ")
		self.__quantite = QtGui.QDoubleSpinBox()
		self.__tvaLabel = QtGui.QLabel("TVA : ")
		self.__tva = QtGui.QDoubleSpinBox()
		self.__tva.setSuffix("%")
		self.__totauxLabel = QtGui.QLabel("HT : 0,00 €\nTVA : 0,00 €\nTTC : 0,00 €")
		self.__ajouterPrestation = QtGui.QPushButton("Ajouter la prestation")
		self.__ajouterPrestation.setIcon(QtGui.QIcon("/usr/share/icons/Humanity/actions/24/dialog-apply.svg"))
		self.__prestationLayout = QtGui.QVBoxLayout()
		self.__listeCategoriesLayout = QtGui.QFormLayout()
		self.__listeCategoriesLayout.addRow("Catégorie :", self.__listeCategories)
		self.__prestationLayout.addLayout(self.__listeCategoriesLayout)
		self.__actionPrestationLayout = QtGui.QHBoxLayout()
		self.__actionPrestationLayout.addWidget(self.__creerPrestation)
		self.__actionPrestationLayout.addWidget(self.__modifierPrestation)
		self.__actionPrestationLayout.addWidget(self.__supprimerPrestation)
		self.__prestationLayout.addLayout(self.__actionPrestationLayout)
		self.__barreRechercheLayout = QtGui.QFormLayout()
		self.__barreRechercheLayout.addRow("Recherche :", self.__barreRecherche)
		self.__prestationLayout.addLayout(self.__barreRechercheLayout)
		self.__prestationLayout.addWidget(self.__listePrestations)
		self.__quantiteTVALayout = QtGui.QHBoxLayout()
		self.__quantiteTVALayout.addWidget(self.__prixUnitaireLabel)
		self.__quantiteTVALayout.addStretch()
		self.__quantiteTVALayout.addWidget(self.__quantiteLabel)
		self.__quantiteTVALayout.addWidget(self.__quantite)
		self.__quantiteTVALayout.addStretch()
		self.__quantiteTVALayout.addWidget(self.__tvaLabel)
		self.__quantiteTVALayout.addWidget(self.__tva)
		self.__prestationLayout.addLayout(self.__quantiteTVALayout)
		self.__prestationLayout.addWidget(self.__totauxLabel)
		self.__prestationLayout.addWidget(self.__ajouterPrestation)
		self.__nomClient = QtGui.QLineEdit("")
		self.__prenomClient = QtGui.QLineEdit("")
		self.__adresseClient = QtGui.QLineEdit("")
		self.__cpClient = QtGui.QLineEdit("")
		self.__cpClient.setInputMask("00000;_")
		self.__villeClient = QtGui.QLineEdit("")
		self.__telephoneClient = QtGui.QLineEdit("")
		self.__telephoneClient.setInputMask("00 00 00 00 00;_")
		self.__courrielClient = QtGui.QLineEdit("")
		self.__nomChantier = QtGui.QLineEdit("")
		self.__nomClientLabel = QtGui.QLabel("Nom :")
		self.__prenomClientLabel = QtGui.QLabel("Prénom :")
		self.__adresseClientLabel = QtGui.QLabel("Adresse :")
		self.__cpClientLabel = QtGui.QLabel("Code postal :")
		self.__villeClientLabel = QtGui.QLabel("Ville :")
		self.__telephoneClientLabel = QtGui.QLabel("Téléphone :")
		self.__courrielClientLabel = QtGui.QLabel("Courriel :")
		self.__nomChantierLabel = QtGui.QLabel("Nom du chantier :")
		self.__ouvrir = QtGui.QPushButton("Ouvrir")
		self.__ouvrir.setIcon(QtGui.QIcon("/usr/share/icons/Humanity/actions/24/document-open.svg"))
		self.__enregistrer = QtGui.QPushButton("Enregistrer")
		self.__enregistrer.setIcon(QtGui.QIcon("/usr/share/icons/Humanity/actions/24/document-save.svg"))
		self.__exporter = QtGui.QPushButton("Exporter")
		self.__exporter.setIcon(QtGui.QIcon("/usr/share/icons/Humanity/actions/24/document-export.svg"))
		self.__monter = QtGui.QPushButton("Monter")
		self.__monter.setIcon(QtGui.QIcon("/usr/share/icons/Humanity/actions/24/go-up.svg"))
		self.__descendre = QtGui.QPushButton("Descendre")
		self.__descendre.setIcon(QtGui.QIcon("/usr/share/icons/Humanity/actions/24/go-down.svg"))
		self.__supprimerLigne = QtGui.QPushButton("Supprimer")
		self.__supprimerLigne.setIcon(QtGui.QIcon("/usr/share/icons/Humanity/actions/24/edit-delete.svg"))
		self.__tableChiffrage = QtGui.QTableWidget(0,6)
		enTeteTable = ("Prestation", "Prix unitaire", "Quantité", "Total HT", "TVA", "Total TTC")
		self.__tableChiffrage.setHorizontalHeaderLabels(enTeteTable)
		self.__totauxEstimationLabel = QtGui.QLabel("HT : 0,00 €\nTVA : 0,00 €\nTTC : 0,00 €")
		self.__chiffrageLayout = QtGui.QVBoxLayout()
		self.__clientFormLigne1 = QtGui.QHBoxLayout()
		self.__clientFormLigne1.addWidget(self.__nomClientLabel)
		self.__clientFormLigne1.addWidget(self.__nomClient)
		self.__clientFormLigne1.addWidget(self.__prenomClientLabel)
		self.__clientFormLigne1.addWidget(self.__prenomClient)
		self.__chiffrageLayout.addLayout(self.__clientFormLigne1)
		self.__clientFormLigne2 = QtGui.QHBoxLayout()
		self.__clientFormLigne2.addWidget(self.__adresseClientLabel)
		self.__clientFormLigne2.addWidget(self.__adresseClient)
		self.__chiffrageLayout.addLayout(self.__clientFormLigne2)
		self.__clientFormLigne3 = QtGui.QHBoxLayout()
		self.__clientFormLigne3.addWidget(self.__cpClientLabel)
		self.__clientFormLigne3.addWidget(self.__cpClient)
		self.__clientFormLigne3.addWidget(self.__villeClientLabel)
		self.__clientFormLigne3.addWidget(self.__villeClient)
		self.__chiffrageLayout.addLayout(self.__clientFormLigne3)
		self.__clientFormLigne4 = QtGui.QHBoxLayout()
		self.__clientFormLigne4.addWidget(self.__telephoneClientLabel)
		self.__clientFormLigne4.addWidget(self.__telephoneClient)
		self.__clientFormLigne4.addWidget(self.__courrielClientLabel)
		self.__clientFormLigne4.addWidget(self.__courrielClient)
		self.__chiffrageLayout.addLayout(self.__clientFormLigne4)
		self.__clientFormLigne5 = QtGui.QHBoxLayout()
		self.__clientFormLigne5.addWidget(self.__nomChantierLabel)
		self.__clientFormLigne5.addWidget(self.__nomChantier)
		self.__chiffrageLayout.addLayout(self.__clientFormLigne5)
		self.__boutonsLayout = QtGui.QHBoxLayout()
		self.__boutonsLayout.addWidget(self.__ouvrir)
		self.__boutonsLayout.addWidget(self.__enregistrer)
		self.__boutonsLayout.addWidget(self.__exporter)
		self.__boutonsLayout.addWidget(self.__monter)
		self.__boutonsLayout.addWidget(self.__descendre)
		self.__boutonsLayout.addWidget(self.__supprimerLigne)
		self.__chiffrageLayout.addLayout(self.__boutonsLayout)
		self.__chiffrageLayout.addWidget(self.__tableChiffrage)
		self.__chiffrageLayout.addWidget(self.__totauxEstimationLabel)
		self.__prestationLayoutWidget = QtGui.QWidget()
		self.__prestationLayoutWidget.setLayout(self.__prestationLayout)
		self.__chiffrageLayoutWidget = QtGui.QWidget()
		self.__chiffrageLayoutWidget.setLayout(self.__chiffrageLayout)
		self.__centralWidget = QtGui.QSplitter()
		self.__centralWidget.addWidget(self.__prestationLayoutWidget)
		self.__centralWidget.addWidget(self.__chiffrageLayoutWidget)
		self.setCentralWidget(self.__centralWidget)
		self.peuplerCategories()
		self.peuplerPrestations()
		self.__listeCategories.currentIndexChanged.connect(self.peuplerPrestations)
		self.__barreRecherche.textChanged.connect(self.peuplerPrestations)
		self.__listePrestations.itemClicked.connect(self.ouvrirFiche)
		self.__quantite.valueChanged.connect(self.calculerTotaux)
		self.__tva.valueChanged.connect(self.calculerTotaux)
		self.__ajouterPrestation.clicked.connect(self.ajouterLigneChiffrage)
		self.__monter.clicked.connect(self.monterLigne)
		self.__descendre.clicked.connect(self.descendreLigne)
		self.__supprimerLigne.clicked.connect(self.supprimerLigne)
		self.__creerPrestation.clicked.connect(self.nouvellePrestation)
		self.__modifierPrestation.clicked.connect(self.modifierPrestation)
		self.__supprimerPrestation.clicked.connect(self.supprimerPrestation)
		self.__enregistrer.clicked.connect(self.enregistrerChiffrage)
		self.__exporter.clicked.connect(self.exporterChiffrage)
		self.__ouvrir.clicked.connect(self.ouvrirPrestation)
		self.show()
	def peuplerCategories(self):
		self.__listeCategories.clear()
		requete = """SELECT categorie FROM Prestations GROUP BY categorie ORDER BY categorie;"""
		curseur.execute(requete)
		self.__listeCategories.insertItems(0, [resultat[0] for resultat in curseur.fetchall()])
	def peuplerPrestations(self):
		self.__listePrestations.clear()
		self.__prestations = []
		requete = """SELECT id, libelle FROM Prestations WHERE categorie = ? AND libelle LIKE ?;"""
		curseur.execute(requete, (self.__listeCategories.currentText(), "%" + self.__barreRecherche.text() + "%"))
		listeAAfficher = []
		for resultat in curseur.fetchall():
			self.__prestations.append({"id":resultat[0], "libelle":resultat[1]})
			listeAAfficher.append(resultat[1])
		self.__listePrestations.insertItems(0, listeAAfficher)
	def ouvrirFiche(self):
		index = self.__listePrestations.currentRow()
		idPrestation = self.__prestations[index]["id"]
		requete = """SELECT prixUnitaire, unite FROM Prestations WHERE id = ?;"""
		curseur.execute(requete, (idPrestation, ))
		self.__prixUnitaire, self.__unite = curseur.fetchone()
		self.__prixUnitaireLabel.setText("Prix unitaire : " + str(self.__prixUnitaire).replace(".",",") + "€/" + self.__unite)
		self.__quantite.setSuffix(" " + self.__unite)
		self.calculerTotaux()
	def calculerTotaux(self):
		self.__prixHT = round(self.__prixUnitaire * self.__quantite.value(), 2)
		self.__prixTVA = round(self.__prixHT * self.__tva.value() / 100.0, 2)
		self.__prixTTC = round(self.__prixHT + self.__prixTVA, 2)
		self.__totauxLabel.setText("HT : " + str(self.__prixHT).replace(".",",") + " €\nTVA : " + str(self.__prixTVA).replace(".",",") + " €\nTTC : " + str(self.__prixTTC).replace(".",",") + " €")
	def ajouterLigneChiffrage(self):
		lignePrestation = {}
		index = self.__listePrestations.currentRow()
		lignePrestation["idPrestation"] = self.__prestations[index]["id"]
		lignePrestation["libelle"] = self.__prestations[index]["libelle"]
		lignePrestation["prixUnitaire"] = self.__prixUnitaire
		lignePrestation["unite"] = self.__unite
		lignePrestation["quantite"] = self.__quantite.value()
		lignePrestation["prixHT"] = self.__prixHT
		lignePrestation["prixTVA"] = self.__prixTVA
		lignePrestation["tva"] = self.__tva.value()
		lignePrestation["prixTTC"] = self.__prixTTC
		self.__lignesChiffrage.append(lignePrestation)
		self.afficherChiffrage()
	def afficherChiffrage(self):
		totalHT = 0.0
		totalTVA = 0.0
		totalTTC = 0.0
		self.__tableChiffrage.setRowCount(len(self.__lignesChiffrage))
		for noLigne, lignePrestation in enumerate(self.__lignesChiffrage):
			self.__tableChiffrage.setItem(noLigne, 0, QtGui.QTableWidgetItem(lignePrestation["libelle"]))
			self.__tableChiffrage.setItem(noLigne, 1, QtGui.QTableWidgetItem(str(lignePrestation["prixUnitaire"]) + "€/" + lignePrestation["unite"]))
			self.__tableChiffrage.setItem(noLigne, 2, QtGui.QTableWidgetItem(str(lignePrestation["quantite"]) + " " + lignePrestation["unite"]))
			self.__tableChiffrage.setItem(noLigne, 3, QtGui.QTableWidgetItem(str(lignePrestation["prixHT"]) + "€"))
			self.__tableChiffrage.setItem(noLigne, 4, QtGui.QTableWidgetItem(str(lignePrestation["prixTVA"]) + "€ (" + str(lignePrestation["tva"]) + "%)"))
			self.__tableChiffrage.setItem(noLigne, 5, QtGui.QTableWidgetItem(str(lignePrestation["prixTTC"]) + "€"))
			totalHT += lignePrestation["prixHT"]
			totalTVA += lignePrestation["prixTVA"]
			totalTTC += lignePrestation["prixTTC"]
		self.__totauxEstimationLabel.setText("HT : " + str(round(totalHT, 2)).replace(".",",") + " €\nTVA : " + str(round(totalTVA, 2)).replace(".",",") + " €\nTTC : " + str(round(totalTTC, 2)).replace(".",",") + " €")
	def monterLigne(self):
		index = self.__tableChiffrage.currentRow()
		if index > 0:
			self.__lignesChiffrage[index-1], self.__lignesChiffrage[index] = self.__lignesChiffrage[index], self.__lignesChiffrage[index-1]
			self.afficherChiffrage()
	def descendreLigne(self):
		index = self.__tableChiffrage.currentRow()
		if index < len(self.__lignesChiffrage)-1:
			self.__lignesChiffrage[index+1], self.__lignesChiffrage[index] = self.__lignesChiffrage[index], self.__lignesChiffrage[index+1]
			self.afficherChiffrage()
	def supprimerLigne(self):
		index = self.__tableChiffrage.currentRow()
		self.__lignesChiffrage.pop(index)
		self.afficherChiffrage()
	def nouvellePrestation(self):
		dialog = EditionPrestation()
		dialog.exec_()
		self.peuplerCategories()
		self.peuplerPrestations()
		self.ouvrirFiche()
	def modifierPrestation(self):
		index = self.__listePrestations.currentRow()
		idPrestation = self.__prestations[index]["id"]
		dialog = EditionPrestation(idPrestation=idPrestation)
		dialog.exec_()
		self.peuplerCategories()
		self.peuplerPrestations()
		self.ouvrirFiche()
	def supprimerPrestation(self):
		index = self.__listePrestations.currentRow()
		idPrestation = self.__prestations[index]["id"]
		requete = """DELETE FROM Prestations WHERE id = ?;"""
		curseur.execute(requete, (idPrestation, ))
		self.peuplerCategories()
		self.peuplerPrestations()
	def enregistrerChiffrage(self):
		nomClient = self.__nomClient.text()
		prenomClient = self.__prenomClient.text()
		adresse = self.__adresseClient.text()
		codePostal = self.__cpClient.text()
		ville = self.__villeClient.text()
		telephone = self.__telephoneClient.text()
		courriel = self.__courrielClient.text()
		nomChantier = self.__nomChantier.text()
		if self.__idChiffrage == None:
			requete = """INSERT INTO Estimations (nomClient, prenomClient, adresse, codePostal, ville, telephone, courriel, nomChantier) VALUES (?, ?, ?, ?, ?, ?, ?, ?);"""
			curseur.execute(requete, (nomClient, prenomClient, adresse, codePostal, ville, telephone, courriel, nomChantier))
			baseDeDonnees.commit()
			self.__idChiffrage = curseur.lastrowid
		else:
			requete = """UPDATE Estimations SET nomClient = ?, prenomClient = ?, adresse = ?, codePostal = ?, ville = ?, telephone = ?, courriel = ?, nomChantier = ? WHERE id = ?;"""
			curseur.execute(requete, (nomClient, prenomClient, adresse, codePostal, ville, telephone, courriel, nomChantier, self.__idChiffrage))
			baseDeDonnees.commit()
		requete = """DELETE FROM LignesEstimations WHERE idEstimation = ?;"""
		curseur.execute(requete, (self.__idChiffrage, ))
		for noLigne, lignePrestation in enumerate(self.__lignesChiffrage):
			requete = """INSERT INTO LignesEstimations (idEstimation, idPrestation, ordre, quantite, tauxTVA) VALUES (?, ?, ?, ?, ?);"""
			curseur.execute(requete, (self.__idChiffrage, lignePrestation["idPrestation"], noLigne, lignePrestation["quantite"], lignePrestation["tva"]))
		baseDeDonnees.commit()
	def exporterChiffrage(self):
		dialogFichierExport = QtGui.QFileDialog()
		adresseFichierExport = dialogFichierExport.getSaveFileName(caption="Exporter le chiffrage", filter="Fichier texte (*.txt)")[0]
		if adresseFichierExport[-4:] != ".txt":
			adresseFichierExport += ".txt"
		fichier = open(adresseFichierExport, "wt")
		fichier.write("Société Bati Plus\n52 rue de Clairecombe\n74930 Moulincourbe\n")
		totalHT = 0.0
		totalTVA = 0.0
		totalTTC = 0.0
		nomClient = self.__nomClient.text()
		prenomClient = self.__prenomClient.text()
		adresse = self.__adresseClient.text()
		codePostal = self.__cpClient.text()
		ville = self.__villeClient.text()
		telephone = self.__telephoneClient.text()
		courriel = self.__courrielClient.text()
		nomChantier = self.__nomChantier.text()
		lignesDestinataire = [prenomClient + " " + nomClient, adresse, codePostal + " " + ville, telephone, courriel]
		for ligne in lignesDestinataire:
			fichier.write("\t\t\t\t\t\t\t" + ligne + "\n")
		fichier.write("Estimation ")
		if self.__idChiffrage != None:
			fichier.write("numéro " + str(self.__idChiffrage) + " ")
		fichier.write("réalisée le " + time.strftime("%d %B %Y") + "\n")
		fichier.write(nomChantier + "\n")
		fichier.write("Prestation\tPrix unitaire\tQuantité\tTotal HT\tTVA\tTotal TTC\n")
		for lignePrestation in self.__lignesChiffrage:
			fichier.write(lignePrestation["libelle"] + "\t")
			fichier.write(str(lignePrestation["prixUnitaire"]) + "€/" + lignePrestation["unite"] + "\t")
			fichier.write(str(lignePrestation["quantite"]) + " " + lignePrestation["unite"] + "\t")
			fichier.write(str(lignePrestation["prixHT"]) + "€\t")
			fichier.write(str(lignePrestation["prixTVA"]) + "€ (" + str(lignePrestation["tva"]) + "%)\t")
			fichier.write(str(lignePrestation["prixTTC"]) + "€\n")
			totalHT += lignePrestation["prixHT"]
			totalTVA += lignePrestation["prixTVA"]
			totalTTC += lignePrestation["prixTTC"]
		fichier.write("Total HT : " + str(round(totalHT, 2)).replace(".",",") + " €\nTotal TVA : " + str(round(totalTVA, 2)).replace(".",",") + " €\nTotal TTC : " + str(round(totalTTC, 2)).replace(".",",") + " €")
		fichier.close()
	def ouvrirPrestation(self):
		dialog = OuvrirPrestation()
		dialog.exec_()
		donneesChiffrage = dialog.getDonneesRetournees()
		self.__idChiffrage = donneesChiffrage["idChiffrage"]
		self.__lignesChiffrage = donneesChiffrage["lignesChiffrage"]
		self.__nomClient.setText(donneesChiffrage["donneesClient"]["nomClient"])
		self.__prenomClient.setText(donneesChiffrage["donneesClient"]["prenomClient"])
		self.__adresseClient.setText(donneesChiffrage["donneesClient"]["adresse"])
		self.__cpClient.setText(donneesChiffrage["donneesClient"]["codePostal"])
		self.__villeClient.setText(donneesChiffrage["donneesClient"]["ville"])
		self.__telephoneClient.setText(donneesChiffrage["donneesClient"]["telephone"])
		self.__courrielClient.setText(donneesChiffrage["donneesClient"]["courriel"])
		self.__nomChantier.setText(donneesChiffrage["donneesClient"]["nomChantier"])
		self.afficherChiffrage()
app = QtGui.QApplication(sys.argv)
fenetre = Tarification()
app.exec_()
baseDeDonnees.close()