Embarquer Python dans les applications Qt

| Florian Link | Qt Quarterly |

Embarquer un langage de script dans une application C++ devient maintenant très courant. À côté des produits principaux, comme Microsoft Office ou Macromedia Director, il y a une tendance croissante que les applications spécialisées offrent des langages de scripts pour les utilisateurs.

Commentez

- Titre : Embarquer Python dans les applications Qt

- Auteurs : Florian Link, Qt Quarterly

- Parution : 28 mai 2012

- Mise à jour : 17 juin 2012

- Licence : Copyright ® 2007 Florian Link. 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. L'article original

- II. Introduction

- III. Les avantages du scripting

- IV. À propos de PythonQt

- V. Mise en route

- VI. Création d'une API de scripting

- VI-A. À propos de Python

- VII. Scripting d'une IHM

- VIII. Module PythonQt

- IX. Décorateurs et wrapper C++

- X. Autres fonctionnalités

- XI. Emplois futurs

- XII. Conclusion

- XIII. Remerciements

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 Embedding Python into Qt Applications écrit par Florian Link paru dans la Qt Quarterly Issue 23.

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

II - Introduction

Depuis plusieurs années, seules deux solutions principales coexistent pour embarquer des langages de script dans les applications Qt : QSA (JavaScript 2.0) de Nokia et PyQt (Python) de Riverbank Computing. L'article Scripting Qt du numéro 10 de Qt Quarterly donne une bonne vue de QSA, PyQt et d'autres solutions en action.

De nouvelles solutions sont apparues depuis que cet article a été écrit et, aujourd'hui, deux nouveaux bindings sont apparus, qui seront présentés ici :

- QtScript - un interpréteur normalisé à l'ECMA avec les bindings Qt - est livré dans Qt 4.3 ;

- PythonQt - utilisé par MeVisLab - est un « binding » dynamique de l'interpréteur Python.

Tandis que QtScript et PythonQt facilitent l'intégration de scripts dans des applications Qt existantes, cet article va se focaliser sur le binding Qt, laissant l'approfondissement de QtScript à un prochain article.

III - Les avantages du scripting

Rendre une application C++ scriptable a plusieurs avantages :

- une application bien construite peut donner un point d'accès facile pour les novices et les utilisateurs avancés aux différentes fonctionnalités ;

- les applications peuvent être facilement étendues sans demander aux utilisateurs de rentrer dans les profondeurs des connaissances en C++ ;

- le scripting facilite la création de macros et de traitements batch ;

- les tests peuvent être automatisés avec le scripting ;

- le scripting est multiplateforme, les scripts peuvent donc fonctionner sur toutes les plateformes sur lesquelles les applications s'exécutent ;

- le scripting peut rendre plus rapide le prototypage : par exemple, l'équipe de support a besoin d'ajouter une fonctionnalité/solution de contournement, un script sera plus rapide à mettre en place qu'une solution C++ avec redéploiement.

Les API de scripting peuvent aller d'une simple interface qui autorise certaines activités telles que les processus en lots ou des tâches applicatives aux applications à part entière qui autorisent la personnalisation des menus et boîtes de dialogues, mais aussi pour accéder aux fonctionnalités du coeur de l'application (par exemple, le JavaScript pour les navigateurs Web).

Quand on évalue les solutions de scripting pour les applications Qt, les fonctionnalités suivantes sont considérées comme avantageuses :

- intégration facile dans les applications Qt existantes ;

- langage de script connu, les nouveaux utilisateurs ne doivent donc pas apprendre un nouveau langage ;

- bonne intégration dans la bibliothèque Qt  : il suffit donc de connaître les signaux, slots et les propriétés pour les utiliser ;

- support de la connexion des types de données entre le langage de script et Qt, idéalement le support de tous les types de QVariant ;

- support du débogage - quand les scripts deviennent longs, le débogage devient une fonctionnalité cruciale.

IV - À propos de PythonQt

Contrairement à PyQt et QtJambi, PythonQt n'est pas prévu pour fournir un support aux développeurs qui écrivent des applications autonomes. À la place, il fournit des installations pour embarquer un interpréteur Python et se focalise sur la mise à disposition de parties de l'application à l'environnement Python.

PythonQt rend extensible l'utilisation du système des métaobjets de Qt. Ainsi, PythonQt peut scripter dynamiquement un QObject sans aucune connaissance de celui-ci, en utilisant les informations fournies par QMetaObject (défini utilisant l'outil moc de Qt des applications C++). Cette approche dynamique autorise différents « binding » scripts d'être embarqués dans la même application, chacune d'elles pouvant utiliser la même API de scripting ; par exemple, JavaScript (QSA ou QtScript) et Python.

Les sections suivantes résument certaines fonctionnalités de PythonQt. Pour une description détaillée des fonctionnalités de l'outil et pour des exemples plus complexes, vous pouvez vous rendre sur le site web du projet.

V - Mise en route

L'exemple suivant montre les étapes pour intégrer PythonQt avec une application Qt.

#include "PythonQt.h"
#include <QApplication>

