I. Les articles originaux

Cet article est la traduction des articles Qt Python SuperHybrids et SuperHybrids part 2, now Qt + PySide d'Oleksandr Iakovliev.

II. Introduction

Vous connaissez probablement PyQt, un binding Python du framework Qt. On peut facilement prototyper du code GUI basé sur Qt très rapidement. En comparaison du même cycle de développement avec Qt, on peut développer de 50 à 100 % plus vite qu'en C++. Le développement sera un peu différent - pas d'étape de compilation -, mais on doit être plus précautionneux pour vérifier que toutes les branches du code exécuté sont testées, car on ne le sait jamais avant qu'elles soient exécutées.

En fait, on peut même trouver PyQt sur Maemo :
http://forums.internettablettalk.com/showthread.php?s=3d53f32b5fe63a5fbd4b64211a4d4676&t=42754

Il n'empêche : PyQt est génial, Python permet de développer rapidement mais on pose souvent la même question. Comment intégrer des scripts Python dans une application et utiliser l'API Qt depuis les scripts ?

Alors que certains pensent que cela n'est pas possible, cet article va montrer la manière de le faire et même de réaliser un exemple qui en est capable.

III. Démarrage

Commençons depuis le début, avec Qt installé en version 4.6.3 et prêt pour utilisation.

D'abord, il faut télécharger des paquets depuis la page d'accueil de PyQt : SIP et PyQt4, les sources seront utilisées dans la suite.

Compiler SIP :

 
Sélectionnez
tar -zxf sip-4.10.3.tar.gz
cd sip-4.10.3
python configure.py
make
sudo make install

Compiler PyQt :

 
Sélectionnez
tar -zxf PyQt-mac-gpl-4.7.4.tar.gz
cd PyQt-mac-gpl-4.7.4
python configure.py
make
sudo make install

Note : il est peut-être nécessaire de définir une variable d'environnement si configure ne fonctionne pas.

 
Sélectionnez
export QMAKESPEC=macx-g++

IV. Une petite application

Maintenant, une petite application Qt qui sera utilisée comme base pour des routines PyQt. Prenons une application basée sur Canvas. Dans une telle application, on peut prototyper des éléments de canevas avec PyQt pendant l'exécution de l'application. Cette application sera simple : deux parties, un éditeur de texte pour le code Python à gauche, le canevas à droite.

PyQtHybrid.pro
Sélectionnez
TEMPLATE = app
CONFIG += qt
QT += core gui
HEADERS += MainWindow.h
SOURCES += MainWindow.cpp Main.cpp
Main.cpp
Sélectionnez
#include <QtGui>
#include "MainWindow.h"

int main(int argc, char ** argv) 
{
    QApplication app(argc, argv);
    MainWindow window;
    window.resize(1000,700);
    window.show();
    return app.exec();
}
MainWindow.h
Sélectionnez
#ifndef MainWindow_H
#define MainWindow_H
 
#include <QMainWindow>
class QPushButton;
class QGraphicsView;
class QGraphicsScene;
class QPlainTextEdit;
 
class MainWindow : public QMainWindow 
{ 
	Q_OBJECT
public:
    MainWindow(QWidget * parent = 0L);
    virtual ~MainWindow();
 
signals:
    void runPythonCode(QString);
 
private slots:
    void runPythonCode();
 
public:
    QGraphicsView * viewer;
    QGraphicsScene * scene;
    QPlainTextEdit * editor;
    QPushButton * pb_commit;
};
#endif // MainWindow_H
MainWindow.cpp
Sélectionnez
#include <QtGui>
#include "MainWindow.h"
 
MainWindow::MainWindow(QWidget * parent):QMainWindow(parent)
{
    QSplitter * splitter = new QSplitter;
    setCentralWidget(splitter);
 
    QWidget * editorContent = new QWidget;
    splitter->addWidget(editorContent);
 
    QVBoxLayout * layout = new QVBoxLayout;
    editorContent->setLayout(layout);
 
    editor = new QPlainTextEdit;
    layout->addWidget(editor);
 
    pb_commit = new QPushButton(tr("Commit"));
    connect(pb_commit, SIGNAL(clicked()),
            this, SLOT(runPythonCode()));
    layout->addWidget(pb_commit);
 
    scene = new QGraphicsScene(this);
    viewer = new QGraphicsView;
    viewer->setScene(scene);
    splitter->addWidget(viewer);
 
    splitter->setSizes(QList<int>() << 400 << 600);
}
 
