I. L'article original

Qt Quarterly est une revue trimestrielle électronique proposée par Nokia à destination des développeurs et utilisateurs de Qt. Vous pouvez trouver les versions originales.

Nokia, Qt, Qt Quarterly et leurs logos sont des marques déposées de Nokia Corporation en Finlande et/ou dans les autres pays. Les autres marques déposées sont détenues par leurs propriétaires respectifs.

Cet article est la traduction de l'article Designing Custom Controls with PyQt de David Boddie paru dans la Qt Quarterly Issue 26.

Cet article est une traduction de l'un des tutoriels en anglais écrits par Nokia Corporation and/or its subsidiary(-ies), inclus dans la documentation de Qt. Les éventuels problèmes résultant d'une mauvaise traduction ne sont pas imputables à Nokia.

II. Introduction

Quand Qt Designer a été conçu et réécrit pour Qt4, l'un des buts principaux était de faciliter l'ajout de contrôles personnalisés aux widgets Qt standards. Bien que les créateurs de Qt Designer visaient les développeurs C++ quand ils ont implémenté cette fonctionnalité, la capacité d'extensibilité n'est pas limitée à ce seul langage – n'importe quel langage disposant d'un binding Qt utilisant le système de métaobjets peut se joindre à la fête.

Image non disponible

Dans cet article, on va montrer comment utiliser PyQt pour créer des widgets utilisables dans Qt Designer, décrire le processus de création de plug-ins et pointer les mécanismes qui exposent le système de métaobjets de Qt à Python.

III. Créer un widget personnalisé

PyQt expose l'API Qt à Python d'une manière assez transparente, rendant la tâche création de widgets personnalisés familière aux développeurs C++. Les nouveaux widgets héritent de QWidget de manière classique comme nous pouvons le voir dans cet exemple pleinement fonctionnel qui permet de saisir des valeurs de latitude et longitude :

 
Sélectionnez
class GeoLocationWidget(QWidget):

  __pyqtSignals__ = ("latitudeChanged(double)",
                     "longitudeChanged(double)")

  def __init__(self, parent = None):

     QWidget.__init__(self, parent)

     latitudeLabel = QLabel(self.tr("Latitude:"))
     self.latitudeSpinBox = QDoubleSpinBox()
     self.latitudeSpinBox.setRange(-90.0, 90.0)
     self.latitudeSpinBox.setDecimals(5)

     longitudeLabel = QLabel(self.tr("Longitude:"))
     self.longitudeSpinBox = QDoubleSpinBox()
     self.longitudeSpinBox.setRange(-180.0, 180.0)
     self.longitudeSpinBox.setDecimals(5)

     self.connect(self.latitudeSpinBox,
         SIGNAL("valueChanged(double)"),
         self, SIGNAL("latitudeChanged(double)"))
     self.connect(self.longitudeSpinBox,
         SIGNAL("valueChanged(double)"),
         self, SIGNAL("longitudeChanged(double)"))

     layout = QGridLayout(self)
     layout.addWidget(latitudeLabel, 0, 0)
     layout.addWidget(self.latitudeSpinBox, 0, 1)
     layout.addWidget(longitudeLabel, 1, 0)
     layout.addWidget(self.longitudeSpinBox, 1, 1)

Deux choses intéressantes à noter. Premièrement, contrairement à Qt Jambi et Qt Script, la syntaxe de connexion des signaux et slots suit le modèle utilisé en C++. Deuxièmement, la déclaration des signaux au début de la définition de la classe n'est pas strictement requise par PyQt – n'importe quel signal, peu importe son nom, peut être émis sans déclaration préalable – on y reviendra plus tard.

Bien que le widget soit utile en l'état, il n'expose aucune propriétés de haut niveau. De la même manière qu'en C++, on les définit en créant des méthodes d'accession et de manipulation à l'intérieur de la définition de la classe et on utilise un petit tour de passe-passe pour les exposer au système de métaobjets de Qt.

Voici les méthodes d'accession et de manipulation pour la propriété latitude :

 
Sélectionnez
  def latitude(self):
     return self.latitudeSpinBox.value()

  @pyqtSignature("setLatitude(double)")

  def setLatitude(self, latitude):

     if latitude != self.latitudeSpinBox.value():
         self.latitudeSpinBox.setValue(latitude)
         self.emit(SIGNAL("latitudeChanged(double)"),
                   latitude)