int main(int argc, char **argv)
{
QApplication qapp(argc, argv);

PythonQt::init();
PythonQtObjectPtr mainModule =
PythonQt::self()->getMainModule();
QVariant result = mainModule.evalScript(
mainModule, "19*2+4", Py_eval_input);
return 0;
}

Premièrement, on initialise un singleton PythonQt, qui gère l'interpréteur Python. On obtient ensuite un pointeur intelligent (PythonQtObjectPtr) vers le module Python principal __main__ (l'endroit où le script sera exécuté) et évalue le code Python présent dans le module.

La variable result, dans ce cas, contient 42, évaluée par Python.

VI - Création d'une API de scripting

L'art de scripter les applications et de trouver le niveau de détail des API de son application qui contentera le mieux les utilisateurs, développeurs et l'équipe de support. Essentiellement, on invente un langage spécifique au domaine de son application qui facilite l'accès à certaines fonctionnalités, sans besoin d'un compilateur C++.

Un cas d'usage typique de PythonQt est d'exposer un simple objet de l'application à Python et ensuite de laisser les utilisateurs, développeurs et équipe de support créer leurs scripts pour modifier l'aspect de leur application via un script.

En général, on crée un nouveau QObject dérivé d'une classe de l'API et on l'utilise comme un adaptateur dans différentes autres classes de l'application. On peut aussi exposer directement plusieurs QObject existants dans l'application, mais on expose normalement plutôt des petites parties pour les scripts utilisateur, ce qui force à garder les interfaces des classes exposées stables - autrement, les scripts utilisateur pourraient ne plus fonctionner ou se comporter de façon inattendue.

La création d'API spécialisées est souvent la meilleure solution, tout en gardant une interface stable et documentée pour les parties de l'application pouvant être scriptées. PythonQt supporte tous les types QVariant, on peut donc créer une API riche qui retourne des types simples comme les QDateTime et QPixmap, ou également les types hiérarchiques QVariantMap contenant des valeurs QVariant.

VI-A - À propos de Python