MainWindow::~MainWindow()
{
    ;
}
 
void MainWindow::runPythonCode()
{
    emit runPythonCode(editor->toPlainText());
}

Au final, on a une fenêtre avec un éditeur à gauche et un canevas à droite.

Image non disponible

V. Support de PyQt

Maintenant, il faut modifier l'application pour qu'elle fonctionne avec PyQt. On va :

  • la changer en bibliothèque dynamique ;
  • créer des wrappers pour la classe MainWindow ;
  • créer une fonction main() en Python au lieu de l'actuel C.

Pour le premier point, il suffit de changer le TEMPLATE à lib dans le fichier de projet, d'exclure main.cpp et de construire une bibliothèque partagée. Le fichier de projet devient donc :

PyQtHybrid.pro
Sélectionnez
TEMPLATE = lib
CONFIG += qt
QT += core gui
HEADERS += MainWindow.h
SOURCES += MainWindow.cpp

Maintenant, un dossier sip dans lequel on crée un wrapper pour notre classe.

MainWindow.sip
Sélectionnez
%Module PyQtHybrid 0
%Import QtGui/QtGuimod.sip
%If (Qt_4_2_0 -)
 
class MainWindow : QMainWindow {
%TypeHeaderCode
#include "../MainWindow.h"
%End
public:
    MainWindow();
    virtual ~MainWindow();
 
signals:
    void runPythonCode(QString);
 
private slots:
    void runPythonCode();
 
public:
    QGraphicsView * viewer;
    QGraphicsScene * scene;
    QPlainTextEdit * editor;
    QPushButton * pb_commit;
};
 
%End

Pour générer un wrapper avec SIP, on doit avoir un configure.py et un PyQtHybridConfig.py.in.

configure.py
Sélectionnez
import os
import sipconfig
from PyQt4 import pyqtconfig
 
build_file = "PyQtHybrid.sbf"
config = pyqtconfig.Configuration()
pyqt_sip_flags = config.pyqt_sip_flags
 
os.system(" ".join([ \
    config.sip_bin, \
    "-c", ".", \
    "-b", build_file, \
    "-I", config.pyqt_sip_dir, \
    pyqt_sip_flags, \
    "MainWindow.sip" \
]))
 
installs = []
installs.append(["MainWindow.sip", os.path.join(config.default_sip_dir, "PyQtHybrid")])
installs.append(["PyQtHybridConfig.py", config.default_mod_dir])
 
makefile = pyqtconfig.QtGuiModuleMakefile(
    configuration=config,
    build_file=build_file,
    installs=installs
)
 
makefile.extra_libs = ["PyQtHybrid"]
makefile.extra_lib_dirs = [".."]
 
makefile.generate()
 
content = {
    "PyQtHybrid_sip_dir":    config.default_sip_dir,
    "PyQtHybrid_sip_flags":  pyqt_sip_flags
}
sipconfig.create_config_module("PyQtHybridConfig.py", "PyQtHybridConfig.py.in", content)
PyQtHybridConfig.py.in
Sélectionnez
from PyQt4 import pyqtconfig
# @SIP_CONFIGURATION@
 
class Configuration(pyqtconfig.Configuration):
    def __init__(self, sub_cfg=None):
        if sub_cfg: cfg = sub_cfg
        else: cfg = []
        cfg.append(_pkg_config)
        pyqtconfig.Configuration.__init__(self, cfg)
 
class PyQtHybridModuleMakefile(pyqtconfig.QtGuiModuleMakefile):
    def finalise(self):
        self.extra_libs.append("PyQtHybrid")
        pyqtconfig.QtGuiModuleMakefile.finalise(self)

Maintenant, il est grand temps de construire le module Python dans le dossier sip (on se basera sur le fait que la bibliothèque C++ est déjà compilée dans le répertoire parent).

 
Sélectionnez
python configure.py
make

Maintenant que tout est prêt, on peut tout connecter dans le script Main.py.

Main.py
Sélectionnez
import sys
sys.path.append('sip')
 
from PyQt4.QtCore import *
from PyQt4.QtGui import *
from PyQtHybrid import *
 
class RunScript(QObject):
    def __init__(self, mainWindow):
        QObject.__init__(self)
        self.mainWindow = mainWindow
 
    def runScript(self, script):
        mainWindow = self.mainWindow
        exec(str(script))
 