Comme dans une classe C++, on définit des méthodes latitude() et setLatitude() pour la propriété. La déclaration précédent la méthode d'affectation (setLatitude()) est un décorateur Python spécial qui indique à Qt que setLatitude() est un slot acceptant des flottants en double précision. En général, ce genre de déclaration n'est pas obligatoire avec PyQt – n'importe quelle fonction ou méthode peut être utilisée comme un slot – mais cela facilite l'interaction avec Qt Designer pour la suite.

La fonction pyqtProperty() est utilisée pour déclarer la propriété à Qt.

 
Sélectionnez
  latitude = pyqtProperty("double", latitude, setLatitude)

Le nom latitude est lié à la propriété résultante et la méthode que nous avons définie plus haut n'est plus directement accessible. Si nous avions voulu la laisser disponible, nous aurions utilisé des noms différents pour la méthode de récupération (latitude()).

IV. Produire un plug-in

Le processus de création d'un widget est très similaire à celui en C++. Chaque classe de widget personnalisé est représentée par une classe de plug-in qui en crée une instance (comme décrit dans le Qt Quarterly 16). La différence principale est que l'interface de plug-in de Qt Designer est utilisée avec des classes de plug-in spécifiques à PyQt.

Image non disponible

Pour rester concis, on ne va regarder que la définition de la classe GeoLocationPlugin :

 
Sélectionnez
class GeoLocationPlugin(QPyDesignerCustomWidgetPlugin):

   def __init__(self, parent = None):

      QPyDesignerCustomWidgetPlugin.__init__(self)
      self.initialized = False

Le code pour initialiser une instance de la classe devrait être familier aux développeurs C++ de plug-ins de widgets.

La méthode initialize() est appelée par Qt Designer après le chargement du plug-in. Les plug-ins profitent de ce moment pour installer leurs extensions dans l'éditeur de formulaire :

 
Sélectionnez
   def initialize(self, formEditor):

      if self.initialized:
          return

      manager = formEditor.extensionManager()
      if manager:
          self.factory = \
              GeoLocationTaskMenuFactory(manager)
          manager.registerExtensions(
              self.factory,
              "com.trolltech.Qt.Designer.TaskMenu")

      self.initialized = True

Pour ce plug-in, on installe une extension de menu de tâches pour laisser l'utilisateur configurer le widget dans le formulaire par le menu contextuel. Ceci est effectué en créant et enregistrant une fabrique de menus de tâches auprès du gestionnaire d'extensions de l'éditeur de formulaire.

Il y a un bon nombre d'autres méthodes qui doivent être réimplémentées dans la classe de plug-in, mais les plus importantes créent des widgets individuels et fournissent des informations sur le nom et l'emplacement de la classe de widgets :

 
Sélectionnez
   def createWidget(self, parent):
      return GeoLocationWidget(parent)

   def name(self):
      return "GeoLocationWidget"

   def includeFile(self):
      return "QQ_Widgets.geolocationwidget"

Pour les widgets créées avec Python, la méthode includeFile() retourne le nom du module qui fournit la classe nommée. Dans ce cas, le module réside dans le paquet QQ_Widgets.

V. Créer un menu

Bien qu'il soit facile d'éditer les propriétés du widget personnalisé dans l'éditeur de propriétés de Qt Designer, on va créer une boîte de dialogue que l'utilisateur peut ouvrir pour les incorporer d'une manière plus naturelle. Cette boîte sera disponible dans une nouvelle entrée dans le menu contextuel du formulaire quand l'utilisateur a sélectionné le widget personnalisé.

