Créations de nouveaux modèles pour PySide/PyQt

Olivier Girardot

Ce tutoriel a pour but de vous initier à la création de nouveaux types de modèles avec les bindingsPythonPyQt et PySide de la bibliothèque C++ Qt.

Qt propose en effet des composants graphiques utilisant le patron de conception MVD (Modèle Vue Délégué), ainsi le même modèle peut être utilisé dans plusieurs types de vues différentes (ex. un tableau, un menu déroulant ou un champ d'autocomplétion…). Nous allons donc illustrer ce principe et apprendre à créer des modèles en exploitant toutes les facilités du framework Qt.

N'hésitez pas à commenter cet article !
Commentez

- Titre : Créations de nouveaux modèles pour PySide/PyQt

- Auteur : Olivier Girardot

- Parution : 4 septembre 2010

- Mise à jour : 24 janvier 2020

- Licence : Les sources présentées sur cette page sont libres de droits et vous pouvez les utiliser à votre convenance. Par contre, la page de présentation constitue une œuvre intellectuelle protégée par les droits d'auteur. Copyright ® 2010 Olivier Girardot. Aucune reproduction, même partielle, ne peut être faite de ce site et de l'ensemble de son contenu : textes, documents, images, etc. sans l'autorisation expresse de l'auteur. Sinon vous encourez selon la loi jusqu'à trois ans de prison et jusqu'à 300 000 € de dommages et intérêts.

Sommaire

- I. Objectifs de cet article

- II. Hiérarchie et vue d'ensemble

- III. Création d'un nouveau modèle de type liste

- IV. Création d'un nouveau modèle de type tableau

- V. Autres modèles de base pour d'autres objectifs

- VI. Créer son modèle depuis la base

- VII. Conclusion

- VIII. Liens pour aller plus loin

- IX. Remerciements

I - Objectifs de cet article

L'objectif de cet article est simple, il doit vous permettre, à la fin, de savoir comment créer des modèles personnalisés pour tout type de vue en utilisant les classes de base de PySide/PyQt.

Ainsi vous saurez quelle classe de base choisir et dans quel cas l'utiliser, ou comment spécialiser de manière exhaustive la classe mère de tous les modèles QAbstractItemModel.

Note sur PySide vs PyQt : ces deux bindings Python sont compatibles en termes d'API, ainsi le code écrit en utilisant l'un devrait fonctionner sans encombre avec l'autre. Néanmoins PySide étant une implémentation plus libre (LGPL) que PyQt et possédant le soutien en termes de développement de Nokia (cf. About PySide), j'ai décidé de me baser sur cette implémentation et d'utiliser la documentation officielle de PySide pour documenter cet article.

C'est un choix personnel et le code présenté en exemple est utilisable pour les deux bibliothèques moyennant les imports adéquats.

II - Hiérarchie et vue d'ensemble

L'implémentation du patron de conception MVD (Modèle Vue Délégué) est pensé de la façon suivante pour Qt :

- le modèle permet de stocker les données, de les modifier et de les distribuer ;

- la vue permet d'afficher les données (en s'appuyant sur un modèle) ;

- enfin les délégués servent à faire la transition entre l'édition des données et leur rendu dans la vue.

Cette définition quelque peu académique peut s'illustrer comme ceci. Imaginez un tableau permettant d'éditer les données d'une base de données :

- la variable de type matrice stockant les données du tableau représente le modèle ;

- le tableau tel que le voit l'utilisateur est la vue ;

- enfin si l'on choisit, par exemple, des composants précis pour éditer certaines cases de ce tableau (ex. un menu déroulant), alors c'est un délégué qui se charge de gérer le cycle de vie de ce composant.

Dans l'esprit de ce patron, Qt a implémenté quelques vues et quelques modèles de base. Pour mieux se représenter le système, voici une petite représentation hiérarchique des classes modèles du framework :

Hiérarchie des modèles

Ainsi que les délégués :

Hiérarchie des délégués

Cet article se pose donc à la fois comme une introduction et comme un approfondissement, car il existe déjà beaucoup de modèles de base répondant aux problématiques les plus courantes. Vous pouvez, par ailleurs, voir la partie ci-après Autres modèles de base pour autres objectifs pour la liste exhaustive des modèles existants et de leur utilisation.