a = QApplication(sys.argv)
w = MainWindow()
r = RunScript(w)
w.setWindowTitle('PyQtHybrid')
w.resize(1000,800)
w.show()
a.connect(w, SIGNAL('runPythonCode(QString)'), r.runScript)
a.connect(a, SIGNAL('lastWindowClosed()'), a, SLOT('quit()') )
a.exec_()

VI. Exécution de la version Python

Maintenant, en lançant ce dernier script, on voit apparaître exactement la même fenêtre. Avec cependant une grande différence : le code entré dans le panneau de gauche peut être exécuté par l'application elle-même. On a donc une application qui peut se programmer elle-même !

Essayons quelques snippets.

 
Sélectionnez
mainWindow.statusBar().show()

La barre d'état est apparue ! Maintenant, plus compliqué : changeons le fond du canevas.

 
Sélectionnez
mainWindow.scene.setBackgroundBrush(QColor('#e0e0ff'))

Peut-on créer de nouveaux objets ? Évidemment que oui.

 
Sélectionnez
li1 = QGraphicsLineItem(10,10, 500,500)
li1.setPen(QPen(QBrush(QColor("#ff0000")), 3.0, Qt.DashLine))
mainWindow.scene.addItem(li1)
Image non disponible

Peut-on créer de nouvelles classes en temps réel et les instancier ? Évidemment ! Dérivons QGraphicsItem pour faire un peu de dessin personnalisé :

 
Sélectionnez
mainWindow.scene.clear()
mainWindow.viewer.setRenderHint(QPainter.Antialiasing)
class MyItem(QGraphicsItem):
    def boundingRect(self):
        return QRectF(-100,-100,200,200)
 
    def paint(self, painter, option, widget):
        g = QLinearGradient(-100,-100, 100,100)
        g.setColorAt(0, QColor('#00ff00'))
        g.setColorAt(1, QColor('#ffffff'))
        painter.setBrush(g)
        p = QPen(QBrush(QColor("#ff0000")), 4, Qt.DashLine)
        painter.setPen(p)
        painter.drawRoundedRect(-100,-100,200,200, 30,30)
 
my1 = MyItem()
mainWindow.scene.addItem(my1)
my1.setPos(200,200)
Image non disponible

VII. Avec PySide

Ce qui précède se base sur PyQt ; on peut aussi l'interpréter pour PySide, puisque ce dernier est sous LGPL.

Considérons que PySide est déjà installé.

Voici la structure des dossiers à utiliser dans ce cas :

 
Sélectionnez
HybridApp/
|-data/
|    |-global.h
|    |-typesystem.xml
|-hybrid/
|    |-MainWindow.h
|    |-MainWindow.cpp
|    |-hybrid.pro
|-hybridpy/
|    |-hybridpy.pro
|-build.sh
|-Main.py

Quelques mots d'explication :

  • le dossier hybrid contient la partie C++ de l'application qui est compilée en bibliothèque partagée ;
  • le dossier hybridpy contient le wrapper pour la partie C++, il compile le module Python qui sera importé dans les parties Python de l'application ;
  • le dossier data contient les définitions de système de type utilisé dans la partie C++ ; il s'agit de fichiers XML qui décrivent le type des objets utilisés et leurs particularités lors de la conversion en Python.

Tout d'abord, reprenons le code de l'application de base précédente en C++, avec sa propre fonction main(). On obtient donc la même fenêtre :

Image non disponible

Maintenant, passons à une partie en Python. On va, comme précédemment, passer l'application en tant que bibliothèque dynamique. Seul changement : la génération des wrappers Python. Elle se fait avec le script build.sh à la racine du projet. En fait, ce script compile tout, y compris la partie C++.

build.sh
Sélectionnez
#!/bin/sh
 
cd hybrid
qmake
make
cd ..
 
cd hybridpy
 
QTGUI_INC=/Library/Frameworks/QtGui.framework/Versions/4/Headers
QTCORE_INC=/Library/Frameworks/QtCore.framework/Versions/4/Headers
QTTYPESYSTEM=/usr/local/share/PySide/typesystems
 
generatorrunner --generatorSet=shiboken \
    ../data/global.h \
    --include-paths=../hybrid:$QTCORE_INC:$QTGUI_INC:/usr/include \
    --typesystem-paths=../data:$QTTYPESYSTEM \
    --output-directory=. \
    ../data/typesystem.xml
 