Cette boîte de dialogue n'est pas très spéciale en elle-même. Elle opère sur un widget qui lui est passé, en fournissant un autre GeoLocationWidget que l'utilisateur peut modifier et prévisualiser.

 
Sélectionnez
class GeoLocationDialog(QDialog):

   def __init__(self, widget, parent = None):

      QDialog.__init__(self, parent)

      self.widget = widget

      self.previewWidget = GeoLocationWidget()
      self.previewWidget.latitude = widget.latitude
      self.previewWidget.longitude = widget.longitude

      buttonBox = QDialogButtonBox()
      okButton = buttonBox.addButton(buttonBox.Ok)
      cancelButton = \
         buttonBox.addButton(buttonBox.Cancel)

      self.connect(okButton, SIGNAL("clicked()"),
                   self.updateWidget)
      self.connect(cancelButton, SIGNAL("clicked()"),
                   self, SLOT("reject()"))

      layout = QGridLayout()
      layout.addWidget(self.previewWidget, 1, 0, 1, 2)
      layout.addWidget(buttonBox, 2, 0, 1, 2)
      self.setLayout(layout)

      self.setWindowTitle(self.tr("Update Location"))

La partie intéressante est le slot où les changements sur le widget de prévisualisation sont effectivement visibles pour le widget sur le formulaire :

 
Sélectionnez
   def updateWidget(self):

      formWindow = \
        QDesignerFormWindowInterface.findFormWindow(
            self.widget)

      if formWindow:
          formWindow.cursor().setProperty("latitude",
              QVariant(self.previewWidget.latitude))
          formWindow.cursor().setProperty("longitude",
              QVariant(self.previewWidget.longitude))

      self.accept()

Ici, on effectue des modifications par le biais de l'interface de la fenêtre de formulaire pour s'assurer que les changements de l'utilisateur sont enregistrés. De cette manière, si l'utilisateur décide qu'il a fait une erreur, il peut simplement défaire les changements comme d'habitude.

La boîte de dialogue est appelée depuis une extension personnalisée du menu de tâches - un objet qui connecte une entrée du menu à du code pour ouvrir la boîte. Une fois créé, il fournit une action Update Location... que l'éditeur de formulaire ajoute à son menu contextuel. On connecte cette action à un slot pour que l'on puisse ouvrir la boîte de dialogue quand l'utilisateur sélectionne l'entrée correspondante dans le menu.

 
Sélectionnez
class GeoLocationMenuEntry(QPyDesignerTaskMenuExtension):

  def __init__(self, widget, parent):

      QPyDesignerTaskMenuExtension.__init__(self, parent)

      self.widget = widget
      self.editStateAction = QAction(
          self.tr("Update Location..."), self)
      self.connect(self.editStateAction,
          SIGNAL("triggered()"), self.updateLocation)

  def preferredEditAction(self):
      return self.editStateAction

  def taskActions(self):
      return [self.editStateAction]

  def updateLocation(self):
      dialog = GeoLocationDialog(self.widget)
      dialog.exec_()

Le slot updateLocation() n'est pas décoré dans ce cas : puisqu'on effectue la connexion, il n'y a pas besoin de le déclarer. Un autre raccourci est l'utilisation d'une liste dans la méthode taskActions().

Chaque extension du menu de tâches est créée par la fabrique de menus de tâches que l'on a enregistrée avec la méthode initialize() du plug-in de widget personnalisé. Quand l'utilisateur ouvre un menu contextuel sur un widget personnalisé, Qt Designer crée une nouvelle extension du menu de tâches en appelant la méthode createExtension() de la fabrique.

 
Sélectionnez
class GeoLocationTaskMenuFactory(QExtensionFactory):

  def __init__(self, parent = None):

      QExtensionFactory.__init__(self, parent)

  def createExtension(self, obj, iid, parent):

      if iid != "com.trolltech.Qt.Designer.TaskMenu":
          return None

      if isinstance(obj, GeoLocationWidget):
          return GeoLocationMenuEntry(obj, parent)

      return None

La méthode createExtension() vérifie que l'extension requise possède la bonne interface pour une telle extension et n'en retourne une instance que si le widget qui la requiert est un GeoLocationWidget.

Avec la fabrique qui crée des objets GeoLocationTaskMenuEntry à la demande, la classe GeoLocationDialog peut être utilisée pour éditer des instances de GeoLocationWidget une fois installée.

VI. Mettre les choses en place

Sur des systèmes où Qt Designer est capable d'utiliser des plug-ins tiers et où PyQt inclut le module QtDesigner, il devrait être possible d'utiliser des plug-ins écrits avec PyQt. À l'inverse des plug-ins C++, ceux écrits avec Python n'ont aucun besoin d'être compilés ou préparés d'une autre manière avant installation, on peut simplement copier les sources à l'emplacement approprié pour que Qt Designer les trouve.

