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.
TEMPLATE = app
CONFIG += qt
QT += core gui
HEADERS += MainWindow.h
SOURCES += MainWindow.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();
}
#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 =
0
L);
virtual
~
MainWindow();
signals
:
void
runPythonCode(QString
);
private
slots
:
void
runPythonCode();
public
:
QGraphicsView
*
viewer;
QGraphicsScene
*
scene;
QPlainTextEdit
*
editor;
QPushButton
*
pb_commit;
}
;
#endif
// MainWindow_H
#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 :
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.
%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.
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)
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.
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++.
#!/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.
<?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>
#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 :
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 :
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 !