qmake
make
cd ..
 
rm -rf PyHybrid.so
ln -s libPyHybrid.dylib PyHybrid.so

Il s'agit simplement d'un appel à generatorrunner avec les chemins pour les inclusions de fichiers Qt, le système de type de Qt et le vôtre. Ensuite, dans le dossier bybridpy, on compile avec QMake le module Python.

data/typesystem.xml
Sélectionnez
<?xml version="1.0"?>
<typesystem package="PyHybrid">
    <load-typesystem name="typesystem_core.xml" generate="no"/>
    <load-typesystem name="typesystem_gui.xml" generate="no"/>
    <object-type name="MainWindow"/>
</typesystem>
data/global.h
Sélectionnez
#undef QT_NO_STL
#undef QT_NO_STL_WCHAR
 
#ifndef NULL
#define NULL    0
#endif
 
#include <MainWindow.h>

Et pour la compilation du module Python :

hybridpy/hybridpy.pro
Sélectionnez
TEMPLATE = lib
QT += core gui
 
INCLUDEPATH += hybrid
INCLUDEPATH += ../hybrid
 
INCLUDEPATH += /usr/include/python2.6
INCLUDEPATH += /usr/local/include/shiboken
INCLUDEPATH += /usr/local/include/PySide
INCLUDEPATH += /usr/local/include/PySide/QtCore
INCLUDEPATH += /usr/local/include/PySide/QtGui
 
LIBS += -ldl -lpython2.6
LIBS += -lpyside
LIBS += -lshiboken
LIBS += -L.. -lHybrid
 
TARGET = ../PyHybrid
 
SOURCES += \
    pyhybrid/pyhybrid_module_wrapper.cpp \
    pyhybrid/mainwindow_wrapper.cpp \

La majorité des chemins utilisés pour les en-têtes Python et Qt pourraient être extraits d'outils comme pkg-config. Ici, les chemins exacts ne sont présentés que pour démontrer ce qui est réellement inclus. Aussi, on pourrait évidemment le faire avec QMake, mais le choix de QMake a été fait pour conserver une meilleure présentation de la logique et des fichiers utilisés.

Tout ceci semble bien complet : si on lance build.sh, on obtient un PyHybrid.so, un module Python. Tout est prêt, il est l'heure de connecter les deux parties dans le script Main.py :

Main.py
Sélectionnez
import sys
from PySide.QtCore import *
from PySide.QtGui import *
from PyHybrid import *
 
class RunScript(QObject):
    def __init__(self, mainWindow):
        QObject.__init__(self)
        self.mainWindow = mainWindow
 
    def runScript(self, script):
        mainWindow = self.mainWindow
        exec(str(script))
 
a = QApplication(sys.argv)
w = MainWindow()
r = RunScript(w)
w.setWindowTitle('PyHybrid')
w.resize(1000,800)
w.show()
a.connect(w, SIGNAL('runPythonCode(QString)'), r.runScript)
a.connect(a, SIGNAL('lastWindowClosed()'), a, SLOT('quit()') )
a.exec_()

En le lançant, on obtient la même fenêtre qu'en C++, à la différence près qu'on exécute le code Python entré dans le panneau à gauche. On peut réaliser les mêmes tests que précédemment et obtenir les mêmes résultats.

VIII. Conclusion

Ainsi, on peut tout faire avec notre petite application : on peut la programmer en temps réel. On peut même générer du code dans le programme et l'exécuter. Cependant, ceci peut aussi être utilisé pour équilibrer les performances avec des prototypages rapides en Python, en transférant du code en C++ pour qu'il s'exécute plus vite.

Un bon exemple : le développement de jeux basés sur des canevas, où il peut être vraiment utile et pratique de travailler en Python pour réaliser la logique, l'affichage et tout le reste et ensuite en migrer une partie en C++.

Quelle est la différence avec l'éditeur utilisé par le développeur ? On code ici l'application pendant qu'elle est lancée. C'est une toute nouvelle approche de la programmation avec Qt. Un problème : on perd tout quand on ferme le programme. Il serait donc utile de modifier l'application pour sauvegarder tous les snippets.

Désormais, c'est à vous de prendre le relais.

Merci à Maxime Gault pour la relecture !