Les modules Python qui fournissent le plug-in et l'extension du menu de tâches sont généralement stockés comme des fichiers dans le même dossier. Les plug-ins Qt Designer écrits en C++ sont généralement dans un sous-dossier designer dans un des répertoires décrits par QLibraryInfo::PluginPath. La convention pour les plug-ins Python est de créer un dossier python à côté des plug-ins C++ et de les y stocker.

Les widgets eux-mêmes doivent être placés dans un endroit standardisé pour que l'interpréteur Python puisse les trouver. Souvent, il est pratique de les mettre dans le dossier site-packages de l'installation de Python, puisqu'ils vont être utilisés par les applications qui utilisent des formulaires les contenant. La procédure d'installation nécessite la création d'un fichier setup.py, qui ne va pas être discutée ici - le code source accompagnant cet article contient un fichier qui peut être utilisé comme point de départ pour d'autres projets.

Alternatives à l'installation, les variables d'environnement peuvent être définies pour définir l'emplacement des plug-ins et des widgets personnalisés. Les développeurs familiers avec Python savent que PYTHONPATH peut être utilisée pour ajouter de nouveaux répertoires à la liste des endroits connus pour les modules et paquets. De même, PYQTDESIGNERPATH peut être utilisée pour ajouter les emplacements des modules Python contenant des plug-ins et des extensions pour Qt Designer.

Une fois le plug-in, l'extension et le widget personnalisé installés ou leur emplacement défini dans les variables d'environnement, on peut lancer Qt Designer et utiliser le widget personnalisé - il devrait être disponible dans la section Qt Quarterly Examples de la boîte de widgets.

Les formulaires qui contiennent des widgets personnalisés peuvent être transformés par pyuic4 en fichiers Python d'une manière similaire à uic en C++. Le code d'importation des widgets personnalisés est généré avec le code de création du formulaire, les développeurs doivent ainsi simplement s'assurer que les modules les contenant sont disponibles quand leurs applications sont lancées.

VII. En coulisse

Lors de l'écriture de la classe GeoLocationWidget, on a utilisé trois fonctionnalités qui ne sont pas toujours nécessaires pour écrire des widgets PyQt :

  • on a déclaré les signaux à l'avance, au lieu de simplement les émettre quand cela est nécessaire ;
  • on a décoré certaines méthodes avec @pyqtSignature() pour indiquer qu'il s'agit de slots qui acceptent certains types de paramètres ;
  • on a créé des propriétés avec pyqtProperty() pour les exposer à l'éditeur de propriétés de Qt Designer.

Puisque l'utilisation de chaque fonctionnalité rend des informations disponibles aux autres composants de Qt, les widgets écrits de cette manière peuvent être fournis à d'autres systèmes de plug-in basés sur QObject, pour autant qu'il y ait moyen d'exécuter leur code source Python.

L'utilisation des déclarations de signaux et de propriétés est aussi utile généralement aux programmeurs Python. En effet, les propriétés définies de cette manière se comportent tout comme des propriétés Python normales. Les déclarations de slots peuvent être utilisées pour aider à les connecter à des slots dans une classe dérivant d'un formulaire.

 
Sélectionnez
   @pyqtSignature("on_pushButton_clicked()")
   def on_pushButton_clicked(self):
      ...
      self.listWidget.addItem(
            u"%i\\xb0 %i' %i\\" N, " % self.dms(lat) + \
            u"%i\\xb0 %i' %i\\" E" % self.dms(lng))

Dans ce cas, le décorateur s'assure que la méthode n'est appelée que lorsque un signal clicked() d'un bouton est émis, mais pas quand le signal clicked(bool) est émis.

VIII. Aller plus loin

Les instructions pour écrire et installer les plug-ins de widget décrites dans cet article sont incluses dans l'archive d'exemples. On encourage à les essayer et à appliquer les mêmes techniques à d'autres widgets.

De plus amples instructions sur les plug-ins Qt Designer, PyQt et les outils Python qui les font fonctionner ensemble sont disponibles sur le site de Riverbank Computing.

Merci à Claude Leloup pour sa relecture orthographique !