Maintenant, voyons comment étendre le système pour, par exemple, créer un nouveau modèle de type liste.

III - Création d'un nouveau modèle de type liste

La création d'un nouveau modèle de type liste passe par la création d'une classe ayant pour parent la classe QAbstractListModel.

Un bon code valant mieux que beaucoup d'explications, voici un exemple simple de création d'un nouveau modèle de type liste :

class SimpleListModel(QAbstractListModel):
# Constructeur
def __init__(self, mlist):
QAbstractListModel.__init__(self)
self._items = mlist

# redéfinition du calcul du nombre de lignes
def rowCount(self, parent = QModelIndex()):
return len(self._items)

# méthode principale gérant l'accès aux données via un index
# avec une notion de role
def data(self, index, role = Qt.DisplayRole):
# Role simple de récupération des données
if role == Qt.DisplayRole:
return self._items[index.row()]
# Role d'habillage des données
elif role == Qt.BackgroundRole:
if index.row() % 2 == 0:
return QColor(Qt.gray)
else:
return QColor(Qt.lightGray)
else:
return None

Cet exemple est volontairement peu loquace en commentaires, la version complète écrite par Robin Burchell est disponible dans le fichier des sources téléchargeables de l'article.

Pour expliciter un peu, dans cet exemple, on étend la classe PySide/PyQt de base QAbstractListModel pour ajouter des fonctionnalités tout en conservant les implémentations des fonctions communes à tous types de listes, comme le tri (méthode sort()).

La documentation officielle est parfaite en elle-même, aussi je vous invite à la consulter dès qu'un point vous demande confirmation (ex. pour le QAbstractListModel). Elle nous précise qu'une sous-classe bien élevée réimplémente la méthode headerData() précisant l'en-tête de la colonne.

De plus, si la vue n'est pas en lecture seule alors il est nécessaire d'implémenter les méthodes suivantes :

- setData(index, value[, role=Qt::EditRole]) : permet de définir les données à la position index dans la table pour le rôle (role) donné ;

- flags() : prenant en entrée l'index d'un élément et retournant l'indicateur Qt.ItemIsEditable ;

- si la liste est de taille variable, les méthodes insertRows() et removeRows() doivent aussi être implémentées en appelant, juste avant et après l'insertion ou la suppression, les méthodes : beginInsertRows() et endInsertRows() ou beginRemoveRows() et endRemoveRows().

Pour avoir plus d'informations sur les différentes méthodes pouvant être réimplémentées, il suffit de se reporter à la partie création d'un modèle depuis la base qui contient la liste exhaustive des méthodes disponibles.

Avec ce petit exemple, on a réussi à implémenter un modèle pouvant gérer de manière personnalisée une simple liste d'objets. La partie suivante va nous permettre de créer un modèle personnalisé de type tableau.

IV - Création d'un nouveau modèle de type tableau

La création d'un nouveau modèle de type tableau passe par la création d'une classe ayant pour parent la classe QabstractTableModel.

Pour approfondir un peu plus, je vais présenter deux exemples, un basique et un porté sur le formatage. Ces deux exemples ont été créés par Eliot auteur du blog SaltyCrane.com et ont été adaptés pour cet article avec son autorisation. Voici donc le premier exemple permettant de créer un modèle simple de tableau :

from PySide.QtCore import *
from PySide.QtGui import *
import sys

# données à représenter
my_array = [['00','01','02'],
['10','11','12'],
['20','21','22']]

def main():
app = QApplication(sys.argv)
w = MyWindow()
w.show()
sys.exit(app.exec_())

# création de la vue et du conteneur
class MyWindow(QWidget):
def __init__(self, *args):
QWidget.__init__(self, *args)

tablemodel = MyTableModel(my_array, self)
tableview = QTableView()
tableview.setModel(tablemodel)

layout = QVBoxLayout(self)
layout.addWidget(tableview)
self.setLayout(layout)

# création du modèle
class MyTableModel(QAbstractTableModel):
def __init__(self, datain, parent=None, *args):
QAbstractTableModel.__init__(self, parent, *args)
self.arraydata = datain

def rowCount(self, parent):
return len(self.arraydata)

def columnCount(self, parent):
return len(self.arraydata[0])

def data(self, index, role):
if not index.isValid():
return QVariant()
elif role != Qt.DisplayRole:
return QVariant()
return QVariant(self.arraydata[index.row()][index.column()])

