Superhybrides Python et Qt

| Oleksandr Iakovliev | Traducteur : Thibaut Cuvelier |

Python est un langage qui permet de prototyper rapidement des applications. Il n'est pas toujours suffisamment rapide pour certaines applications, notamment du côté de PyQt ou de PySide, bindings Python de Qt. À ce moment-là, on peut commencer à migrer du code vers le C++ : ici sera montrée une technique qui permet d'appeler du code C++ depuis un script Python.

Commentez

- Titre : Superhybrides Python et Qt

- Auteurs : Oleksandr Iakovliev, Traducteur : Thibaut Cuvelier

- Parution : 12 mars 2011

- Mise à jour : 23 mars 2011

- Licence : Copyright ® 2010 Oleksandr Iakovliev. 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. Les articles originaux

- II. Introduction

- III. Démarrage

- IV. Une petite application

- V. Support de PyQt

- VI. Exécution de la version Python

- VII. Avec PySide

- VIII. Conclusion

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 :

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

Compiler PyQt :

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.

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

TEMPLATE = app
CONFIG += qt
QT += core gui
HEADERS += MainWindow.h
SOURCES += MainWindow.cpp Main.cpp

Main.cpp

#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

#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

#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.

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

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

%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

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

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).

python configure.py
make

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

Main.py

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.

mainWindow.statusBar().show()

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

mainWindow.scene.setBackgroundBrush(QColor('#e0e0ff'))

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

li1 = QGraphicsLineItem(10,10, 500,500)
li1.setPen(QPen(QBrush(QColor("#ff0000")), 3.0, Qt.DashLine))
mainWindow.scene.addItem(li1)

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é :

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)

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 :

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 :

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

#!/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

<?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

#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

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

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 !