Python (http://python.org/) est un langage de programmation orienté objet avec une communauté d'utilisateurs croissante et un certain nombre de modules standard.

Python a été créé pour être facilement « extensible et embarqué ». Les bibliothèques sont écrites en C et C++ et peuvent être enveloppées pour une utilisation par des programmes Python, l'interpréteur peut être embarqué dans des applications pour fournir les services de scripts.

Il existe plusieurs applications connues qui embarquent le support de Python avec les scripts :

- Bender (http://www.blender.org/) ;

- Autodesk Maya (http://www.autodesk.fr/) ;

- OpenOffice.org (http://www.openoffice.org/fr/) ;

- MeVisLab (http://www.mevislab.de/) ;

- Scribus (http://www.scribus.net/canvas/Scribus) ;

- EVE Online (http://www.eveonline.com/).

VII - Scripting d'une IHM

Voici maintenant un exemple simple qui va créer une petite interface utilisateur Qt contenant un groupe de widgets qui sera exposé à Python à travers le nom « box ».

Le code C++ pour définir l'interface utilisateur ressemble à ceci :

QGroupBox *box = new QGroupBox;
QTextBrowser *browser = new QTextBrowser(box);
QLineEdit *edit = new QLineEdit(box);
QPushButton *button = new QPushButton(box);

button->setObjectName("button1");
edit->setObjectName("edit");
browser->setObjectName("browser");

mainModule.addObject("box", box);

Maintenant, on crée le script Python qui va utiliser PythonQt pour accéder à l'IHM. Premièrement, on peut voir qu'on accède facilement aux propriétés Qt et aux objets enfants :

# Paramétrage du titre de la boîte de dialogue via sa propriété
box.title = 'PythonQt Example'

# Paramétrage du contenu HTML du QTextBrowser
box.browser.html = 'Hello <b>Qt</b>!'

# Paramétrage du titre du bouton
box.button1.text = 'Append Text'

# Paramétrage du contenu de la zone de texte
box.edit.text = '42'

On note que chaque chaîne de caractères Python est automatiquement convertie en une QString quand elle est assignée à une propriété QString.

Les signaux émanant des objets C++ peuvent être connectés aux fonctions Python. On définit une fonction classique et on connecte le signal clicked() du bouton et le signal returnPressed() de la zone de texte à cette fonction :

def appendLine():
box.browser.append(box.edit.text)

box.button1.connect('clicked()', appendLine)
box.edit.connect('returnPressed()', appendLine)

Le groupe de widgets est affiché comme un widget de haut niveau de façon habituelle :

box.show()

Pour évaluer le script, on doit appeler une fonction spéciale dans le module principal. Ici, on inclut le script comme un fichier dans le système de ressources Qt, on doit donc ajouter le préfixe habituel « : » :

mainModule.evalFile(":GettingStarted.py");

Maintenant, lorsqu'on appuie sur la touche Entrée dans la zone de texte ou qu'on clique sur le bouton, le texte venant de la zone de texte est ajouté au texte présent dans la zone correspondant au QTextBrowser en utilisant la fonction python appendLine().

VIII - Module PythonQt

Les scripts peuvent faire plus de choses que le traitement de données, les connexions et l'appel de fonctions. Par exemple, il peut être nécessaire pour les scripts de créer de nouveaux objets d'un certain type pour les fournir à l'application.

Pour satisfaire ce besoin, PythonQt contient un module Python nommé PythonQt qui peut être utilisé pour accéder aux constructeurs et aux membres statiques de tous les objets connus. Ceci inclut les types QVariant et l'espace de noms Qt.

Voici un exemple d'utilisation du module PythonQt :

from PythonQt import *

# Accès aux valeurs des énumérations de l'espace de noms Qt.
print Qt.AlignLeft

# Accès à une méthode statique de QDate.
print QDate.currentDate()

# Création d'un objet QSize.
a = QSize(1,2)

IX - Décorateurs et wrapper C++

Un problème majeur qui survient avec cette approche dynamique de PythonQt est que seuls les slots sont appelés en venant de Python. Il n'y a pas d'autres moyens de réaliser du script dynamique dans les méthodes C++, car le système de métaobjets de Qt (moc) ne peut pas générer des informations d'exécution dans ce cas.

PythonQt introduit le concept de « slot décorateur », qui permet de réutiliser le mécanisme pour invoquer dynamiquement les slots Qt et pour supporter les constructeurs, destructeurs, méthodes statiques et méthodes non statiques avec un appel direct. La première idée est d'introduire un nouvel objet dérivé de QObject « objets décorateurs » (à ne pas confondre avec les décorateurs Python) qui suit la convention de nommage des « slots décorateurs » et qui sont utilisés par Python pour réaliser, par exemple, un appel normal au constructeur.

Ceci autorise les classes C++ et les classes dérivées de QObject d'être étendues avec des attributs additionnels créés dans la hiérarchie de la classe.

L'exemple suivant montre quelques exemples de décorateurs :

class PyExampleDecorators : public QObject
{
Q_OBJECT

public slots:
QVariant new_QSize(const QPoint &p)
{ return QSize(p.x(), p.y()); }

QPushButton *new_QPushButton(const QString &text,
QWidget *parent = 0)
{ return new QPushButton(text, parent); }

QWidget *static_QWidget_mouseGrabber()
{ return QWidget::mouseGrabber(); }

void move(QWidget *w, const QPoint &p) { w->move(p); }
void move(QWidget *w, int x, int y) { w->move(x, y); }
};

Après avoir enregistré l'exemple précédent avec PythonQt (via PythonQt::addDecorators()), PythonQt fournit maintenant :

- un constructeur retournant un type QVariant et prenant un objet QPoint en paramètre ;

- un constructeur pour QPushButton qui prend en paramètres une chaîne de caractères et un widget parent ;

- une nouvelle méthode statique mouseGrabber() retournant un QWidget ;

- un nouveau slot pour QWidget rendant move() appelable (move() n'est pas un slot dans QWidget) ;

- un nouveau slot pour QWidget, surchargeant la méthode move() précédente.

L'approche du décorateur est assez puissante, vu que l'on autorise l'ajout de nouvelles fonctionnalités n'importe où dans la hiérarchie des classes, sans devoir convertir les arguments manuellement (c'est-à-dire convertir les arguments du constructeur de Python vers Qt). La réalisation d'une méthode non slot disponible avec PythonQt revient à faire une déclaration d'une ligne qui aura juste à renvoyer l'appel vers C++.

X - Autres fonctionnalités

PythonQt fournit également un nombre de fonctionnalités « avancées » qui ne seront pas couvertes ici. Les plus intéressantes sont les suivantes :

- modification d'objets C++ dérivés d'objets non QObject ;

- support des types QVariant personnalisés ;

- interface pour créer notre propre remplacement d'importation : par exemple, les scripts Python peuvent être signés/vérifiés avant d'être exécutés.

XI - Emplois futurs

PythonQt a été écrit pour rendre MeVisLab scriptable, ceci donne maintenant un niveau satisfaisant de stabilité. Il est maintenant facile d'embarquer le support de script Python dans une application Qt existante mais ne reprend pas la couverture globale de l'API Qt que fournit PyQt.

Je tiens à remercier mon entreprise MeVis Research, qui m'a autorisé la réalisation de PythonQt comme un projet open source sur SourceForge, avec une licence GNU GPL. Vous pouvez vous rendre sur la page d'accueil du projet sur SourceForge pour plus d'informations.

La totalité du code source des exemples montrés dans cet article est disponible dans le code source de PythonQt, disponible sur SourceForge.

XII - Conclusion

Il est toujours possible d'utiliser PythonQt, le projet étant toujours disponible sur SourceForge. Il suffit de télécharger le projet et de la compiler.

Néanmoins, la dernière version stable du projet date de 2010, malgré cela une version plus récente de PythonQt est disponible directement sur le dépôt SVN du projet.

XIII - Remerciements

Au nom de toute l'équipe Qt, j'aimerais adresser le plus grand remerciement à Nokia pour nous avoir autorisés à traduire cet article !

Je tiens à remercier Thibaut Cuvelier pour ses conseils et  Claude Leloup pour sa relecture.