if __name__ == "__main__":
main()

Cet exemple n'est pas si différent du modèle précédent, on a juste adapté la structure des données pour gérer un tableau bidimensionnel plutôt qu'une liste simple. Mais cet exemple nous permet de profiter des facilités de la syntaxe de Python notamment pour la méthode columnCount() où l'on récupère la taille d'une tranche du tableau.

L'exemple suivant accorde une attention particulière au formatage des données et présente un petit programme permettant avec PySide/PyQt d'afficher le résultat de la commande « dir c:/ » sous MS Windows :

import re
import os
import sys
from PySide.QtCore import *
from PySide.QtGui import *

def main():
app = QApplication(sys.argv)
w = MyWindow()
w.show()
sys.exit(app.exec_())

class MyWindow(QWidget):
def __init__(self, *args):
QWidget.__init__(self, *args)

# création du tableau
self.get_table_data()
table = self.createTable()

# mise en page
layout = QVBoxLayout()
layout.addWidget(table)
self.setLayout(layout)

def get_table_data(self):
stdouterr = os.popen4("dir c:\\")[1].read()
lines = stdouterr.splitlines()
lines = lines[5:]
lines = lines[:-2]
self.tabledata = [re.split(r"\s+", line, 4)
for line in lines]

def createTable(self):
# création de la vue
tv = QTableView()

# création du modèle
header = ['date', 'time', '', 'size', 'filename']
tm = MyTableModel(self.tabledata, header, self)
tv.setModel(tm)

# rajout d'une taille minimale
self.setMinimumSize(400, 300)

# et suppression du quadrillage
tv.setShowGrid(False)

# mise en place de la police
font = QFont("Courier New", 8)
tv.setFont(font)

# suppression de l'en-tête vertical
vh = tv.verticalHeader()
vh.setVisible(False)

# mise en place de l'en-tête horizontal
hh = tv.horizontalHeader()
hh.setStretchLastSection(True)

# redimensionnement de la largeur des colonnes
tv.resizeColumnsToContents()

# définition de la largeur des cellules
nrows = len(self.tabledata)
for row in xrange(nrows):
tv.setRowHeight(row, 18)

return tv

class MyTableModel(QAbstractTableModel):
def __init__(self, datain, headerdata, parent=None, *args):
QAbstractTableModel.__init__(self, parent, *args)
self.arraydata = datain
self.headerdata = headerdata

def rowCount(self, parent):
return len(self.arraydata)

def columnCount(self, parent):
return len(self.arraydata[0])

def data(self, index, role):
if not index.isValid():
return QVariant()
elif role != Qt.DisplayRole:
return QVariant()
return QVariant(self.arraydata[index.row()][index.column()])

def headerData(self, col, orientation, role):
if orientation == Qt.Horizontal and role == Qt.DisplayRole:
return QVariant(self.headerdata[col])
return QVariant()

if __name__ == "__main__":
main()

Ce code (conçu pour Python 2.4) permet de bien illustrer la mise en place des en-têtes de colonnes et d'un formatage précis. Pour ceux souhaitant l'exécuter avec Python 3, une version modifiée et mise au goût du jour est disponible dans le zip des sources de l'article.

On sait maintenant comment gérer des modèles de types liste et tableau, mais de nombreux autres modèles existent déjà nativement dans la bibliothèque Qt et la partie suivante va les présenter avec leurs particularités.

V - Autres modèles de base pour d'autres objectifs

Nous allons évoquer dans cette partie les différents modèles existants de base dans PySide/PyQt :

- le QStringListModel est utilisé pour stocker des objets QString dans une simple liste ;

- le QDirModel fournit des informations sur les fichiers et répertoires du système de fichiers local ;

- le QStandardItemModel permet de gérer des structures complexes d'objets (arbres, listes, tableaux…) en se servant du composant de base QStandardItem. Pour plus d'informations sur la manière de l'utiliser, consultez la documentation officielle qui présente de très bons exemples ;

- enfin les modèles QSqlQueryModel (lecture seule), QSqlTableModel et QSqlRelationalTableModel permettent de gérer plus facilement les liens avec une base de données en utilisant une approche modèle/vue. Vous pouvez trouver un exemple d'utilisation de ce type de modèles dans mon précédent tutoriel : Introduction et prise en main de PyQt. Chacun de ces modèles se distingue par sa gestion des particularités des bases de données, ainsi le modèle le plus précis le QSqlRelationalTableModel permet d'obtenir un tableau éditable où les clés étrangères sont directement gérées par des menus déroulants.

Modèle QSqlRelationalTable avec gestion d'une clé étrangère

VI - Créer son modèle depuis la base

Créer son propre modèle est un sujet assez avancé qui ne doit pas être abordé à la légère sous peine de perdre de longues heures en débogage. Commençons donc par nous fixer un objectif : nous allons créer à partir de la classe mère QAbstractItemModel un modèle nous permettant d'afficher des données dans une vue arborescente la QTreeView.

Notre objectif : la QTreeView

Nous allons, pour aborder cette tâche, nous baser sur une approche existante, celle de Hardcoded Software (cf. section liens), en la simplifiant un peu et en se concentrant sur le code et son explication, mais d'abord un petit point sur les concepts abordés dans ce code.

Tout d'abord, parlons des QModelIndex, leur rôle est de fournir une indexation des éléments de notre modèle conditionnée par la ligne, la colonne, et (potentiellement) le QModelIndex de son parent (d'où la création d'une hiérarchie et donc d'un arbre). Il est important de lire la description détaillée du QModelIndex et de la QAbstractItemModel pour bien savoir où mettre les pieds et ce que l'un attend de l'autre.

Le code ci-dessous va créer deux classes abstraites qui vont implémenter deux méthodes, très difficile à gérer index() et parent(), tout en restant suffisamment génériques pour pouvoir être utilisées dans n'importe quelle situation où l'on veut utiliser une vue en arbre. Pour expliquer les méthodes du code ci-dessous je parlerai donc d'indice en faisant référence au QModelIndex unique de chaque objet du modèle.

- La méthode index() : permet de récupérer l'indice servant d'accesseur à un objet du modèle. Chaque objet du modèle a un indice et cette méthode permet de le récupérer, moyennant le triplet unique (ligne, colonne, parent) ;

- La méthode parent() : permet de récupérer le parent de l'indice donné en paramètre ou un indice invalide si celui-ci n'a pas de parent.

Maintenant place au code :

# tout d'abord, créons un objet qui va jouer le rôle de nœud dans notre vue arborescente
# il est volontairement simple pour être adaptable à toutes situations
# avec :
#   - un parent ;
#   - un fils ;
#   - et un enregistrement associé (la valeur du nœud).
class TreeNode(object):
def __init__(self, parent, row):
self.parent = parent
self.row = row
self.subnodes = self._getChildren()

def _getChildren(self):
raise NotImplementedError()

# Nous allons créer une classe mère de nos futurs modèles
# pour limiter la complexité et gérer ce que l'on peut gérer en amont :
class TreeModel(QAbstractItemModel):
def __init__(self):
QAbstractItemModel.__init__(self)
self.rootNodes = self._getRootNodes()

# à implémenter par la future classe fille
def _getRootNodes(self):
raise NotImplementedError()

# cette méthode héritée de QAbstractItemModel doit retourner
# l'indice de l'enregistrement en entrée moyennant le parent (un QModelIndex)
# cf. paragraphe suivant pour plus d'explications.
def index(self, row, column, parent):
# si l'indice du parent est invalide
if not parent.isValid():
return self.createIndex(row, column, self.rootNodes[row])
parentNode = parent.internalPointer()
return self.createIndex(row, column, parentNode.subnodes[row])

# cette méthode héritée de QAbstractItemModel doit retourner
# l'indice du parent de l'indice donné en paramètre
# ou un indice invalide (QModelIndex()) si le nœud n'a pas de parent
# ou si la requête est incorrecte
# c.f. paragraphe suivant pour plus d'explications.
def parent(self, index):
if not index.isValid():
return QModelIndex()
# on récupère l'objet sous-jacent avec la méthode internalPointer de l'indice
node = index.internalPointer()
if node.parent is None:
return QModelIndex()
else:
# si tout est valide alors on crée l'indice associé pointant vers le parent
return self.createIndex(node.parent.row, 0, node.parent)

def reset(self):
self.rootNodes = self._getRootNodes()
QAbstractItemModel.reset(self)

def rowCount(self, parent):
if not parent.isValid():
return len(self.rootNodes)
node = parent.internalPointer()
return len(node.subnodes)

Avec l'implémentation de ces deux méthodes complexes (index() et parent()), on simplifie grandement notre modèle qui est maintenant utilisable :

class NamedElement(object): # notre structure interne pour gérer les objets
def __init__(self, name, subelements):
self.name = name
self.subelements = subelements

# notre nœud concret implémentant getChildren
class NamedNode(TreeNode):
def __init__(self, ref, parent, row):
self.ref = ref
TreeNode.__init__(self, parent, row)

# renvoie la liste des nœuds fils en utilisant la liste subelements de
# notre objet (interne) NamedElement
def _getChildren(self):
return [NamedNode(elem, self, index)
for index, elem in enumerate(self.ref.subelements)]

# et enfin notre modèle avec
class NamesModel(TreeModel):
def __init__(self, rootElements):
self.rootElements = rootElements
TreeModel.__init__(self)

def _getRootNodes(self):
return [NamedNode(elem, None, index)
for index, elem in enumerate(self.rootElements)]

def columnCount(self, parent):
return 1

# permet de récupérer les données liées à un indice et un rôle.
# ces données peuvent ainsi varier selon le rôle.
def data(self, index, role):
if not index.isValid():
return None
node = index.internalPointer()
if role == Qt.DisplayRole and index.column() == 0:
return node.ref.name
return None

def headerData(self, section, orientation, role):
if orientation == Qt.Horizontal and role == Qt.DisplayRole \
and section == 0:
return 'Name'
return None

Ce n'est pas du code prêt à l'emploi pour aller en production, mais il est suffisamment basique pour permettre de bien comprendre les concepts en jeu. Pour sa part Hardcoded Software a distribué, en plus de ce code, une version libre (sous licence BSD) du code qu'ils utilisent avec des améliorations de conception et de performance disponible ici.

J'espère que cet exemple vous aura permis de bien appréhender la création d'un modèle à partir de la base. Bien sûr, ce code se base sur une représentation orientée arbre, mais je trouvais important d'aborder la question de la QTreeView. En effet, il est plus probable que vous partiez du QAbstractItemModel pour créer une vue arborescente, les modèles QAbstractListModel et QAbstractTableModel étant plus adaptés pour d'autres types de vues.

Cet exemple permet aussi de bien saisir les limitations du patron MVD, car, si on est parti du principe que modèles et vues étaient combinables indépendamment, quand il s'agit d'un modèle d'arbre, l'implémentation devient rapidement spécifique et complexe.

La complexité autour de la création de son propre modèle a d'ailleurs poussé un des développeurs de Qt à construire un petit outil de vérification des modèles : ModelTest. Cet outil en C++ n'est pas disponible pour PySide/PyQt, mais n'hésitez pas à me contacter si ce défi vous tente, je serais ravi d'aider.

VII - Conclusion

En conclusion, on a pu voir de nombreux modèles existant au sein de PySide/PyQt qui permettent de réaliser beaucoup sans avoir à recoder son propre modèle, ainsi que comment étendre certains modèles, voire créer son propre modèle en partant de la base.

Compte tenu des possibilités du système, il reste assez rare de créer son propre modèle, mais j'espère que cet article vous permettra de vous lancer sans appréhension dans cette tâche, n'hésitez pas d'ailleurs à contribuer en retour, en communiquant ces modèles à la communauté pour permettre à chacun d'en profiter et d'apprendre.

VIII - Liens pour aller plus loin

- Tutoriel de Robin Burchell sur la création d'un modèle de type Liste

- Documentation officielle de Qt sur le QAbstractItemModel

- Documentation officielle sur les bonnes pratiques pour étendre les classes modèles ;

- Blog de SaltyCrane avec de nombreux articles autour de PyQt

- Création d'un modèle à partir de QAbstractItemModel pour une QTreeView par Hardcodefd Software

- Discussion sur les interfaces graphiques pour Python sur developpez.net

IX - Remerciements

Je tiens à remercier Thibaut Cuvelier responsable Qt pour son aide dans la rédaction de cet article, Robin Burchell et Eliot - SaltyCrane pour leur aide et leur expertise autour PySide/PyQt et enfin Jacques Thery pour sa relecture attentive.