Programmation Python/Version imprimable3
Une version à jour et éditable de ce livre est disponible sur Wikilivres,
une bibliothèque de livres pédagogiques, à l'URL :
https://fr.wikibooks.org/wiki/Programmation_Python
Bibliothèques pour Python
Une des grandes forces du langage Python réside dans le nombre important de bibliothèques logicielles externes disponibles. Une bibliothèque est un ensemble de fonctions. Celles-ci sont regroupées et mises à disposition afin de pouvoir être utilisées sans avoir à les réécrire.
Celles-ci permettent de faire : du calcul numérique, du graphisme, de la programmation internet ou réseau, du formatage de texte, de la génération de documents...
Bibliothèques standards
modifierLa distribution standard de Python contient un certain nombre de bibliothèques qui ont été considérées comme suffisamment génériques pour intéresser la majorité des utilisateurs.
Leur utilisation est relativement bien expliquée dans la documentation de la distribution.
Les couches de présentation des applications (couche IHM avec wxPython, pyQt, PyKDE Tk, tkinter 3000, pyGTK, pybwidget, Pmw, TIX)
les couches controller des serveurs d'application Web (analyse HTML -htmllib, xmllib, urlParse, mimetools- Serveur d'application : Zope - Django, Turbogears, CherryPy, Plone, GGI)
les couches Modele d'accès aux données (MySQL -MySQLdb- , Oracle -dcoracle-, MS SQL Server, PostgreSQL -psycopg-, FireBird -kinterbasdb- , SybODBC, GadFly, PySqlite, Zodb- BDD objet -)
la couche de persistance XML (4Suite, PySimpleXML, XmlSerializer, Jaxml) ou spécifique à Python (Cpickle, Shelve)
les couches d'accès au middleware ICE, COM/CORBA/.NET (win32, OmniORB, Ironpython) : programmation orientée composant (pont vers des bibliothèques Fortran, C et C++)
les couches de communication standalone (port série : pySerial, port parallèle : pyParallel) , réseau (Twisted, urllib2, HTMLparser, ftplib], socket, poplib, rfc822, mailbox, mhlib, imaplib, smtplib, telnet, etc.)
les couches de frameWork bas niveau (ajout de capacité de script. exemple : Boost.Python)
Les couches multimédia : traitement d'image (PIL)
Les couches utilitaires :
- de gestion de l'heure (datetime, time)
- de compression (gzip)
- de codage/décodage de données binaires (hashlib -md5, sha- , base64, binHex, binascii)
- de structure de données (decimal, deque, array, dico, list, queue, heapq)
- de parallélisme (thread)
- d'expressions régulières (re)
- de différences (difflib)
- d’accès au dll ou.so (ctype)
- de manipulation de chaînes (string, str, StringIO)
- de parseur (standard - shlex, ConfigParser, email, parser, cmd - ou Tierce- pyBison, ples, pyparsing, ply, PyGgy, Yapps, pyLR)
- de calcul (math, numarrray - tableaux multidimensionnaires - , cmath, random)
- de log (logging)
- de test unitaire (unittest)
Le déploiement se fait soit en utilisant des modules d'installation standardisés (distutils), soit en générant un exécutable qui ne nécessite plus l'existence de l'interpréteur sur les machines cibles (Windows : py2exe, Cx_freeze; Unix : freeze)
Bibliothèques alternatives
modifierLes bibliothèques les plus populaires et les plus utilisées (XML, interfaces graphiques...) bénéficient de pages dédiées sur le site principal du langage.
Il existe de nombreuses bibliothèques Python parmi lesquelles :
Nom | Description |
---|---|
CGAL[1] | Computational Geometry Algorithms Library : CGAL-Python bindings pour la CGAL library |
CherryPy | Framework de développement web. |
Cocos2d[2] | Frameworks multiplateformes pour construire des jeux 2d, démos ou des applications interactives graphiques en openGL (nécessite Pyglet) |
CWM[3] | Modules de parseur pour le web sémantique |
directpython[4] | Binding vers Direct X |
Django | Framework de développement web. |
epydoc[5] | Utilisé pour générer la documentation |
FANN[6] | Fast Artificial Neural Network Library : binding Python pour FANN |
Gadfly | Base de données |
GENA[7] | Algorithme génétique en Python |
GMPY[8] | General Multiprecision PYthon : interface vers la bibliothèque de précision arithmétique GNU GMP |
gnuplot-py[9] | Bibliothèque pour s'interfacer avec gnuplot |
guidata[10] | Librairie graphique basée sur Qt dédiée à l'affichage de données |
guiqwt[11] | Librairie graphique basée sur Pyqwt dédiée à l'affichage de courbes |
Karrigell[12] | framework de développement web. |
Kinterbasdb | Base de données |
Matplotlib[13] | Bibliothèque de dessin de courbe 2D (très riche) |
Mahotas | Bibliothèque d'analyse d'images |
Maximum Entropy Modeling Toolkit[14] | Framework qui met en œuvre le principe de l'entropie maximum |
MayaVi2[15] | Visualisation des données scientifiques en 2D/3D |
MySQLdb | Base de données |
Buzhug | Base de données |
Orange|[16] | Technique d'exploration de données, data mining |
Panda 3D[17] | Moteur 3D |
PIL[18] | Manipulation et traitement d'image |
Pmw | Interface graphique |
Pybwidget | Interface graphique |
Psycopg | Base de données |
Py2exe[19] | Créer un exécutable Windows pour vos scripts |
pybel | Interface pour la bibliothèque Open source de CHIMIE Open Babel. |
Pychinko | Implémentation de l'algorithme de Rete (pour le chaînage avant) |
pyCLIPS[20] | Module pour scripter le système expert CLIPS |
pydot[21] | Module pour scripter le générateur de graphique Graphviz |
Pygame[22] | Module de création de jeu 2D |
Pyglet[23] | module de création de jeu 2D utilisant l'openGL |
pygsl[24] | Interface vers GNU scientific library (gsl): vecteur, matrice, transformation de fourrier, recuit simulé, algèbre linéaire... |
PyGTK | Interface graphique |
PyIMSL Studio[25] | Framework pour le calcul scientifique s'appuyant sur les bibliothèques mathématiques et statistiques IMSL |
pyinstaller[26] | Création d'exécutable pour toute plateforme |
Pylons[27] | Framework de développement web. |
pymedia[28] | Module pour manipulations de fichiers wav, mp3, ogg, avi, divx, dvd, cdda, etc. |
PyML[29] | Python machine learning package : framework pour l'apprentissage par les machines (data mining ...) |
pyMPI[30] | Calcul parallèle |
PyNGL[31] | Bibliothèque pour créer des graphes 2D |
PyOgre[32] | Moteur 3D |
PyPar[33] | Calcul parallèle |
pyParallel[34] | Accès aux ports parallèles |
pyro[35] | Middleware Python Remote Objects |
pyrorobotics[36] | Environnement pour l'étude de la robotique et l'intelligence artificielle. Réseaux de neurones |
pySerial[37] | Manipulation des ports séries, non intégrés par défaut, bien que très souvent utilisés dans le monde industriel |
PySFML[38] | module de création de jeu 2D |
PyUSB[39] | Manipulation du port USB |
PyVISA[40] | Contrôle des ports GPIB, RS232, and USB |
pyX[41] | Python graphics package - Analyse de donnée |
SAGE[42] | logiciel d'algèbre et de géométrie (alternative à MAthematica, Maple ...) géométrie, théorie des nombres, cryptographie, calcul numérique... |
Scikit-learn | Librairie d'apprentissage automatique |
scipy et NumPy[43] | Framework pour le calcul scientifique. Interpolation, intégration (ODE integrators), algèbre linéaire (LAPACK), Interpolation, systèmes dynamiques (PyDSTool) |
SCons[44] | Alternative puissante à make (next-generation build tool) |
simPy[45] | Simulation de systèmes dynamiques à file d'attente |
Soya[46] | Moteur 3D |
SVGdraw[47] | Création d'image au format SVG (Scalable Vector Graphics) |
Tkinter 3000 | Interface graphique |
TurboGears | framework de développement web. |
Twisted[48] | Pluggable, asynchronous protocols (TCP, UDP, SSL/TLS, multicast, Unix sockets, HTTP, NNTP, IMAP, SSH, IRC, FTP) |
VPython[49] | Simulation 3D basée sur OpenGl |
Web2py[50] | framework de développement web. |
wxPython[51] | Bibliothèque d'accès à un toolkit très puissant (en particulier pour les interfaces graphiques) |
Zope[52] | Serveur d'application web orienté objet et base de données Objet |
Références
modifier- ↑ http://cgal-python.gforge.inria.fr/
- ↑ http://cocos2d.org/
- ↑ http://infomesh.net/2001/cwm/
- ↑ http://directpython.sourceforge.net/
- ↑ http://epydoc.sourceforge.net/
- ↑ http://leenissen.dk/fann/index.php
- ↑ http://www.madiku.org/ylrt3i0sfy/?p=1291
- ↑ http://gmpy.sourceforge.net/
- ↑ http://gnuplot-py.sourceforge.net/
- ↑ http://pypi.python.org/pypi/guidata/
- ↑ http://packages.python.org/guiqwt/
- ↑ http://karrigell.sourceforge.net
- ↑ http://matplotlib.sourceforge.net/
- ↑ http://homepages.inf.ed.ac.uk/s0450736/maxent_toolkit.html
- ↑ https://svn.enthought.com/enthought/wiki/MayaVi
- ↑ http://www.ailab.si/orange
- ↑ http://www.panda3d.org/
- ↑ http://www.pythonware.com/products/pil/
- ↑ http://www.py2exe.org/ (version pour Python 2.7)
- ↑ http://pyclips.sourceforge.net/
- ↑ http://dkbza.org/pydot.html
- ↑ http://www.pygame.org/news.html
- ↑ http://www.pyglet.org/
- ↑ http://pygsl.sourceforge.net/
- ↑ http://sites.google.com/site/roguewavesoftwarefrance/produits/PyIMSL-Studio
- ↑ http://pyinstaller.python-hosting.com/
- ↑ http://www.pylonshq.com
- ↑ http://pymedia.org/
- ↑ http://pyml.sourceforge.net/
- ↑ http://pympi.sourceforge.net/index.html
- ↑ http://www.pyngl.ucar.edu/index.shtml
- ↑ http://www.ogre3d.org/
- ↑ http://datamining.anu.edu.au/~ole/pypar/
- ↑ http://pyserial.sourceforge.net/pyparallel.html
- ↑ http://pyro.sourceforge.net/
- ↑ https://pypi.python.org/pypi/pyRobotics/1.5
- ↑ http://pyserial.sourceforge.net
- ↑ http://www.sfml-dev.org/tutorials/1.6/start-python-fr.php
- ↑ http://pyusb.berlios.de/
- ↑ http://pyvisa.sourceforge.net/
- ↑ http://pyx.sourceforge.net/
- ↑ http://modular.math.washington.edu/sage/
- ↑ http://www.scipy.org/
- ↑ http://www.scons.org/
- ↑ http://simpy.sourceforge.net/
- ↑ http://home.gna.org/oomadness/en/soya3d/index.html
- ↑ http://www2.sfk.nl/svg
- ↑ http://twistedmatrix.com/trac/
- ↑ http://www.vpython.org/
- ↑ http://www.web2py.com
- ↑ http://www.wxpython.org/
- ↑ http://www.zope.org/
L'interface graphique
L'interface graphique pour Python
modifierComme dans tout langage de programmation, l’exécution du code ne se voit pas par l'utilisateur !
Jusqu’à maintenant, la seule relation entre le programme une fois lancé et l'utilisateur était le terminal :
- Un print par-ci, pour donner des informations à l'utilisateur, uniquement sous forme d'une chaine de caractères.
- Un input par-là, afin que l'utilisateur puisse envoyer des données, toujours sous forme d'une chaine de caractères, au programme.
Dorénavant, ayant des acquis sur la programmation dans ce langage, il va vous être enfin possible de donner du style à vos programme en créant une véritable Interface Homme-Machine (IHM) !
Pour cela, python intègre déjà avec son interpréteur : Tkinter, qui est une bibliothèque graphique libre. Créer vos Interfaces Homme-Machine avec cette bibliothèque permettra à l'utilisateur de n'avoir aucune bibliothèque à télécharger en plus de votre code. Il sera donc très portable !
Sinon, dans les bibliothèques graphiques libres, les principaux modules sont :
Inutile d'en dire davantage ; les liens Wikipédia sont très bien documentés. Sachez qu'il n'y a pas une solution unique ; ces bibliothèques ont toutes leurs caractéristiques ; tout avis ici ne serait que subjectif ; par conséquent, la seule chose qu'il reste à dire est : essayez-les, et choisissez celle que vous voudrez... L’esprit du libre, c'est aussi d'avoir le choix !
Utilisation de fenêtres et de graphismes
Jusqu'à présent, nous avons utilisé Python exclusivement « en mode texte ». Nous avons procédé ainsi parce qu'il nous fallait absolument d'abord dégager un certain nombre de concepts élémentaires ainsi que la structure de base du langage, avant d'envisager des expériences impliquant des objets informatiques plus élaborés (fenêtres, images, sons, etc.). Nous pouvons à présent nous permettre une petite incursion dans le vaste domaine des interfaces graphiques, mais ce ne sera qu'un premier amuse-gueule : il nous reste en effet encore bien des choses fondamentales à apprendre, et pour nombre d'entre elles l'approche textuelle reste la plus abordable.
Interfaces graphiques (GUI)
modifierSi vous ne le saviez pas encore, apprenez dès à présent que le domaine des interfaces graphiques (ou GUI : Graphical User Interface) est extrêmement complexe. Chaque système d'exploitation peut en effet proposer plusieurs « bibliothèques » de fonctions graphiques de base, auxquelles viennent fréquemment s'ajouter de nombreux compléments, plus ou moins spécifiques de langages de programmation particuliers. Tous ces composants sont généralement présentés comme des classes d'objets, dont il vous faudra étudier les attributs et les méthodes.
Avec Python, la bibliothèque graphique la plus utilisée jusqu'à présent est la bibliothèque Tkinter, qui est une adaptation de la bibliothèque Tk développée à l'origine pour le langage Tcl. Plusieurs autres bibliothèques graphiques fort intéressantes ont été proposées pour Python : wxPython, pyQT, pyGTK, etc. Il existe également des possibilités d'utiliser les bibliothèques de widgets Java et les MFC de Windows.
Dans le cadre de ces notes, nous nous limiterons cependant à Tkinter, dont il existe fort heureusement des versions similaires (et gratuites) pour les plates-formes Linux, Windows et Mac.
Premiers pas avec Tkinter
modifierPour la suite des explications, nous supposerons bien évidemment que le module Tkinter a déjà été installé sur votre système. Pour pouvoir en utiliser les fonctionnalités dans un script Python, il faut que l'une des premières lignes de ce script contienne l'instruction d'importation :
from tkinter import *
Attention, suivant les versions de Python (a priori à partir de la version 3) l'appel du module Tkinter se fait avec un t minuscule :
from tkinter import *
Comme toujours sous Python, il n'est même pas nécessaire d'écrire un script. Vous pouvez faire un grand nombre d'expériences directement à la ligne de commande, en ayant simplement lancé Python en mode interactif. Dans l'exemple qui suit, nous allons créer une fenêtre très simple, et y ajouter deux widgets[1] typiques : un bout de texte (ou label) et un bouton (ou button).
>>> from tkinter import *
>>> fen1 = Tk()
>>> tex1 = Label(fen1, text='Bonjour tout le monde !', fg='red')
>>> tex1.pack()
>>> bou1 = Button(fen1, text='Quitter', command = fen1.destroy)
>>> bou1.pack()
>>> fen1.mainloop()
Note : Suivant la version de Python utilisée, vous verrez déjà apparaître la fenêtre d'application immédiatement après avoir entré la deuxième commande de cet exemple, ou bien seulement après la septième[2].
Examinons à présent plus en détail chacune des lignes de commandes exécutées :
- Comme cela a déjà été expliqué précédemment, il est aisé de construire différents modules Python, qui contiendront des scripts, des définitions de fonctions, des classes d'objets, etc. On peut alors importer tout ou partie de ces modules dans n'importe quel programme, ou même dans l'interpréteur fonctionnant en mode interactif (c'est-à-dire directement à la ligne de commande). C'est ce que nous faisons à la première ligne de notre exemple :
from tkinter import *
consiste à importer toutes les classes contenues dans le module Tkinter. Nous devrons de plus en plus souvent parler de ces classes. En programmation, on appelle ainsi des générateurs d'objets, lesquels sont eux-mêmes des morceaux de programmes réutilisables. Nous n'allons pas essayer de vous fournir dès à présent une définition définitive et précise de ce que sont les objets et les classes, mais plutôt vous proposer d'en utiliser directement quelques-un(e)s. Nous affinerons notre compréhension petit à petit par la suite. - À la deuxième ligne de notre exemple :
fen1 = Tk()
, nous utilisons l'une des classes du module Tkinter, la classeTk()
, et nous en créons une instance (autre terme désignant un objet spécifique), à savoir la fenêtrefen1
. Ce processus d'instanciation d'un objet à partir d'une classe est une opération fondamentale dans les techniques actuelles de programmation. Celles-ci font en effet de plus en plus souvent appel à une méthodologie que l'on appelle programmation orientée objet (ou OOP : Object Oriented Programming). La classe est en quelque sorte un modèle général (ou un moule) à partir duquel on demande à la machine de construire un objet informatique particulier. La classe contient toute une série de définitions et d'options diverses, dont nous n'utilisons qu'une partie dans l'objet que nous créons à partir d'elle. Ainsi la classeTk()
, qui est l'une des classes les plus fondamentales de la bibliothèque Tkinter, contient tout ce qu'il faut pour engendrer différents types de fenêtres d'application, de tailles ou de couleurs diverses, avec ou sans barre de menus, etc. Nous nous en servons ici pour créer notre objet graphique de base, à savoir la fenêtre qui contiendra tout le reste. Dans les parenthèses deTk()
, nous pourrions préciser différentes options, mais nous laisserons cela pour un peu plus tard. L'instruction d'instanciation ressemble à une simple affectation de variable. Comprenons bien cependant qu'il se passe ici deux choses à la fois :- la création d'un nouvel objet, (lequel peut être complexe et donc occuper un espace mémoire considérable)
- l'affectation d'une variable qui va désormais servir de référence pour manipuler l'objet[3].
- À la troisième ligne :
tex1 = Label(fen1, text='Bonjour tout le monde !', fg='red')
, nous créons un autre objet (un widget), cette fois à partir de la classeLabel()
. Comme son nom l'indique, cette classe définit toutes sortes d'étiquettes (ou de libellés). En fait, il s'agit tout simplement de fragments de texte quelconques, utilisables pour afficher des informations et des messages divers à l'intérieur d'une fenêtre. Nous efforçant d'apprendre au passage la manière correcte d'exprimer les choses, nous dirons donc que nous créons ici l'objet tex1 par instanciation de la classeLabel()
. Quels arguments avons-nous donc fournis pour cette instanciation ?- Le premier argument transmis (
fen1
), indique que le nouveau widget que nous sommes en train de créer sera contenu dans un autre widget préexistant, que nous désignons donc ici comme son « maître » : l'objetfen1
est le widget maître de l'objettex1
. (On pourra dire aussi que l'objettex1
est un widget esclave de l'objetfen1
).
- Les deux arguments suivants servent à préciser la forme exacte que doit prendre notre widget. Ce sont en effet deux options de création, chacune fournie sous la forme d'une chaîne de caractères : d'abord le texte de l'étiquette, ensuite sa couleur d'avant-plan (ou foreground, en abrégé
fg
). Ainsi le texte que nous voulons afficher est bien défini, et il doit apparaître coloré en rouge.
Label()
. Nous ne devons indiquer des options que pour les caractéristiques que nous souhaitons différentes du modèle standard. - Le premier argument transmis (
- À la quatrième ligne de notre exemple :
tex1.pack()
, nous activons une méthode associée à l'objettex1
: la méthodepack()
. Nous avons déjà rencontré ce terme de méthode (à propos des listes, notamment). Une méthode est une fonction intégrée à un objet (on dira aussi qu'elle est encapsulée dans l'objet). Nous apprendrons bientôt qu'un objet informatique est en fait un morceau de programme contenant toujours :- Un certain nombre de données (numériques ou autres), contenues dans des variables de types divers : on les appelle les attributs (ou les propriétés) de l'objet.
- Un certain nombre de procédures ou de fonctions (qui sont donc des algorithmes) : on les appelle les méthodes de l'objet.
pack()
fait partie d'un ensemble de méthodes qui sont applicables non seulement aux widgets de la classeLabel()
, mais aussi à la plupart des autres widgets Tkinter, et qui agissent sur leur disposition géométrique dans la fenêtre. Comme vous pouvez le constater par vous-même si vous entrez les commandes de notre exemple une par une, la méthodepack()
réduit automatiquement la taille de la fenêtre « maître » afin qu'elle soit juste assez grande pour contenir les widgets « esclaves » définis au préalable. - À la cinquième ligne :
bou1 = Button(fen1, text='Quitter', command = fen1.destroy)
, nous créons notre second widget « esclave » : un bouton. Comme nous l'avons fait pour le widget précédent, nous appelons la classeButton()
en fournissant entre parenthèses un certain nombre d'arguments. Étant donné qu'il s'agit cette fois d'un objet interactif, nous devons préciser avec l'optioncommand
ce qui devra se passer lorsque l'utilisateur effectuera un clic sur le bouton. Dans ce cas précis, nous actionnerons la méthodedestroy
associée à l'objetfen1
, ce qui devrait provoquer l'effacement de la fenêtre. - La sixième ligne utilise la méthode
pack()
pour adapter la géométrie de la fenêtre au nouvel objet que nous venons d'y intégrer. - La septième ligne :
fen1.mainloop()
est très importante, parce que c'est elle qui provoque le démarrage du réceptionnaire d'événements associé à la fenêtre. Cette instruction est nécessaire pour que votre application soit « à l'affût » des clics de souris, des pressions exercées sur les touches du clavier, etc. C'est donc cette instruction qui « la met en marche », en quelque sorte. Comme son nom l'indique (mainloop), il s'agit d'une méthode de l'objetfen1
, qui active une boucle de programme, laquelle « tournera » en permanence en tâche de fond, dans l'attente de messages émis par le système d'exploitation de l'ordinateur. Celui-ci interroge en effet sans cesse son environnement, notamment au niveau des périphériques d'entrée (souris, clavier, etc.). Lorsqu'un événement quelconque est détecté, divers messages décrivant cet événement sont expédiés aux programmes qui souhaitent en être avertis. Voyons cela un peu plus en détail.
Programmes pilotés par des événements
modifierVous venez d'expérimenter votre premier programme utilisant une interface graphique. Ce type de programme est structuré d'une manière différente des scripts « textuels » étudiés auparavant.
Tous les programmes d'ordinateur comportent grosso-modo trois phases principales : une phase d'initialisation, laquelle contient les instructions qui préparent le travail à effectuer (appel des modules externes nécessaires, ouverture de fichiers, connexion à un serveur de bases de données ou à l'internet, etc.), une phase centrale où l'on trouve la véritable fonctionnalité du programme (c'est-à-dire tout ce qu'il est censé faire : afficher des données à l'écran, effectuer des calculs, modifier le contenu d'un fichier, imprimer, etc.), et enfin une phase de terminaison qui sert à clôturer « proprement » les opérations (c'est-à-dire fermer les fichiers restés ouverts, couper les connexions externes, etc.)
Dans un programme « en mode texte », ces trois phases sont simplement organisées suivant un schéma linéaire comme dans l'illustration ci-contre. En conséquence, ces programmes se caractérisent par une interactivité très limitée avec l'utilisateur. Celui-ci ne dispose pratiquement d'aucune liberté : il lui est demandé de temps à autre d'entrer des données au clavier, mais toujours dans un ordre prédéterminé correspondant à la séquence d'instructions du programme.
Dans le cas d'un programme qui utilise une interface graphique, par contre, l'organisation interne est différente. On dit d'un tel programme qu'il est piloté par les événements. Après sa phase d'initialisation, un programme de ce type se met en quelque sorte « en attente », et passe la main à un autre logiciel, lequel est plus ou moins intimement intégré au système d'exploitation de l'ordinateur et « tourne » en permanence.
Ce réceptionnaire d'événements scrute sans cesse tous les périphériques (clavier, souris, horloge, modem, etc.) et réagit immédiatement lorsqu'un événement y est détecté.
Un tel événement peut être une action quelconque de l'utilisateur : déplacement de la souris, appui sur une touche, etc., mais aussi un événement externe ou un automatisme (top d'horloge, par ex.)
Lorsqu'il détecte un événement, le réceptionnaire envoie un message spécifique au programme[4], lequel doit être conçu pour réagir en conséquence.
La phase d'initialisation d'un programme utilisant une interface graphique comporte un ensemble d'instructions qui mettent en place les divers composants interactifs de cette interface (fenêtres, boutons, cases à cocher, etc.). D'autres instructions définissent les messages d'événements qui devront être pris en charge : on peut en effet décider que le programme ne réagira qu'à certains événements en ignorant tous les autres.
Alors que dans un programme « textuel », la phase centrale est constituée d'une suite d'instructions qui décrivent à l'avance l'ordre dans lequel la machine devra exécuter ses différentes tâches (même s'il est prévu des cheminements différents en réponse à certaines conditions rencontrées en cours de route), on ne trouve dans la phase centrale d'un programme avec interface graphique qu'un ensemble de fonctions indépendantes. Chacune de ces fonctions est appelée spécifiquement lorsqu'un événement particulier est détecté par le système d'exploitation : elle effectue alors le travail que l'on attend du programme en réponse à cet événement, et rien d'autre[5].
Il est important de bien comprendre ici que pendant tout ce temps, le réceptionnaire continue à « tourner » et à guetter l'apparition d'autres événements éventuels.
S'il arrive d'autres événements, il peut donc se faire qu'une seconde fonction (ou une 3e, une 4e, ...) soit activée et commence à effectuer son travail « en parallèle » avec la première qui n'a pas encore terminé le sien. En particulier, la même fonction peut être appelée plusieurs fois en réponse à l'occurrence de quelques événements identiques, la même tâche étant alors effectuée en plusieurs exemplaires concurrents. Les systèmes d'exploitation et les langages modernes permettent en effet ce parallélisme que l'on appelle aussi multitâche.
Au chapitre précédent de ces notes, nous vous avons déjà fait remarquer que la structure du texte d'un programme n'indique pas directement l'ordre dans lequel les instructions seront finalement exécutées. Cette remarque s'applique encore bien davantage dans le cas d'un programme avec interface graphique, puisque l'ordre dans lequel les fonctions sont appelées n'est plus inscrit nulle part dans le programme. Ce sont les événements qui pilotent !
Tout ceci doit vous paraître un peu compliqué. Nous allons l'illustrer dans quelques exemples.
Exemple graphique : tracé de lignes dans un canevas
modifierLe script décrit ci-dessous crée une fenêtre comportant trois boutons et un canevas. Suivant la terminologie de Tkinter, un canevas est une surface rectangulaire délimitée, dans laquelle on peut installer ensuite divers dessins et images à l'aide de méthodes spécifiques.
Lorsque l'on actionne le bouton « Tracer une ligne », une nouvelle ligne colorée apparaît sur le canevas, avec à chaque fois une inclinaison différente de la précédente.
Si l'on actionne le bouton « Autre couleur », une nouvelle couleur est tirée au hasard dans une série limitée. Cette couleur est celle qui s'appliquera aux tracés suivants.
Le bouton « Quitter » sert bien évidemment à terminer l'application en refermant la fenêtre.
# Petit exercice utilisant la bibliothèque graphique Tkinter
from tkinter import *
from random import randrange
# --- définition des fonctions gestionnaires d'événements : ---
def drawline():
"Tracé d'une ligne dans le canevas can1"
global x1, y1, x2, y2, coul
can1.create_line(x1,y1,x2,y2,width=2,fill=coul)
# modification des coordonnées pour la ligne suivante :
y2, y1 = y2+10, y1-10
def changecolor():
"Changement aléatoire de la couleur du tracé"
global coul
pal=['purple','cyan','maroon','green','red','blue','orange','yellow']
c = randrange(8) # => génère un nombre aléatoire de 0 à 7
coul = pal[c]
#------ Programme principal -------
# les variables suivantes seront utilisées de manière globale :
x1, y1, x2, y2 = 10, 190, 190, 10 # coordonnées de la ligne
coul = 'dark green' # couleur de la ligne
# Création du widget principal ("maître") :
fen1 = Tk()
# création des widgets "esclaves" :
can1 = Canvas(fen1,bg='dark grey',height=200,width=200)
can1.pack(side=LEFT)
bou1 = Button(fen1,text='Quitter',command=fen1.quit)
bou1.pack(side=BOTTOM)
bou2 = Button(fen1,text='Tracer une ligne',command=drawline)
bou2.pack()
bou3 = Button(fen1,text='Autre couleur',command=changecolor)
bou3.pack()
fen1.mainloop() # démarrage du réceptionnaire d'événements
fen1.destroy() # destruction (fermeture) de la fenêtre
Conformément à ce que nous avons expliqué dans le texte des pages précédentes, la fonctionnalité de ce programme est essentiellement assurée par les deux fonctions drawline()
et changecolor()
, qui seront activées par des événements, ceux-ci étant eux-mêmes définis dans la phase d'initialisation.
Dans cette phase d'initialisation, on commence par importer l'intégralité du module Tkinter ainsi qu'une fonction du module random qui permet de tirer des nombres au hasard. On crée ensuite les différents widgets par instanciation à partir des classes Tk()
, Canvas()
et Button()
. Remarquons au passage que le mot canevas s'écrit différemment en français et en anglais !
L'initialisation se termine avec l'instruction fen1.mainloop()
qui démarre le réceptionnaire d'événements. Les instructions qui suivent ne seront exécutées qu'à la sortie de cette boucle, sortie elle-même déclenchée par la méthode fen1.quit()
(voir ci-après).
L'option command
utilisée dans l'instruction d'instanciation des boutons permet de désigner la fonction qui devra être appelée lorsqu'un événement « clic gauche de la souris sur le widget » se produira. Il s'agit en fait d'un raccourci pour cet événement particulier, qui vous est proposé par Tkinter pour votre facilité parce que cet événement est celui que l'on associe naturellement à un widget de type bouton.
Les fonctions de ce script peuvent modifier les valeurs de certaines variables qui ont été définies au niveau principal du programme. Cela est rendu possible grâce à l'instruction global utilisée dans la définition de ces fonctions. Nous nous permettrons de procéder ainsi pendant quelque temps encore (ne serait-ce que pour vous habituer à distinguer les comportements des variables locales et globales), mais cette pratique n'est pas tout à fait recommandable, surtout lorsqu'il s'agit d'écrire de grands programmes.
Dans notre fonction changecolor()
, une couleur est choisie au hasard dans une liste. Nous utilisons pour ce faire la fonction randrange()
importée du module random. Appelée avec un argument N, cette fonction renvoie un nombre entier, tiré au hasard entre zéro et N-1.
La commande liée au bouton « Quitter » appelle la méthode quit()
de la fenêtre fen1
. Cette méthode sert à fermer (quitter) le réceptionnaire d'événements (mainloop) associé à cette fenêtre. Lorsque cette méthode est activée, l'exécution du programme se poursuit avec les instructions qui suivent l'appel de mainloop. Dans notre exemple, cela consiste donc à effacer (destroy) la fenêtre.
Exercices : modifications au programme « Tracé de lignes » ci-dessus.
- Comment faut-il modifier le programme pour ne plus avoir que des lignes de couleur
cyan
,maroon
etgreen
? - Comment modifier le programme pour que toutes les lignes tracées soient horizontales et parallèles ?
- Agrandissez le canevas de manière à lui donner une largeur de 500 unités et une hauteur de 650. Modifiez également la taille des lignes, afin que leurs extrémités se confondent avec les bords du canevas.
- Ajoutez une fonction
drawline2
qui tracera deux lignes rouges en croix au centre du canevas : l'une horizontale et l'autre verticale. Ajoutez également un bouton portant l'indication « viseur ». Un clic sur ce bouton devra provoquer l'affichage de la croix. - Reprenez le programme initial. Remplacez la méthode
create_line
parcreate_rectangle
. Que se passe-t-il ?
- De la même façon, essayez aussi
create_arc
,create_oval
, etcreate_polygon
. - Pour chacune de ces méthodes, notez ce qu'indiquent les coordonnées fournies en paramètres.
- Remarque : pour le polygone, il est nécessaire de modifier légèrement le programme.
- Supprimez la ligne
global x1, y1, x2, y2
dans la fonctiondrawline
du programme original. Que se passe-t-il ? Pourquoi ? - Si vous placez plutôt
x1, y1, x2, y2
entre les parenthèses, dans la ligne de définition de la fonctiondrawline
, de manière à transmettre ces variables à la fonction en tant que paramètres, le programme fonctionne-t-il encore ? (N'oubliez pas de modifier aussi la ligne du programme qui fait appel à cette fonction !) - Si vous définissez
x1, y1, x2, y2 = 10, 390, 390, 10
à la place deglobal x1, y1, ...
, que se passe-t-il ? Pourquoi ? Quelle conclusion pouvez-vous tirer de tout cela ?
- Supprimez la ligne
- Anneaux olympiques :
- Créez un court programme qui dessinera les cinq anneaux olympiques dans un rectangle de fond blanc (white). Un bouton « Quitter » doit permettre de fermer la fenêtre.
- Modifiez le programme ci-dessus en y ajoutant cinq boutons. Chacun de ces boutons provoquera le tracé de chacun des cinq anneaux.
Solution
- Réfléchissez !
- Réfléchissez !
- Réfléchissez !
- Réfléchissez !
- Réfléchissez !
- Réfléchissez !
-
from tkinter import * # Coordonnées X,Y des 5 anneaux : coord = [[20,30], [120,30], [220, 30], [70,80], [170,80]] # Couleurs des 5 anneaux : coul = ["red", "yellow", "blue", "green", "black"] base = Tk() can = Canvas(base, width =335, height =200, bg ="white") can.pack() bou = Button(base, text ="Quitter", command =base.quit) bou.pack(side = RIGHT) # Dessin des 5 anneaux : i = 0 while i < 5: x1, y1 = coord[i][0], coord[i][1] can.create_oval(x1, y1, x1+100, y1 +100, width =2, outline =coul[i]) i = i +1 base.mainloop()
Variante :
from tkinter import * # Dessin des cinq anneaux : def dessineCercle(i): x1, y1 = coord[i][0], coord[i][1] can.create_oval(x1, y1, x1+100, y1 +100, width =2, outline =coul[i]) def a1(): dessineCercle(0) def a2(): dessineCercle(1) def a3(): dessineCercle(2) def a4(): dessineCercle(3) def a5(): dessineCercle(4) # Coordonnées X,Y des cinq anneaux : coord = [[20,30], [120,30], [220, 30], [70,80], [170,80]] # Couleurs des 5 anneaux : coul = ["red", "yellow", "blue", "green", "black"] base = Tk() can = Canvas(base, width =335, height =200, bg ="white") can.pack() bou = Button(base, text ="Quitter", command =base.quit) bou.pack(side = RIGHT) # Installation des cinq boutons : Button(base, text='1', command = a1).pack(side =LEFT) Button(base, text='2', command = a2).pack(side =LEFT) Button(base, text='3', command = a3).pack(side =LEFT) Button(base, text='4', command = a4).pack(side =LEFT) Button(base, text='5', command = a5).pack(side =LEFT) base.mainloop()
Exemple graphique : deux dessins alternés
modifierCet autre exemple vous montrera comment vous pouvez exploiter les connaissances que vous avez acquises précédemment concernant les boucles, les listes et les fonctions, afin de réaliser de nombreux dessins avec seulement quelques lignes de code. Il s'agit d'une petite application qui affiche l'un ou l'autre des deux dessins reproduits ci-contre, en fonction du bouton choisi.
from tkinter import *
def cercle(x, y, r, coul ='black'):
"tracé d'un cercle de centre (x,y) et de rayon r"
can.create_oval(x-r, y-r, x+r, y+r, outline=coul)
def figure_1():
"dessiner une cible"
# Effacer d'abord tout dessin préexistant :
can.delete(ALL)
# tracer les deux lignes (vert. et horiz.) :
can.create_line(100, 0, 100, 200, fill ='blue')
can.create_line(0, 100, 200, 100, fill ='blue')
# tracer plusieurs cercles concentriques :
rayon = 15
while rayon < 100:
cercle(100, 100, rayon)
rayon += 15
def figure_2():
"dessiner un visage simplifié"
# Effacer d'abord tout dessin préexistant :
can.delete(ALL)
# Les caractéristiques de chaque cercle sont
# placées dans une liste de listes :
cc =[[100, 100, 80, 'red'], # visage
[70, 70, 15, 'blue'], # yeux
[130, 70, 15, 'blue'],
[70, 70, 5, 'black'],
[130, 70, 5, 'black'],
[44, 115, 20, 'red'], # joues
[156, 115, 20, 'red'],
[100, 95, 15, 'purple'], # nez
[100, 145, 30, 'purple']] # bouche
# on trace tous les cercles à l'aide d'une boucle :
i =0
while i < len(cc): # parcours de la liste
el = cc[i] # chaque élément est lui-même une liste
cercle(el[0], el[1], el[2], el[3])
i += 1
##### Programme principal : ############
fen = Tk()
can = Canvas(fen, width =200, height =200, bg ='ivory')
can.pack(side =TOP, padx =5, pady =5)
b1 = Button(fen, text ='dessin 1', command =figure_1)
b1.pack(side =LEFT, padx =3, pady =3)
b2 = Button(fen, text ='dessin 2', command =figure_2)
b2.pack(side =RIGHT, padx =3, pady =3)
fen.mainloop()
Commençons par analyser le programme principal, à la fin du script :
Nous y créons une fenêtre, par instanciation d'un objet de la classe Tk()
dans la variable fen
.
Ensuite, nous installons 3 widgets dans cette fenêtre : un canevas et deux boutons. Le canevas est instancié dans la variable can
, et les deux boutons dans les variables b1
et b2
. Comme dans le script précédent, les widgets sont mis en place dans la fenêtre à l'aide de leur méthode pack()
, mais cette fois nous utilisons celle-ci avec des options :
- L'option side peut accepter les valeurs
TOP
,BOTTOM
,LEFT
ouRIGHT
, pour « pousser » le widget du côté correspondant dans la fenêtre.
- Les options
padx
etpady
permettent de réserver un petit espace autour du widget. Cet espace est exprimé en nombre de pixels : padx réserve un espace à gauche et à droite du widget,pady
réserve un espace au-dessus et au-dessous du widget.
Les boutons commandent l'affichage des deux dessins, en invoquant les fonctions figure_1()
et figure_2()
. Considérant que nous aurions à tracer un certain nombre de cercles dans ces dessins, nous avons estimé qu'il serait bien utile de définir d'abord une fonction cercle()
spécialisée. En effet : Vous savez probablement déjà que le canevas Tkinter est doté d'une méthode create_oval()
qui permet de dessiner des ellipses quelconques (et donc aussi des cercles), mais cette méthode doit être invoquée avec quatre arguments qui seront les coordonnées des coins supérieur gauche et inférieur droit d'un rectangle fictif, dans lequel l'ellipse viendra alors s'inscrire. Cela n'est pas très pratique dans le cas particulier du cercle : il nous semblera plus naturel de commander ce tracé en fournissant les coordonnées de son centre ainsi que son rayon. C'est ce que nous obtiendrons avec notre fonction cercle()
, laquelle invoque la méthode create_oval()
en effectuant la conversion des coordonnées. Remarquez que cette fonction attend un argument facultatif en ce qui concerne la couleur du cercle à tracer (noir par défaut).
L'efficacité de cette approche apparaît clairement dans la fonction figure_1()
, ou nous trouvons une simple boucle de répétition pour dessiner toute la série de cercles (de même centre et de rayon croissant). Notez au passage l'utilisation de l'opérateur += qui permet d'incrémenter une variable (dans notre exemple, la variable r voit sa valeur augmenter de 15 unités à chaque itération).
Le second dessin est un peu plus complexe, parce qu'il est composé de cercles de tailles variées centrés sur des points différents. Nous pouvons tout de même tracer tous ces cercles à l'aide d'une seule boucle de répétition, si nous mettons à profit nos connaissances concernant les listes.
En effet. Ce qui différencie les cercles que nous voulons tracer tient en quatre caractéristiques : coordonnées x et y du centre, rayon et couleur. Pour chaque cercle, nous pouvons placer ces quatre caractéristiques dans une petite liste, et rassembler toutes les petites listes ainsi obtenues dans une autre liste plus grande. Nous disposerons ainsi d'une liste de listes, qu'il suffira ensuite de parcourir à l'aide d'une boucle pour effectuer les tracés correspondants.
Exercices
- Inspirez-vous du script précédent pour écrire une petite application qui fait apparaître un damier (dessin de cases noires sur fond blanc) lorsque l'on clique sur un bouton :
- À l'application de l'exercice précédent, ajoutez un bouton qui fera apparaître des pions au hasard sur le damier (chaque pression sur le bouton fera apparaître un nouveau pion).
Solution
- Voir ci dessous
-
# Dessin d'un damier, avec placement de pions au hasard from tkinter import * from random import randrange # générateur de nombres aléatoires def damier(): "dessiner dix lignes de carrés avec décalage alterné" y = 0 while y < 10: if y % 2 == 0: # une fois sur deux, on x = 0 # commencera la ligne de else: # carrés avec un décalage x = 1 # de la taille d'un carré ligne_de_carres(x*c, y*c) y += 1 def ligne_de_carres(x, y): "dessiner une ligne de carrés, en partant de x, y" i = 0 while i < 10: can.create_rectangle(x, y, x+c, y+c, fill='navy') i += 1 x += c*2 # espacer les carrés def cercle(x, y, r, coul): "dessiner un cercle de centre x,y et de rayon r" can.create_oval(x-r, y-r, x+r, y+r, fill=coul) def ajouter_pion(): "dessiner un pion au hasard sur le damier" # tirer au hasard les coordonnées du pion : x = c/2 + randrange(10) * c y = c/2 + randrange(10) * c cercle(x, y, c/3, 'red') ##### Programme principal : ############ # Tâchez de bien "paramétrer" vos programmes, comme nous l'avons # fait dans ce script. Celui-ci peut en effet tracer des damiers # de n'importe quelle taille en changeant seulement la valeur # d'une seule variable, à savoir la dimension des carrés : c = 30 # taille des carrés fen = Tk() can = Canvas(fen, width =c*10, height =c*10, bg ='ivory') can.pack(side =TOP, padx =5, pady =5) b1 = Button(fen, text ='damier', command =damier) b1.pack(side =LEFT, padx =3, pady =3) b2 = Button(fen, text ='pions', command =ajouter_pion) b2.pack(side =RIGHT, padx =3, pady =3) fen.mainloop()#
Exemple graphique : calculatrice minimaliste
modifierBien que très court, le petit script ci-dessous implémente une calculatrice complète, avec laquelle vous pourrez même effectuer des calculs comportant des parenthèses et des fonctions scientifiques. N'y voyez rien d'extraordinaire. Toute cette fonctionnalité n'est qu'une conséquence du fait que vous utilisez un interpréteur plutôt qu'un compilateur pour exécuter vos programmes.
Comme vous le savez, le compilateur n'intervient qu'une seule fois, pour traduire l'ensemble de votre code source en un programme exécutable. Son rôle est donc terminé avant même l'exécution du programme. L'interpréteur, quant à lui, est toujours actif pendant l'exécution du programme, et donc tout à fait disponible pour traduire un nouveau code source quelconque, comme par exemple une expression mathématique entrée au clavier par l'utilisateur.
Les langages interprétés disposent donc toujours de fonctions permettant d'évaluer une chaîne de caractères comme une suite d'instructions du langage lui-même. Il devient alors possible de construire en peu de lignes des structures de programmes très dynamiques. Dans l'exemple ci-dessous, nous utilisons la fonction intégrée eval()
pour analyser l'expression mathématique entrée par l'utilisateur dans le champ prévu à cet effet, et nous n'avons plus ensuite qu'à afficher le résultat.
# Exercice utilisant la bibliothèque graphique Tkinter et le module math
from tkinter import *
from math import *
# définition de l'action à effectuer si l'utilisateur actionne
# la touche "enter" alors qu'il édite le champ d'entrée :
def evaluer(event):
chaine.configure(text = "Résultat = " + str(eval(entree.get())))
# ----- Programme principal : -----
fenetre = Tk()
entree = Entry(fenetre)
entree.bind("<Return>", evaluer)
chaine = Label(fenetre)
entree.pack()
chaine.pack()
fenetre.mainloop()
Au début du script, nous commençons par importer les modules Tkinter et math, ce dernier étant nécessaire afin que la dite calculatrice puisse disposer de toutes les fonctions mathématiques et scientifiques usuelles : sinus, cosinus, racine carrée, etc.
Ensuite nous définissons une fonction evaluer()
, qui sera en fait la commande exécutée par le programme lorsque l'utilisateur actionnera la touche Return (ou Enter) après avoir entré une expression mathématique quelconque dans le champ d'entrée décrit plus loin.
Cette fonction utilise la méthode configure()
du widget chaine
[6], pour modifier son attribut text. L'attribut en question reçoit donc ici une nouvelle valeur, déterminée par ce que nous avons écrit à la droite du signe égale : il s'agit en l'occurrence d'une chaîne de caractères construite dynamiquement, à l'aide de deux fonctions intégrées dans Python : eval()
et str()
, et d'une méthode associée à un widget Tkinter : la méthode get()
.
eval()
fait appel à l'interpréteur pour évaluer une expression Python qui lui est transmise dans une chaîne de caractères. Le résultat de l'évaluation est fourni en retour. Exemple :
chaine = "(25 + 8)/3" # chaîne contenant une expression mathématique
res = eval(chaine) # évaluation de l'expression contenue dans la chaîne
print res +5 # => le contenu de la variable res est numérique
str()
transforme une expression numérique en chaîne de caractères. Nous devons faire appel à cette fonction parce que la précédente renvoie une valeur numérique, que nous convertissons à nouveau en chaîne de caractères pour pouvoir l'incorporer au message « Résultat = ».
get()
est une méthode associée aux widgets de la classe Entry. Dans notre petit programme exemple, nous utilisons un widget de ce type pour permettre à l'utilisateur d'entrer une expression numérique quelconque à l'aide de son clavier. La méthode get()
permet en quelque sorte « d'extraire » du widget « entree » la chaîne de caractères qui lui a été fournie par l'utilisateur.
Le corps du programme principal contient la phase d'initialisation, qui se termine par la mise en route de l'observateur d'événements (mainloop). On y trouve l'instanciation d'une fenêtre Tk()
, contenant un widget « chaine » instancié à partir de la classe Label()
, et un widget « entree » instancié à partir de la classe Entry()
.
Attention, à présent : afin que ce dernier widget puisse vraiment faire son travail, c'est-à-dire transmettre au programme l'expression que l'utilisateur y aura encodée, nous lui associons un événement à l'aide de la méthode bind()
[7] :
entree.bind("<Return>",evaluer)
Cette instruction signifie : « Lier l'événement “pression sur la touche Return” à l'objet <entree>, le gestionnaire de cet événement étant la fonction <evaluer> ».
L'événement à prendre en charge est décrit dans une chaîne de caractères spécifique (dans notre exemple, il s'agit de la chaîne <Return>
). Il existe un grand nombre de ces événements (mouvements et clics de la souris, enfoncement des touches du clavier, positionnement et redimensionnement des fenêtres, passage au premier plan, etc.). Vous trouverez la liste des chaînes spécifiques de tous ces événements dans les ouvrages de référence traitant de Tkinter.
Profitons de l'occasion pour observer encore une fois la syntaxe des instructions destinées à mettre en œuvre une méthode associée à un objet :
objet.méthode(arguments)
On écrit d'abord le nom de l'objet sur lequel on désire intervenir, puis le point (qui fait office d'opérateur), puis le nom de la méthode à mettre en œuvre ; entre les parenthèses associées à cette méthode, on indique enfin les arguments qu'on souhaite lui transmettre.
Exemple graphique : détection et positionnement d'un clic de souris
modifierDans la définition de la fonction « evaluer » de l'exemple précédent, vous aurez remarqué que nous avons fourni un argument event (entre les parenthèses).
Cet argument est obligatoire. Lorsque vous définissez une fonction gestionnaire d'événement qui est associée à un widget quelconque à l'aide de sa méthode bind(), vous devez toujours l'utiliser comme premier argument. Il s'agit d'un objet Python standard, créé automatiquement, qui permet de transmettre au gestionnaire d'événement un certain nombre d'attributs de cet événement :
- le type d'événement : déplacement de la souris, enfoncement ou relâchement de l'un de ses boutons, appui sur une touche du clavier, entrée du curseur dans une zone prédéfinie, ouverture ou fermeture d'une fenêtre, etc.
- une série de propriétés de l'événement : l'instant où il s'est produit, ses coordonnées, les caractéristiques du ou des widget(s) concerné(s), etc.
Nous n'allons pas entrer dans trop de détails. Si vous voulez bien encoder et expérimenter le petit script ci-dessous, vous aurez vite compris le principe.
# Détection et positionnement d'un clic de souris dans une fenêtre :
from tkinter import *
def pointeur(event):
chaine.configure(text = "Clic détecté en X =" + str(event.x) +\
", Y =" + str(event.y))
fen = Tk()
cadre = Frame(fen, width =200, height =150, bg="light yellow")
cadre.bind("<Button-1>", pointeur)
cadre.pack()
chaine = Label(fen)
chaine.pack()
fen.mainloop()
Le script fait apparaître une fenêtre contenant un cadre
(frame) rectangulaire de couleur jaune pâle, dans lequel l'utilisateur est invité à effectuer des clics de souris.
La méthode bind()
du widget cadre associe l'événement <clic à l'aide du premier bouton de la souris> au gestionnaire d'événement « pointeur ».
Ce gestionnaire d'événement peut utiliser les attributs x et y de l'objet event généré automatiquement par Python, pour construire la chaîne de caractères qui affichera la position de la souris au moment du clic.
Exercices
- Modifiez le script ci-dessus de manière à faire apparaître un petit cercle rouge à l'endroit où l'utilisateur a effectué son clic (vous devrez d'abord remplacer le widget Frame par un widget Canvas).
Solution
Solution
def avance(event):
can1.coords(oval,event.x-r,event.y-r,event.x+r,event.y+r)
x,y,r=10,10,10
def oval(x,y,r,coul):
can1.create_oval(x-r,y-r,x+r,y+r,outline=coul)
fen1=Tk() fen1.title("Exercice d'animation avec Tkinter")
can1=Canvas(fen1,bg='dark grey',height=300,width=300) oval=can1.create_oval(x,y,x+30,y+30,width=2,fill='red') can1.bind("<Button-1>",avance) can1.pack() can1.pack(side=LEFT) Button(fen1,text='Quitter',command=fen1.quit).pack(side=BOTTOM) fen1.mainloop() fen1.destroy()
Les classes de widgets Tkinter
modifierIl existe 15 classes de base pour les widgets Tkinter :
Widget | Description |
Button | Un bouton classique, à utiliser pour provoquer l'exécution d'une commande quelconque. |
Canvas | Un espace pour disposer divers éléments graphiques. Ce widget peut être utilisé pour dessiner, créer des éditeurs graphiques, et aussi pour implémenter des widgets personnalisés. |
Checkbutton | Une « case à cocher » qui peut prendre deux états distincts (la case est cochée ou non). Un clic sur ce widget provoque le changement d'état. |
Entry | Un champ d'entrée, dans lequel l'utilisateur du programme pourra insérer un texte quelconque à partir du clavier. |
Frame | Une surface rectangulaire dans la fenêtre, où l'on peut disposer d'autres widgets. Cette surface peut être colorée. Elle peut aussi être décorée d'une bordure. |
Label | Un texte (ou libellé) quelconque (éventuellement une image). |
Listbox | Une liste de choix proposés à l'utilisateur, généralement présentés dans une sorte de boîte. On peut également configurer la Listbox de telle manière qu'elle se comporte comme une série de « boutons radio » ou de cases à cocher. |
Menu | Un menu. Ce peut être un menu déroulant attaché à la barre de titre, ou bien un menu « popup » apparaissant n'importe où à la suite d'un clic. |
Menubutton | Un bouton-menu, à utiliser pour implémenter des menus déroulants. |
Message | Permet d'afficher un texte. Ce widget est une variante du widget Label, qui permet d'adapter automatiquement le texte affiché à une certaine taille ou à un certain rapport largeur/hauteur. |
Radiobutton | Représente (par un point noir dans un petit cercle) une des valeurs d'une variable qui peut en posséder plusieurs. Cliquer sur un « bouton radio » donne la valeur correspondante à la variable, et "vide" tous les autres boutons radio associés à la même variable. |
Scale | Vous permet de faire varier de manière très visuelle la valeur d'une variable, en déplaçant un curseur le long d'une règle. |
Scrollbar | « ascenseur » ou « barre de défilement » que vous pouvez utiliser en association avec les autres widgets : Canvas, Entry, Listbox, Text. |
Text | Affichage de texte formaté. Permet aussi à l'utilisateur d'éditer le texte affiché. Des images peuvent également être insérées. |
Toplevel | Une fenêtre affichée séparément, « par-dessus ». |
Ces classes de widgets intègrent chacune un grand nombre de méthodes et on peut aussi leur associer (lier) des événements. En outre, tous ces widgets peuvent être positionnés dans les fenêtres à l'aide de trois méthodes différentes : la méthode grid()
, la méthode pack()
et la méthode place()
.
L'utilité de ces méthodes apparaît clairement lorsque l'on s'efforce de réaliser des programmes portables (c'est-à-dire susceptibles de fonctionner indifféremment sur des systèmes d'exploitation aussi différents que Unix, MacOS ou Windows), et dont les fenêtres soient redimensionnables.
Utilisation de la méthode grid() pour contrôler la disposition des widgets
modifierJusqu'à présent, nous avons toujours disposé les widgets dans leur fenêtre, à l'aide de la méthode pack()
. Cette méthode présentait l'avantage d'être extraordinairement simple, mais elle ne nous donnait pas beaucoup de liberté pour disposer les widgets à notre guise. Comment faire, par exemple, pour obtenir la fenêtre ci-contre ?
Nous pourrions effectuer un certain nombre de tentatives en fournissant à la méthode pack()
des arguments de type « side = », comme nous l'avons déjà fait précédemment, mais cela ne nous mène pas très loin. Essayons par exemple :
from tkinter import *
fen1 = Tk()
txt1 = Label(fen1, text = 'Premier champ :')
txt2 = Label(fen1, text = 'Second :')
entr1 = Entry(fen1)
entr2 = Entry(fen1)
txt1.pack(side =LEFT)
txt2.pack(side =LEFT)
entr1.pack(side =RIGHT)
entr2.pack(side =RIGHT)
fen1.mainloop()
... mais le résultat n'est pas vraiment celui que nous recherchions !!! :
Pour mieux comprendre comment fonctionne la méthode pack()
, vous pouvez encore essayer différentes combinaisons d'options, telles que side =TOP
, side =BOTTOM
, pour chacun de ces quatre widgets. Mais vous n'arriverez certainement pas à obtenir ce qui vous a été demandé. Vous pourriez peut-être y parvenir en définissant deux widgets Frame()
supplémentaires, et en y incorporant ensuite séparément les widgets Label()
et Entry()
. Cela devient fort compliqué.
Il est temps que nous apprenions à utiliser une autre approche du problème. Veuillez donc analyser le script ci-dessous : il contient en effet (presque) la solution :
from tkinter import *
fen1 = Tk()
txt1 = Label(fen1, text = 'Premier champ :')
txt2 = Label(fen1, text = 'Second :')
entr1 = Entry(fen1)
entr2 = Entry(fen1)
txt1.grid(row =0)
txt2.grid(row =1)
entr1.grid(row =0, column =1)
entr2.grid(row =1, column =1)
fen1.mainloop()
Dans ce script, nous avons donc remplacé la méthode pack()
par la méthode grid()
. Comme vous pouvez le constater, l'utilisation de la méthode grid()
est très simple. Cette méthode considère la fenêtre comme un tableau (ou une grille). Il suffit alors de lui indiquer dans quelle ligne (row) et dans quelle colonne (column) de ce tableau on souhaite placer les widgets. On peut numéroter les lignes et les colonnes comme on veut, en partant de zéro, ou de un, ou encore d'un nombre quelconque : Tkinter ignorera les lignes et colonnes vides. Notez cependant que si vous ne fournissez aucun numéro pour une ligne ou une colonne, la valeur par défaut sera zéro.
Tkinter détermine automatiquement le nombre de lignes et de colonnes nécessaire. Mais ce n'est pas tout : si vous examinez en détail la petite fenêtre produite par le script ci-dessus, vous constaterez que nous n'avons pas encore tout à fait atteint le but poursuivi. Les deux chaînes apparaissant dans la partie gauche de la fenêtre sont centrées, alors que nous souhaitions les aligner l'une et l'autre par la droite. Pour obtenir ce résultat, il nous suffit d'ajouter un argument dans l'appel de la méthode grid()
utilisée pour ces widgets. L'option sticky
peut prendre l'une des quatre valeurs N
, S
, W
, E
(les quatre points cardinaux en anglais). En fonction de cette valeur, on obtiendra un alignement des widgets par le haut, par le bas, par la gauche ou par la droite. Remplacez donc les deux premières instructions grid()
du script par :
txt1.grid(row =0, sticky =E)
txt2.grid(row =1, sticky =E)
… et vous atteindrez enfin exactement le but recherché.
Analysons à présent la fenêtre suivante :
modifierCette fenêtre comporte 3 colonnes : une première avec les 3 chaînes de caractères, une seconde avec les 3 champs d'entrée, et une troisième avec l'image. Les deux premières colonnes comportent chacune 3 lignes, mais l'image située dans la dernière colonne s'étale en quelque sorte sur les trois.
Le code correspondant est le suivant :
from tkinter import *
fen1 = Tk()
# création de widgets 'Label' et 'Entry' :
txt1 = Label(fen1, text ='Premier champ :')
txt2 = Label(fen1, text ='Second :')
txt3 = Label(fen1, text ='Troisième :')
entr1 = Entry(fen1)
entr2 = Entry(fen1)
entr3 = Entry(fen1)
# création d'un widget 'Canvas' contenant une image bitmap :
can1 = Canvas(fen1, width =160, height =160, bg ='white')
photo = PhotoImage(file ='Martin_P.gif')
item = can1.create_image(80, 80, image =photo)
# Mise en page à l'aide de la méthode 'grid' :
txt1.grid(row =1, sticky =E)
txt2.grid(row =2, sticky =E)
txt3.grid(row =3, sticky =E)
entr1.grid(row =1, column =2)
entr2.grid(row =2, column =2)
entr3.grid(row =3, column =2)
can1.grid(row =1, column =3, rowspan =3, padx =10, pady =5)
# démarrage :
fen1.mainloop()
Pour pouvoir faire fonctionner ce script, il vous faudra probablement remplacer le nom du fichier image (Martin_P.gif) par le nom d'une image de votre choix. Attention : la bibliothèque Tkinter standard n'accepte qu'un petit nombre de formats pour cette image. Choisissez de préférence le format GIF.
Nous pouvons remarquer un certain nombre de choses dans ce script :
modifier- La technique utilisée pour incorporer une image :
Tkinter ne permet pas d'insérer directement une image dans une fenêtre. Il faut d'abord installer un canevas, et ensuite positionner l'image dans celui-ci. Nous avons opté pour un canevas de couleur blanche, afin de pouvoir le distinguer de la fenêtre. Vous pouvez remplacer le paramètrebg ='white'
parbg ='gray'
si vous souhaitez que le canevas devienne invisible. Étant donné qu'il existe de nombreux types d'images, nous devons en outre déclarer l'objet image comme étant un bitmap GIF, à partir de la classePhotoImage()
[8]. - La ligne où nous installons l'image dans le canevas est la ligne :
item = can1.create_image(80, 80, image =photo)
Pour employer un vocabulaire correct, nous dirons que nous utilisons ici la méthodecreate_image()
associée à l'objetcan1
(lequel objet est lui-même une instance de la classeCanvas
). Les deux premiers arguments transmis (80
,80
) indiquent les coordonnées x et y du canevas où il faut placer le centre de l'image. (Les dimensions du canevas étant de 160x160, notre choix aboutira donc à un centrage de l'image au milieu du canevas). - La numérotation des lignes et colonnes dans la méthode grid() :
On peut constater que la numérotation des lignes et des colonnes dans la méthode
grid()
utilisée ici commence cette fois à partir de 1 (et non à partir de zéro comme dans le script précédent). Comme nous l'avons déjà signalé plus haut, ce choix de numérotation est tout à fait libre.
On pourrait tout aussi bien numéroter : 5, 10, 15, 20... puisque Tkinter ignore les lignes et les colonnes vides. Numéroter à partir de l augmente probablement la lisibilité de notre code. - Les arguments utilisés avec
grid()
pour positionner le canevas :
can1.grid(row =1, column =3, rowspan =3, padx =10, pady =5)
Les deux premiers arguments indiquent que le canevas sera placé dans la première ligne de la troisième colonne. Le troisième (rowspan =3
) indique qu'il pourra « s'étaler » sur trois lignes.
Les deux derniers (padx =10, pady =5) indiquent la dimension de l'espace qu'il faut réserver autour de ce widget (en largeur et en hauteur). - Et tant que nous y sommes, profitons de cet exemple de script que nous avons déjà bien décortiqué, pour apprendre à simplifier quelque peu notre code…
Composition d'instructions pour écrire un code plus compact
modifierDu fait que Python est un langage de programmation de haut niveau, il est souvent possible (et souhaitable) de retravailler un script afin de le rendre plus compact.
Vous pouvez par exemple assez fréquemment utiliser la composition d'instructions pour appliquer la méthode de mise en page des widgets (grid(), pack() ou place()) au moment même où vous créez ces widgets. Le code correspondant devient alors un peu plus simple, et parfois plus lisible. Vous pouvez par exemple remplacer les deux lignes :
txt1 = Label(fen1, text ='Premier champ :')
txt1.grid(row =1, sticky =E)
du script précédent par une seule, telle que :
Label(fen1, text ='Premier champ :').grid(row =1, sticky =E)
Dans cette nouvelle écriture, vous pouvez constater que nous faisons l'économie de la variable intermédiaire txt1
. Nous avions utilisé cette variable pour bien dégager les étapes successives de notre démarche, mais elle n'est pas toujours indispensable. Le simple fait d'invoquer la classe Label()
provoque en effet l'instanciation d'un objet de cette classe, même si l'on ne mémorise pas la référence de cet objet dans une variable (Tkinter la conserve de toute façon dans sa représentation interne de la fenêtre). Si l'on procède ainsi, la référence est perdue pour le restant du script, mais elle peut tout de même être transmise à une méthode de mise en page telle que grid()
au moment même de l'instanciation, en une seule instruction composée. Voyons cela un peu plus en détail :
Jusqu'à présent, nous avons créé des objets divers (par instanciation à partir d'une classe quelconque), en les affectant à chaque fois à des variables. Par exemple, lorsque nous avons écrit :
txt1 = Label(fen1, text ='Premier champ :')
Nous avons créé une instance de la classe Label()
, que nous avons assignée à la variable txt1
.
La variable txt1
peut alors être utilisée pour faire référence à cette instance, partout ailleurs dans le script, mais dans les faits nous ne l'utilisons qu'une seule fois pour lui appliquer la méthode grid()
, le widget dont il est question n'étant rien d'autre qu'une simple étiquette descriptive. Or, créer ainsi une nouvelle variable pour n'y faire référence ensuite qu'une seule fois (et directement après sa création) n'est pas une pratique très recommandable, puisqu'elle consiste à réserver inutilement un certain espace mémoire.
Lorsque ce genre de situation se présente, il est plus judicieux d'utiliser la composition d'instructions. Par exemple, on préférera le plus souvent remplacer les deux instructions :
somme = 45 + 72
print somme
par une seule instruction composée, telle que :
print 45 + 72
On fait ainsi l'économie d'une variable.
De la même manière, lorsque l'on met en place des widgets auxquels on ne souhaite plus revenir par après, comme c'est souvent le cas pour les widgets de la classe Label()
, on peut en général appliquer la méthode de mise en page (grid()
, pack()
ou place()
) directement au moment de la création du widget, en une seule instruction composée.
Cela s'applique seulement aux widgets qui ne sont plus référencés après qu'on les ait créés. Tous les autres doivent impérativement être assignés à des variables, afin que l'on puisse encore interagir avec eux ailleurs dans le script.
Et dans ce cas, il faut obligatoirement utiliser deux instructions distinctes, l'une pour instancier le widget et l'autre pour lui appliquer ensuite la méthode de mise en page. Vous ne pouvez pas, par exemple, construire une instruction composée telle que :
entree = Entry(fen1).pack() # faute de programmation !!!
En apparence, cette instruction devrait instancier un nouveau widget et l'assigner à la variable entree, la mise en page s'effectuant dans la même opération à l'aide de la méthode pack()
.
Dans la réalité, cette instruction produit bel et bien un nouveau widget de la classe Entry()
, et la méthode pack()
effectue bel et bien sa mise en page dans la fenêtre, mais la valeur qui est mémorisée dans la variable entree est la valeur de retour de la méthode pack()
: ce n'est pas la référence du widget. Et vous ne pouvez rien faire de cette valeur de retour : il s'agit d'un objet vide : None
.
Pour obtenir une vraie référence du widget, vous devez utiliser deux instructions :
entree = Entry(fen1) # instanciation du widget
entree.pack() # application de la mise en page
Note : Lorsque vous utilisez la méthode grid()
, vous pouvez simplifier encore un peu votre code, en omettant l'indication de nombreux numéros de lignes et de colonnes. À partir du moment où c'est la la méthode grid()
qui est utilisée pour positionner les widgets, Tkinter considère en effet qu'il existe forcément des lignes et des colonnes11. Si un numéro de ligne ou de colonne n'est pas indiqué, le widget correspondant est placé dans la première case vide disponible.
Le script ci-dessous intègre les simplifications que nous venons d'expliquer :
from tkinter import *
fen1 = Tk()
# création de widgets Label(), Entry(), et Checkbutton() :
Label(fen1, text = 'Premier champ :').grid(sticky =E)
Label(fen1, text = 'Second :').grid(sticky =E)
Label(fen1, text = 'Troisième :').grid(sticky =E)
entr1 = Entry(fen1)
entr2 = Entry(fen1) # ces widgets devront certainement
entr3 = Entry(fen1) # être référencés plus loin :
entr1.grid(row =0, column =1) # il faut donc les assigner chacun
entr2.grid(row =1, column =1) # à une variable distincte
entr3.grid(row =2, column =1)
chek1 = Checkbutton(fen1, text ='Case à cocher, pour voir')
chek1.grid(columnspan =2)
# création d'un widget 'Canvas' contenant une image bitmap :
can1 = Canvas(fen1, width =160, height =160, bg ='white')
photo = PhotoImage(file ='Martin_P.gif')
can1.create_image(80,80, image =photo)
can1.grid(row =0, column =2, rowspan =4, padx =10, pady =5)
# démarrage :
fen1.mainloop()
Modification des propriétés d'un objet - Animation
modifierÀ ce stade de votre apprentissage, vous souhaitez certainement pouvoir faire apparaître un petit dessin quelconque dans un canevas, et puis le déplacer à volonté, par exemple à l'aide de boutons.
Veuillez donc écrire, tester, puis analyser le script ci-dessous :
from tkinter import *
# procédure générale de déplacement :
def avance(gd, hb):
global x1, y1
x1, y1 = x1 +gd, y1 +hb
can1.coords(oval1, x1, y1, x1+30, y1+30)
# gestionnaires d'événements :
def depl_gauche():
avance(-10, 0)
def depl_droite():
avance(10, 0)
def depl_haut():
avance(0, -10)
def depl_bas():
avance(0, 10)
#------ Programme principal -------
# les variables suivantes seront utilisées de manière globale :
x1, y1 = 10, 10 # coordonnées initiales
# Création du widget principal ("maître") :
fen1 = Tk()
fen1.title("Exercice d'animation avec Tkinter")
# création des widgets "esclaves" :
can1 = Canvas(fen1,bg='dark grey',height=300,width=300)
oval1 = can1.create_oval(x1,y1,x1+30,y1+30,width=2,fill='red')
can1.pack(side=LEFT)
Button(fen1,text='Quitter',command=fen1.quit).pack(side=BOTTOM)
Button(fen1,text='Gauche',command=depl_gauche).pack()
Button(fen1,text='Droite',command=depl_droite).pack()
Button(fen1,text='Haut',command=depl_haut).pack()
Button(fen1,text='Bas',command=depl_bas).pack()
# démarrage du réceptionnaire d'évènements (boucle principale) :
fen1.mainloop()
Le corps de ce programme reprend de nombreuses éléments connus : nous y créons une fenêtre fen1, dans laquelle nous installons un canevas contenant lui-même un cercle coloré, plus cinq boutons de contrôle. Veuillez remarquer au passage que nous n'instancions pas les widgets boutons dans des variables (c'est inutile, puisque nous n'y faisons plus référence par après) : nous devons donc appliquer la méthode pack()
directement au moment de la création de ces objets.
La vraie nouveauté de ce programme réside dans la fonction avance()
définie au début du script. Chaque fois qu'elle sera appelée, cette fonction redéfinira les coordonnées de l'objet « cercle coloré » que nous avons installé dans le canevas, ce qui provoquera l'animation de cet objet.
Cette manière de procéder est tout à fait caractéristique de la programmation « orientée objet » :
On commence par créer des objets, et puis on agit sur ces objets en modifiant leurs propriétés, par l'intermédiaire de méthodes.
En programmation procédurale « à l'ancienne » (c'est-à-dire sans utilisation d'objets), on anime des figures en les effaçant à un endroit pour les redessiner ensuite un petit peu plus loin. En programmation « orientée objet », par contre, ces tâches sont prises en charge automatiquement par les classes dont les objets dérivent, et il ne faut donc pas perdre son temps à les reprogrammer.
Exercices
- Écrivez un programme qui fait apparaître une fenêtre avec un canevas. Dans ce canevas on verra deux cercles (de tailles et de couleurs différentes), qui sont censés représenter deux astres. Des boutons doivent permettre de les déplacer à volonté tous les deux dans toutes les directions. Sous le canevas, le programme doit afficher en permanence : a) la distance séparant les deux astres ; b) la force gravitationnelle qu'ils exercent l'un sur l'autre (Penser à afficher en haut de fenêtre les masses choisies pour chacun d'eux, ainsi que l'échelle des distances). Dans cet exercice, vous utiliserez évidemment la loi universelle de la gravitation de Newton.
- En vous inspirant du programme qui détecte les clics de souris dans un canevas, modifiez le programme ci-dessus pour y réduire le nombre de boutons : pour déplacer un astre, il suffira de le choisir avec un bouton, et ensuite de cliquer sur le canevas pour que cet astre se positionne à l'endroit où l'on a cliqué.
- Extension du programme ci-dessus. Faire apparaître un troisième astre, et afficher en permanence la force résultante agissant sur chacun des trois (en effet : chacun subit en permanence l'attraction gravitationnelle exercée par les deux autres !).
- Même exercice avec des charges électriques (loi de Coulomb). Donner cette fois une possibilité de choisir le signe des charges.
- Écrivez un petit programme qui fait apparaître une fenêtre avec deux champs : l'un indique une température en degrés Celsius, et l'autre la même température exprimée en degrés Fahrenheit. Chaque fois que l'on change une quelconque des deux températures, l'autre est corrigée en conséquence. Pour convertir les degrés Fahrenheit en Celsius et vice-versa, on utilise la formule . (cf. manuel de Physique). Revoyez aussi le petit programme concernant la calculatrice simplifiée.
- Écrivez un programme qui fasse apparaître une fenêtre avec un canevas. Dans ce canevas, placez un petit cercle censé représenter une balle. Sous le canevas, placez un bouton. Chaque fois que l'on clique sur le bouton, la balle doit avancer d'une petite distance vers la droite, jusqu'à ce qu'elle atteigne l'extrémité du canevas. Si l'on continue à cliquer, la balle doit alors revenir en arrière jusqu'à l'autre extrémité, et ainsi de suite.
- Améliorez le programme ci-dessus pour que la balle décrive cette fois une trajectoire circulaire ou elliptique dans le canevas (lorsque l'on clique continuellement). Note : pour arriver au résultat escompté, vous devrez nécessairement définir une variable qui représentera l'angle décrit, et utiliser les fonctions sinus et cosinus pour positionner la balle en fonction de cet angle.
- Modifiez le programme ci-dessus, de telle manière que la balle en se déplaçant laisse derrière elle une trace de la trajectoire décrite.
- Modifiez le programme ci-dessus de manière à tracer d'autres figures. Consultez votre professeur pour des suggestions (courbes de Lissajous).
- Écrivez un programme qui fasse apparaître une fenêtre avec un canevas et un bouton. Dans le canevas, tracez un rectangle gris foncé, lequel représentera une route, et par-dessus, une série de rectangles jaunes censés représenter un passage pour piétons. Ajoutez quatre cercles colorés pour figurer les feux de circulation concernant les piétons et les véhicules. Chaque utilisation du bouton devra provoquer le changement de couleur des feux :
- Écrivez un programme qui montre un canevas dans lequel est dessiné un circuit électrique simple (générateur + interrupteur + résistance). La fenêtre doit être pourvue de champs d'entrée qui permettront de paramétrer chaque élément (c'est-à-dire choisir les valeurs des résistances et tensions). L'interrupteur doit être fonctionnel (Prévoyez un bouton « Marche/arrêt » pour cela). Des « étiquettes » doivent afficher en permanence les tensions et intensités résultant des choix opérés par l'utilisateur.
Solution
-
# Simulation du phénomène de gravitation universelle from tkinter import * from math import sqrt def distance(x1, y1, x2, y2): "distance séparant les points x1,y1 et x2,y2" d = sqrt((x2-x1)**2 + (y2-y1)**2) # théorème de Pythagore return d def forceG(m1, m2, di): "force de gravitation s'exerçant entre m1 et m2 pour une distance di" return m1*m2*6.67e-11/di**2 # loi de Newton def avance(n, gd, hb): "déplacement de l'astre n, de gauche à droite ou de haut en bas" global x, y, step # nouvelles coordonnées : x[n], y[n] = x[n] +gd, y[n] +hb # déplacement du dessin dans le canevas : can.coords(astre[n], x[n]-10, y[n]-10, x[n]+10, y[n]+10) # calcul de la nouvelle interdistance : di = distance(x[0], y[0], x[1], y[1]) # conversion de la distance "écran" en distance "astronomique" : diA = di*1e9 # (1 pixel => 1 million de km) # calcul de la force de gravitation correspondante : f = forceG(m1, m2, diA) # affichage des nouvelles valeurs de distance et force : valDis.configure(text="Distance = " +str(diA) +" m") valFor.configure(text="Force = " +str(f) +" N") # adaptation du "pas" de déplacement en fonction de la distance : step = di/10 def gauche1(): avance(0, -step, 0) def droite1(): avance(0, step, 0) def haut1(): avance(0, 0, -step) def bas1(): avance(0, 0, step) def gauche2(): avance(1, -step, 0) def droite2(): avance (1, step, 0) def haut2(): avance(1, 0, -step) def bas2(): avance(1, 0, step) # Masses des deux astres : m1 = 6e24 # (valeur de la masse de la terre, en kg) m2 = 6e24 # astre = [0]*2 # liste servant à mémoriser les références des dessins x =[50., 350.] # liste des coord. X de chaque astre (à l'écran) y =[100., 100.] # liste des coord. Y de chaque astre step =10 # "pas" de déplacement initial # Construction de la fenêtre : fen = Tk() fen.title(' Gravitation universelle suivant Newton') # Libellés : valM1 = Label(fen, text="M1 = " +str(m1) +" kg") valM1.grid(row =1, column =0) valM2 = Label(fen, text="M2 = " +str(m2) +" kg") valM2.grid(row =1, column =1) valDis = Label(fen, text="Distance") valDis.grid(row =3, column =0) valFor = Label(fen, text="Force") valFor.grid(row =3, column =1) # Canevas avec le dessin des 2 astres: can = Canvas(fen, bg ="light yellow", width =400, height =200) can.grid(row =2, column =0, columnspan =2) astre[0] = can.create_oval(x[0]-10, y[0]-10, x[0]+10, y[0]+10, fill ="red", width =1) astre[1] = can.create_oval(x[1]-10, y[1]-10, x[1]+10, y[1]+10, fill ="blue", width =1) # 2 groupes de 4 boutons, chacun installé dans un cadre (frame) : fra1 = Frame(fen) fra1.grid(row =4, column =0, sticky =W, padx =10) Button(fra1, text="<-", fg ='red',command =gauche1).pack(side =LEFT) Button(fra1, text="->", fg ='red', command =droite1).pack(side =LEFT) Button(fra1, text="^", fg ='red', command =haut1).pack(side =LEFT) Button(fra1, text="v", fg ='red', command =bas1).pack(side =LEFT) fra2 = Frame(fen) fra2.grid(row =4, column =1, sticky =E, padx =10) Button(fra2, text="<-", fg ='blue', command =gauche2).pack(side =LEFT) Button(fra2, text="->", fg ='blue', command =droite2).pack(side =LEFT) Button(fra2, text="^", fg ='blue', command =haut2).pack(side =LEFT) Button(fra2, text="v", fg ='blue', command =bas2).pack(side =LEFT) fen.mainloop()
- Réfléchissez !
- Réfléchissez !
- Réfléchissez !
-
# Conversions de températures Fahrenheit <=> Celsius from tkinter import * def convFar(event): "valeur de cette température, exprimée en degrés Fahrenheit" tF = eval(champTC.get()) varTF.set(str(tF*1.8 +32)) def convCel(event): "valeur de cette température, exprimée en degrés Celsius" tC = eval(champTF.get()) varTC.set(str((tC-32)/1.8)) fen = Tk() fen.title('Fahrenheit/Celsius') Label(fen, text='Temp. Celsius :').grid(row =0, column =0) # "variable Tkinter" associée au champ d'entrée. Cet "objet-variable" # assure l'interface entre TCL et Python (voir notes, page 165) : varTC =StringVar() champTC = Entry(fen, textvariable =varTC) champTC.bind("<Return>", convFar) champTC.grid(row =0, column =1) # Initialisation du contenu de la variable Tkinter : varTC.set("100.0") Label(fen, text='Temp. Fahrenheit :').grid(row =1, column =0) varTF =StringVar() champTF = Entry(fen, textvariable =varTF) champTF.bind("<Return>", convCel) champTF.grid(row =1, column =1) varTF.set("212.0") fen.mainloop()
- Réfléchissez !
- Voir ci dessous.
- Voir ci dessous.
-
# Cercles et courbes de Lissajous from tkinter import * from math import sin, cos def move(): global ang, x, y # on mémorise les coord. précédentes avant de calculer les nouvelles : xp, yp = x, y # rotation d'un angle de 0.1 radian : ang = ang +.1 # sinus et cosinus de cet angle => coord. d'un point du cercle trigono. x, y = sin(ang), cos(ang) # Variante déterminant une courbe de Lissajous avec f1/f2 = 2/3 : # x, y = sin(2*ang), cos(3*ang) # mise à l'échelle (120 = rayon du cercle, (150,150) = centre du canevas) x, y = x*120 + 150, y*120 + 150 can.coords(balle, x-10, y-10, x+10, y+10) can.create_line(xp, yp, x, y, fill ="blue") ang, x, y = 0., 150., 270. fen = Tk() fen.title('Courbes de Lissajous') can = Canvas(fen, width =300, height=300, bg="white") can.pack() balle = can.create_oval(x-10, y-10, x+10, y+10, fill='red') Button(fen, text='Go', command =move).pack() fen.mainloop()
- Réfléchissez !
- Réfléchissez !
Animation automatique - Récursivité
modifierPour conclure cette première prise de contact avec l'interface graphique Tkinter, voici un dernier exemple d'animation, qui fonctionne cette fois de manière autonome dès qu'on l'a mise en marche.
from tkinter import *
# définition des gestionnaires
# d'événements :
def move():
"déplacement de la balle"
global x1, y1, dx, dy, flag
x1, y1 = x1 +dx, y1 + dy
if x1 >210:
x1, dx, dy = 210, 0, 15
if y1 >210:
y1, dx, dy = 210, -15, 0
if x1 <10:
x1, dx, dy = 10, 0, -15
if y1 <10:
y1, dx, dy = 10, 15, 0
can1.coords(oval1,x1,y1,x1+30,y1+30)
if flag >0:
fen1.after(50,move) # => boucler après 50 millisecondes
def stop_it():
"arret de l'animation"
global flag
flag =0
def start_it():
"démarrage de l'animation"
global flag
if flag ==0: # pour ne lancer qu'une seule boucle
flag =1
move()
#========== Programme principal =============
# les variables suivantes seront utilisées de manière globale :
x1, y1 = 10, 10 # coordonnées initiales
dx, dy = 15, 0 # 'pas' du déplacement
flag =0 # commutateur
# Création du widget principal ("parent") :
fen1 = Tk()
fen1.title("Exercice d'animation avec Tkinter")
# création des widgets "enfants" :
can1 = Canvas(fen1,bg='dark grey',height=250, width=250)
can1.pack(side=LEFT, padx =5, pady =5)
oval1 = can1.create_oval(x1, y1, x1+30, y1+30, width=2, fill='red')
bou1 = Button(fen1,text='Quitter', width =8, command=fen1.quit)
bou1.pack(side=BOTTOM)
bou2 = Button(fen1, text='Démarrer', width =8, command=start_it)
bou2.pack()
bou3 = Button(fen1, text='Arrêter', width =8, command=stop_it)
bou3.pack()
# démarrage du réceptionnaire d'évènements (boucle principale) :
fen1.mainloop()
La seule nouveauté mise en œuvre dans ce script se trouve tout à la fin de la définition de la fonction move()
: vous y noterez l'utilisation de la méthode after()
. Cette méthode peut s'appliquer à un widget quelconque. Elle déclenche l'appel d'une fonction après qu'un certain laps de temps se soit écoulé. Ainsi par exemple, window.after(200,qqc)
déclenche pour le widget window un appel de la fonction qqc() après une pause de 200 millisecondes.
Dans notre script, la fonction qui est appelée par la méthode after()
est la fonction move()
elle-même. Nous utilisons donc ici pour la première fois une technique de programmation très puissante, que l'on appelle récursivité. Pour faire simple, nous dirons que la récursivité est ce qui se passe lorsqu'une fonction s'appelle elle-même. On obtient bien évidemment ainsi un bouclage, qui peut se perpétuer indéfiniment si l'on ne prévoit pas aussi un moyen pour l'interrompre.
Voyons comment cela fonctionne dans notre exemple :
La fonction move()
est invoquée une première fois lorsque l'on clique sur le bouton « Démarrer ». Elle effectue son travail (c'est-à-dire positionner la balle), puis elle s'invoque elle-même après une petite pause. Elle repart donc pour un second tour, puis s'invoque elle-même à nouveau, et ainsi de suite indéfiniment…
C'est du moins ce qui se passerait si nous n'avions pas pris la précaution de placer quelque part dans la boucle une instruction de sortie. En l'occurrence, il s'agit d'un simple test conditionnel : à chaque itération de la boucle, nous examinons le contenu de la variable flag
à l'aide d'une instruction if
. Si le contenu de la variable flag
est zéro, alors le bouclage ne s'effectue plus et l'animation s'arrête. flag
étant une variable globale, nous pouvons aisément changer sa valeur à l'aide d'autres fonctions, celles que nous avons associées aux boutons « Démarrer » et « Arrêter ».
Nous obtenons ainsi un mécanisme simple pour lancer ou arrêter notre animation :
Un premier clic sur le bouton « Démarrer » assigne une valeur non-nulle à la variable flag
, puis provoque immédiatement un premier appel de la fonction move()
. Celle-ci s'exécute et continue ensuite à s'appeler elle-même toutes les 50 millisecondes, tant que flag
ne revient pas à zéro. Si l'on continue à cliquer sur le bouton « Démarrer », la fonction move()
ne peut plus être appelée tant que la valeur de flag
vaut 1. On évite ainsi le démarrage de plusieurs boucles concurrentes.
Le bouton « Arrêter » remet flag
à zéro, et la boucle s'interrompt.
Exercices
- Dans la fonction start_it(), supprimez l'instruction if flag == 0: (et l'indentation des deux lignes suivantes). Que se passe-t-il ? (Cliquez plusieurs fois sur le bouton « Démarrer »).
Tâchez d'exprimer le plus clairement possible votre explication des faits observés. - Modifiez le programme de telle façon que la balle change de couleur à chaque « virage ».
- Modifiez le programme de telle façon que la balle effectue des mouvements obliques comme une bille de billard qui rebondit sur les bandes (« en zigzag »).
- Modifiez le programme de manière à obtenir d'autres mouvements. Tâchez par exemple d'obtenir un mouvement circulaire.
- Modifiez ce programme, ou bien écrivez-en un autre similaire, de manière à simuler le mouvement d'une balle qui tombe (sous l'effet de la pesanteur), et rebondit sur le sol. Attention : il s'agit cette fois de mouvements accélérés !
- À partir des scripts précédents, vous pouvez à présent écrire un programme de jeu fonctionnant de la manière suivante :
Une balle se déplace au hasard sur un canevas, à vitesse faible. Le joueur doit essayer de cliquer sur cette balle à l'aide de la souris. S'il y arrive, il gagne un point mais la balle se déplace désormais un peu plus vite, et ainsi de suite. Arrêter le jeu après un certain nombre de clics et afficher le score atteint. - Variante du jeu précédent : chaque fois que le joueur parvient à « l'attraper », la balle devient plus petite (elle peut également changer de couleur).
- Écrivez un programme dans lequel évoluent plusieurs balles de couleurs différentes, qui rebondissent les unes sur les autres ainsi que sur les parois.
- Perfectionnez le jeu des précédents exercices en y intégrant l'algorithme ci-dessus. Il s'agit à présent pour le joueur de cliquer seulement sur la balle rouge. Un clic erroné (sur une balle d'une autre couleur) lui fait perdre des points.
- Écrivez un programme qui simule le mouvement de 2 planètes tournant autour du soleil sur des orbites circulaires différentes (ou deux électrons tournant autour d'un noyau d'atome...).
- Écrivez un programme pour le jeu du serpent : un « serpent » (constitué en faite d'une courte ligne de carrés) se déplace sur le canevas dans l'une des 4 directions : droite, gauche, haut, bas. Le joueur peut à tout moment changer la direction suivie par le serpent à l'aide des touches fléchées du clavier. Sur le canevas se trouvent également des « proies » (des petits cercles fixes disposés au hasard). Il faut diriger le serpent de manière à ce qu'il « mange » les proies sans arriver en contact avec les bords du canevas. À chaque fois qu'une proie est mangée, le serpent s'allonge d'un carré, le joueur gagne un point, et une nouvelle proie apparaît ailleurs. La partie s'arrête lorsque le serpent touche l'une des parois, ou lorsqu'il a atteint une certaine taille.
- Perfectionnement du jeu précédent : la partie s'arrête également si le serpent « se recoupe ».
Solution
- Réfléchissez !
- Réfléchissez !
- Réfléchissez !
- Réfléchissez !
-
# Chutes et rebonds from tkinter import * def move(): global x, y, v, dx, dv, flag xp, yp = x, y # mémorisation des coordonnées précédentes # déplacement horizontal : if x > 385 or x < 15 : # rebond sur les parois latérales : dx = -dx # on inverse le déplacement x = x + dx # variation de la vitesse verticale (toujours vers le bas): v = v + dv # déplacement vertical (proportionnel à la vitesse) y = y + v if y > 240: # niveau du sol à 240 pixels : y = 240 # défense d'aller + loin ! v = -v # rebond : la vitesse s'inverse # on repositionne la balle : can.coords(balle, x-10, y-10, x+10, y+10) # on trace un bout de trajectoire : can.create_line(xp, yp, x, y, fill ='light grey') # ... et on remet ça jusqu'à plus soif : if flag > 0: fen.after(50,move) def start(): global flag flag = flag +1 if flag == 1: move() def stop(): global flag flag =0 # initialisation des coordonnées, des vitesses et du témoin d'animation : x, y, v, dx, dv, flag = 15, 15, 0, 6, 5, 0 fen = Tk() fen.title(' Chutes et rebonds') can = Canvas(fen, width =400, height=250, bg="white") can.pack() balle = can.create_oval(x-10, y-10, x+10, y+10, fill='red') Button(fen, text='Start', command =start).pack(side =LEFT, padx =10) Button(fen, text='Stop', command =stop).pack(side =LEFT) Button(fen, text='Quitter', command =fen.quit).pack(side =RIGHT, padx =10) fen.mainloop()
- Réfléchissez !
- Réfléchissez !
- Réfléchissez !
- Réfléchissez !
- Réfléchissez !
- Réfléchissez !
- Réfléchissez !
Notes
modifier- ↑ widget est le résultat de la contraction de l'expression window gadget. Dans certains environnements de programmation, on appellera cela plutôt un « contrôle » ou un « composant graphique ». Ce terme désigne en fait toute entité susceptible d'être placée dans une fenêtre d'application, comme par exemple un bouton, une case à cocher, une image, etc., et parfois aussi la fenêtre elle-même.
- ↑ Si vous effectuez cet exercice sous Windows, nous vous conseillons d'utiliser de préférence une version standard de Python dans une fenêtre DOS ou dans IDLE plutôt que PythonWin. Vous pourrez mieux observer ce qui se passe après l'entrée de chaque commande.
- ↑ Cette concision du langage est une conséquence du typage dynamique des variables en vigueur sous Python. D'autres langages utilisent une instruction particulière (telle que
new
) pour instancier un nouvel objet. Exemple :
maVoiture = new Cadillac
(instanciation d'un objet de classeCadillac
, référencé dans la variablemaVoiture
) - ↑ Ces messages sont souvent notés WM (Window messages) dans un environnement graphique constitué de fenêtres (avec de nombreuses zones réactives : boutons, cases à cocher, menus déroulants, etc.). Dans la description des algorithmes, il arrive fréquemment aussi qu'on confonde ces messages avec les événements eux-mêmes.
- ↑ Au sens strict, une telle fonction qui ne devra renvoyer aucune valeur est donc plutôt une procédure.
- ↑ La méthode
configure()
peut s'appliquer à n'importe quel widget préexistant, pour en modifier les propriétés. - ↑ En anglais, le mot bind signifie « lier »
- ↑ 1.Il existe d'autres classes d'images, mais pour les utiliser il faut importer dans le script d'autres modules graphiques que la seule bibliothèque Tkinter. Vous pouvez par exemple expérimenter la bibliothèque PIL (Python Imaging Library).
Tkinter
Créer des interfaces python avec Tkinter
modifierQue faut-il posséder d'abord ?
modifierTkinter (pour Tool kit interface) est une boîte à outils d'interface graphique pour Python.
L'interface Tkinter s'installe avec Python. Il suffit donc juste d'installer Python 2.3 ou 2.4 ou supérieur pour pouvoir utiliser Tkinter. Sinon :
apt-get install python-tk
Ensuite il vous suffit d'importer la bibliothèque dans votre programme :
from tkinter import *
Principes de base
modifierCréez un fichier texte dans le dossier python 2.4 ou python 2.3.
Donnez-lui le nom que vous voulez, mais pour que votre fichier puisse être interprété par python, il doit porter l'extension .py ou .pyw
Créez donc un fichier du type : monfichier.py (dans ce cas, la console s'affichera, ce qui peut être pratique pour suivre le fonctionnement du programme)
ou monfichier.pyw (dans ce cas la console ne s'ouvrira pas : c'est ce type d'extension qu'il faut utiliser pour la version définitive du programme).
Pour modifier le programme :
- clic droit sur le fichier
- ouvrir avec un logiciel d'édition de texte pur.
Créer une fenêtre pour l'application
modifierPropriétés et méthodes de l'objet fenêtre
modifierle programme est le suivant :
from tkinter import * # le programme va aller chercher toutes les fonctions de la bibliothèque Tkinter
Fenetre= Tk() # vous pouvez choisir le nom que vous voulez pour votre fenêtre
Fenetre.mainloop() # lance la boucle principale
Qu'est-ce qu'un widget ?
modifierwidget : contraction de l'anglais windows gadget (gadget fenêtre). Les widgets sont tous les objets graphiques que l'on peut insérer dans une interface (fenêtre). Les principaux sont :
- Les boutons : Button (pour commander une action)
- Les labels : Label (pour insérer un texte)
- Les zones de saisie : Entry (pour permettre l'entrée d'une donnée)
- Les canevas : Canvas (pour insérer des dessins)
Chaque widget a des propriétés et des méthodes qui permettent de régler son apparence et les interactions avec l'utilisateur.
Le widget Button
modifierChaque widget (objet d'interface) doit être créé puis placé dans la fenêtre
#!/usr/bin/python # Emplacement de l’interpréteur Python (sous Linux)
# -*- coding: utf-8 -*- # Définition l'encodage des caractères
from tkinter import * # le programme va aller chercher toutes les fonctions de la bibliothèque Tkinter
Fenetre= Tk() # création de la fenêtre, avec un nom de votre choix
bouton=Button(Fenetre, text="quitter", command=Fenetre.destroy) # Bouton qui détruit la fenêtre
bouton.pack() # insère le bouton dans la fenêtre
Fenetre.mainloop() # lance la boucle principale
- La commande Fenetre.destroy() est une méthode de destruction qui s'applique à l'objet fenêtre. La pression du bouton a donc pour conséquence la fermeture de la fenêtre Fenetre
propriétés et méthodes de l'objet "bouton"
modifier- Ce qui sera affiché sur votre bouton est contenu dans la propriété "text", passée ici en paramètre de l'objet "bouton".
- La procédure "command" permet une action lorsqu'on clique sur ce bouton. Cette procédure peut également être choisie parmi les fonctions définies dans le programme.
Le widget Label
modifierL'incontournable "Hello world"
#!/usr/bin/python # Emplacement de l’interpréteur Python (sous Linux)
# -*- coding: utf-8 -*- # Définition l'encodage des caractères
from tkinter import *
Fenetre=Tk()
texte=Label(Fenetre, text="Hello World")
texte['fg']='black' # Création du texte "Hello World" de couleur noire
texte.pack() # Insère le texte dans la fenêtre
Fenetre.mainloop()
Propriétés et méthodes de l'objet label
modifier- "fg" contient la couleur du texte (en anglais)
- "bg" contient la couleur de fond du texte (en anglais)
Le widget Entry
modifierPropriétés et méthodes de l'objet Entry
modifierL'objet Entry() est une zone de saisie de texte que l'on crée de la manière suivante:
#!/usr/bin/python # Emplacement de l’interpréteur Python (sous Linux)
# -*- coding: utf-8 -*- # Définition l'encodage des caractères
from tkinter import * #On importe l'ensemble du module Tkinter
Fenetre = Tk()
Entree = Entry(Fenetre) # On définit l'objet Entry qui porte le nom Entree
Entree.pack() # On place "Entree"
Fenetre.mainloop() # On lance la boucle du programme
Vous pouvez l'utiliser dans des situations plus complexes comme, par exemple, un formulaire que je vous laisserai examiner:
#!/usr/bin/python # Emplacement de l’interpréteur Python (sous Linux)
# -*- coding: utf-8 -*- # Définition l'encodage des caractères
from tkinter import *
def repondre():
affichage['text'] = reponse.get() # lecture du contenu du widget "reponse"
Fenetre = Tk()
Fenetre.title('Mon nom')
nom = Label(Fenetre, text = 'Votre nom :')
reponse = Entry(Fenetre)
valeur = Button(Fenetre, text =' Valider', command=repondre)
affichage = Label(Fenetre, width=30)
votre_nom=Label(Fenetre, text='Votre nom est :')
nom.pack()
reponse.pack()
valeur.pack()
votre_nom.pack()
affichage.pack()
Fenetre.mainloop()
Le widget Canvas
modifierLe widget Canvas (canevas, en français) est une zone de dessin rectangulaire.
Notons que l'angle haut gauche du canevas est l'origine des coordonnées (x,y)=(0,0).
Un exemple d'utilisation :
#!/usr/bin/python # Emplacement de l’interpréteur Python (sous Linux)
# -*- coding: utf-8 -*- # Définition l'encodage des caractères
from tkinter import *
racine= Tk()
zone_dessin = Canvas(racine, width=500, height=500) #Définit les dimensions du canevas
zone_dessin.pack() #Affiche le canevas
zone_dessin.create_line(0,0,500,500) #Dessine une ligne en diagonale
zone_dessin.create_rectangle(100,100,200,200) #dessine un rectangle
bouton_sortir = Button(racine,text="Sortir",command=racine.destroy)
bouton_sortir.pack()
racine.mainloop()
Quelques propriétés de l'objet Canvas
modifierLes propriétés sont définies en paramètre lors de la construction de l'objet
- height : Hauteur Y du canvas
- width : Largeur X du canvas
- bg : Couleur de fond du canvas
- bd : Taille en pixels du bord du canvas (2 par défaut)
- relief : Style de la bordure (flat (par défaut),raised,sunken,groove,ridge)
- ...
Quelques méthodes du widget Canvas
modifier- .create_arc(): Pour créer un arc de cercle
- .create_bitmap(): Image bitmap
- .create_image(): Image graphique
- .create_line(): Pour créer une ligne
- .create_oval(): Pour créer un cercle ou une ellipse
- .create_polygon(): Pour créer un polygone
- .create_rectangle(): Pour créer un rectangle
- .create_text(): Texte
- .create_window(): Une fenêtre rectangulaire
Exemple supplémentaire
modifier#!/usr/bin/python
# -*- coding: utf-8 -*-
from tkinter import *
Fenetre=Tk() #La fonction Tk() du module Tkinter permet de créer une fenêtre qui se nomme Fenetre
Fenetre.title("Mon programme avec Tkinter") # Donne un titre à la fenêtre (par défaut c'est Tk)
# Dans Fenetre nous allons créer un objet type Canvas qui se nomme zone_dessin
# Nous donnons des valeurs aux propriétés "width", "height", "bg", "bd", "relief"
zone_dessin = Canvas(Fenetre,width=500,height=500,
bg='yellow',bd=8,relief="ridge")
zone_dessin.pack() #Affiche le Canvas
#Nous allons maintenant utiliser quelques méthodes du widget "zone_dessin"
zone_dessin.create_line(0,0,500,500,fill='red',width=4) # Dessine une ligne
zone_dessin.create_line(0,500,500,0,fill='red',width=4) # Dessine une ligne
zone_dessin.create_rectangle(150,150,350,350) # Dessine un rectangle
zone_dessin.create_oval(150,150,350,350,fill='white',width=4) # Dessine un cercle
# boutons_sortir est un widget de type "Button"
# dont nous définissons les propriétés "text" et "command")
bouton_sortir= Button(Fenetre,text="Sortir",command=Fenetre.destroy)
# la commande "destroy" appliquée à la fenêtre détruit l'objet "Fenetre" et clôture le programme
bouton_sortir.pack()
Fenetre.mainloop() # Lancement de la boucle du programme, en attente d'événements (clavier, souris,...)
Images .gif
modifier#!/usr/bin/python
# -*- coding: utf-8 -*-
from tkinter import *
Fenetre=Tk()
photo=PhotoImage(file="Wikibooks.gif")
labl = Label(Fenetre, image=photo)
labl.pack()
Fenetre.mainloop()
Installation des Python méga-widgets
modifierPython megawidgets (Pmw) est un créateur de widget utilisant Tkinter.
Visitez le site web : http://pmw.sourceforge.net et cliquez sur le lien : « Download Pmw12tar.gz » pour télécharger le fichier correspondant.
Décomprimez ce fichier archive dans un répertoire temporaire, à l'aide d'un logiciel de décompression tel que tar, Winzip, Info-Zip, unzip…
Recopiez l'intégralité du sous-répertoire Pmw qui s'est créé automatiquement, dans le répertoire où se trouve déjà l'essentiel de votre installation de Python.
Sous Windows, il s'agira par exemple de C:\Python23.
Sous Linux, il s'agira vraisemblablement de /usr/lib/python.
Voir aussi
modifier- PMW (Python megawidgets) pour ajouter des menus déroulants, cases à cocher et autres boites de dialogues.
- PIL (Python Imaging Library) pour incruster des images.
- py2.exe : pour créer des exécutables (limité à Python 2.6).
- tkRAD: Tkinter XML widget builder : pour générer automatiquement des widgets Tkinter à partir d'un fichier source XML.
- (anglais) Site officiel
Et pour quelques widgets de plus...
Les pages qui suivent contiennent des indications et et des exemples complémentaires qui pourront vous être utiles pour le développement de vos projets personnels. Il ne s'agit évidemment pas d'une documentation de référence complète sur Tkinter. Pour en savoir plus, vous devrez tôt ou tard consulter des ouvrages spécialisés, comme par exemple l'excellent Python and Tkinter programming de John E. Grayson, dont vous trouverez la référence complète à la page.
Les « boutons radio »
modifierLes widgets « boutons radio » permettent de proposer à l'utilisateur un ensemble de choix mutuellement exclusifs. On les appelle ainsi par analogie avec les boutons de sélection que l'on trouvait jadis sur les postes de radio. Ces boutons étaient conçus de telle manière qu'un seul à la fois pouvait être enfoncé : tous les autres ressortaient automatiquement.
La caractéristique essentielle de ces widgets est qu'on les utilise toujours par groupes. Tous les boutons radio faisant partie d'un même groupe sont associés à une seule et même variable Tkinter, mais chacun d'entre eux se voit aussi attribuer une valeur particulière.
Lorsque l'utilisateur sélectionne l'un des boutons, la valeur correspondant à ce bouton est affectée à la variable Tkinter commune.
from Tkinter import *
class RadioDemo(Frame):
"""Démo : utilisation de widgets 'boutons radio'"""
def __init__(self, boss =None):
"""Création d'un champ d'entrée avec 4 boutons radio"""
Frame.__init__(self)
self.pack()
# Champ d'entrée contenant un petit texte :
self.texte = Entry(self, width =30, font ="Arial 14")
self.texte.insert(END, "La programmation, c'est génial")
self.texte.pack(padx =8, pady =8)
# Nom français et nom technique des quatre styles de police :
stylePoliceFr =["Normal", "Gras", "Italique", "Gras/Italique"]
stylePoliceTk =["normal", "bold", "italic" , "bold italic"]
# Le style actuel est mémorisé dans un 'objet-variable' Tkinter ;
self.choixPolice = StringVar()
self.choixPolice.set(stylePoliceTk[0])
# Création des quatre 'boutons radio' :
for n in range(4):
bout = Radiobutton(self,
text = stylePoliceFr[n],
variable = self.choixPolice,
value = stylePoliceTk[n],
command = self.changePolice)
bout.pack(side =LEFT, padx =5)
def changePolice(self):
"""Remplacement du style de la police actuelle"""
police = "Arial 15 " + self.choixPolice.get()
self.texte.configure(font =police)
if __name__ == '__main__':
RadioDemo().mainloop()
- Commentaires
- Ligne 3 : Cette fois encore, nous préférons construire notre petite application comme une classe dérivée de la classe
Frame()
, ce qui nous permettrait éventuellement de l'intégrer sans difficulté dans une application plus importante. - Ligne 8 : En général, on applique les méthodes de positionnement des widgets (
pack()
,grid()
, ouplace()
) après instanciation de ceux-ci, ce qui permet de choisir librement leur disposition à l'intérieur des fenêtres maîtresses. Comme nous le montrons ici, il est cependant tout à fait possible de déjà prévoir ce positionnement dans le constructeur du widget. - Ligne 11 : Les widgets de la classe
Entry
disposent de plusieurs méthodes pour accéder à la chaîne de caractères affichée. La méthodeget()
permet de récupérer la chaîne entière. La méthodedelete()
permet d'en effacer tout ou partie. La méthodeinsert()
permet d'insérer de nouveaux caractères à un emplacement quelconque (c'est-à-dire au début, à la fin, ou même à l'intérieur d'une chaîne préexistante éventuelle). Cette méthode s'utilise donc avec deux arguments, le premier indiquant l'emplacement de l'insertion (utilisez 0 pour insérer au début,END
pour insérer à la fin, ou encore un indice numérique quelconque pour désigner un caractère dans la chaîne). - Lignes 14-15 : Plutôt que de les instancier dans des instructions séparées, nous préférons créer nos quatre boutons à l'aide d'une boucle. Les options spécifiques à chacun d'eux sont d'abord préparées dans les deux listes
stylePoliceFr
etstylePoliceTk
: la première contient les petits textes qui devront s'afficher en regard de chaque bouton, et la seconde les valeurs qui devront leur être associées. - Lignes 17-18 : Comme expliqué précédemment, les quatre boutons forment un groupe autour d'une variable commune. Cette variable prendra la valeur associée au bouton radio que l'utilisateur décidera de choisir. Nous ne pouvons cependant pas utiliser une variable ordinaire pour remplir ce rôle, parce que les attributs internes des objets Tkinter ne sont accessibles qu'au travers de méthodes spécifiques. Une fois de plus, nous utilisons donc ici un objet-variable Tkinter, de type 'chaîne de caractères', que nous instancions à partir de la classe
StringVar()
, et auquel nous donnons une valeur par défaut à la ligne 18. - Lignes 20 à 26 : Instanciation des quatre boutons radio. Chacun d'entre eux se voit attribuer une étiquette et une valeur différentes, mais tous sont associés à la même variable Tkinter commune (
self.choixPolice
). Tous invoquent également la même méthodeself.changePolice()
, chaque fois que l'utilisateur effectue un clic de souris sur l'un ou l'autre. - Lignes 28 à 31 : Le changement de police s'obtient par re-configuration de l'option font du widget
Entry
. Cette option attend un tuple contenant le nom de la police, sa taille, et éventuellement son style. Si le nom de la police ne contient pas d'espaces, le tuple peut aussi être remplacé par une chaîne de caractères. Exemples :
('Arial', 12, 'italic')
('Helvetica', 10)
('Times New Roman', 12, 'bold italic')
"Verdana 14 bold"
"President 18 italic"
Utilisation des cadres (frames) pour la composition d'une fenêtre
modifierVous avez déjà abondamment utilisé la classe de widgets Frame()
(« cadre », en français), notamment pour créer de nouveaux widgets complexes par dérivation.
Le petit script ci-dessous vous montre l'utilité de cette même classe pour regrouper des ensembles de widgets et les disposer d'une manière déterminée dans une fenêtre. Il vous démontre également l'utilisation de certaines options décoratives (bordures, relief, etc.).
Pour composer la fenêtre ci-contre, nous avons utilisé deux cadres f1
et f2
, de manière à réaliser deux groupes de widgets bien distincts, l'un à gauche et l'autre à droite. Nous avons coloré ces deux cadres pour bien les mettre en évidence, mais ce n'est évidemment pas indispensable.
Le cadre f1
contient lui-même 6 autres cadres, qui contiennent chacun un widget de la classe Label()
. Le cadre f2
contient un widget Canvas()
et un widget Button()
. Les couleurs et garnitures sont de simples options.
from Tkinter import * #1
#2
fen = Tk() #3
fen.title("Fenêtre composée à l'aide de frames") #4
fen.geometry("300x300") #5
#6
f1 = Frame(fen, bg = '#80c0c0') #7
f1.pack(side =LEFT, padx =5) #8
#9
fint = [0]*6 #10
for (n, col, rel, txt) in [(0, 'grey50', RAISED, 'Relief sortant'), #11
(1, 'grey60', SUNKEN, 'Relief rentrant'), #12
(2, 'grey70', FLAT, 'Pas de relief'), #13
(3, 'grey80', RIDGE, 'Crête'), #14
(4, 'grey90', GROOVE, 'Sillon'), #15
(5, 'grey100', SOLID, 'Bordure')]: #16
fint[n] = Frame(f1, bd =2, relief =rel) #17
e = Label(fint[n], text =txt, width =15, bg =col) #18
e.pack(side =LEFT, padx =5, pady =5) #19
fint[n].pack(side =TOP, padx =10, pady =5) #20
#21
f2 = Frame(fen, bg ='#d0d0b0', bd =2, relief =GROOVE) #22
f2.pack(side =RIGHT, padx =5) #23
#24
can = Canvas(f2, width =80, height =80, bg ='white', bd =2, relief =SOLID) #25
can.pack(padx =15, pady =15) #26
bou =Button(f2, text='Bouton') #27
bou.pack() #28
#29
fen.mainloop() #30
- Lignes 3 à 5 : Afin de simplifier au maximum la démonstration, nous ne programmons pas cet exemple comme une nouvelle classe. Remarquez à la ligne 5 l'utilité de la méthode
geometry()
pour fixer les dimensions de la fenêtre principale.
- Ligne 7 : Instanciation du cadre de gauche. La couleur de fond (une variété de bleu cyan) est déterminée par l'argument
bg
(background). Cette chaîne de caractères contient en notation hexadécimale la description des trois composantes rouge, verte et bleue de la teinte que l'on souhaite obtenir : Après le caractère # signalant que ce qui suit est une valeur numérique hexadécimale, on trouve trois groupes de deux symboles alphanumériques. Chacun de ces groupes représente un nombre compris entre 1 et 255. Ainsi80
correspond à 128, etc0
correspond à 192 en notation décimale. Dans notre exemple, les composantes rouge, verte et bleue de la teinte à représenter valent donc respectivement 128, 192 & 192.
En application de cette technique descriptive, le noir serait obtenu avec#000000
, le blanc avec#ffffff
, le rouge pur avec#ff0000
, un bleu sombre avec#000050
, etc.
- Ligne 8 : Puisque nous lui appliquons la méthode
pack()
, le cadre sera automatiquement dimensionné par son contenu. L'option side=LEFT
le positionnera à gauche dans sa fenêtre maîtresse. L'optionpadx =5
ménagera un espace de 5 pixels à sa gauche et à sa droite (nous pouvons traduire « padx » par « espacement horizontal »).
- Ligne 10 : Dans le cadre
f1
que nous venons de préparer, nous avons l'intention de regrouper 6 autres cadres similaires contenant chacun une étiquette. Le code correspondant sera plus simple et plus efficient si nous instancions ces widgets dans une liste plutôt que dans des variables indépendantes. Nous préparons donc cette liste avec 6 éléments que nous remplacerons plus loin.
- Lignes 11 à 16 : Pour construire nos 6 cadres similaires, nous allons parcourir une liste de 6 tuples contenant les caractéristiques particulières de chaque cadre. Chacun de ces tuples est constitué de 4 éléments : un indice, une constante Tkinter définissant un type de relief, et deux chaînes de caractères décrivant respectivement la couleur et le texte de l'étiquette.
La bouclefor
effectue 6 itérations pour parcourir les 6 éléments de la liste. À chaque itération, le contenu d'un des tuples est affecté aux variablesn
,col
,rel
ettxt
(et ensuite les instructions des lignes 17 à 20 sont exécutées). Le parcours d'une liste de tuples à l'aide d'une boucle for constitue donc une construction particulièrement compacte, qui permet de réaliser de nombreuses affectations avec un très petit nombre d'instructions.
- Ligne 17 : Les 6 cadres sont instanciés comme des éléments de la liste
fint
. Chacun d'entre eux est agrémenté d'une bordure décorative de 2 pixels de large, avec un certain effet de relief.
- Lignes 18-20 : Les étiquettes ont toutes la même taille, mais leurs textes et leurs couleurs de fond diffèrent. Du fait de l'utilisation de la méthode
pack()
, c'est la dimension des étiquettes qui détermine la taille des petits cadres. Ceux-ci à leur tour déterminent la taille du cadre qui les regroupe (le cadref1
). Les optionspadx
etpady
permettent de réserver un petit espace autour de chaque étiquette, et un autre autour de chaque petit cadre. L'optionside =TOP
positionne les 6 petits cadres les uns en dessous des autres dans le cadre conteneurf1
.
- Lignes 22-23 : Préparation du cadre
f2
(cadre de droite). Sa couleur sera une variété de jaune, et nous l'entourerons d'une bordure décorative ayant l'aspect d'un sillon.
- Lignes 25 à 28 : Le cadre
f2
contiendra un canevas et un bouton. Notez encore une fois l'utilisation des optionspadx
etpady
pour ménager des espaces autour des widgets (Considérez par exemple le cas du bouton, pour lequel cette option n'a pas été utilisée : de ce fait, il entre en contact avec la bordure du cadre qui l'entoure). Comme nous l'avons fait pour les cadres, nous avons placé une bordure autour du canevas. Sachez que d'autres widgets acceptent également ce genre de décoration : boutons, champs d'entrée, etc.
Comment déplacer des dessins à l'aide de la souris
modifierLe widget canevas est l'un des points forts de la bibliothèque graphique Tkinter. Il intègre en effet un grand nombre de dispositifs très efficaces pour manipuler des dessins. Le script ci-après est destiné à vous montrer quelques techniques de base. Si vous voulez en savoir plus, notamment en ce qui concerne la manipulation de dessins composés de plusieurs parties, veuillez consulter l'un ou l'autre ouvrage de référence traitant de Tkinter.
Au démarrage de notre petite application, une série de dessins sont tracés au hasard dans un canevas (il s'agit en l'occurrence de simples ellipses colorées). Vous pouvez déplacer n'importe lequel de ces dessins en les « saisissant » à l'aide de votre souris.
Lorsqu'un dessin est déplacé, il passe à l'avant-plan par rapport aux autres, et sa bordure apparaît plus épaisse pendant toute la durée de sa manipulation.
Pour bien comprendre la technique utilisée, vous devez vous rappeler qu'un logiciel utilisant une interface graphique est un logiciel « piloté par les événements » (revoyez au besoin les explications de la page ). Dans cette application, nous allons mettre en place un mécanisme qui réagit aux événements : « enfoncement du bouton gauche de la souris », « déplacement de la souris, le bouton gauche restant enfoncé », « relâchement du bouton gauche ».Ces événements sont générés par le système d'exploitation et pris en charge par l'interface Tkinter. Notre travail de programmation consistera donc simplement à les associer à des gestionnaires différents (fonctions ou méthodes).
# Exemple montrant comment faire en sorte que les objets dessinés dans un
# canevas puissent être manipulés à l'aide de la souris
from Tkinter import *
from random import randrange
class Draw(Frame):
"classe définissant la fenêtre principale du programme"
def __init__(self):
Frame.__init__(self)
# mise en place du canevas - dessin de 15 ellipses colorées :
self.c = Canvas(self, width =400, height =300, bg ='ivory')
self.c.pack(padx =5, pady =3)
for i in range(15):
# tirage d'une couleur au hasard :
coul =['brown','red','orange','yellow','green','cyan','blue',
'violet', 'purple'][randrange(9)]
# tracé d'une ellipse avec coordonnées aléatoires :
x1, y1 = randrange(300), randrange(200)
x2, y2 = x1 + randrange(10, 150), y1 + randrange(10, 150)
self.c.create_oval(x1, y1, x2, y2, fill =coul)
# liaison d'événements <souris> au widget <canevas> :
self.c.bind("<Button-1>", self.mouseDown)
self.c.bind("<Button1-Motion>", self.mouseMove)
self.c.bind("<Button1-ButtonRelease>", self.mouseUp)
# mise en place d'un bouton de sortie :
b_fin = Button(self, text ='Terminer', bg ='royal blue', fg ='white',
font =('Helvetica', 10, 'bold'), command =self.quit)
b_fin.pack(pady =2)
self.pack()
def mouseDown(self, event):
"Op. à effectuer quand le bouton gauche de la souris est enfoncé"
self.currObject =None
# event.x et event.y contiennent les coordonnées du clic effectué :
self.x1, self.y1 = event.x, event.y
# <find_closest> renvoie la référence du dessin le plus proche :
self.selObject = self.c.find_closest(self.x1, self.y1)
# modification de l'épaisseur du contour du dessin :
self.c.itemconfig(self.selObject, width =3)
# <lift> fait passer le dessin à l'avant-plan :
self.c.lift(self.selObject)
def mouseMove(self, event):
"Op. à effectuer quand la souris se déplace, bouton gauche enfoncé"
x2, y2 = event.x, event.y
dx, dy = x2 -self.x1, y2 -self.y1
if self.selObject:
self.c.move(self.selObject, dx, dy)
self.x1, self.y1 = x2, y2
def mouseUp(self, event):
"Op. à effectuer quand le bouton gauche de la souris est relâché"
if self.selObject:
self.c.itemconfig(self.selObject, width =1)
self.selObject =None
if __name__ == '__main__':
Draw().mainloop()
- Commentaires
Le script contient essentiellement la définition d'une classe graphique dérivée de Frame()
.
Comme c'est souvent le cas pour les programmes exploitant les classes d'objets, le corps principal du script se résume à une seule instruction composée, dans laquelle on réalise deux opérations consécutives : instanciation d'un objet de la classe définie précédemment, et activation de sa méthode mainloop()
(laquelle démarre l'observateur d'événements).
Le constructeur de la classe Draw()
présente une structure qui doit vous être devenue familière, à savoir : appel au constructeur de la classe parente, puis mise en place de divers widgets.
Dans le widget canevas, nous instancions 15 dessins sans nous préoccuper de conserver leurs références dans des variables. Nous pouvons procéder ainsi parce que Tkinter conserve lui-même une référence interne pour chacun de ces objets. (Si vous travaillez avec d'autres bibliothèques graphiques, vous devrez probablement prévoir une mémorisation de ces références).
Les dessins sont de simples ellipses colorées. Leur couleur est choisie au hasard dans une liste de 9 possibilités, l'indice de la couleur choisie étant déterminé par la fonction randrange()
importée du module random
.
Le mécanisme d'interaction est installé ensuite : on associe les trois identificateurs d'événements <Button-1>
, <Button1-Motion>
et <Button1-ButtonRelease>
concernant le widget canevas, aux noms des trois méthodes choisies comme gestionnaires d'événements.
Lorsque l'utilisateur enfonce le bouton gauche de sa souris, la méthode mouseDown()
est donc activée, et le système d'exploitation lui transmet en argument un objet event
, dont les attributs x
et y
contiennent les coordonnées du curseur souris dans le canevas, déterminées au moment du clic.
Nous mémorisons directement ces coordonnées dans les variables d'instance self.x1
et self.x2
, car nous en aurons besoin par ailleurs. Ensuite, nous utilisons la méthode find_closest()
du widget canevas, qui nous renvoie la référence du dessin le plus proche. (Note : cette méthode bien pratique renvoie toujours une référence, même si le clic de souris n'a pas été effectué à l'intérieur du dessin).
Le reste est facile : la référence du dessin sélectionné est mémorisée dans une variable d'instance, et nous pouvons faire appel à d'autres méthodes du widget canevas pour modifier ses caractéristiques. En l'occurrence, nous utilisons les méthodes itemconfig()
et lift()
pour épaissir son contour et le faire passer à l'avant-plan.
Le « transport » du dessin est assuré par la méthode mouseMove()
, invoquée à chaque fois que la souris se déplace alors que son bouton gauche est resté enfoncé. L'objet event contient cette fois encore les coordonnées du curseur souris, au terme de ce déplacement. Nous nous en servons pour calculer les différences entre ces nouvelles coordonnées et les précédentes, afin de pouvoir les transmettre à la méthode move()
du widget canevas, qui effectuera le transport proprement dit.
Nous ne pouvons cependant faire appel à cette méthode que s'il existe effectivement un objet sélectionné, et il nous faut veiller également à mémoriser les nouvelles coordonnées acquises.
La méthode mouseUp()
termine le travail. Lorsque le dessin transporté est arrivé à destination, il reste à annuler la sélection et rendre au contour son épaisseur initiale. Ceci ne peut être envisagé que s'il existe effectivement une sélection, bien entendu.
Python Mega Widgets
modifierLes modules Pmw constituent une extension intéressante de Tkinter. Entièrement écrits en Python, ils contiennent toute une bibliothèque de widgets composites, construits à partir des classes de base de Tkinter. Dotés de fonctionnalités très étendues, ces widgets peuvent se révéler fort précieux pour le développement rapide d'applications complexes. Si vous souhaitez les utiliser, sachez cependant que les modules Pmw ne font pas partie de l'installation standard de Python : vous devrez donc toujours vérifier leur présence sur les machines cibles de vos programmes.
Il existe un grand nombre de ces méga-widgets. Nous n'en présenterons ici que quelques-uns parmi les plus utiles. Vous pouvez rapidement vous faire une idée plus complète de leurs multiples possibilités, en essayant les scripts de démonstration qui les accompagnent (lancez par exemple le script all.py , situé dans le répertoire .../Pmw/demos
).
« Combo Box »
modifierLes méga-widgets s'utilisent aisément. La petite application ci-après vous montre comment mettre en œuvre un widget de type ComboBox
(boîte de liste combinée à un champ d'entrée). Nous l'avons configuré de la manière la plus habituelle (avec une boîte de liste déroulante).
Lorsque l'utilisateur de notre petit programme choisit une couleur dans la liste déroulante (il peut aussi entrer un nom de couleur directement dans le champ d'entrée), cette couleur devient automatiquement la couleur de fond pour la fenêtre maîtresse.
Dans cette fenêtre maîtresse, nous avons ajouté un libellé et un bouton, afin de vous montrer comment vous pouvez accéder à la sélection opérée précédemment dans le ComboBox lui-même (le bouton provoque l'affichage du nom de la dernière couleur choisie).
from Tkinter import *
import Pmw
def changeCoul(col):
fen.configure(background = col)
def changeLabel():
lab.configure(text = combo.get())
couleurs = ('navy', 'royal blue', 'steelblue1', 'cadet blue',
'lawn green', 'forest green', 'dark red',
'grey80','grey60', 'grey40', 'grey20')
fen = Pmw.initialise()
bou = Button(fen, text ="Test", command =changeLabel)
bou.grid(row =1, column =0, padx =8, pady =6)
lab = Label(fen, text ='néant', bg ='ivory')
lab.grid(row =1, column =1, padx =8)
combo = Pmw.ComboBox(fen, labelpos = NW,
label_text = 'Choisissez la couleur :',
scrolledlist_items = couleurs,
listheight = 150,
selectioncommand = changeCoul)
combo.grid(row =2, columnspan =2, padx =10, pady =10)
fen.mainloop()
- Commentaires
- Lignes 1 & 2 : On commence par importer les composants habituels de Tkinter, ainsi que le module Pmw.
- Ligne 14 : Pour créer la fenêtre maîtresse, il faut utiliser de préférence la méthode
Pmw.initialise()
, plutôt que d'instancier directement un objet de la classeTk()
. Cette méthode veille en effet à mettre en place tout ce qui est nécessaire afin que les widgets esclaves de cette fenêtre puissent être détruits correctement lorsque la fenêtre elle-même sera détruite. Cette méthode installe également un meilleur gestionnaire des messages d'erreurs.
- Ligne 12 : L'option
labelpos
détermine l'emplacement du libellé qui accompagne le champ d'entrée. Dans notre exemple, nous l'avons placé au-dessus, mais vous pourriez préférer le placer ailleurs, à gauche par exemple (labelpos = W
). Notez que cette option est indispensable si vous souhaitez un libellé (pas de valeur par défaut).
- Ligne 14 : L'option
selectioncommand
transmet un argument à la fonction invoquée : l'item sélectionné dans la boîte de liste. Vous pourrez également retrouver cette sélection à l'aide de la méthodeget()
, comme nous le faisons à la ligne 8 pour actualiser le libellé.
Remarque concernant l'entrée de caractères accentués
modifierVeuillez donc prendre bonne note que lorsque vous entrez dans l'un de ces widgets une chaîne contenant un ou plusieurs caractères non-ASCII (tel qu'une lettre accentuée, par exemple), Tkinter encode cette chaîne suivant la norme UTF-8. Si votre ordinateur utilise plutôt le codage Latin-1 par défaut (ce qui est très souvent le cas), vous devrez convertir la chaîne avant de pouvoir l'afficher.
Cela peut se faire très aisément en utilisant la fonction intégrée encode()
. Exemple :
# -*- coding: Latin-1 -*- from Tkinter import * def imprimer(): ch1 = e.get() # le widget Entry renvoie une chaîne utf8 ch2 = ch1.encode("Latin-1") # conversion utf8 -> Latin-1 print ch2 f = Tk() e = Entry(f) e.pack() Button(f, text ="afficher", command =imprimer).pack() f.mainloop()
Essayez ce petit script en entrant des chaînes avec caractères accentués dans le champ d'entrée.
Essayez encore, mais en remplaçant l'instruction print ch2
par print ch1
. Concluez.
« Scrolled Text »
modifierCe méga-widget étend les possibilités du widget Text sandard, en lui associant un cadre, un libellé (titre) et des barres de défilement.
Comme le démontrera le petit script ci-dessous, il sert fondamentalement à afficher des textes, mais ceux-ci peuvent être mis en forme et intégrer des images.
Vous pouvez également rendre « cliquables » les éléments affichés (textes ou images), et vous en servir pour déclencher toutes sortes de mécanismes.
Dans l'application qui génère la figure ci-dessus, par exemple, le fait de cliquer sur le nom « Jean de la Fontaine » provoque le défilement automatique du texte (scrolling), jusqu'à ce qu'une rubrique décrivant cet auteur devienne visible dans le widget (Voir page suivante le script correspondant).
D'autres fonctionnalités sont présentes, mais nous ne présenterons ici que les plus fondamentales. Veuillez donc consulter les démos et exemples accompagnant Pmw pour en savoir davantage.
Gestion du texte affiché : Vous pouvez accéder à n'importe quelle portion du texte pris en charge par le widget grâce à deux concepts complémentaires, les indices et les balises :
- Chaque caractère du texte affiché est référencé par un indice, lequel doit être une chaîne de caractères contenant deux valeurs numériques reliées par un point (ex :
"5.2"
). Ces deux valeurs indiquent respectivement le numéro de ligne et le numéro de colonne où se situe le caractère.
- N'importe quelle portion du texte peut être associée à une ou plusieurs balise(s), dont vous choisissez librement le nom et les propriétés. Celles-ci vous permettent de définir la police, les couleurs d'avant- et d'arrière-plan, les événements associés, etc.
Note : Pour la bonne compréhension du script ci-dessous, veuillez considérer que le texte de la fable traitée doit être accessible, dans un fichier nommé CorbRenard.txt.
from Tkinter import * #1
import Pmw #2
#3
def action(event=None): #4
"""défilement du texte jusqu'à la balise <cible>""" #5
index = st.tag_nextrange('cible', '0.0', END) #6
st.see(index[0]) #7
#8
# Instanciation d'une fenêtre contenant un widget ScrolledText : #9
fen = Pmw.initialise() #10
st = Pmw.ScrolledText(fen, #11
labelpos =N, #12
label_text ="Petite démo du widget ScrolledText", #13
label_font ='Times 14 bold italic', #14
label_fg = 'navy', label_pady =5, #15
text_font='Helvetica 11 normal', text_bg ='ivory', #16
text_padx =10, text_pady =10, text_wrap ='none', #17
borderframe =1, #18
borderframe_borderwidth =3, #19
borderframe_relief =SOLID, #20
usehullsize =1, #21
hull_width =370, hull_height =240) #22
st.pack(expand =YES, fill =BOTH, padx =8, pady =8) #23
#24
# Définition de balises, liaison d'un gestionnaire d'événement au clic de souris : #25
st.tag_configure('titre', foreground ='brown', font ='Helvetica 11 bold italic') #26
st.tag_configure('lien', foreground ='blue', font ='Helvetica 11 bold') #27
st.tag_configure('cible', foreground ='forest green', font ='Times 11 bold') #28
st.tag_bind('lien', '<Button-1>', action) #29
#30
titre ="""Le Corbeau et le Renard #31
par Jean de la Fontaine, auteur français #32
\n""" #33
auteur =""" #34
Jean de la Fontaine #35
écrivain français (1621-1695) #36
célèbre pour ses Contes en vers, #37
et surtout ses Fables, publiées #38
de 1668 à 1694.""" #39
#40
# Remplissage du widget Text (2 techniques) : #41
st.importfile('CorbRenard.txt') #42
st.insert('0.0', titre, 'titre') #43
st.insert(END, auteur, 'cible') #44
# Insertion d'une image : #45
photo =PhotoImage(file= 'Penguin.gif') #46
st.image_create('6.14', image =photo) #47
# Mise en œuvre dynamique d'une balise : #48
st.tag_add('lien', '2.4', '2.23') #49
#50
fen.mainloop() #51
- Commentaires
- Lignes 4-7 : Cette fonction est un gestionnaire d'événement, qui est appelé lorsque l'utilisateur effectue un clic de souris sur le nom de l'auteur (cf. lignes 27 et 29). À la ligne 6, on utilise la méthode
tag_nextrange()
du widget pour trouver les indices de la portion de texte associée à la balisecible
. La recherche de ces index est limitée au domaine défini par les 2e et 3e arguments (dans notre exemple, on recherche du début à la fin du texte entier). La méthodetag_nextrange()
renvoie une liste de deux indices (ceux des premier et dernier caractères de la portion de texte associée à la balise « cible »). À la ligne 7, nous nous servons d'un seul de ces index (le premier) pour activer la méthodesee()
. Celle-ci provoque un défilement automatique du texte (scrolling), de telle manière que le caractère correspondant à l'index transmis devienne visible dans le widget (avec en général un certain nombre des caractères qui suivent).
- Lignes 9 à 23 : Construction classique d'une fenêtre destinée à afficher un seul widget. Dans le code d'instanciation du widget, nous avons inclus un certain nombre d'options destinées à vous montrer une petite partie des nombreuses possibilités de configuration.
- Ligne 12 : L'option labelpos détermine l'emplacement du libellé (titre) par rapport à la fenêtre de texte. Les valeurs acceptées s'inspirent des lettres utilisées pour désigner les points cardinaux (N, S, E, W, ou encore NE, NW, SE, SW). Si vous ne souhaitez pas afficher un libellé, il vous suffit tout simplement de ne pas utiliser cette option.
- Lignes 13 à 15 : Le libellé n'est rien d'autre qu'un widget
Label
standard, intégré dans le widget compositeScrolledText
. On peut accéder à toutes ses options de configuration, en utilisant la syntaxe qui est présentée dans ces lignes : on y voit qu'il suffit d'associer le préfixelabel_
au nom de l'option que l'on souhaite activer, pour définir aisément les couleurs d'avant- et d'arrière-plans, la police, la taille, et même l'espacement à réserver autour du widget (optionpady
).
- Lignes 16-17 : En utilisant une technique similaire à celle qui est décrite ci-dessus pour le libellé, on peut accéder aux options de configuration du widget
Text
intégré dansScrolledText
. Il suffit cette fois d'associer aux noms d'option le préfixetext_
.
- Lignes 18 à 20 : Il est prévu un cadre (un widget
Frame
) autour du widgetText
. L'optionborderframe = 1
permet de le faire apparaître. On accède ensuite à ses options de configuration d'une manière similaire à celle qui a été décrite ci-dessus pourlabel_
ettext_
.
- Lignes 21-22 : Ces options permettent de fixer globalement les dimensions du widget. Une autre possibilité serait de définir plutôt les dimensions de son composant
Text
(par exemple à l'aide d'options telles quetext_width
ettext_height
), mais alors les dimensions globales du widget risqueraient de changer en fonction du contenu (apparition/disparition automatique de barres de défilement). Note : le mot hull désigne le contenant global, c'est à dire le méga-widget lui-même.
- Ligne 23 : Les options
expand = YES
etfill = BOTH
de la méthodepack()
indiquent que le widget concerné pourra être redimensionné à volonté, dans ses 2 dimensions horiz. et verticale.
- Lignes 26 à 29 : Ces lignes définissent les trois balises
titre
,lien
etcible
ainsi que le formatage du texte qui leur sera associé. La ligne 29 précise en outre que le texte associé à la baliselien
sera « cliquable », avec indication du gestionnaire d'événement correspondant.
- Ligne 42 : Importation de texte à partir d'un fichier. Note : Il est possible de préciser l'endroit exact où devra se faire l'insertion, en fournissant un index comme second argument.
- Lignes 43-44 : Ces instructions insèrent des fragments de texte (respectivement au début et à la fin du texte préexistant), en associant une balise à chacun d'eux.
- Ligne 49 : L'association des balises au texte est dynamique. À tout moment, vous pouvez activer une nouvelle association (comme nous le faisons ici en rattachant la balise « lien » à une portion de texte préexistante). Note : pour « détacher » une balise, utilisez la méthode
tag_delete()
.
« Scrolled Canvas »
modifierLe script ci-après vous montre comment vous pouvez exploiter le méga-widget
ScrolledCanvas
, lequel étend les possibilités du widget Canvas
standard en lui associant des barres de défilement, un libellé et un cadre. Notre exemple constitue en fait un petit jeu d'adresse, dans lequel l'utilisateur doit réussir à cliquer sur un bouton qui s'esquive sans cesse. (Note : si vous éprouvez vraiment des difficultés pour l'attraper, commencez d'abord par dilater la fenêtre).
Le widget Canvas
est très versatile : il vous permet de combiner à volonté des dessins, des images bitmap, des fragments de texte, et même d'autres widgets, dans un espace parfaitement extensible. Si vous souhaitez développer l'un ou l'autre jeu graphique, c'est évidemment le widget qu'il vous faut apprendre à maîtriser en priorité.
Comprenez bien cependant que les indications que nous vous fournissons à ce sujet dans les présentes notes sont forcément très incomplètes. Leur objectif est seulement de vous aider à comprendre quelques concepts de base, afin que vous puissiez ensuite consulter les ouvrages de référence spécialisés dans de bonnes conditions.
Notre petite application se présente comme une nouvelle classe FenPrinc()
, obtenue par dérivation à partir de la classe de méga-widgets Pmw.ScrolledCanvas()
. Elle contient donc un grand canevas muni de barres de défilement, dans lequel nous commençons par planter un décor constitué de 80 ellipses de couleur dont l'emplacement et les dimensions sont tirés au hasard.
Nous y ajoutons également un petit clin d'œil sous la forme d'une image bitmap, destinée avant tout à vous rappeler comment vous pouvez gérer ce type de ressource.
Nous y installons enfin un véritable widget : un simple bouton, en l'occurrence, mais la technique mise en œuvre pourrait s'appliquer à n'importe quel autre type de widget, y compris un gros widget composite comme ceux que nous avons développés précédemment. Cette grande souplesse dans le développement d'applications complexes est l'un des principaux bénéfices apportés par le mode de programmation « orientée objet ».
Le bouton s'anime dès qu'on l'a enfoncé une première fois. Dans votre analyse du script ci-après, soyez attentifs aux méthodes utilisées pour modifier les propriétés d'un objet existant.
from Tkinter import * #1
import Pmw #2
from random import randrange #3
#4
Pmw.initialise() #5
coul =['sienna','maroon','brown','pink','tan','wheat','gold','orange','plum', #6
'red','khaki','indian red','thistle','firebrick','salmon','coral'] #7
#8
class FenPrinc(Pmw.ScrolledCanvas): #9
"""Fenêtre principale : canevas extensible avec barres de défilement""" #10
def __init__(self): #11
Pmw.ScrolledCanvas.__init__(self, #12
usehullsize =1, hull_width =500, hull_height =300, #13
canvas_bg ='grey40', canvasmargin =10, #14
labelpos =N, label_text ='Attrapez le bouton !', #15
borderframe =1, #16
borderframe_borderwidth =3) #17
# Les options ci-dessous doivent être précisées après initialisation : #18
self.configure(vscrollmode ='dynamic', hscrollmode ='dynamic') #19
self.pack(padx =5, pady =5, expand =YES, fill =BOTH) #20
#21
self.can = self.interior() # accès au composant canevas #22
# Décor : tracé d'une série d'ellipses aléatoires : #23
for r in range(80): #24
x1, y1 = randrange(-800,800), randrange(-800,800) #25
x2, y2 = x1 + randrange(40,300), y1 + randrange(40,300) #26
couleur = coul[randrange(0,16)] #27
self.can.create_oval(x1, y1, x2, y2, fill=couleur, outline='black') #28
# Ajout d'une petite image GIF : #29
self.img = PhotoImage(file ='linux2.gif') #30
self.can.create_image(50, 20, image =self.img) #31
# Dessin du bouton à attraper : #32
self.x, self.y = 50, 100 #33
self.bou = Button(self.can, text ="Start", command =self.start) #34
self.fb = self.can.create_window(self.x, self.y, window =self.bou) #35
self.resizescrollregion() #36
#37
def anim(self): #38
if self.run ==0: #39
return #40
self.x += randrange(-60, 61) #41
self.y += randrange(-60, 61) #42
self.can.coords(self.fb, self.x, self.y) #43
self.configure(label_text = 'Cherchez en %s %s' % (self.x, self.y)) #44
self.resizescrollregion() #45
self.after(250, self.anim) #46
#47
def stop(self): #48
self.run =0 #49
self.bou.configure(text ="Restart", command =self.start) #50
#51
def start(self): #52
self.bou.configure(text ="Attrapez-moi !", command =self.stop) #53
self.run =1 #54
self.anim() #55
#56
##### Main Program ############## #57
#58
if __name__ == '__main__': #59
FenPrinc().mainloop() #60
- Commentaires
- Ligne 6 : Tous ces noms de couleurs sont acceptés par Tkinter. Vous pourriez bien évidemment les remplacer par des descriptions hexadécimales, comme nous l'avons expliqué page
.
- Lignes 12 à 17 : Ces options sont très similaires à celles que nous avons décrites plus haut pour le widget ScrolledText. Le présent méga-widget intègre un composant
Frame
, un composantLabel
, un composantCanvas
et deux composantsScrollbar
. On accède aux options de configuration de ces composants à l'aide d'une syntaxe qui relie le nom du composant et celui de l'option par l'intermédiaire d'un caractère « souligné ».
- Ligne 19 : Ces options définissent le mode d'apparition des barres de défilement. En mode « static », elles sont toujours présentes. En mode « dynamic », elles disparaissent si les dimensions du canevas deviennent inférieures à celles de la fenêtre de visualisation.
- Ligne 22 : La méthode
interior()
renvoie la référence du composantCanvas
intégré dans le méga-widgetScrolledCanvas
. Les instructions suivantes (lignes 23 à 35) installent ensuite toute une série d'éléments dans ce canevas : des dessins, une image et un bouton.
- Lignes 25 à 27 : La fonction
randrange()
permet de tirer au hasard un nombre entier compris dans un certain intervalle (Veuillez vous référer aux explications de la page
).
- Ligne 35 : C'est la méthode
create_window()
du widgetCanvas
qui permet d'y insérer n'importe quel autre widget (y compris un widget composite). Le widget à insérer doit cependant avoir été défini lui-même au préalable comme un esclave du canevas ou de sa fenêtre maîtresse.
La méthodecreate_window()
attend trois arguments : les coordonnées X et Y du point où l'on souhaite insérer le widget, et la référence de ce widget.
- Ligne 36 : La méthode
resizescrollregion()
réajuste la situation des barres de défilement de manière à ce qu'elles soient en accord avec la portion du canevas actuellement affichée.
- Lignes 38 à 46 : Cette méthode est utilisée pour l'animation du bouton. Après avoir repositionné le bouton au hasard à une certaine distance de sa position précédente, elle se ré-appelle elle-même après une pause de 250 millisecondes. Ce bouclage s'effectue sans cesse, aussi longtemps que la variable
self.run
contient une valeur non-nulle.
- Lignes 48 à 55 : Ces deux gestionnaires d'événement sont associés au bouton en alternance. Ils servent évidemment à démarrer et à arrêter l'animation.
Barres d'outils avec bulles d'aide - expressions lambda
modifierDe nombreux programmes comportent une ou plusieurs « barres d'outils » (toolbar) constituées de petits boutons sur lesquels sont représentés des pictogrammes (icônes). Cette façon de faire permet de proposer à l'utilisateur un grand nombre de commandes spécialisées, sans que celles-ci n'occupent une place excessive à l'écran (un petit dessin vaut mieux qu'un long discours, dit-on).
La signification de ces pictogrammes n'est cependant pas toujours évidente, surtout pour les utilisateurs néophytes. Il est donc vivement conseillé de compléter les barres d'outils à l'aide d'un système de bulles d'aide (tool tips), qui sont des petits messages explicatifs apparaissant automatiquement lorsque la souris survole les boutons concernés.
L'application décrite ci-après comporte une barre d'outils et un canevas. Lorsque l'utilisateur clique sur l'un des boutons de la barre, le pictogramme qu'il porte est recopié dans le canevas, à un emplacement choisi au hasard :
Dans notre exemple, chaque bouton apparaît entouré d'un sillon. Vous pouvez aisément obtenir d'autres aspects en choisissant judicieusement les options relief
et bd
(bordure) dans l'instruction d'instanciation des boutons. En particulier, vous pouvez choisir relief = FLAT
et bd = 0
pour obtenir des petits boutons « plats », sans aucun relief.
La mise en place des bulles d'aide est un jeu d'enfant. Il suffit d'instancier un seul objet Pmw.Balloon
pour l'ensemble de l'application, puis d'associer un texte à chacun des widgets auxquels on souhaite associer une bulle d'aide, en faisant appel autant de fois que nécessaire à la méthode bind()
de cet objet.
from Tkinter import *
import Pmw
from random import randrange
# noms des fichiers contenant les icônes (format GIF):
images =('floppy_2','papi2','pion_1','pion_2','help_4')
textes =('sauvegarde','papillon','joueur 1','joueur 2','Aide')
class Application(Frame):
def __init__(self):
Frame.__init__(self)
# Création d'un objet <bulle d'aide> (un seul suffit) :
tip = Pmw.Balloon(self)
# Création de la barre d'outils (c'est un simple cadre) :
toolbar = Frame(self, bd =1)
toolbar.pack(expand =YES, fill =X)
# Nombre de boutons à construire :
nBou = len(images)
# Les icônes des boutons doivent être placées dans des variables
# persistantes. Une liste fera l'affaire :
self.photoI =[None]*nBou
for b in range(nBou):
# Création de l'icône (objet PhotoImage Tkinter) :
self.photoI[b] =PhotoImage(file = images[b] +'.gif')
# Création du bouton.:
# On utilise une expression "lambda" pour transmettre
# un argument à la méthode invoquée comme commande :
bou = Button(toolbar, image =self.photoI[b], relief =GROOVE,
command = lambda arg =b: self.action(arg))
bou.pack(side =LEFT)
# association du bouton avec un texte d'aide (bulle) :
tip.bind(bou, textes[b])
self.ca = Canvas(self, width =400, height =200, bg ='orange')
self.ca.pack()
self.pack()
def action(self, b):
"l'icône du bouton b est recopiée dans le canevas"
x, y = randrange(25,375), randrange(25,175)
self.ca.create_image(x, y, image =self.photoI[b])
Application().mainloop()
Métaprogrammation. Expressions lambda :
modifierVous savez qu'en règle générale, on associe à chaque bouton une commande, laquelle est une méthode ou une fonction particulière qui se charge d'effectuer le travail lorsque le bouton est activé. Or dans l'application présente, tous les boutons doivent faire à peu près la même chose (recopier un dessin dans le canevas), la seule différence entre eux étant le dessin concerné.
Pour simplifier notre code, nous voudrions donc pouvoir associer l'option command
de tous nos boutons avec une seule et même méthode (ce sera la méthode action()
), mais en lui transmettant à chaque fois la référence du bouton particulier utilisé, de manière à ce que l'action accomplie puisse être différente pour chacun d'eux.
Une difficulté se présente, cependant, parce que l'option command du widget Button
accepte seulement une valeur ou une expression, et non une instruction. Il est donc permis de lui indiquer la référence d'une fonction, mais pas de l'invoquer véritablement en lui transmettant des arguments éventuels (c'est la raison pour laquelle on indique le nom de cette fonction sans lui adjoindre de parenthèses).
On peut résoudre cette difficulté de deux manières :
-
li> Du fait de son caractère dynamique, Python accepte qu'un programme puisse se modifier lui-même, par exemple en définissant de nouvelles fonctions au cours de son exécution (c'est le concept de métaprogrammation).
Il est donc possible de définir à la volée une fonction qui utilise des paramètres, en indiquant pour chacun de ceux-ci une valeur par défaut, et ensuite d'invoquer cette même fonction sans arguments là où ceux-ci ne sont pas autorisés. Puisque la fonction est définie en cours d'exécution, les valeurs par défaut peuvent être les contenus de variables, et le résultat de l'opération est un véritable transfert d'arguments.
Pour illustrer cette technique, remplacez les lignes 27 à 31 du script par les suivantes :
# Création du bouton.: # On définit à la volée une fonction avec un paramètre, dont # la valeur par défaut est l'argument à transmettre. # Cette fonction appelle la méthode qui nécessite un argument : def agir(arg = b): self.action(arg) # La commande associée au bouton appelle la fonction ci-dessus : bou = Button(toolbar, image =self.photoI[b], relief =GROOVE, command = agir)
def
, mais avec la différence que lambda
étant une expression et non une instruction, on peut l'utiliser comme interface afin d'invoquer une fonction (avec passage d'arguments) là où ce n'est normalement pas possible. Notez au passage qu'une telle fonction est anonyme (elle ne possède pas de nom).
Par exemple, l'instruction :
lambda ar1=b, ar2=c : bidule(ar1,ar2)
renvoie la référence d'une fonction anonyme qui aura elle-même invoqué la fonction bidule()
en lui transmettant les arguments b
et c
, ceux-ci étant utilisés comme valeurs par défaut dans la définition des paramètres de la fonction.
Cette technique utilise finalement le même principe que la précédente, mais elle présente l'avantage d'être plus concise, raison pour laquelle nous l'avons utilisée dans notre script. En revanche, elle est un peu plus difficile à comprendre :
command = lambda arg =b: self.action(arg)
Dans cette portion d'instruction, la commande associée au bouton se réfère à une fonction anonyme dont le paramètre arg
possède une valeur par défaut : la valeur de l'argument b
.
Invoquée sans argument par la commande, cette fonction anonyme peut tout de même utiliser son paramètre (avec la valeur par défaut) pour faire appel à la méthode cible self.action()
, et l'on obtient ainsi un véritable transfert d'argument vers cette méthode.Nous ne détaillerons pas davantage ici la question des expressions lambda, car elle déborde du cadre que nous nous sommes fixés pour cet ouvrage d'initiation. Si vous souhaitez en savoir plus, veuillez donc consulter l'un ou l'autre des ouvrages de référence cités dans la bibliographie.
Fenêtres avec menus
modifierNous allons décrire à présent la construction d'une fenêtre d'application dotée de différents types de menus « déroulants », chacun de ces menus pouvant être « détaché » de l'application principale pour devenir lui-même une petite fenêtre indépendante, comme dans l'illustration ci-dessous.
Cet exercice un peu plus long nous servira également de révision, et nous le réaliserons par étapes, en appliquant une stratégie de programmation que l'on appelle développement incrémental.
Comme nous l'avons déjà expliqué précédemment[1], cette méthode consiste à commencer l'écriture d'un programme par une ébauche, qui ne comporte que quelques lignes seulement mais qui est déjà fonctionnelle. On teste alors cette ébauche soigneusement afin d'en éliminer les bugs éventuels. Lorsque l'ébauche fonctionne correctement, on y ajoute une fonctionnalité supplémentaire. On teste ce complément jusqu'à ce qu'il donne entière satisfaction, puis on en ajoute un autre, et ainsi de suite...
Cela ne signifie pas que vous pouvez commencer directement à programmer sans avoir au préalable effectué une analyse sérieuse du projet, dont au moins les grandes lignes devront être convenablement décrites dans un cahier des charges clairement rédigé.
- Cahier des charges de l'exercice
Notre application comportera simplement une barre de menus et un canevas. Les différentes rubriques et options des menus ne serviront qu'à faire apparaître des fragments de texte dans le canevas ou à modifier des détails de décoration, mais ce seront avant tout des exemples variés, destinés à donner un aperçu des nombreuses possibilités offertes par ce type de widget, accessoire indispensable de toute application moderne d'une certaine importance.
Nous souhaitons également que le code produit dans cet exercice soit bien structuré. Pour ce faire, nous ferons usage de deux classes : une classe pour l'application principale, et une autre pour la barre de menus. Nous voulons procéder ainsi afin de bien mettre en évidence la construction d'une application type incorporant plusieurs classes d'objets interactifs.
Première ébauche du programme :
modifierLorsque l'on construit l'ébauche d'un programme, il faut tâcher d'y faire apparaître le plus tôt possible la structure d'ensemble, avec les relations entre les principaux blocs qui constitueront l'application définitive. C'est ce que nous nous sommes efforcés de faire dans l'exemple ci-dessous :
from Tkinter import * #1
#2
class MenuBar(Frame): #3
"""Barre de menus déroulants""" #4
def __init__(self, boss =None): #5
Frame.__init__(self, borderwidth =2) #6
#7
##### Menu <Fichier> ##### #8
fileMenu = Menubutton(self, text ='Fichier') #9
fileMenu.pack(side =LEFT) #10
# Partie "déroulante" : #11
me1 = Menu(fileMenu) #12
me1.add_command(label ='Effacer', underline =0, #13
command = boss.effacer) #14
me1.add_command(label ='Terminer', underline =0, #15
command = boss.quit) #16
# Intégration du menu : #17
fileMenu.configure(menu = me1) #18
#19
class Application(Frame): #20
"""Application principale""" #21
def __init__(self, boss =None): #22
Frame.__init__(self) #23
self.master.title('Fenêtre avec menus') #24
mBar = MenuBar(self) #25
mBar.pack() #26
self.can = Canvas(self, bg='light grey', height=190, #27
width=250, borderwidth =2) #28
self.can.pack() #29
self.pack() #30
#31
def effacer(self): #32
self.can.delete(ALL) #33
#34
if __name__ == '__main__': #35
app = Application() #36
app.mainloop() #37
Veuillez donc encoder ces lignes et en tester l'exécution. Vous devriez obtenir une fenêtre avec un canevas gris clair surmonté d'une barre de menus. À ce stade, la barre de menus ne comporte encore que la seule rubrique « Fichier ».
Cliquez sur la rubrique « fichier » pour faire apparaître le menu correspondant : l'option « Effacer » n'est pas encore fonctionnelle (elle servira à effacer le contenu du canevas), mais l'option « Terminer » devrait déjà vous permettre de fermer proprement l'application.
Comme tous les menus gérés par Tkinter, le menu que vous avez créé peut être converti en menu « flottant » : il suffit de cliquer sur la ligne pointillée apparaissant en-tête de menu. Vous obtenez ainsi une petite fenêtre satellite, que vous pouvez alors positionner où bon vous semble sur le bureau.
- Analyse du script
La structure de ce petit programme devrait désormais vous apparaître familière : afin que les classes définies dans ce script puissent éventuellement être (ré)utilisées dans d'autres projets par importation, comme nous l'avons déjà expliqué précédemment[2], le corps principal du programme (lignes 35 à 37) comporte l'instruction classique : if __name__ == '__main__'
:
Les deux instructions qui suivent consistent seulement à instancier un objet app
et à faire fonctionner sa méthode mainloop()
. Comme vous le savez certainement, nous aurions pu également condenser ces deux instructions en une seule.
L'essentiel du programme se trouve cependant dans les définitions de classes qui précèdent :
La classe MenuBar()
contient la description de la barre de menus. Dans l'état présent du script, elle se résume à une ébauche de constructeur.
- Ligne 5 : Le paramètre boss réceptionne la référence de la fenêtre maîtresse du widget au moment de son instanciation. Cette référence va nous permettre d'invoquer les méthodes associées à cette fenêtre maîtresse, aux lignes 14 & 16.
- Ligne 6 : Activation obligatoire du constructeur de la classe parente.
- Ligne 9 : Instanciation d'un widget de la classe
Menubutton()
, défini comme un « esclave » deself
(c'est-à-dire l'objet composite « barre de menus » dont nous sommes occupés à définir la classe). Comme l'indique son nom, ce type de widget se comporte un peu comme un bouton : une action se produit lorsque l'on clique dessus.
- Ligne 12 : Afin que cette action consiste en l'apparition véritable d'un menu, il reste encore à définir celui-ci : ce sera encore un nouveau widget, de la classe
Menu()
cette fois, défini lui-même comme un « esclave » du widgetMenubutton
instancié à la ligne 9.
- Lignes 13 à 16 : On peut appliquer aux widgets de la classe
Menu()
un certain nombre de méthodes spécifiques, chacune d'elles acceptant de nombreuses options. Nous utilisons ici la méthodeadd_command()
pour installer dans le menu les deux items « Effacer » et « Terminer ». Nous y intégrons tout de suite l'option underline, qui sert à définir un raccourci clavier : cette option indique en effet lequel des caractères de l'item doit apparaître souligné à l'écran. L'utilisateur sait alors qu'il lui suffit de frapper ce caractère au clavier pour que l'action correspondant à cet item soit activée (comme s'il avait cliqué dessus à l'aide de la souris).
L'action à déclencher lorsque l'utilisateur sélectionne l'item est désignée par l'option command. Dans notre script, les commandes invoquées sont toutes les deux des méthodes de la fenêtre maîtresse, dont la référence aura été transmise au présent widget au moment de son instanciation par l'intermédiaire du paramètreboss
. La méthodeeffacer()
, que nous définissons nous-même plus loin, servira à vider le canevas. La méthode prédéfiniequit()
provoque la sortie de la bouclemainloop()
et donc l'arrêt du réceptionnaire d'événements associé à la fenêtre d'application.
- Ligne 18 : Lorsque les items du menu ont été définis, il reste encore à reconfigurer le widget maître
Menubutton
de manière à ce que son option « menu » désigne effectivement le Menu que nous venons de construire. En effet, nous ne pouvions pas déjà préciser cette option lors de la définition initiale du widgetMenubutton
, puisqu'à ce stade leMenu
n'existait pas encore. Nous ne pouvions pas non plus définir le widgetMenu
en premier lieu, puisque celui-ci doit être défini comme un « esclave » du widgetMenubutton
. Il faut donc bien procéder en trois étapes comme nous l'avons fait, en faisant appel à la méthodeconfigure()
. (Cette méthode peut être appliquée à n'importe quel widget préexistant pour en modifier l'une ou l'autre option).
La classe Application()
contient la description de la fenêtre principale du programme ainsi que les méthodes gestionnaires d'événements qui lui sont associées.
- Ligne 20 : Nous préférons faire dériver notre application de la classe
Frame()
, qui présente de nombreuses options, plutôt que de la classe primordialeTk()
. De cette manière, l'application toute entière est encapsulée dans un widget, lequel pourra éventuellement être intégré par la suite dans une application plus importante. Rappelons que de toute manière, Tkinter instanciera automatiquement une fenêtre maîtresse de typeTk()
pour contenir de cette Frame.
- Lignes 23-24 : Après l'indispensable activation du constructeur de la classe parente, nous utilisons l'attribut master que Tkinter associe automatiquement à chaque widget, pour référencer la fenêtre principale de l'application (la fenêtre maîtresse dont nous venons de parler au paragraphe précédent) et en redéfinir le bandeau-titre.
- Lignes 25 à 29 : Instanciation de deux widgets esclaves pour notre Frame principale. La « barre de menus » est évidemment le widget défini dans l'autre classe.
- Ligne 30 : Comme n'importe quel autre widget, notre Frame principale doit être mise en place.
- Lignes 32-33 : La méthode servant à effacer le canevas est définie dans la classe présente (puisque l'objet canevas en fait partie), mais elle est invoquée par l'option command d'un widget esclave défini dans l'autre classe. Comme nous l'avons expliqué plus haut, ce widget esclave reçoit la référence de son widget maître par l'intermédiaire du paramètre
boss
. Toutes ces références sont hiérarchisées à l'aide de la qualification des noms par points.
Ajout de la rubrique « Musiciens »
modifierContinuez le développement de ce petit programme, en ajoutant les lignes suivantes dans le constructeur de la classe MenuBar()
(après la ligne 18) :
##### Menu <Musiciens> ##### self.musi = Menubutton(self, text ='Musiciens') self.musi.pack(side =LEFT, padx ='3') # Partie "déroulante" du menu <Musiciens> : me1 = Menu(self.musi) me1.add_command(label ='17e siècle', underline =1, foreground ='red', background ='yellow', font =('Comic Sans MS', 11), command = boss.showMusi17) me1.add_command(label ='18e siècle', underline =1, foreground='royal blue', background ='white', font =('Comic Sans MS', 11, 'bold'), command = boss.showMusi18) # Intégration du menu : self.musi.configure(menu = me1)
... ainsi que les définitions de méthodes suivantes à la classe Application() (après la ligne 33) :
def showMusi17(self): self.can.create_text(10, 10, anchor =NW, text ='H. Purcell', font=('Times', 20, 'bold'), fill ='yellow') def showMusi18(self): self.can.create_text(245, 40, anchor =NE, text ="W. A. Mozart", font =('Times', 20, 'italic'), fill ='dark green')
Lorsque vous y aurez ajouté toutes ces lignes, sauvegardez le script et exécutez-le.
Votre barre de menus comporte à présent une rubrique supplémentaire : la rubrique « Musiciens ».
Le menu correspondant propose deux items qui sont affichés avec des couleurs et des polices personnalisées. Vous pourrez vous inspirer de ces techniques décoratives pour vos projets personnels. À utiliser avec modération !
Les commandes que nous avons associées à ces items sont évidemment simplifiées afin de ne pas alourdir l'exercice : elles provoquent l'affichage de petits textes sur le canevas.
- Analyse du script
Les seules nouveautés introduites dans ces lignes concernent l'utilisation de polices de caractères bien déterminées (option font
), ainsi que de couleurs pour l'avant-plan (option foreground
) et le fond (option background
) des textes affichés.
Veuillez noter encore une fois l'utilisation de l'option underline
pour désigner les caractères correspondant à des raccourcis claviers (en n'oubliant pas que la numérotation des caractères d'une chaîne commence à partir de zéro), et surtout que l'option command
de ces widgets accède aux méthodes de l'autre classe, par l'intermédiaire de la référence mémorisée dans l'attribut boss
.
La méthode create_text()
du canevas doit être utilisée avec deux arguments numériques, qui sont les coordonnées X et Y d'un point dans le canevas. Le texte transmis sera positionné par rapport à ce point, en fonction de la valeur choisie pour l'option anchor : Celle-ci détermine comment le fragment de texte doit être « ancré » au point choisi dans le canevas, par son centre, par son coin supérieur gauche, etc., en fonction d'une syntaxe qui utilise l'analogie des points cardinaux géographiques (NW = angle supérieur gauche, SE = angle inférieur droit, CENTER = centre, etc.)
Ajout de la rubrique « Peintres » :
modifierCette nouvelle rubrique est construite d'une manière assez semblable à la précédente, mais nous lui avons ajouté une fonctionnalité supplémentaire : des menus « en cascade ». Veuillez donc ajouter les lignes suivantes dans le constructeur de la classe MenuBar()
:
##### Menu <Peintres> ##### self.pein = Menubutton(self, text ='Peintres') self.pein.pack(side =LEFT, padx='3') # Partie "déroulante" : me1 = Menu(self.pein) me1.add_command(label ='classiques', state=DISABLED) me1.add_command(label ='romantiques', underline =0, command = boss.showRomanti) # Sous-menu pour les peintres impressionistes : me2 = Menu(me1) me2.add_command(label ='Claude Monet', underline =7, command = boss.tabMonet) me2.add_command(label ='Auguste Renoir', underline =8, command = boss.tabRenoir) me2.add_command(label ='Edgar Degas', underline =6, command = boss.tabDegas) # Intégration du sous-menu : me1.add_cascade(label ='impressionistes', underline=0, menu =me2) # Intégration du menu : self.pein.configure(menu =me1)
... et les définitions suivantes dans la classe Application() :
def showRomanti(self): self.can.create_text(245, 70, anchor =NE, text = "E. Delacroix", font =('Times', 20, 'bold italic'), fill ='blue') def tabMonet(self): self.can.create_text(10, 100, anchor =NW, text = 'Nymphéas à Giverny', font =('Technical', 20), fill ='red') def tabRenoir(self): self.can.create_text(10, 130, anchor =NW, text = 'Le moulin de la galette', font =('Dom Casual BT', 20), fill ='maroon') def tabDegas(self): self.can.create_text(10, 160, anchor =NW, text = 'Danseuses au repos', font =('President', 20), fill ='purple')
- Analyse du script
Vous pouvez réaliser aisément des menus en cascade, en enchaînant des sous-menus les uns aux autres jusqu'à un niveau quelconque (il vous est cependant déconseillé d'aller au-delà de 5 niveaux successifs : vos utilisateurs s'y perdraient).
Un sous-menu est défini comme un menu « esclave » du menu de niveau précédent (dans notre exemple, me2 est défini comme un menu « esclave » de me1
). L'intégration est assurée ensuite à l'aide de la méthode add_cascade()
.
L'un des items est désactivé (option state = DISABLED
). L'exemple suivant vous montrera comment vous pouvez activer ou désactiver à volonté des items, par programme.
Ajout de la rubrique « Options » :
modifierLa définition de cette rubrique est un peu plus compliquée, parce que nous allons y intégrer l'utilisation de variables internes à Tkinter.
Les fonctionnalités de ce menu sont cependant beaucoup plus élaborées : les options ajoutées permettent en effet d'activer ou de désactiver à volonté les rubriques « Musiciens » et « Peintres », et vous pouvez également modifier à volonté l'aspect de la barre de menus elle-même.
Veuillez donc ajouter les lignes suivantes dans le constructeur de la classe MenuBar()
:
##### Menu <Options> ##### optMenu = Menubutton(self, text ='Options') optMenu.pack(side =LEFT, padx ='3') # Variables Tkinter : self.relief = IntVar() self.actPein = IntVar() self.actMusi = IntVar() # Partie "déroulante" du menu : self.mo = Menu(optMenu) self.mo.add_command(label = 'Activer :', foreground ='blue') self.mo.add_checkbutton(label ='musiciens', command = self.choixActifs, variable =self.actMusi) self.mo.add_checkbutton(label ='peintres', command = self.choixActifs, variable =self.actPein) self.mo.add_separator() self.mo.add_command(label = 'Relief :', foreground ='blue') for (v, lab) in [(0,'aucun'), (1,'sorti'), (2,'rentré'), (3,'sillon'), (4,'crête'), (5,'bordure')]: self.mo.add_radiobutton(label =lab, variable =self.relief, value =v, command =self.reliefBarre) # Intégration du menu : optMenu.configure(menu = self.mo)
... ainsi que les définitions de méthodes suivantes (toujours dans la classe MenuBar()
) :
def reliefBarre(self): choix = self.relief.get() self.configure(relief =[FLAT,RAISED,SUNKEN,GROOVE,RIDGE,SOLID][choix]) def choixActifs(self): p = self.actPein.get() m = self.actMusi.get() self.pein.configure(state =[DISABLED, NORMAL][p]) self.musi.configure(state =[DISABLED, NORMAL][m])
- Analyse du script
- Menu avec « cases à cocher »
Notre nouveau menu déroulant comporte deux parties. Afin de bien les mettre en évidence, nous avons inséré une ligne de séparation ainsi que deux « faux items » (« Activer : » et « Relief : ») qui servent simplement de titres. Nous faisons apparaître ceux-ci en couleur pour que l'utilisateur ne les confonde pas avec de véritables commandes.
Les items de la première partie sont dotées de « cases à cocher ». Lorsque l'utilisateur effectue un clic de souris sur l'un ou l'autre de ces items, les options correspondantes sont activées ou désactivées, et ces états « actif / inactif » sont affichés sous la forme d'une coche. Les instructions qui servent à mettre en place ce type de rubrique sont assez explicites. Elles présentent en effet ces items comme des widgets de type chekbutton
:
self.mo.add_checkbutton(label = 'musiciens', command = choixActifs, variable = mbu.me1.music)
Il est important de comprendre ici que ce type de widget comporte nécessairement une variable interne, destinée à mémoriser l'état « actif / inactif » du widget. Cette variable ne peut pas être une variable Python ordinaire, parce que les classes de la bibliothèque Tkinter sont écrites dans un autre langage. Et par conséquent, on ne pourra accéder à une telle variable interne qu'à travers une interface. Cette interface, appelée « variable Tkinter », est en fait un objet, que l'on crée à partir d'une classe particulière, qui fait partie du module Tkinter au même titre que les classes de widgets. L'utilisation de ces « objets-variables » est relativement simple :
- La classe
IntVar()
permet de créer des objets équivalents à des variables de type entier. On commence donc par créer un ou plusieurs de ces objets-variables, que l'on mémorise dans notre exemple comme de nouveaux attribiuts d'instance :self.actMusi =IntVar()
Après cette affectation, l'objet référencé dansself.actMusi
contient désormais l'équivalent d'une variable de type entier, dans un format spécifique à Tkinter. - Ensuite, on associe l'option
variable
de l'objetcheckbutton
à la variable Tkinter ainsi définie :self.mo.add_checkbutton(label ='musiciens', variable =self.actMusi)
- Il est nécessaire de procéder ainsi en deux étapes, parce que Tkinter ne peut pas directement assigner des valeurs aux variables Python. Pour une raison similaire, il n'est pas possible à Python de lire directement le contenu d'une variable Tkinter. Il faut utiliser pour cela une méthode spécifique de cette classe d'objets : la méthode
get()
[3] :m = self.actMusi.get()
Dans cette instruction, nous affectons àm
(variable ordinaire de Python) le contenu d'une variable Tkinter (laquelle est elle-même associée à un widget bien déterminé).
Tout ce qui précède peut vous paraître un peu compliqué. Considérez simplement qu'il s'agit de votre première rencontre avec les problèmes d'interfaçage entre deux langages de programmation différents, utilisés ensemble dans un projet composite.
- Menu avec choix exclusifs
La deuxième partie du menu « Options » permet à l'utilisateur de choisir l'aspect que prendra la barre de menus, parmi six possibilités. Il va de soi que l'on ne peut activer qu'une seule de ces possibilités à la fois. Pour mettre en place ce genre de fonctionnalité, on fait classiquement appel appel à des widgets de type « boutons radio ». La caractéristique essentielle de ces widgets est que plusieurs d'entre eux doivent être associés à une seule et même variable Tkinter. À chaque bouton radio correspond alors une valeur particulière, et c'est cette valeur qui est affectée à la variable lorsque l'utilisateur sélectionne le bouton.
Ainsi, l'instruction :
self.mo.add_radiobutton(label ='sillon', variable =self.relief, value =3, command =self.reliefBarre)
configure un item du menu «Options» de telle manière qu'il se comporte comme un bouton radio.
Lorsque l'utilisateur sélectionne cet item, la valeur 3 est affectée à la variable Tkinter self.relief
(celle-ci étant désignée à l'aide de l'option variable
du widget), et un appel est lancé en direction de la méthode reliefBarre()
. Celle-ci récupère alors la valeur mémorisée dans la variable Tkinter pour effectuer son travail.
Dans le contexte particulier de ce menu, nous souhaitons proposer 6 possibilités différentes à l'utilisateur. Il nous faut donc six « boutons radio », pour lesquels nous pourrions encoder six instructions similaires à celle que nous avons reproduite ci-dessus, chacune d'elles ne différant des cinq autres que par ses options value
et label
. Dans une situation de ce genre, la bonne pratique de programmation consiste à placer les valeurs de ces options dans une liste, et à parcourir ensuite cette liste à l'aide d'une boucle for
, afin d'instancier les widgets avec une instruction commune :
for (v, lab) in [(0,'aucun'), (1,'sorti'), (2,'rentré'), (3,'sillon'), (4,'crête'), (5,'bordure')]: self.mo.add_radiobutton(label =lab, variable =self.relief, value =v, command =self.reliefBarre)
La liste utilisée est une liste de six tuples (valeur, libellé). À chacune des 6 itérations de la boucle, un nouvel item radiobutton
est instancié, dont les options label
et value
sont extraites de la liste par l'intermédiaire des variables lab
et v
.
Dans vos projets personnels, il vous arrivera fréquemment de constater que vous pouvez ainsi remplacer des suites d'instructions similaires, par une structure de programmation plus compacte (en général, la combinaison d'une liste et d'une boucle, comme dans l'exemple ci-dessus).
Vous découvrirez petit à petit encore d'autres techniques pour alléger votre code : nous en fournissons encore un exemple dans le paragraphe suivant. Tâchez cependant de garder à l'esprit cette règle essentielle, qu'un bon programme doit avant tout rester lisible et commenté.
- Contrôle du flux d'exécution à l'aide d'une liste
Veuillez à présent considérer la définition de la méthode reliefBarre()
:
À la première ligne, la méthode get()
nous permet de récupérer l'état d'une variable Tkinter qui contient le numéro du choix opéré par l'utilisateur dans le sous-menu « Relief : ».
À la seconde ligne, nous utilisons le contenu de la variable choix
pour extraire d'une liste de six éléments celui qui nous intéresse. Par exemple, si choix
contient la valeur 2
, c'est l'option SUNKEN
qui sera utilisée pour reconfigurer le widget.
La variable choix
est donc utilisée ici comme un index, servant à désigner un élément de la liste. En lieu et place de cette construction compacte, nous aurions pu programmer une série de tests conditionnels, comme par exemple :
if choix ==0: self.configure(relief =FLAT) elif choix ==1: self.configure(relief =RAISED) elif choix ==2: self.configure(relief =SUNKEN) ... etc.
D'un point de vue strictement fonctionnel, le résultat serait exactement le même. Vous admettrez cependant que la construction que nous avons choisie est d'autant plus efficiente, que le nombre de possibilités de choix est élevé. Imaginez par exemple que l'un de vos programmes personnels doive effectuer une sélection dans un très grand nombre d'éléments : avec une construction du type ci-dessus, vous seriez peut-être amené à encoder plusieurs pages de « elif
» !
Nous utilisons encore la même technique dans la méthode choixActifs()
. Ainsi l'instruction :
self.pein.configure(state =[DISABLED, NORMAL][p])
utilise le contenu de la variable p
comme index pour désigner lequel des deux états DISABLED
, NORMAL
doit être sélectionné pour reconfigurer le menu « Peintres ».
Lorsqu'elle est appelée, la méthode choixActifs()
reconfigure donc les deux rubriques « Peintres » et « Musiciens » de la barre de menus, pour les faire apparaître « normales » ou « désactivées » en fonction de l'état des variables m
et p
, lesquelles sont elles-mêmes le reflet de variables Tkinter.
Ces variables intermédiaires m
et p
ne servent en fait qu'à clarifier le script. Il serait en effet parfaitement possible de les éliminer, et de rendre le script encore plus compact, en utilisant la composition d'instructions. On pourrait par exemple remplacer les deux instructions :
m = self.actMusi.get() self.musi.configure(state =[DISABLED, NORMAL][m])
par une seule, telle que :
self.musi.configure(state =[DISABLED, NORMAL][self.actMusi.get()])
Notez cependant que ce que l'on gagne en compacité se paie d'une certaine perte de lisibilité.
- Pré-sélection d'une rubrique
Pour terminer cet exercice, voyons encore comment vous pouvez déterminer à l'avance certaines sélections, ou bien les modifier par programme.
Veuillez donc ajouter l'instruction suivante dans le constructeur de la classe Application()
(juste avant l'instruction self.pack()
, par exemple) :
mBar.mo.invoke(2)
Lorsque vous exécutez le script ainsi modifié, vous constatez qu'au départ la rubrique « Musiciens » de la barre de menus est active, alors que la rubrique « Peintres » ne l'est pas. Programmées comme elles le sont, ces deux rubriques devraient être actives toutes deux par défaut. Et c'est effectivement ce qui se passe si nous supprimons l'instruction :
mBar.mo.invoke(2)
Nous vous avons suggéré d'ajouter cette instruction au script, pour vous montrer comment vous pouvez effectuer par programme la même opération que celle que l'on obtient normalement avec un clic de souris.
L'instruction ci-dessus invoque le widget mBar.mo
en actionnant la commande associée au deuxième item de ce widget. En consultant le listing, vous pouvez vérifier que ce deuxième item est bien l'objet de type checkbutton qui active/désactive le menu « Peintres » (Rappelons encore une fois que l'on numérote toujours à partir de zéro).
Au démarrage du programme, tout se passe donc comme si l'utilisateur effectuait tout de suite un premier clic sur la rubrique « Peintres » du menu « Options », ce qui a pour effet de désactiver le menu correspondant.Mais si vous voulez le réactiver , vous devrez éteinde votre ordinateur et le tour sera joué !
Notes
modifier
Turtle
Un peu de détente avec le module turtle
modifierTurtle est un module graphique du langage de programmation Python. Il est inspiré de la programmation Logo et permet de déplacer une tortue sur l’écran.
Comme nous venons de le voir, l'une des grandes qualités de Python est qu'il est extrêmement facile de lui ajouter de nombreuses fonctionnalités par importation de divers modules.
Pour illustrer cela, et nous amuser un peu avec d'autres objets que des nombres, nous allons explorer un module Python qui permet de réaliser des « graphiques tortue », c'est-à-dire des dessins géométriques correspondant à la piste laissée derrière elle par une petite « tortue » virtuelle, dont nous contrôlons les déplacements sur l'écran de l'ordinateur à l'aide d'instructions simples.
Activer cette tortue est un vrai jeu d'enfant. Plutôt que de vous donner de longues explications, nous vous invitons à essayer tout de suite :
>>> from turtle import *
>>> forward(90)
>>> left(60)
>>> color('blue')
>>> forward(50)
L'exercice est évidemment plus riche si l'on utilise des boucles :
>>> reset()
>>> a = 0
>>> while a <12:
a+=1
forward(150)
left(150)
Attention cependant : avant de lancer un tel script, assurez-vous toujours qu'il ne comporte pas de boucle sans fin, car si c'est le cas vous risquez de ne plus pouvoir reprendre le contrôle des opérations (en cas de boucle infinie, pressez CTRL + C, ou alors arrêter Python dans le gestionnaire de tâches sous Windows ou le moniteur d'activité sur Mac et Linux).
Fonctions disponibles
modifierPour utiliser une fonction il faut inscrire son nom et lui donner une valeur dans les parenthèses.
Les principales fonctions mises à votre disposition dans le module turtle sont les suivantes :
Déplacement
modifierforward(distance)
: Avance d’une distance donnée. Type de donnée : pixel.backward(distance)
: Recule d’une distance donnée. Type de donnée : pixel.left(angle)
: Pivote vers la gauche. Type de donnée : angle.right(angle)
: Pivote vers la droite. Type de donnée : angle.goto(x, y)
: Va à l’endroit de coordonnées (x, y) - Type de donnée : pixel /!\ Ne pas oublier d'utiliser la fonctionup()
avant d'utilisergoto()
car sinon il tracera le parcours effectué.
Gestion de l'écran, de l'affichage
modifierreset()
: Efface l’écran, recentre la tortue et remet les variables à zéro.up()
: Relève le crayon (pour pouvoir avancer sans dessiner).down()
: Abaisse le crayon (pour recommencer à dessiner).ht()
: Masque la tortue.st()
: Afficher la tortue.title(titre)
: Donne un titre à la fenêtre (par défaut le titre est "Turtle Graphics"). Type de donnée : chaîne de caractère.width(épaisseur)
: Choisit l’épaisseur du tracé. Type de donnée : pixel.speed(vitesse)
: Choisit la vitesse à laquelle se déplace le curseur. Type de donnée : chaîne de caractère. Vitesses proposées :"slowest"
=> Le plus lent"slow"
=> Lent"normal"
=> Normal"fast"
=> Rapide"fastest"
=> Le plus rapide
Gestion de la couleur
modifiercolor("couleur")
: Détermine la couleur du tracé (noir par défaut). Type de donnée : chaîne de caractère- bgcolor("couleur") : Préciser la couleur de l'arrière plan de la scène.
fill(1)
: Remplit un contour fermé à l’aide de la couleur sélectionnée.
La couleur peut être :
- une couleur prédéfinie précisée entre apostrophes droites (exemples : 'red', 'yellow', 'green', 'blue', 'brown', 'violet', 'purple', etc.),
- une couleur RVB avec trois variables r, v, et b comprises entre 0 et 1 (exemple : 1,0,0 pour le rouge).
Couleurs proposées :
- "blue" => Bleu
- "red" => Rouge
- "green" => Vert
- "yellow" => Jaune
- "brown" => Marron
- "black" => Noir
- "white" => Blanc
- "pink" => Rose
- "orange" => Orange
- "purple" => Violet
- "grey" => Gris
Le remplissage peut se faire de deux manières. Par exemple pour remplir un carré :
begin_fill()
forward(100)
left(90)
forward(100)
left(90)
forward(100)
left(90)
forward(100)
end_fill()
|
fill(1)
forward(100)
left(90)
forward(100)
left(90)
forward(100)
left(90)
forward(100)
fill(0)
|
Objets utiles
modifiercircle(rayon, angle)
: Trace un cercle de rayon donné. L’argument facultatif angle indique l’angle de l’arc de cercle (par défaut 360, soit un cercle complet). Type de donnée : rayon en pixel, angle un angle en degré.write(texte)
: Écrit du texte. Type de donnée : chaîne de caractère.
Compléments
modifier- Le module turtle s'appuie sur le module tkinter (Tkinter pour les versions de python antérieures à 3), ça permet d'en utiliser les fonctionnalités. Par exemple pour enregistrer sa production au format postscript :
import turtle
#......
#.......
turtle.getcanvas().postscript(file="monoeuvre.eps")
- Le module turtle permet de manipuler plusieurs tortues.
fred = turtle.Turtle()
martha = turtle.Turtle()
fred.forward(100)
martha.left(45)
Si les méthodes associées aux tortues sont utilisées sans référence à une tortue particulière, elles s'appliquent à une tortue "anonyme". Celle-ci est automatiquement créée pour être l'objet de la méthode.
De même, si aucune fenêtre d'affichage n'existe, une telle fenêtre est automatiquement créée.
Ces automatismes, permettent avec simplicité, le lancement de l'environnement de dessin, dès l'utilisation d'une méthode tortue. Ils sont réellement pratiques, pour faciliter l'initiation au langage algorithmique, ce qui est la fonction d'origine des langages à géométries tortues.
Cependant ces automatismes peuvent ensuite nuire à la compréhension de l'aspect objet du langage, en devenant une source de confusion quant aux instanciations d'objets, tortue (anonyme) ou écran, réalisées ainsi implicitement.
C'est pour cela qu'il vaut donc mieux, ensuite, les expliciter.
scene = turtle.Screen()
scene.setup(width=0.5, height=1.0, startx=-1, starty=0)
# width vraie dimension si entier, proportion de l'écran si décimal
# startx entier, position par rapport à la gauche de l'écran si positif, par rapport à la droite si négatif
Exemples
modifier- Python 3 par exemple/Turtle
- Un exemple de fonction avec le module Turtle a déjà été présenté sur la page fonctions. Vous y trouverez aussi trois petits exercices.
Tracer une spirale quelconque
modifierfrom turtle import *
angle = 5
distance = 10
compteur = 0
while compteur <= 30:
forward(distance)
left(angle)
compteur += 1
angle += 2
Mais aussi de manière plus simple:
from turtle import *
speed("fastest") #Pour l'aider à aller plus vite
rayon = 1 #Le premier rayon par défaut
rayonSpiral = 100
while(rayon < rayonSpiral):
circle(rayon, 180)
rayon += 2 #écartement entre 2 demi-cercles de la spirale
Reproduction de l'œuvre "composition arithmétique" de Theo Van Doesburg
modifierfrom turtle import *
from math import *
def cadre(x):
begin_fill()
for i in range(4):
forward(x)
left(90)
end_fill()
def carre (x): # x est la taille du cadre
y=(sqrt(2*x**2))/3 # y est la taille du carré
up()
left (45)
forward(y)
down()
begin_fill()
for i in range (4):
forward (y)
left(90)
end_fill()
up()
forward(y/2)
right(135)
forward(x/2)
left(90)
x=600 # initialisation
up() # on se déplace en (-x, -x)
goto(-x/2,-x/2)
down()
color("grey") # couleur du cadre principal
cadre(x) # on dessine le cadre principal
color ("white") # couleur des carrés
for i in range (4): # on dessine les 4 carrés succesifs
carre(x/2**i)
ht() # on cache la tortue à la fin du tracé
getcanvas().postscript(file="monoeuvre.eps") # on récupère le fichier image au format eps
done()
Tracer une lanterne
modifierfrom turtle import *
import math
def lanterne(l = 100):
left(90)
forward(l)
right(90)
forward(l)
right(90)
forward(l)
right(90)
forward(l)
right(135)
forward(l * math.sqrt(2))
left(90)
fillcolor("red")
begin_fill()
forward(l * math.sqrt(2) / 2)
left(90)
forward(l * math.sqrt(2) / 2)
end_fill()
left(90)
forward(l * math.sqrt(2))
right(45)
lanterne(l = 100)
ht()
done()
Tracer un parcours/message
modifierLes traces laissées par la tortue peuvent aussi servir à afficher des messages secrets, des itinéraires... à partir d'éléments simples. On peut attribuer, par exemple aux 4 lettres A, R, D et G les fonctions Avancer, Reculer, tourner à Droite et tourner à Gauche. La tortue pourra ainsi se déplacer selon un ordre compact tel que "ADADADAD" pour tracer un carré.
Exemple permettant de tracer un mot mystère (ici un pseudonyme).
from turtle import *
def parcours(instruction, pas):
""" fonction gérant l'affichage.
arguments :
- instruction : une chaîne de caractère composée de A, R, D ou G
- pas : un entier, donnant en pixel le déplacement de chaque instruction Avancer ou Reculer
"""
for iteration in range(len(instruction)):
if instruction[iteration] == "A" :
forward(pas)
elif instruction[iteration] == "R":
backward(pas)
elif instruction[iteration] == "D":
right(90)
elif instruction[iteration] == "G":
left(90)
else: #pour gérer tout ce qui n'est pas A, G, D, R
pass
# Variable avec le mot mystère
pseudo = "RRDAAGAADAADAADDAAAGAAAARRRRDAGAAAAARRRRRDAAGAADAARRGAADAADAAAAGAGAAAADADAAAAGAGAAAARRRRDAGAAAADAADAAAADAADDAAAAARRGAAAADAA"
#Appel de la fonction pour afficher pseudo avec un pas de 10
parcours(pseudo, 10)
Sources
modifier
Les threads
Les threads Python sont utilisés pour lancer de multiples processus légers (tâches, appels de fonction) en même temps (mais cela ne veut pas forcément dire sur plusieurs processeurs). Les threads Python n'accélèreront donc pas un programme qui utilise déjà 100 % du temps d'un CPU (voir la programmation parallèle[1] pour cela).
Les threads Python sont utilisés si une tâche fait attendre les autres. C'est le cas par exemple, des interactions avec un serveur web, ou lors de l'utilisation de la fonction sleep
.
Exemples
modifierAppel de fonction
modifierPour afficher les nombres de 1 à 10, en attendant une seconde entre chaque :
#!/usr/bin/env python
# coding: utf-8
import threading
import time
def loop1_10():
for i in range(1, 11):
time.sleep(1)
print(i)
threading.Thread(target=loop1_10).start()
Objet
modifier#!/usr/bin/env python
# coding: utf-8
import threading
import time
from __future__ import print_function
class MyThread(threading.Thread):
def run(self):
print("{} started!".format(self.getName())) # affiche "Thread-x started!"
time.sleep(1) # simule un travail d'une seconde
print("{} finished!".format(self.getName())) # affiche "Thread-x finished!"
if __name__ == '__main__':
for x in range(4): # Quatre fois...
mythread = MyThread(name = "Thread-{}".format(x + 1)) # ...Instancie un thread et lui donne un ID unique
mythread.start() # ...Démarre le thread
time.sleep(.9) # ...Attend 0,9 secondes avant d'en démarrer un autre
Résultat :
Thread-1 started! Thread-2 started! Thread-1 finished! Thread-3 started! Thread-2 finished! Thread-4 started! Thread-3 finished! Thread-4 finished!
Note : cet exemple diminue l'IDLE (temps d'inactivité) dans Windows XP, alors que cela fonctionne dans WING IDE, Netbeans, et Eclipse. Cela peut poser problème en remplaçant sleep(1)
par (2), et range(4)
par range(10)
. Le Thread-2 finit alors en première ligne (avant d'avoir commencé).
Références
modifier
XML
Il existe dans Python une bibliothèque permettant de manipuler les fichiers XML. Elle implémente la manière SAX (Simple API for XML) et DOM (Document Object Model).
Voyons comment manipuler simplement les fichiers XML grâce à la méthode SAX.
La méthode SAX
modifierCette méthode est basée sur les évènements : une fonction est appelée lorsque l'on ouvre une balise, une autre lorsque le programme rencontre du texte, et une autre encore lorsque la balise se ferme.
Ces évènements sont définis dans une classe nommée interface. Cette classe doit dériver de ContentHandler, contenu dans le module xml.sax, et peut implémenter les fonctions suivantes :
- startElement() est la fonction appelée lors de l' ouverture d' une balise. Les deux arguments sont le nom et un dictionnaire contenant les attributs.
- endElement() est la fonction appelée lors de la fermeture d' une balise. La fonction prend le nom de la balise en argument.
- characters() est appelée lorsque le parseur rencontre un caractère en dehors d' une balise. Ce caractère est passé en paramètre.
Une fois cette classe faite, il faut créer un parseur. Cela est fait grâce à la fonction make_parser(), située elle aussi dans le module xml.sax. On lui associe ensuite une instance de l'interface avec la méthode setContentHandler(), et on lance l'analyse du fichier grâce à la méthode parse, qui prend le nom du fichier en argument.
Voici donc le code final :
from xml.sax.handler import ContentHandler
import xml.sax
import sys
class compteurElements(ContentHandler):
def __init__(self):
self.elem={}
def startElement(self,name,attr):
if not self.elem.has_key(name):
self.elem[name] = 0
self.elem[name] += 1
def characters(self,ch):
print ch
def endElement (self, name):
print name + ":" + str(self.elem[name])
parser = xml.sax.make_parser()
handler = compteurElements()
parser.setContentHandler(handler)
parser.parse(sys.argv[1])
|
Exemple 1 : Un compteur d'éléments bavard |
La méthode DOM
modifierLe module se nomme xml.dom
[1] :
import xml.dom
Références
modifier
Tests
Tests unitaires
modifierLa bibliothèque unittest[1] permet d'automatiser les tests unitaires.
Syntaxe
modifierLa méthode assertEqual() permet de vérifier la valeur deux deux objets, généralement une constante et une variable issue du code à tester :
import unittest
x = 1
y = 1
self.assertEqual(x, y)
Méthodes de tests possibles :
- assertAlmostEqual(a, b)
- assertEqual(a, b)
- assertFalse(a)
- assertIn(a, b)
- assertIs(a, b)
- assertIsInstance(a, b)
- assertIsNone(a)
- assertIsNot(a, b)
- assertIsNotNone(a)
- assertNotAlmostEqual(a, b)
- assertNotEqual(a, b)
- assertNotEqual(a, b)
- assertNotIn(a, b)
- assertNotIsInstance(a, b)
- assertNotRegex(a, b)
- assertRaises(a)
- assertRaisesRegex(a)
- assertRegex(a, b)
- assertTrue(a)
Exécution
modifierSi le test est dans le fichier TestClass dossier tests/ :
python -m unittest tests/TestClass.py
ou
python -m unittest tests.TestClass
Dans cette deuxième syntaxe, on peut aussi rajouter le nom de la méthode à tester après la classe.
Sinon, il existe plus rapide : dans PyCharm il suffit de cliquer sur le triangle vert situé dans la marge du test pour le lancer.
Exemples
modifierExemple simple
modifier#!/usr/bin/env python
# coding: utf-8
import unittest
import tested_class
class TestClass(unittest.TestCase):
def test_replace(self):
test_input = 'test'
test_output = 'Test'
self.assertEqual(test_output, tested_class.replace(test_input))
if __name__ == '__main__':
unittest.main()
Pywikibot
modifierLe framework Pywikipedia propose toute une série de tests basés sur unittest.
- Le télécharger sur http://tools.wmflabs.org/pywikibot/
- Conformément au manuel tests/README.rst, lancer
python pwb.py tests/api_tests.py -v
.
Tests fonctionnels
modifierIl existe le framework pytest[2].
Références
modifier
Fabric
Fabric est une bibliothèque pour manipuler, en lignes de commandes, des serveurs distants en SSH[1].
Installation
modifierLa version 1 fonctionne sur Python 2 et la 2 sur Python 3.
sudo pip install fabric==1.14.1
Lister les fichiers et sous-dossiers d'un dossier
modifierfrom fabric.api import env, run
env.host_string = 'mon serveur'
env.user = 'mon utilisateur'
string = run("for i in %s*; do echo $i; done" % mon_dossier_distant)
files = string.replace("\r","").split("\n")
print(files)
Références
modifier
Web
Il existe différents frameworks et serveurs web pour créer des applications web en Python, dont :
Django
Django est un framework web écrit en Python qui facilite la création d'une application web.
Installation
modifierInstallation officielle[1] :
pip install Django
Utilisation
modifierRéférences
modifier
Karrigell
Karrigell est un canevas (framework) de développement web écrit en Python très simple d'utilisation.
À l'inverse d'autres frameworks écrits en Python, il ne nécessite pas d'apprendre un métalangage, car il reste très proche de la syntaxe python. Le code est donc simple à écrire et facile à maintenir.
Karrigell intègre un serveur web très efficace, mais il peut aussi travailler derrière d'autres serveurs web (Apache, Lighttpd et Xitami...).
Les bases de données courantes (sqlite, MySql, PostGresQL, etc.) peuvent être utilisées, cependant Karrigell inclut la base Buzhug, écrite par l'auteur, dans le même esprit que Karrigell.
Intérêt de Karrigell
modifierTout comme le PHP, Karrigell permet d’intégrer des scripts python à l’intérieur d’un document html en utilisant les symboles "<%" … "%>". Mais on peut également inclure du code HTML dans des scripts python.
Karrigell permet également de créer des sites d'une seule pièce, avec un service Karrigell (fichier .ks) dont les fonctions sont considérées comme des pages. Les fonctions dont le nom commencent par '_' sont privées.
L'utilisation du "karrigell service" (.ks) permet cependant d'avoir une certaine logique d'application dans un seul script. En outre, il est tout à fait possible d'inclure des scripts, ou des modules afin d'améliorer la lisibilité du script.
Karrigell offre un puissant système de template nommé cheetah[1] et propose un module HTMLTags pour des balises HTML valides.
Installation de Karrigell
modifierDécompresser le fichier que vous avez téléchargé (au format .zip, .tgz ou .tar.bz2) dans un répertoire tel que /home/myname/karrigell puis se placer dans le répertoire Karrigell-2.4.0.
Ouvrez une fenêtre console (un terminal), mettez-vous dans le Karrigell-2.4.0 (cd /home/myname/karrigell/Karrigell-2.4.0) et exécutez :
python Karrigell.py
Vous devriez voir apparaître le message :
Karrigell 2.4.0 running on port 80 Type Ctrl+C to stop
Configuration de Karrigell, mon premier site
modifierPour tester les exemples suivants, créons le repertoire "monsite" de notre site dans Karrigell-2.4.0/webapps/monsite
Puis se placer dans Karrigell-2.4.0/conf/ et ouvrir le fichier Karrigell.ini
Modifier les variables :
[Directories]
root = /home/myname/karrigell/Karrigell-2.4.0/webapps/monsite
[VirtualHost monsite.net]
root = /home/myname/karrigell/Karrigell-2.4.0/webapps/monsite
[Server]
port=8081
Ainsi lorsqu'on relancera le serveur, dans le terminal :
python Karrigell.py
Il affichera:
Karrigell 2.4.0 running on port 8081 Type Ctrl+C to stop
Il suffit alors de se connecter en localhost via le navigateur: http://localhost:8081
Exemple de script python dans un document HTML
modifierDans le répertoire "monsite", créer un fichier index.html
<h1>
La date courante est:
<%
import time
print time.strftime("%d:%m:%y",time.localtime(time.time()))
%>
</h1>
Puis avec le navigateur : http://localhost:8081
Exemple de code HTML dans un script Python
modifierDans le répertoire "monsite", créer un fichier index.py
telephone={'guitare - chant':'Jean-Louis Aubert',
'guitare':'Louis Bertignac',
'basse':'Corine Marienneau',
'batterie':'Richard Kolinka'}
"""
<table border=1>
<tr backgroundcolor=green>
<td>Le plus grand groupe français</td>
</tr>
</table>
<table>
"""
for item in telephone.keys():
print "<tr><td>%s</td><td>%s</td></tr>" %(item,telephone[item])
"</table>"
Puis avec le navigateur : http://localhost:8081
Exemple de script Karrigell Service
modifierDans le répertoire "monsite", créer un fichier index.ks dans lequel on inclut le code suivant :
def index():
print '<a href="page1?nom=bar">Aller vers foo</a>'
def page1(nom):
print '<IMG SRC="../picture.jpg">'
print nom
Puis avec le navigateur : http://localhost:8081
Exemple d'importation de module dans un script Karrigell Service
modifierDans le répertoire "monsite", créer un fichier conf.cfg, un script index.ks, et un script maconfig.py qui sera le module.
Le fichier conf.cfg :
[DEFAULT]
# Chemin par défaut où sera installée la base buzhug.
# Décommenter pour activer la variable de configuration
# path = /tmp/test
path = /home/monpseudo/karrigell/webapps/monsite/mabase/
le script index.ks
#!/usr/bin/python
# -*- coding: iso-8859-1 -*-
import os, sys
from maconfig import Konfig
def index():
maconfig = Konfig()
print maconfig.lire_chemin_base_config()
Le module maconfig.py:
#!/usr/bin/python
# -*- coding: iso-8859-1 -*-
from ConfigParser import *
import os
class Konfig (object):
"""
Objectif: Lire le fichier de configuration et renvoyer son contenu.
usage :
x.lire_chemin_base_config()
"""
def lire_chemin_base_config(self):
"""
Renvoie la valeur de path du fichier de configuration
"""
# tester si le fichier existe et le lire. Renvoi le résultat
if os.path.exists('conf.cfg'):
configdict = ConfigParser()
configdict.read('conf.cfg')
self.path = configdict.defaults()['path']
return self.path
Karrigell Intègre
modifier- Un serveur web
- Implémentation d'un mécanisme d'authentification et de gestion des utilisateurs
- Des sessions
- Accès facile aux données d'environnement, formulaires et base de données (Buzhug)
- Moteur de template Cheetah
Extensions
modifier- La liste exclut les extensions normalement prises en charge par un serveur
- .hip : HTML Inside Python
- .pih : Python Inside HTML
- .py : Fichier Python/ironpython
- .ks : "Karrigell service" = script Python dans lequel les fonctions sont associées à une URL
Références
modifierVoir aussi
modifier- anglais Site officiel (anglais)
- anglais Site officiel de la version Version 3.0
- français Site officiel (français)
- anglais buzhug (base de données en Python)
- anglais xRope (IDE de développement Python qui supporte le format de fichiers Karrigell)
Aiohttp
Aiohttp est un framework web pour le Python qui reposer sur la boucle d'évènements asyncio.
À savoir
modifierLa manière dont il résout les routes se fait en vérifiant toutes les ressources enregistrées[1], c'est donc un processus O(N), il est souhaitable de limiter en général le nombre de routes par application.
Références
modifier- ↑ https://github.com/aio-libs/aiohttp/blob/fbce0ac513f689cb396ae16b80b374e13e5c07ea/aiohttp/web_urldispatcher.py#L990
Exemples de scripts
Exemples de programmes écrits dans le langage de programmation Python dans sa version 3.3
Exemples de codes représentatifs
modifierUne introduction
modifierageDeEricPraline = 50
ageDeHenryWensleydale = 20
# Test
if 0 > ageDeEricPraline > 150 or ageDeEricPraline > ageDeHenryWensleydale:
print("l'age de Eric Praline est incorrect")
#echange : swap
ageDeHenryWensleydale, ageDeEricPraline = ageDeEricPraline,ageDeHenryWensleydale
print("age de Henry Wensleydale = %d , age de Eric Praline = %d" %(ageDeHenryWensleydale,ageDeEricPraline))
#>>>
#l'age de Eric Praline est incorrect
#age de Henry Wensleydale = 50 , age de Eric Praline = 20
Les structures de données en natif
modifier# Les listes :
maListeDeMeubles = ['table','chaise','frigo']
maListeDeMeubles.sort() #Tri de la liste
for unMeuble in maListeDeMeubles:
print('longueur de la chaîne ', unMeuble, '=', len(unMeuble))
#les listes imbriquées:
laListeDesNombresPairs = [unNombre for unNombre in range(1000) if unNombre % 2 == 0]
print (laListeDesNombresPairs)
#Les dictionnaires :
unAnnuaire = {'Laurent': 6389565, 'Paul': 6356785}
for unNom, x in unAnnuaire.items():
print("le nom %s a pour numéro de téléphone %d" %(unNom, x))
#Les tuples (n-uplet) : séquence constante
Couleur = ('Rouge', 'Bleu', 'Vert')
print(Couleur[0], Couleur[1], Couleur[2])
PointDeReference = (100, 200)
print(" x0 = %d y0 = %d " %(PointDeReference[0],PointDeReference[1]))
Accès a une base de données
modifier# Avec MySql :
import MySQLdb, pprint
uneConnexionBDD = MySQLdb.connect(host ='192.32.12.10',
user ='admin',
passwd ='988',
db ='uneBase')
leCurseur = uneConnexionBDD.cursor()
unAuteur = "'Zola'"
leCurseur.execute(""" SELECT title, description FROM books WHERE author = %s """ % (unAuteur,))
pprint.pprint(leCurseur.fetchall())
leCurseur.query("update books set title='assommoir' where author='Zola'")
uneConnexionBDD.commit()
# Par ODBC :
import dbi, odbc
connexion = odbc.odbc('mondsn/monlogin/monpassword')
leCurseur = connexion.cursor()
leCurseur.execute('select clientid, name, city from client')
print(leCurseur.fetchall())
Programmation réseau - Internet
modifier#Lire une page Web et afficher le source HTML
import urllib
print(urllib.urlopen)
#Usage de FTP : démonstration des paramètres nommés
import ftplib
uneSessionFTP = ftplib.FTP( host = '198.12.45.123',
user = 'MonLogin',
passwd = 'MonMotDePasse')
unFichier = open('monFichier.txt','rb')
uneSessionFTP.storbinary( 'STOR monFichier.txt', unFichier ) #envoi du fichier
unFichier.close()
uneSessionFTP.quit()
Tracé de courbes avec matplotlib
modifierimport math
from pylab import plot, axis, savefig, show, title
plot( [1,2,3,4],
[1,4,9,16])
axis( [1, 4, 0, 16] )
title( 'ma courbe' )
savefig( 'c:\\uneCourbeSauvee.png' )
show()
Utilitaires de la bibliothèque standard
modifier#Encodage d'une image binaire sous forme de chaîne (Pour les protocoles qui n'acceptent que l'ASCII)
lesDonneesImage = open('uneImage.gif','rb').read()
laChaineZippee = lesDonneesImage.encode('zlib').encode('base64')
#Exemple d'une applet scripté
from javax.swing import *
class MyApplet( JApplet ):
def init( self ):
unLabel= JLabel("Jython améliore la productivité")
self.getContentPane().add( unLabel )
#Envoi par mail du contenu d'un fichier
import smtplib
from email.MIMEText import MIMEText
IPduServeur = 'localhost'
monFichierMail = open ('monMailAEnvoyer.txt', 'rb') # lecture du fichier
leMessage = MIMEText ( monFichierMail.read() ) # création d'un message text/plain
monFichierMail.close()
leMessage['Subject'] = "Confirmation de l'exigence A441"
leMessage['From'] = "expediteur@phales.com"
leMessage['To'] = "destinataire@ecn.com"
leServeurSMTP = smtplib.SMTP(IPduServeur) # envoi du messge
leServeurSMTP.connect()
leServeurSMTP.sendmail('expediteur@phales.com',
'destinataire@ecn.com',
leMessage.as_string())
leServeurSMTP.quit()
Classe
modifierExemple de classe (voir programmation orientée objet).
class Fruit :
def __init__(self) :
pass
class Pomme(Fruit):
"""
Cette classe représente une pomme.
"""
Mangeurs = ["Jacques", "Nicolas","Virgile"]
def __init__(self, couleur):
"""
Pour construire une Pomme, donnez sa couleur.
"""
Fruit.__init__(self)
self._couleur = couleur
def couleur(self):
"""
Retourne la couleur de la Pomme.
"""
return self._couleur
def comestible(self, mangeur):
"""
Dit si la pomme est comestible ou non,
en fonction du mangeur.
"""
if mangeur in self.Mangeurs:
print(mangeur, "mange des pommes")
else:
print (mangeur, "n'aime pas les pommes")
petitePomme = Pomme("verte")
petitePomme.comestible("Pierre") # Pierre n'aime pas les pommes
petitePomme.comestible("Nicolas") # Nicolas mange des pommes
On remarque notamment la présence de documentation (optionnelle bien sûr) directement dans le code. La commande help() permet d'obtenir, dans l'interpréteur Python, cette aide directement :
>>> help(Pomme)
donne :
Help on class Pomme in module __main__: class Pomme(Fruit) | Cette classe représente une pomme. | | Methods defined here: | | __init__(self, couleur) | Pour construire une Pomme, donnez sa couleur. | | comestible(self, mangeur) | Dit si la pomme est comestible ou non, | en fonction du mangeur. | | couleur(self) | Retourne la couleur de la Pomme. | | ---------------------------------------------------------------------- | Data and other attributes defined here: | | Mangeurs = ['Jacques', 'Nicolas', 'Virgile']
WMI (sous Windows 2000/XP)
modifierLister les processus et leur nom
import wmi
controler = wmi.WMI ()
for unProcessus in controler.Win32_Process ():
print (unProcessus.ProcessId, unProcessus.Name)
Rebooter une machine à distance
import wmi
controler = wmi.WMI (computer = "Nom_Machine_Distante",
privileges = ["RemoteShutdown"] )
os = controler.Win32_OperatingSystem (Primary=1)[0]
os.Reboot ()
Automation Win32
modifier#Exemple de pilotage de CATIA par la technologie COM/DCOM
import win32com.client
catiaApplication = win32com.client.Dispatch("CATIA.Application")
monDocument = catiaApplication.ActiveDocument.Product
nombreDeProduit = monDocument.Products.Count
for produits in range(nombreDeProduit):
produits += 1
print (monDocument.Products.Item(produits).Name)
#Exemple de pilotage du lecteur multimédia
from win32com.client import Dispatch
leMediaPlayer = Dispatch("WMPlayer.OCX")
uneMorceauDeMusique = leMediaPlayer.newMedia("C:/mesDoc/monTitre.wma") # ou mp3
leMediaPlayer.currentPlaylist.appendItem(uneMorceauDeMusique)
leMediaPlayer.controls.play()
raw_input("appuyez sur la touche ENTRÉE pour arrêter la musique")
leMediaPlayer.leMediaPlayer.stop()
ctypes
modifierctypes permet d'accéder directement à des dll.
import ctypes
# modification du fond d’écran de windows
SPI_SETDESKWALLPAPER = 20
ctypes.windll.user32.SystemParametersInfoA(SPI_SETDESKWALLPAPER,
0,
"C:\\Mes documents\\Mes images\\maPhoto.bmp" ,
0)
# Resolution de l'écran
SM_CXSCREEN = 0
SM_CYSCREEN = 1
taille_x = ctypes.windll.user32.GetSystemMetrics(SM_CXSCREEN)
taille_y = ctypes.windll.user32.GetSystemMetrics(SM_CYSCREEN)
print ("La résolution d'écran est %d par %d" % (taille_x, taille_y))
Data Mining - Réseaux bayésiens avec reverend
modifier# les filtres Baysesiens permettent de classifier des données.
# Exemple d'application : reconnaissance automatique de la langue, anti-Spam, cryptographie...
from reverend.thomas import Bayes
unReseauBayesiens = Bayes()
uneListeDePropositions = [('math', 'on fait des calculs , des additions, des multiplications'),
('math', '3 + 4 = 7 ; 10 + 7 = 17 ; 99/11 = 9'),
('math', '12 + 10 -5 = 17 ; 70-10 = 60'),
('math', 'le carré des 2 côtés du triangle sont liés par une relation'),
('math', 'la fonction sinus ( sin ) sert à représenter les phénomènes cycliques'),
('math', 'sin (PI) = 3.14159 et cosinus (0) = 1' ),
('philosophie', 'je disserte sur le sens de la vie'),
('philosophie', 'être ou ne pas être, telle est la question'),
('philosophie', 'je sais que je ne sais rien'),
('philosophie', 'les phénomènes sont réels à condition que nous le souhaitions'),
('philosophie', 'la raison est-elle toujours raisonnable ?'),
('philosophie', 'le cerveau peut-il être compris ?'),
('philosophie', "l'univers peut-il être l'objet de connaissance ?"),
('philosophie', 'le calcul a-t-il des limites intrinsèques ?'),
('philosophie', "une relation peut être durable si l'homme la souhaite")]
for uneCategorie, uneProposition in uneListeDePropositions:
unReseauBayesiens.train( uneCategorie, uneProposition ) # entrainement du réseau
phraseAnalyse1 = 'voici un résultat : 66/6 = 11 '
phraseAnalyse2 = "je ne saurais dire s'il pourra tout comprendre ... "
phraseAnalyse3 = "le phénomène de la pluie pourrait être d'origine divine"
phraseAnalyse4 = 'la représentation bourbakiste des chiffres assure leur détermination'
for unePhrase in (phraseAnalyse1, phraseAnalyse2, phraseAnalyse3, phraseAnalyse4) :
solutions = unReseauBayesiens.guess(unePhrase) # calculs de la catégorie
categorie = solutions[0][0]
probabilite = solutions[0][1]
print ("la phrase '%s' est de catégorie '%s' avec une probabilité de '%d /100' " %(unePhrase,categorie,probabilite *100))
# Résultats :
# la phrase 'voici un résultat : 66/6 = 11 ' est de catégorie 'math' avec une probabilité de '99 /100'
# la phrase 'je ne saurais dire s'il pourra tout comprendre ... ' est de catégorie 'philosophie' avec une probabilité de '99 /100'
# la phrase 'le phénomène de la pluie pourrait être d'origine divine' est de catégorie 'philosophie' avec une probabilité de '92 /100'
# la phrase 'la représentation bourbakiste des chiffres assure leur détermination' est de catégorie 'philosophie' avec une probabilité de '55 /100'
Implémentation du crible d'Ératosthène
modifierCe script calcule les nombres premiers inférieurs à 200 en appliquant la méthode du crible d'Ératosthène.
# calcul des nombres premiers inférieurs à N
# initialisation
#!/usr/bin/python
# -*- coding: utf-8 -*-
N = 200
liste = range(2, N) # liste de 2 à N
nombre = 2
while nombre*nombre <= N: # tant que le nb premier < à la
# racine carrée de N
for i in liste[liste.index(nombre) + 1:]: # parcourt la liste à partir de ce nombre
if i % nombre == 0: # un multiple du nombre est trouvé
liste.remove(i) # ou "del liste[ liste.index(i) ]" :
# on le raye de la liste
nombre = liste[liste.index(nombre) + 1] # on prend le nombre suivant non rayé
print (liste) # affichage du résultat
Voici une version algorithmiquement équivalente mais beaucoup plus rapide qui exploite la puissance du type liste de Python :
N = 1000 # on cherche les nombres premiers inférieurs à N
liste = range(N+1) # création de la liste des entiers de 0 à N
liste[1] = 0 # 1 n'est pas premier
nombre = 2
while nombre ** 2 <= N: # tant que le nombre à examiner est inférieur à
# la racine carrée de N
liste[nombre*2 :: nombre] = [0] * ((N // nombre) - 1) # éliminer tous les
# multiples du nombre
# passer au nombre non examine suivant
nombre += 1
while not liste[nombre]:
nombre += 1
liste = filter (None, liste)
print (liste) # et à la fin, on affiche le résultat
Que l'on peut enfin réduire à :
N, nombre = 1000, 2
liste, liste[1] = range(N+1), 0
while nombre**2 <= N:
liste[nombre*2 :: nombre] = [0]*len( liste[nombre*2 :: nombre] )
nombre += 1
print (filter(None, liste))
Les fonctions, les types et les classes sont des objets
modifier#Les fonctions sont des objets
def uneFonction ( unEntier, uneChaine):
"documentation de la fonction"
print ("unEntier : " + str( unEntier ))
#Vous pouvez d'ailleurs ajouter des attributs à la fonction:
uneFonction.compteur = 10
print ("la fonction a pour nom :" + uneFonction.__name__)
print ("la fonction a pour documentation : " + uneFonction.__doc__)
print (uneFonction.compteur)
>>la fonction a pour nom :uneFonction
la fonction a pour documentation : documentation de la fonction
10
#Les types de base sont des objets
valeurOpposee = -25 .__neg__() # equivalent à - (-25)
print ("la valeur opposée de -25 est : " + str ( valeurOpposee ))
#Les classes sont des objets
class Insecte : pass # On definit une classe mère, qui ne fait rien pour l'instant
class Fourmi(Insecte): # Definition d'une classe dérivée
def marcher (self):
print ("je marche")
print ("la classe Fourmi a pour classe mère :" + str ( Fourmi.__bases__))
#l'opposé de -25 est : 25
#la classe Fourmi a pour classe mère :(<class __main__.Insecte at 0x02B69A20>,)
Graphique
modifierTkinter
modifierCet exemple montre comment se servir de la bibliothèque Tkinter pour créer une interface graphique.
from Tkinter import *
fen = Tk()
text1 = Label(fen, text='Bonjour !')
entr1 = Entry(fen)
bout1 = Button(fen, text='Quitter', command=fen.destroy)
text1.grid( row=1, column=1)
entr1.grid( row=1, column=2)
bout1.grid( row=2, column=1)
fen.mainloop()
WxPython
modifierCet exemple crée en WxPython une fenêtre et un boite modale sur appui bouton - l'utilisation d’éditeur WYSIWYG comme glade ou boa constructor est recommandée pour les grandes applications
from wxPython.wx import *
ID_OK_BOUTON=100
class PanneauPerso( wxPanel ):
def __init__(self, parent, id):
wxPanel.__init__(self, parent, id)
self.unChampText = wxStaticText(self, -1, "TextAvantLeClic", wxPoint(5, 5))
self.unChampBouton = wxButton(self, ID_OK_BOUTON, "CliquerMoi", wxPoint(100, 5))
EVT_BUTTON(self, ID_OK_BOUTON, self.OnClick) # enregistrement du callback
def OnClick(self,event):
self.unChampText.SetLabel('ApresLeClic') # sur appui bouton, modifier le champ texte
b = wxMessageDialog( self, "Une Boite de dialogue","titre", wxOK)
b.ShowModal() # montrer un e boite de dialogue
b.Destroy()
monApplication = wxPySimpleApp()
maFenetre = wxFrame(None, -1, "Titre de l Application")
PanneauPerso ( maFenetre , -1)
maFenetre.Show(1)
monApplication .MainLoop()
Analyse de programmes concrets
Dans ce chapitre, nous allons nous efforcer d'illustrer la démarche de conception d'un programme graphique, depuis ses premières ébauches jusqu'à un stade de développement relativement avancé. Nous souhaitons montrer ainsi combien la programmation orientée objet peut faciliter et surtout sécuriser la stratégie de développement incrémental que nous préconisons.
L'utilisation de classes s'impose, lorsque l'on constate qu'un projet en cours de réalisation se révèle nettement plus complexe que ce que l'on avait imaginé au départ. Vous vivrez certainement vous-même des cheminements similaires à celui que nous décrivons ci-dessous.
Jeu des bombardes
modifierCe projet de jeu[1] s'inspire d'un travail similaire réalisé par des élèves de terminale.
Il est vivement recommandé de commencer l'ébauche d'un tel projet par une série de petits dessins et de schémas, dans lesquels seront décrits les différents éléments graphiques à construire, ainsi qu'un maximum de cas d'utilisations. Si vous rechignez à utiliser pour cela la bonne vieille technologie papier/crayon (laquelle a pourtant bien fait ses preuves), vous pouvez tirer profit d'un logiciel de dessin technique, tel l'utilitaire Draw de la suite bureautique OpenOffice.org[2].
L'idée de départ est simple : deux joueurs s'affrontent au canon. Chacun doit ajuster son angle de tir pour tâcher d'atteindre son adversaire, les obus décrivant une trajectoire balistique.
L'emplacement des canons est défini au début du jeu de manière aléatoire (tout au moins en hauteur). Après chaque tir, les canons se déplacent (afin d'accroître l'intérêt du jeu, l'ajustement des tirs étant ainsi rendu plus difficile). Les coups au but sont comptabilisés.
Le dessin préliminaire que nous avons reproduit à la page précédente est l'une des formes que peut prendre votre travail d'analyse. Avant de commencer le développement d'un projet de programmation, il vous faut en effet toujours vous efforcer d'établir un cahier des charges détaillé. Cette étude préalable est très importante. La plupart des débutants commencent bien trop vite à écrire de nombreuses lignes de code au départ d'une vague idée, en négligeant de rechercher la structure d'ensemble. Leur programmation risque alors de devenir chaotique, parce qu'ils devront de toute façon mettre en place cette structure tôt ou tard. Ils s'apercevront alors bien souvent qu'il leur faut supprimer et ré-écrire des pans entiers d'un projet qu'ils ont conçu d'une manière trop monolithique et/ou mal paramétrée.
- Trop monolithique : cela signifie que l'on a négligé de décomposer un problème complexe en plusieurs sous-problèmes plus simples. Par exemple, on a imbriqué plusieurs niveaux successifs d'instructions composées, au lieu de faire appel à des fonctions ou à des classes.
- Mal paramétrée : cela signifie que l'on a traité seulement un cas particulier, au lieu d'envisager le cas général. Par exemple, on a donné à un objet graphique des dimensions fixes, au lieu de prévoir des variables pour permettre son redimensionnement.
Vous devez donc toujours commencer le développement d'un projet par une phase d'analyse aussi fouillée que possible, et concrétiser le résultat de cette analyse dans un ensemble de documents (schémas, plans, descriptions...) qui constitueront le cahier des charges. Pour les projets de grande envergure, il existe d'ailleurs des méthodes d'analyse très élaborées (UML, Merise...) que nous ne pouvons nous permettre de décrire ici car elles font l'objet de livres entiers.
Cela étant dit, il faut malheureusement admettre qu'il est très difficile (et même probablement impossible) de réaliser dès le départ l'analyse tout à fait complète d'un projet de programmation. C'est seulement lorsqu'il commence à fonctionner véritablement qu'un programme révèle ses faiblesses. On constate alors qu'il reste des cas d'utilisation ou des contraintes qui n'avaient pas été prévues au départ. D'autre part, un projet logiciel est pratiquement toujours destiné à évoluer : il vous arrivera fréquemment de devoir modifier le cahier des charges au cours du développement lui-même, pas nécessairement parce que l'analyse initiale a été mal faite, mais tout simplement parce que l'on souhaite encore ajouter des fonctionnalités supplémentaires.
En conclusion, tâchez de toujours aborder un nouveau projet de programmation en respectant les deux consignes suivantes :
- Décrivez votre projet en profondeur avant de commencer la rédaction des premières lignes de code, en vous efforçant de mettre en évidence les composants principaux et les relations qui les lient (pensez notamment à décrire les différents cas d'utilisation de votre programme).
- Lorsque vous commencerez sa réalisation effective, évitez de vous laisser entraîner à rédiger de trop grands blocs d'instructions. Veillez au contraire à découper votre application en un certain nombre de composants paramétrables bien encapsulés, de telle manière que vous puissiez aisément modifier l'un ou l'autre d'entre eux sans compromettre le fonctionnement des autres, et peut-être même les réutiliser dans différents contextes si le besoin s'en fait sentir.
C'est pour satisfaire cette exigence que la programmation orientée objets est a été inventée.
Considérons par exemple l'ébauche dessinée à la page précédente.
L'apprenti programmeur sera peut-être tenté de commencer la réalisation de ce jeu en n'utilisant que la programmation procédurale seule (c'est-à-dire en omettant de définir de nouvelles classes). C'est d'ailleurs ainsi que nous avons procédé nous-même lors de notre première approche des interfaces graphiques, tout au long du chapitre 8. Cette façon de procéder ne se justifie cependant que pour de tout petits programmes (des exercices ou des tests préliminaires). Lorsque l'on s'attaque à un projet d'une certaine importance, la complexité des problèmes qui se présentent se révèle rapidement trop importante, et il devient alors indispensable de fragmenter et de compartimenter.
L'outil logiciel qui va permettre cette fragmentation est la classe.
Nous pouvons peut-être mieux comprendre son utilité en nous aidant d'une analogie :
Tous les appareils électroniques sont constitués d'un petit nombre de composants de base, à savoir des transistors, des diodes, des résistances, des condensateurs, etc. Les premiers ordinateurs ont été construits directement à partir de ces composants. Ils étaient volumineux, très chers, et pourtant ils n'avaient que très peu de fonctionnalités et tombaient fréquemment en panne.
On a alors développé différentes techniques pour encapsuler dans un même boîtier un certain nombre de composants électroniques de base. Pour utiliser ces nouveaux circuits intégrés, il n'était plus nécessaire de connaître leur contenu exact : seule importait leur fonction globale. Les premières fonctions intégrées étaient encore relativement simples : c'étaient par exemple des portes logiques, des bascules, etc. En combinant ces circuits entre eux, on obtenait des caractéristiques plus élaborées, telles que des registres ou des décodeurs, qui purent à leur tour être intégrés, et ainsi de suite, jusqu'aux microprocesseurs actuels. Ceux-ci contiennent dorénavant plusieurs millions de composants, et pourtant leur fiabilité reste extrêmement élevée.
En conséquence, pour l'électronicien moderne qui veut construire par exemple un compteur binaire (circuit qui nécessite un certain nombre de bascules), il est évidemment bien plus simple, plus rapide et plus sûr de se servir de bascules intégrées, plutôt que de s'échiner à combiner sans erreur plusieurs centaines de transistors et de résistances.
D'une manière analogue, le programmeur moderne que vous êtes peut bénéficier du travail accumulé par ses prédécesseurs en utilisant la fonctionnalité intégrée dans les nombreuses bibliothèques de classes déjà disponibles pour Python. Mieux encore, il peut aisément créer lui-même de nouvelles classes pour encapsuler les principaux composants de son application, particulièrement ceux qui y apparaissent en plusieurs exemplaires. Procéder ainsi est plus simple, plus rapide et plus sûr que de multiplier les blocs d'instructions similaires dans un corps de programme monolithique, de plus en plus volumineux et de moins en moins compréhensible.
Examinons par exemple notre ébauche dessinée. Les composants les plus importants de ce jeu sont bien évidemment les petits canons, qu'il faudra pouvoir dessiner à différents emplacements et dans différentes orientations, et dont il nous faudra au moins deux exemplaires.
Plutôt que de les dessiner morceau par morceau dans le canevas au fur et à mesure du déroulement du jeu, nous avons intérêt à les considérer comme des objets logiciels à part entière, dotés de plusieurs propriétés ainsi que d'un certain comportement (ce que nous voulons exprimer par là est le fait qu'il devront être dotés de divers mécanismes, que nous pourrons activer par programme à l'aide de méthodes particulières). Il est donc certainement judicieux de leur consacrer une classe spécifique.
Prototypage d'une classe « Canon »
modifierEn définissant une telle classe, nous gagnons sur plusieurs tableaux. Non seulement nous rassemblons ainsi tout le code correspondant au dessin et au fonctionnement du canon dans une même « capsule », bien à l'écart du reste du programme, mais de surcroît nous nous donnons la possibilité d'instancier aisément un nombre quelconque de ces canons dans le jeu, ce qui nous ouvre des perspectives de développements ultérieurs.
Lorsqu'une première implémentation de la classe Canon()
aura été construite et testée, il sera également possible de la perfectionner en la dotant de caractéristiques supplémentaires, sans modifier (ou très peu) son interface, c'est-à-dire en quelque sorte son « mode d'emploi » : à savoir les instructions nécessaires pour l'instancier et l'utiliser dans des applications diverses.
Entrons à présent dans le vif du sujet.
Le dessin de notre canon peut être simplifié à l'extrême. Nous avons estimé qu'il pouvait se résumer à un cercle combiné avec un rectangle, celui-ci pouvant d'ailleurs être lui-même considéré comme un simple segment de ligne droite particulièrement épais.
Si l'ensemble est rempli d'une couleur uniforme (en noir, par exemple), nous obtiendrons ainsi une sorte de petite bombarde suffisamment crédible.
Dans la suite du raisonnement, nous admettrons que la position du canon est en fait la position du centre du cercle (coordonnées x et y dans le dessin ci-contre). Ce point clé indique également l'axe de rotation de la buse du canon, ainsi que l'une des extrémités de la ligne épaisse qui représentera cette buse.
Pour terminer notre dessin, il nous restera alors à déterminer les coordonnées de l'autre extrémité de cette ligne. Ces coordonnées peuvent être calculées sans grande difficulté, à la condition de nous remémorer deux concepts fondamentaux de la trigonométrie (le sinus et le cosinus) que vous devez certainement bien connaître :
Dans un triangle rectangle, le rapport entre le côté opposé à un angle et l'hypoténuse du triangle est une propriété spécifique de cet angle qu'on appelle sinus de l'angle. Le cosinus du même angle est le rapport entre le côté adjacent à l'angle et l'hypoténuse.
Ainsi, dans le schéma ci-contre : sinα = a/h et cosα = b/h.
Pour représenter la buse de notre canon, en supposant que nous connaissions sa longueur l et l'angle de tir α , il nous faut donc tracer un segment de ligne droite épaisse, à partir des coordonnées du centre du cercle (x et y), jusqu'à un autre point situé plus à droite et plus haut, l'écart horizontal Δx étant égal à l.cos α , et l'écart vertical Δy étant égal à l.sin α.
En résumant tout ce qui précède, dessiner un canon au point x, y consistera simplement à :
- tracer un cercle noir centré sur x, y ;
- tracer une ligne noire épaisse depuis le point x, y jusqu'au point x + l.cos α, y + l.sin α.
Nous pouvons à présent commencer à envisager une ébauche de programmation correspondant à une classe « Canon ». Il n'est pas encore question ici de programmer le jeu proprement dit. Nous voulons seulement vérifier si l'analyse que nous avons faite jusqu'à présent « tient la route », en réalisant un premier prototype fonctionnel.
Un prototype est un petit programme destiné à expérimenter une idée, que l'on se propose d'intégrer ensuite dans une application plus vaste. Du fait de sa simplicité et de sa concision, Python se prête fort bien à l'élaboration de prototypes, et de nombreux programmeurs l'utilisent pour mettre au point divers composants logiciels qu'ils reprogrammeront éventuellement ensuite dans d'autres langages plus « lourds », tels que le C par exemple.
Dans notre premier prototype, la classe Canon()
ne comporte que deux méthodes : un constructeur qui crée les éléments de base du dessin, et une méthode permettant de modifier celui-ci à volonté pour ajuster l'angle de tir (l'inclinaison de la buse). Comme nous l'avons souvent fait dans d'autres exemples, nous inclurons quelques lignes de code à la fin du script afin de pouvoir tester la classe tout de suite :
from Tkinter import *
from math import pi, sin, cos
class Canon:
"""Petit canon graphique"""
def __init__(self, boss, x, y):
self.boss = boss # référence du canevas
self.x1, self.y1 = x, y # axe de rotation du canon
# dessiner la buse du canon, à l'horizontale pour commencer :
self.lbu = 50 # longueur de la buse
self.x2, self.y2 = x + self.lbu, y
self.buse = boss.create_line(self.x1, self.y1, self.x2, self.y2,
width =10)
# dessiner ensuite le corps du canon par-dessus :
r = 15 # rayon du cercle
boss.create_oval(x-r, y-r, x+r, y+r, fill='blue', width =3)
def orienter(self, angle):
"choisir l'angle de tir du canon"
# rem : le paramètre <angle> est reçu en tant que chaîne de car.
# il faut le traduire en nombre réel, puis convertir en radians :
self.angle = float(angle)*2*pi/360
self.x2 = self.x1 + self.lbu*cos(self.angle)
self.y2 = self.y1 - self.lbu*sin(self.angle)
self.boss.coords(self.buse, self.x1, self.y1, self.x2, self.y2)
if __name__ == '__main__':
# Code pour tester sommairement la classe Canon :
f = Tk()
can = Canvas(f,width =250, height =250, bg ='ivory')
can.pack(padx =10, pady =10)
c1 = Canon(can, 50, 200)
s1 =Scale(f, label='hausse', from_=90, to=0, command=c1.orienter)
s1.pack(side=LEFT, pady =5, padx =20)
s1.set(25) # angle de tir initial
f.mainloop()
- Commentaires
- Ligne 6 : Dans la liste des paramètres qui devront être transmis au constructeur lors de l'instanciation, nous prévoyons les coordonnées x et y, qui indiqueront l'emplacement du canon dans le canevas, mais également une référence au canevas lui-même (la variable
boss
). Cette référence est indispensable : elle sera utilisée pour invoquer les méthodes du canevas. Nous pourrions inclure aussi un paramètre pour choisir un angle de tir initial, mais puisque nous avons l'intention d'implémenter une méthode spécifique pour régler cette orientation, il sera plus judicieux de faire appel à celle-ci au moment voulu. - Lignes 7 et 8 : Ces références seront utilisées un peu partout dans les différentes méthodes que nous allons développer dans la classe. Il faut donc en faire des attributs d'instance.
- Lignes 9 à 16 : Nous dessinons la buse d'abord, et le corps du canon ensuite. Ainsi une partie de la buse reste cachée. Cela nous permet de colorer éventuellement le corps du canon.
- Lignes 18 à 25 : Cette méthode sera invoquée avec un argument « angle », lequel sera fourni en degrés (comptés à partir de l'horizontale). S'il est produit à l'aide d'un widget tel que Entry ou Scale, il sera transmis sous la forme d'une chaîne de caractères, et nous devrons donc le convertir d'abord en nombre réel avant de l'utiliser dans nos calculs (ceux-ci ont été décrits à la page précédente).
- Lignes 27 à 38 : Pour tester notre nouvelle classe, nous ferons usage d'un widget Scale. Pour définir la position initiale de son curseur, et donc fixer l'angle de hausse initial du canon, nous devons faire appel à sa méthode
set()
(ligne 36).
Ajout de méthodes au prototype
modifierNotre prototype est fonctionnel, mais beaucoup trop rudimentaire. Nous devons à présent le perfectionner pour lui ajouter la capacité de tirer des obus.
Ceux-ci seront traités plutôt comme des « boulets » : ce seront de simples petits cercles que nous ferons partir de la bouche du canon avec une vitesse initiale d'orientation identique à celle de sa buse. Pour leur faire suivre une trajectoire réaliste, nous devons à présent nous replonger dans notre cours de physique :
Comment un objet laissé à lui-même évolue-t-il dans l'espace, si l'on néglige les phénomènes secondaires tels que la résistance de l'air ?
Ce problème peut vous paraître complexe, mais en réalité sa résolution est très simple : il vous suffit d'admettre que le boulet se déplace à la fois horizontalement et verticalement, et que ces deux mouvements simultanés sont tout à fait indépendants l'un de l'autre.
Vous allez donc établir une boucle d'animation dans laquelle vous recalculez les nouvelles coordonnées x et y du boulet à intervalles de temps réguliers, en sachant que :
- Le mouvement horizontal est uniforme. À chaque itération, il vous suffit d'augmenter graduellement la coordonnée
x
du boulet, en lui ajoutant toujours un même déplacement Δx.
- Le mouvement vertical est uniformément accéléré. Cela signifie simplement qu'à chaque itération, vous devez ajouter à la coordonnée y un déplacement Δy qui augmente lui-même graduellement, toujours de la même quantité.
Voyons cela dans le script :
Pour commencer, il faut ajouter les lignes suivantes à la fin de la méthode constructeur. Elles vont servir à créer l'objet « obus », et à préparer une variable d'instance qui servira d'interrupteur de l'animation. L'obus est créé au départ avec des dimensions minimales (un cercle d'un seul pixel) afin de rester presqu'invisible :
# dessiner un obus (réduit à un simple point, avant animation) :
self.obus =boss.create_oval(x, y, x, y, fill='red')
self.anim =False # interrupteur d'animation
# retrouver la largeur et la hauteur du canevas :
self.xMax =int(boss.cget('width'))
self.yMax =int(boss.cget('height'))
Les deux dernières lignes utilisent la méthode cget()
du widget « maître » (le canevas, ici), afin de retrouver certaines de ses caractéristiques. Nous voulons en effet que notre classe Canon
soit généraliste, c'est-à-dire réutilisable dans n'importe quel contexte, et nous ne pouvons donc pas tabler à l'avance sur des dimensions particulières pour le canevas dans lequel ce canon sera utilisé.
Ensuite, nous devons ajouter deux nouvelles méthodes : l'une pour déclencher le tir, et l'autre pour gérer l'animation du boulet une fois que celui-ci aura été lancé :
def feu(self):
"déclencher le tir d'un obus"
if not self.anim:
self.anim =True
# position de départ de l'obus (c'est la bouche du canon) :
self.boss.coords(self.obus, self.x2 -3, self.y2 -3,
self.x2 +3, self.y2 +3)
v =15 # vitesse initiale
# composantes verticale et horizontale de cette vitesse :
self.vy = -v *sin(self.angle)
self.vx = v *cos(self.angle)
self.animer_obus()
def animer_obus(self):
"animation de l'obus (trajectoire balistique)"
if self.anim:
self.boss.move(self.obus, int(self.vx), int(self.vy))
c = self.boss.coords(self.obus) # coord. résultantes
xo, yo = c[0] +3, c[1] +3 # coord. du centre de l'obus
if yo > self.yMax or xo > self.xMax:
self.anim =False # arrêter l'animation
self.vy += .5
self.boss.after(30, self.animer_obus)
- Commentaires
- Lignes 1 à 4 : Cette méthode sera invoquée par appui sur un bouton. Elle déclenche le mouvement de l'obus, et attribue une valeur « vraie » à notre « interrupteur d'animation » (la variable
self.anim
: voir ci-après). Il faut cependant nous assurer que pendant toute la durée de cette animation, un nouvel appui sur le bouton ne puisse pas activer d'autres boucles d'animation parasites. C'est le rôle du test effectué à la ligne 3 : le bloc d'instruction qui suit ne peut s'exécuter que si la variableself.anim
possède la valeur « faux », ce qui signifie que l'animation n'a pas encore commencé.
- Lignes 5 à 7 : Le canevas Tkinter dispose de deux méthodes pour déplacer les objets graphiques. La méthode
coords()
effectue un positionnement absolu ; il faut cependant lui fournir toutes les coordonnées de l'objet (comme si on le redessinait). La méthodemove()
, utilisée à la ligne 17, provoque quant à elle un déplacement relatif ; elle s'utilise avec deux arguments seulement : les composantes horizontale et verticale du déplacement souhaité.
- Lignes 8 à 12 : La vitesse initiale de l'obus est choisie à la ligne 8. Comme nous l'avons expliqué à la page précédente, le mouvement du boulet est la résultante d'un mouvement horizontal et d'un mouvement vertical. Nous connaissons la valeur de la vitesse initiale ainsi que son inclinaison (c'est-à-dire l'angle de tir). Pour déterminer les composantes horizontale et verticale de cette vitesse, il nous suffit d'utiliser des relations trigonométriques tout à fait similaires à que celles que nous avons déjà exploitées pour dessiner la buse du canon. Le signe - utilisé à la ligne 9 provient du fait que les coordonnées verticales se comptent de haut en bas. La ligne 12 active l'animation proprement dite.
- Lignes 14 à 23 : Cette procédure se ré-appelle elle-même toutes les 30 millisecondes par l'intermédiaire de la méthode
after()
invoquée à la ligne 23. Cela continue aussi longtemps que la variableself.anim
(notre « interrupteur d'animation ») reste « vraie », condition qui changera lorsque les coordonnées de l'obus sortiront des limites imposées (test de la ligne 20).
- Lignes 18, 19 : Pour retrouver ces coordonnées après chaque déplacement, on fait appel encore une fois à la méthode
coords()
du canevas : utilisée avec la référence d'un objet graphique comme unique argument, elle renvoie ses quatre coordonnées dans un tuple.
- Lignes 17 et 22 : La coordonnée horizontale de l'obus augmente toujours de la même quantité (mouvement uniforme), tandis que la coordonnée verticale augmente d'une quantité qui est elle-même augmentée à chaque fois à la ligne 24 (mouvement uniformément accéléré). Le résultat est une trajectoire parabolique.
+=
permet d'incrémenter une variable : « a += 3 » équivaut à « a = a + 3 ».Il reste enfin à ajouter un bouton déclencheur dans la fenêtre principale. Une ligne telle que la suivante (à insérer dans le code de test) fera parfaitement l'affaire :
Button(f, text='Feu !', command =c1.feu).pack(side=LEFT)
Développement de l'application
modifierDisposant désormais d'une classe d'objets « canon » assez bien dégrossie, nous pouvons à présent envisager l'élaboration de l'application proprement dite. Et puisque nous sommes décidés à exploiter la méthodologie de la programmation orientée objet, nous devons concevoir cette application comme un ensemble d'objets qui interagissent par l'intermédiaire de leurs méthodes.
Plusieurs de ces objets proviendront de classes préexistantes, bien entendu : ainsi le canevas, les boutons, etc. Mais nous avons vu dans les pages précédentes que nous avons intérêt à regrouper des ensembles bien délimités de ces objets basiques dans de nouvelles classes, chaque fois que nous pouvons identifier pour ces ensembles une fonctionnalité particulière. C'était le cas par exemple pour cet ensemble de cercles et de lignes mobiles que nous avons décidé d'appeler « canon ».
Pouvons-nous encore distinguer dans notre projet initial d'autres composants qui mériteraient d'être encapsulés dans des nouvelles classes ? Certainement. Il y a par exemple le pupitre de contrôle que nous voulons associer à chaque canon : nous pouvons y rassembler le dispositif de réglage de la hausse (l'angle de tir), le bouton de mise à feu, le score réalisé, et peut-être d'autres indications encore, comme le nom du joueur, par exemple. Il est d'autant plus intéressant de lui consacrer une classe particulière, que nous savons d'emblée qu'il nous en faudra deux instances.
Il y a aussi l'application elle-même, bien sûr. En l'encapsulant dans une classe, nous en ferons notre objet principal, celui qui dirigera tous les autres.
Veuillez à présent analyser le script ci-dessous. Vous y retrouverez la classe Canon()
encore davantage développée : nous y avons ajouté quelques attributs et trois méthodes supplémentaires, afin de pouvoir gérer les déplacements du canon lui-même, ainsi que les coups au but.
La classe Application()
remplace désormais le code de test des prototypes précédents. Nous y instancions deux objets Canon()
, et deux objets de la nouvelle classe Pupitre()
, que nous plaçons dans des dictionnaires en prévision de développements ultérieurs (nous pouvons en effet imaginer d'augmenter le nombre de canons et donc de pupitres). Le jeu est à présent fonctionnel : les canons se déplacent après chaque tir, et les coups au but sont comptabilisés.
from Tkinter import *
from math import sin, cos, pi
from random import randrange
class Canon:
"""Petit canon graphique"""
def __init__(self, boss, id, x, y, sens, coul):
self.boss = boss # réf. du canevas
self.appli = boss.master # réf. de la fenêtre d'application
self.id = id # identifiant du canon (chaîne)
self.coul = coul # couleur associée au canon
self.x1, self.y1 = x, y # axe de rotation du canon
self.sens = sens # sens de tir (-1:gauche, +1:droite)
self.lbu = 30 # longueur de la buse
self.angle = 0 # hausse par défaut (angle de tir)
# retrouver la largeur et la hauteur du canevas :
self.xMax = int(boss.cget('width'))
self.yMax = int(boss.cget('height'))
# dessiner la buse du canon (horizontale) :
self.x2, self.y2 = x + self.lbu * sens, y
self.buse = boss.create_line(self.x1, self.y1,
self.x2, self.y2, width =10)
# dessiner le corps du canon (cercle de couleur) :
self.rc = 15 # rayon du cercle
self.corps = boss.create_oval(x -self.rc, y -self.rc, x +self.rc,
y +self.rc, fill =coul)
# pré-dessiner un obus caché (point en dehors du canevas) :
self.obus = boss.create_oval(-10, -10, -10, -10, fill='red')
self.anim = False # indicateurs d'animation
self.explo = False # et d'explosion
def orienter(self, angle):
"régler la hausse du canon"
# rem: le paramètre <angle> est reçu en tant que chaîne.
# Il faut donc le traduire en réel, puis le convertir en radians :
self.angle = float(angle)*pi/180
# rem: utiliser la méthode coords de préférence avec des entiers :
self.x2 = int(self.x1 + self.lbu * cos(self.angle) * self.sens)
self.y2 = int(self.y1 - self.lbu * sin(self.angle))
self.boss.coords(self.buse, self.x1, self.y1, self.x2, self.y2)
def deplacer(self, x, y):
"amener le canon dans une nouvelle position x, y"
dx, dy = x -self.x1, y -self.y1 # valeur du déplacement
self.boss.move(self.buse, dx, dy)
self.boss.move(self.corps, dx, dy)
self.x1 += dx
self.y1 += dy
self.x2 += dx
self.y2 += dy
def feu(self):
"tir d'un obus - seulement si le précédent a fini son vol"
if not (self.anim or self.explo):
self.anim =True
# récupérer la description de tous les canons présents :
self.guns = self.appli.dictionnaireCanons()
# position de départ de l'obus (c'est la bouche du canon) :
self.boss.coords(self.obus, self.x2 -3, self.y2 -3,
self.x2 +3, self.y2 +3)
v = 17 # vitesse initiale
# composantes verticale et horizontale de cette vitesse :
self.vy = -v *sin(self.angle)
self.vx = v *cos(self.angle) *self.sens
self.animer_obus()
return True # => signaler que le coup est parti
else:
return False # => le coup n'a pas pu être tiré
def animer_obus(self):
"animer l'obus (trajectoire balistique)"
if self.anim:
self.boss.move(self.obus, int(self.vx), int(self.vy))
c = self.boss.coords(self.obus) # coord. résultantes
xo, yo = c[0] +3, c[1] +3 # coord. du centre de l'obus
self.test_obstacle(xo, yo) # a-t-on atteint un obstacle ?
self.vy += .4 # accélération verticale
self.boss.after(20, self.animer_obus)
else:
# animation terminée - cacher l'obus et déplacer les canons :
self.fin_animation()
def test_obstacle(self, xo, yo):
"évaluer si l'obus a atteint une cible ou les limites du jeu"
if yo >self.yMax or xo <0 or xo >self.xMax:
self.anim =False
return
# analyser le dictionnaire des canons pour voir si les coord.
# de l'un d'entre eux sont proches de celles de l'obus :
for id in self.guns: # id = clef dans dictionn.
gun = self.guns[id] # valeur correspondante
if xo < gun.x1 +self.rc and xo > gun.x1 -self.rc \
and yo < gun.y1 +self.rc and yo > gun.y1 -self.rc :
self.anim =False
# dessiner l'explosion de l'obus (cercle jaune) :
self.explo = self.boss.create_oval(xo -12, yo -12,
xo +12, yo +12, fill ='yellow', width =0)
self.hit =id # référence de la cible touchée
self.boss.after(150, self.fin_explosion)
break
def fin_explosion(self):
"effacer l'explosion ; ré-initaliser l'obus ; gérer le score"
self.boss.delete(self.explo) # effacer l'explosion
self.explo =False # autoriser un nouveau tir
# signaler le succès à la fenêtre maîtresse :
self.appli.goal(self.id, self.hit)
def fin_animation(self):
"actions à accomplir lorsque l'obus a terminé sa trajectoire"
self.appli.disperser() # déplacer les canons
# cacher l'obus (en l'expédiant hors du canevas) :
self.boss.coords(self.obus, -10, -10, -10, -10)
class Pupitre(Frame):
"""Pupitre de pointage associé à un canon"""
def __init__(self, boss, canon):
Frame.__init__(self, bd =3, relief =GROOVE)
self.score =0
self.appli =boss # réf. de l'application
self.canon =canon # réf. du canon associé
# Système de réglage de l'angle de tir :
self.regl =Scale(self, from_ =75, to =-15, troughcolor=canon.coul,
command =self.orienter)
self.regl.set(45) # angle initial de tir
self.regl.pack(side =LEFT)
# Étiquette d'identification du canon :
Label(self, text =canon.id).pack(side =TOP, anchor =W, pady =5)
# Bouton de tir :
self.bTir =Button(self, text ='Feu !', command =self.tirer)
self.bTir.pack(side =BOTTOM, padx =5, pady =5)
Label(self, text ="points").pack()
self.points =Label(self, text=' 0 ', bg ='white')
self.points.pack()
# positionner à gauche ou à droite suivant le sens du canon :
if canon.sens == -1:
self.pack(padx =5, pady =5, side =RIGHT)
else:
self.pack(padx =5, pady =5, side =LEFT)
def tirer(self):
"déclencher le tir du canon associé"
self.canon.feu()
def orienter(self, angle):
"ajuster la hausse du canon associé"
self.canon.orienter(angle)
def attribuerPoint(self, p):
"incrémenter ou décrémenter le score, de <p> points"
self.score += p
self.points.config(text = ' %s ' % self.score)
class Application(Frame):
'''Fenêtre principale de l'application'''
def __init__(self):
Frame.__init__(self)
self.master.title('>>>>> Boum ! Boum ! <<<<<')
self.pack()
self.jeu = Canvas(self, width =400, height =250, bg ='ivory',
bd =3, relief =SUNKEN)
self.jeu.pack(padx =8, pady =8, side =TOP)
self.guns ={} # dictionnaire des canons présents
self.pupi ={} # dictionnaire des pupitres présents
# Instanciation de 2 'objets canons (+1, -1 = sens opposés) :
self.guns["Billy"] = Canon(self.jeu, "Billy", 30, 200, 1, "red")
self.guns["Linus"] = Canon(self.jeu, "Linus", 370,200,-1, "blue")
# Instanciation de 2 pupitres de pointage associés à ces canons :
self.pupi["Billy"] = Pupitre(self, self.guns["Billy"])
self.pupi["Linus"] = Pupitre(self, self.guns["Linus"])
def disperser(self):
"déplacer aléatoirement les canons"
for id in self.guns:
gun =self.guns[id]
# positionner à gauche ou à droite, suivant sens du canon :
if gun.sens == -1 :
x = randrange(320,380)
else:
x = randrange(20,80)
# déplacement proprement dit :
gun.deplacer(x, randrange(150,240))
def goal(self, i, j):
"le canon <i> signale qu'il a atteint l'adversaire <j>"
if i != j:
self.pupi[i].attribuerPoint(1)
else:
self.pupi[i].attribuerPoint(-1)
def dictionnaireCanons(self):
"renvoyer le dictionnaire décrivant les canons présents"
return self.guns
if __name__ =='__main__':
Application().mainloop()
- Commentaires
- Ligne 7 : Par rapport au prototype, trois paramètres ont été ajoutés à la méthode constructeur. Le paramètre id nous permet d'identifier chaque instance de la classe
Canon()
à l'aide d'un nom quelconque. Le paramètre sens indique s'il s'agit d'un canon qui tire vers la droite (sens = 1
) ou vers la gauche (sens = -1
). Le paramètre coul spécifie la couleur associée au canon.
- Ligne 9 : Il faut savoir que tous les widgets Tkinter possèdent un attribut master qui contient la référence leur widget maître éventuel (leur « contenant »). Cette référence est donc pour nous celle de l'application principale. (Nous avons implémenté nous-mêmes une technique similaire pour référencer le canevas, à l'aide de l'attribut
boss
).
- Lignes 42 à 50 : Cette méthode permet d'amener le canon dans un nouvel emplacement. Elle servira à repositionner les canons au hasard après chaque tir, ce qui augmente l'intérêt du jeu.
- Lignes 56, 57 : Nous essayons de construire notre classe canon de telle manière qu'elle puisse être réutilisée dans des projets plus vastes, impliquant un nombre quelconque d'objets canons qui pourront apparaître et disparaître au fil des combats. Dans cette perspective, il faut que nous puissions disposer d'une description de tous les canons présents, avant chaque tir, de manière à pouvoir déterminer si une cible a été touchée ou non. Cette description est gérée par l'application principale, dans un dictionnaire, dont on peut lui demander une copie par l'intermédiaire de sa méthode
dictionnaireCanons()
.
- Lignes 66 à 68 : Dans cette même perspective généraliste, il peut être utile d'informer éventuellement le programme appelant que le coup a effectivement été tiré ou non.
- Ligne 76 : L'animation de l'obus est désormais traitée par deux méthodes complémentaires. Afin de clarifier le code, nous avons placé dans une méthode distincte les instructions servant à déterminer si une cible a été atteinte (méthode
test_obstacle()
).
- Lignes 79 à 81 : Nous avons vu précédemment que l'on interrompt l'animation de l'obus en attribuant une valeur « fausse » à la variable
self.anim
. La méthodeanimer_obus()
cesse alors de boucler et exécute le code de la ligne 81.
- Lignes 83 à 100 : Cette méthode évalue si les coordonnées actuelles de l'obus sortent des limites de la fenêtre, ou encore si elles s'approchent de celles d'un autre canon. Dans les deux cas, l'interrupteur d'animation est actionné, mais dans le second, on dessine une « explosion » jaune, et la référence du canon touché est mémorisée. La méthode annexe
fin_explosion()
est invoquée après un court laps de temps pour terminer le travail, c'est-à-dire effacer le cercle d'explosion et envoyer un message signalant le coup au but à la fenêtre maîtresse.
- Lignes 115 à 153 : La classe
Pupitre()
définit un nouveau widget par dérivation de la classeFrame()
, selon une technique qui doit désormais vous être devenue familière. Ce nouveau widget regroupe les commandes de hausse et de tir, ainsi que l'afficheur de points associés à un canon bien déterminé. La correspondance visuelle entre les deux est assurée par l'adoption d'une couleur commune. Les méthodestirer()
etorienter()
communiquent avec l'objetCanon()
associé, par l'intermédiaire des méthodes de celui-ci.
- Lignes 155 à 172 : La fenêtre d'application est elle aussi un widget dérivé de
Frame()
. Son constructeur instancie les deux canons et leurs pupitres de pointage, en plaçant ces objets dans les deux dictionnairesself.guns
etself.pupi
. Cela permet d'effectuer ensuite divers traitements systématiques sur chacun d'eux (comme à la méthode suivante). En procédant ainsi, on se réserve en outre la possibilité d'augmenter sans effort le nombre de ces canons si nécessaire, dans les développements ultérieurs du programme.
- Lignes 174 à 184 : Cette méthode est invoquée après chaque tir pour déplacer aléatoirement les deux canons, ce qui augmente la difficulté du jeu.
Développements complémentaires
modifierTel qu'il vient d'être décrit, notre programme correspond déjà plus ou moins au cahier des charges initial, mais il est évident que nous pouvons continuer à le perfectionner.
Nous devrions par exemple mieux le paramétrer. Qu'est-ce à dire ? Dans sa forme actuelle, notre jeu comporte un canevas de taille prédéterminée (400 x 250 pixels, voir ligne 161). Si nous voulons modifier ces valeurs, nous devons veiller à modifier aussi les autres lignes du script où ces dimensions interviennent (comme aux lignes 168-169, ou 179-184). De telles lignes interdépendantes risquent de devenir nombreuses si nous ajoutons encore d'autres fonctionnalités. Il serait donc plus judicieux de dimensionner le canevas à l'aide de variables, dont la valeur serait définie en un seul endroit. Ces variables seraient ensuite exploitées dans toutes les lignes d'instructions où les dimensions du canevas interviennent.
Nous avons déjà effectué une partie de ce travail : dans la classe Canon(), en effet, les dimensions du canevas sont récupérées à l'aide d'une méthode prédéfinie (voir lignes 17-18), et placées dans des attributs d'instance qui peuvent être utilisés partout dans la classe.
Après chaque tir, nous provoquons un déplacement aléatoire des canons, en redéfinissant leurs coordonnées au hasard. Il serait probablement plus réaliste de provoquer de véritables déplacements relatifs, plutôt que de redéfinir au hasard des positions absolues. Pour ce faire, il suffit de retravailler la méthode deplacer()
de la classe Canon()
. En fait, il serait encore plus intéressant de faire en sorte que cette méthode puisse produire à volonté, aussi bien un déplacement relatif qu'un positionnement absolu, en fonction d'une valeur transmise en argument.
Le système de commande des tirs devrait être amélioré : puisque nous ne disposons que d'une seule souris, il faut demander aux joueurs de tirer à tour de rôle, et nous n'avons mis en place aucun mécanisme pour les forcer à le faire. Une meilleure approche consisterait à prévoir des commandes de hausse et de tir utilisant certaines touches du clavier, qui soient distinctes pour les deux joueurs.
Mais le développement le plus intéressant pour notre programme serait certainement d'en faire une application réseau. Le jeu serait alors installé sur plusieurs machines communicantes, chaque joueur ayant le contrôle d'un seul canon. Il serait d'ailleurs encore plus attrayant de permettre la mise en œuvre de plus de deux canons, de manière à autoriser des combats impliquant davantage de joueurs.
Ce type de développement suppose cependant que nous ayons appris à maîtriser au préalable deux domaines de programmation qui débordent un peu le cadre de ce cours :
- la technique des sockets, qui permet d'établir une communication entre deux ordinateurs ;
- la technique des threads, qui permet à un même programme d'effectuer plusieurs tâches simultanément (cela nous sera nécessaire, si nous voulons construire une application capable de communiquer en même temps avec plusieurs partenaires).
Ces matières ne font pas strictement partie des objectifs que nous nous sommes fixés pour ce cours, et leur traitement nécessite à lui seul un chapitre entier. Nous n'aborderons donc pas cette question ici. Que ceux que le sujet intéresse se rassurent cependant : ce chapitre existe, mais sous la forme d'un complément à la fin du livre (chapitre 18) : vous y trouverez la version réseau de notre jeu de bombardes.
En attendant, voyons tout de même comment nous pouvons encore progresser, en apportant à notre projet quelques améliorations qui en feront un jeu pour 4 joueurs. Nous nous efforcerons aussi de mettre en place une programmation bien compartimentée, de manière à ce que les méthodes de nos classes soient réutilisables dans une large mesure. Nous allons voir au passage comment cette évolution peut se faire sans modifier le code existant, en utilisant l'héritage pour produire de nouvelles classes à partir de celles qui sont déjà écrites.
Commençons par sauvegarder notre ouvrage précédent dans un fichier, dont nous admettrons pour la suite de ce texte que le nom est : canon03.py.
Nous disposons ainsi d'un module Python, que nous pouvons importer dans un nouveau script à l'aide d'une seule ligne d'instruction. En exploitant cette technique, nous continuons à perfectionner notre application, en ne conservant sous les yeux que les nouveautés :
from Tkinter import *
from math import sin, cos, pi
from random import randrange
import canon03
class Canon(canon03.Canon):
"""Canon amélioré"""
def __init__(self, boss, id, x, y, sens, coul):
canon03.Canon.__init__(self, boss, id, x, y, sens, coul)
def deplacer(self, x, y, rel =False):
"déplacement, relatif si <rel> est vrai, absolu si <rel> est faux"
if rel:
dx, dy = x, y
else:
dx, dy = x -self.x1, y -self.y1
# limites horizontales :
if self.sens ==1:
xa, xb = 20, int(self.xMax *.33)
else:
xa, xb = int(self.xMax *.66), self.xMax -20
# ne déplacer que dans ces limites :
if self.x1 +dx < xa:
dx = xa -self.x1
elif self.x1 +dx > xb:
dx = xb -self.x1
# limites verticales :
ya, yb = int(self.yMax *.4), self.yMax -20
# ne déplacer que dans ces limites :
if self.y1 +dy < ya:
dy = ya -self.y1
elif self.y1 +dy > yb:
dy = yb -self.y1
# déplacement de la buse et du corps du canon :
self.boss.move(self.buse, dx, dy)
self.boss.move(self.corps, dx, dy)
# renvoyer les nouvelles coord. au programme appelant :
self.x1 += dx
self.y1 += dy
self.x2 += dx
self.y2 += dy
return self.x1, self.y1
def fin_animation(self):
"actions à accomplir lorsque l'obus a terminé sa trajectoire"
# déplacer le canon qui vient de tirer :
self.appli.depl_aleat_canon(self.id)
# cacher l'obus (en l'expédiant hors du canevas) :
self.boss.coords(self.obus, -10, -10, -10, -10)
def effacer(self):
"faire disparaître le canon du canevas"
self.boss.delete(self.buse)
self.boss.delete(self.corps)
self.boss.delete(self.obus)
class AppBombardes(Frame):
'''Fenêtre principale de l'application'''
def __init__(self, larg_c, haut_c):
Frame.__init__(self)
self.pack()
self.xm, self.ym = larg_c, haut_c
self.jeu = Canvas(self, width =self.xm, height =self.ym,
bg ='ivory', bd =3, relief =SUNKEN)
self.jeu.pack(padx =4, pady =4, side =TOP)
self.guns ={} # dictionnaire des canons présents
self.pupi ={} # dictionnaire des pupitres présents
self.specificites() # objets différents dans classes dérivées
def specificites(self):
"instanciation des canons et des pupitres de pointage"
self.master.title('<<< Jeu des bombardes >>>')
id_list =[("Paul","red"),("Roméo","cyan"),
("Virginie","orange"),("Juliette","blue")]
s = False
for id, coul in id_list:
if s:
sens =1
else:
sens =-1
x, y = self.coord_aleat(sens)
self.guns[id] = Canon(self.jeu, id, x, y, sens, coul)
self.pupi[id] = canon03.Pupitre(self, self.guns[id])
s = not s # changer de côté à chaque itération
def depl_aleat_canon(self, id):
"déplacer aléatoirement le canon <id>"
gun =self.guns[id]
dx, dy = randrange(-60, 61), randrange(-60, 61)
# déplacement (avec récupération des nouvelles coordonnées) :
x, y = gun.deplacer(dx, dy, True)
return x, y
def coord_aleat(self, s):
"coordonnées aléatoires, à gauche (s =1) ou à droite (s =-1)"
y =randrange(int(self.ym /2), self.ym -20)
if s == -1:
x =randrange(int(self.xm *.7), self.xm -20)
else:
x =randrange(20, int(self.xm *.3))
return x, y
def goal(self, i, j):
"le canon n°i signale qu'il a atteint l'adversaire n°j"
# de quel camp font-ils partie chacun ?
ti, tj = self.guns[i].sens, self.guns[j].sens
if ti != tj : # ils sont de sens opposés :
p = 1 # on gagne 1 point
else: # ils sont dans le même sens :
p = -2 # on a touché un allié !!
self.pupi[i].attribuerPoint(p)
# celui qui est touché perd de toute façon un point :
self.pupi[j].attribuerPoint(-1)
def dictionnaireCanons(self):
"renvoyer le dictionnaire décrivant les canons présents"
return self.guns
if __name__ =='__main__':
AppBombardes(650,300).mainloop()
- Commentaires
- Ligne 6 : La forme d'importation utilisée à la ligne 4 nous permet de redéfinir une nouvelle classe Canon() dérivée de la précédente, tout en lui conservant le même nom. De cette manière, les portions de code qui utilisent cette classe ne devront pas être modifiées (Cela n'aurait pas été possible si nous avions utilisé par exemple : « from canon03 import * »).
- Lignes 11 à 16 : La méthode définie ici porte le même nom qu'une méthode de la classe parente. Elle va donc remplacer celle-ci dans la nouvelle classe (On pourra dire également que la méthode deplacer() a été surchargée). Lorsque l'on réalise ce genre de modification, on s'efforce en général de faire en sorte que la nouvelle méthode effectue le même travail que l'ancienne quand elle est invoquée de la même façon que l'était cette dernière. On s'assure ainsi que les applications qui utilisaient la classe parente pourront aussi utiliser la classe fille, sans devoir être elles-mêmes modifiées. Nous obtenons ce résultat en ajoutant un ou plusieurs paramètres, dont les valeurs par défaut forceront l'ancien comportement. Ainsi, lorsque l'on ne fournit aucun argument pour le paramètre rel, les paramètres x et y sont utilisés comme des coordonnées absolues (ancien comportement de la méthode). Par contre, si l'on fournit pour rel un argument « vrai », alors les paramètres x et y sont traités comme des déplacements relatifs (nouveau comportement).
- Lignes 17 à 33 : Les déplacements demandés seront produits aléatoirement. Il nous faut donc prévoir un système de barrières logicielles, afin d'éviter que l'objet ainsi déplacé ne sorte du canevas.
- Ligne 42 : Nous renvoyons les coordonnées résultantes au programme appelant. Il se peut en effet que celui-ci commande un déplacement du canon sans connaître sa position initiale.
- Lignes 44 à 49 : Il s'agit encore une fois de surcharger une méthode qui existait dans la classe parente, de manière à obtenir un comportement différent : après chaque tir, désormais on ne disperse plus tous les canons présents, mais seulement celui qui vient de tirer.
- Lignes 51 à 55 : Méthode ajoutée en prévision d'applications qui souhaiteraient installer ou retirer des canons au fil du déroulement du jeu.
- Lignes 57 et suivantes : Cette nouvelle classe est conçue dès le départ de manière telle qu'elle puisse aisément être dérivée. C'est la raison pour laquelle nous avons fragmenté son constructeur en deux parties : La méthode
__init__()
contient le code commun à tous les objets, aussi bien ceux qui seront instanciés à partir de cette classe que ceux qui seront instanciés à partir d'une classe dérivée éventuelle. La méthode specificites() contient des portions de code plus spécifiques : cette méthode est clairement destinée à être surchargée dans les classes dérivées éventuelles.
Jeu de Ping
modifierDans les pages qui suivent, vous trouverez le script correspondant à un petit programme complet. Ce programme vous est fourni à titre d'exemple de ce que vous pouvez envisager de développer vous-même comme projet personnel de synthèse. Il vous montre encore une fois comment vous pouvez utiliser plusieurs classes afin de construire un script bien structuré.
Principe
modifierLe « jeu » mis en œuvre ici est plutôt une sorte d'exercice mathématique. Il se joue sur un panneau ou est représenté un quadrillage de dimensions variables, dont toutes les cases sont occupées par des pions. Ces pions possèdent chacun une face blanche et une face noire (comme les pions du jeu Othello/Reversi), et au début de l'exercice ils présentent tous leur face blanche par-dessus.
Lorsque l'on clique sur un pion à l'aide de la souris, les 8 pions adjacents se retournent.
Le jeu consiste alors à essayer de retourner tous les pions, en cliquant sur certains d'entre eux.
L'exercice est très facile avec une grille de 2 x 2 cases (il suffit de cliquer sur chacun des 4 pions). Il devient plus difficile avec des grilles plus grandes, et est même tout à fait impossible avec certaines d'entre elles. À vous de déterminer lesquelles ! (Ne négligez pas d'étudier le cas des grilles 1 x n)
Programmation
modifierLorsque vous développez un projet logiciel, veillez toujours à faire l'effort de décrire votre démarche le plus clairement possible. Commencez par établir un cahier des charges détaillé, et ne négligez pas de commenter ensuite très soigneusement votre code, au fur et à mesure de son élaboration (et non après coup !).
En procédant ainsi, vous vous forcez vous-même à exprimer ce que vous souhaitez que la machine fasse, ce qui vous aide à analyser les problèmes et à structurer convenablement votre code.
- Cahier des charges du logiciel à développer
- L'application sera construite sur la base d'une fenêtre principale comportant le panneau de jeu et une barre de menus.
- L'ensemble devra être extensible à volonté par l'utilisateur, les cases du panneau devant cependant rester carrées.
- Les options du menu permettront de :
- choisir les dimensions de la grille (en nombre de cases) ;
- réinitialiser le jeu (c'est-à-dire disposer tous les pions avec leur face blanche au-dessus) ;
- afficher le principe du jeu dans une fenêtre d'aide ;
- terminer.(fermer l'application).
- La programmation fera appel à trois classes :
- une classe principale ;
- une classe pour la barre de menus ;
- une classe pour le panneau de jeu ;
- Le panneau de jeu sera dessiné dans un canevas, lui-même installé dans un cadre (frame). En fonction des redimensionnements opérés par l'utilisateur, le cadre occupera à chaque fois toute la place disponible : il se présente donc au programmeur comme un rectangle quelconque, dont les dimensions doivent servir de base au calcul des dimensions de la grille à dessiner.
- Puisque les cases de cette grille doivent rester carrées, il est facile de commencer par calculer leur taille maximale, puis d'établir les dimensions du canevas en fonction de celle-ci.
- Gestion du clic de souris : on liera au canevas une méthode-gestionnaire pour l'événement <clic du bouton gauche>. Les coordonnées de l'événement serviront à déterminer dans quelle case de la grille (n° de ligne et n° de colonne) le clic a été effectué, quelles que soient les dimensions de cette grille. Dans les 8 cases adjacentes, les pions présents seront alors « retournés » (échange des couleurs noire et blanche).
###########################################
# Jeu de ping #
# Références : Voir article de la revue #
# <Pour la science>, Aout 2002 #
# #
# (C) Gérard Swinnen (Verviers, Belgique) #
# http://www.ulg.ac.be/cifen/inforef/swi #
# #
# Version du 29/09/2002 - Licence : GPL #
###########################################
from Tkinter import *
class MenuBar(Frame):
"""Barre de menus déroulants"""
def __init__(self, boss =None):
Frame.__init__(self, borderwidth =2, relief =GROOVE)
##### Menu <Fichier> #####
fileMenu = Menubutton(self, text ='Fichier')
fileMenu.pack(side =LEFT, padx =5)
me1 = Menu(fileMenu)
me1.add_command(label ='Options', underline =0,
command = boss.options)
me1.add_command(label ='Restart', underline =0,
command = boss.reset)
me1.add_command(label ='Terminer', underline =0,
command = boss.quit)
fileMenu.configure(menu = me1)
##### Menu <Aide> #####
helpMenu = Menubutton(self, text ='Aide')
helpMenu.pack(side =LEFT, padx =5)
me1 = Menu(helpMenu)
me1.add_command(label ='Principe du jeu', underline =0,
command = boss.principe)
me1.add_command(label ='À propos ...', underline =0,
command = boss.aPropos)
helpMenu.configure(menu = me1)
class Panneau(Frame):
"""Panneau de jeu (grille de n x m cases)"""
def __init__(self, boss =None):
# Ce panneau de jeu est constitué d'un cadre redimensionnable
# contenant lui-même un canevas. À chaque redimensionnement du
# cadre, on calcule la plus grande taille possible pour les
# cases (carrées) de la grille, et on adapte les dimensions du
# canevas en conséquence.
Frame.__init__(self)
self.nlig, self.ncol = 4, 4 # Grille initiale = 4 x 4
# Liaison de l'événement <resize> à un gestionnaire approprié :
self.bind("<Configure>", self.redim)
# Canevas :
self.can =Canvas(self, bg ="dark olive green", borderwidth =0,
highlightthickness =1, highlightbackground ="white")
# Liaison de l'événement <clic de souris> à son gestionnaire :
self.can.bind("<Button-1>", self.clic)
self.can.pack()
self.initJeu()
def initJeu(self):
"Initialisation de la liste mémorisant l'état du jeu"
self.etat =[] # construction d'une liste de listes
for i in range(12): # (équivalente à un tableau
self.etat.append([0]*12) # de 12 lignes x 12 colonnes)
def redim(self, event):
"Opérations effectuées à chaque redimensionnement"
# Les propriétés associées à l'événement de reconfiguration
# contiennent les nouvelles dimensions du cadre :
self.width, self.height = event.width -4, event.height -4
# La différence de 4 pixels sert à compenser l'épaisseur
# de la 'highlightbordure" entourant le canevas)
self.traceGrille()
def traceGrille(self):
"Dessin de la grille, en fonction des options & dimensions"
# largeur et hauteur maximales possibles pour les cases :
lmax = self.width/self.ncol
hmax = self.height/self.nlig
# Le côté d'une case sera égal à la plus petite de ces dimensions :
self.cote = min(lmax, hmax)
# -> établissement de nouvelles dimensions pour le canevas :
larg, haut = self.cote*self.ncol, self.cote*self.nlig
self.can.configure(width =larg, height =haut)
# Tracé de la grille :
self.can.delete(ALL) # Effacement dessins antérieurs
s =self.cote
for l in range(self.nlig -1): # lignes horizontales
self.can.create_line(0, s, larg, s, fill="white")
s +=self.cote
s =self.cote
for c in range(self.ncol -1): # lignes verticales
self.can.create_line(s, 0, s, haut, fill ="white")
s +=self.cote
# Tracé de tous les pions, blancs ou noirs suivant l'état du jeu :
for l in range(self.nlig):
for c in range(self.ncol):
x1 = c *self.cote +5 # taille des pions =
x2 = (c +1)*self.cote -5 # taille de la case -10
y1 = l *self.cote +5 #
y2 = (l +1)*self.cote -5
coul =["white","black"][self.etat[l][c]]
self.can.create_oval(x1, y1, x2, y2, outline ="grey",
width =1, fill =coul)
def clic(self, event):
"Gestion du clic de souris : retournement des pions"
# On commence par déterminer la ligne et la colonne :
lig, col = event.y/self.cote, event.x/self.cote
# On traite ensuite les 8 cases adjacentes :
for l in range(lig -1, lig+2):
if l <0 or l >= self.nlig:
continue
for c in range(col -1, col +2):
if c <0 or c >= self.ncol:
continue
if l ==lig and c ==col:
continue
# Retournement du pion par inversion logique :
self.etat[l][c] = not (self.etat[l][c])
self.traceGrille()
class Ping(Frame):
"""corps principal du programme"""
def __init__(self):
Frame.__init__(self)
self.master.geometry("400x300")
self.master.title(" Jeu de Ping")
self.mbar = MenuBar(self)
self.mbar.pack(side =TOP, expand =NO, fill =X)
self.jeu =Panneau(self)
self.jeu.pack(expand =YES, fill=BOTH, padx =8, pady =8)
self.pack()
def options(self):
"Choix du nombre de lignes et de colonnes pour la grille"
opt =Toplevel(self)
curL =Scale(opt, length =200, label ="Nombre de lignes :",
orient =HORIZONTAL,
from_ =1, to =12, command =self.majLignes)
curL.set(self.jeu.nlig) # position initiale du curseur
curL.pack()
curH =Scale(opt, length =200, label ="Nombre de colonnes :",
orient =HORIZONTAL,
from_ =1, to =12, command =self.majColonnes)
curH.set(self.jeu.ncol)
curH.pack()
def majColonnes(self, n):
self.jeu.ncol = int(n)
self.jeu.traceGrille()
def majLignes(self, n):
self.jeu.nlig = int(n)
self.jeu.traceGrille()
def reset(self):
self.jeu.initJeu()
self.jeu.traceGrille()
def principe(self):
"Fenêtre-message contenant la description sommaire du principe du jeu"
msg =Toplevel(self)
Message(msg, bg ="navy", fg ="ivory", width =400,
font ="Helvetica 10 bold",
text ="Les pions de ce jeu possèdent chacun une face blanche et "\
"une face noire. Lorsque l'on clique sur un pion, les 8 "\
"pions adjacents se retournent.\nLe jeu consiste à essayer "\
"de les retourner tous.\n\nSi l'exercice se révèle très facile "\
"avec une grille de 2 x 2 cases. Il devient plus difficile avec "\
"des grilles plus grandes. Il est même tout à fait impossible "\
"avec certaines grilles.\nÀ vous de déterminer lesquelles !\n\n"\
"Réf : revue 'Pour la Science' - Aout 2002")\
.pack(padx =10, pady =10)
def aPropos(self):
"Fenêtre-message indiquant l'auteur et le type de licence"
msg =Toplevel(self)
Message(msg, width =200, aspect =100, justify =CENTER,
text ="Jeu de Ping\n\n(C) Gérard Swinnen, Aout 2002.\n"\
"Licence = GPL").pack(padx =10, pady =10)
if __name__ == '__main__':
Ping().mainloop()
http://www.ulg.ac.be/cifen/inforef/swi/python.htm
Notes
modifier- ↑ Nous n'hésitons pas à discuter ici le développement d'un logiciel de jeu, parce qu'il s'agit d'un domaine directement accessible à tous, et dans lequel les objectifs concrets sont aisément identifiables. Il va de soi que les mêmes techniques de développement peuvent s'appliquer à d'autres applications plus « sérieuses ».
- ↑ Il s'agit d'une suite bureautique complète, libre et gratuite, largement compatible avec MS-Office, disponible pour Linux, Windows, MacOS, Solaris ... Le présent manuel a été entièrement rédigé avec son traitement de textes. Vous pouvez vous la procurer par téléchargement depuis le site Web : http://www.openoffice.org
Problèmes connus
Ce chapitre aide à résoudre les problèmes les plus courants, selon la situation ou le message d'erreur rencontré.
Les opérations avec un caractère non-ASCII ne fonctionne pas
modifierAjouter l'encodage sous le shebang : # coding: utf-8
.
Le regex ajoute le symbole � à la place des pipes (|)
modifierEncoder la chaine du pipe sous la forme d'une chaîne litérale Unicode :
- Python 2.x :
ur''
. - Python 3.x+ :
r''
.
re.sub() ne trouve pas une expression trouvée par re.search()
modifierDans un contexte multi-ligne, re.sub() ne recherche pas tout comme re.search() (qui a un global flag).
Utiliser ma_sous_chaine[re.search(regex, ma_chaine).start():re.search(regex, ma_chaine).end()]
.
Messages d'erreur
modifierImportError: bad magic number in...
modifierSi vous avez les fichiers sources correspondants, forcez une nouvelle compilation des fichiers sources en supprimant les fichiers compilés :
Linux / MacOS X | rm *.pyc
|
Windows | del *.pyc
|
Sinon, essayez d'obtenir les fichiers sources, ou utilisez la version de Python utilisée par ces fichiers.
Import error: No module named monModule
modifierIl suffit de modifier le PYTHONPATH pour qu'il trouve le module mentionné, par exemple derrière une condition s'assurant que la machine qui exécute le script contient le répertoire du module :
import sys, socket
if socket.gethostname() == "MonUbuntu":
sys.path.append(u'/usr/local/lib/python2.7/dist-packages/requests')
else:
import requests
Dans le cas des tests unitaires, il faut en faire un package : créer __init__.py dans le dossier des tests pour qu'il importe le dossier du code à tester.
IndexError: list index out of range
modifierAjouter un test sur la taille du tableau avant d'y accéder, ex :
if len(tableau) > 0: print tableau[0]
NameError: global name 'ma_fonction' is not defined
modifierSi les imports sont fait avec *, les spécifier.
sre_constants.error: bad character range
modifierArrive quand on inverse les paramètres dans re.compile(paramètre1).search(paramètre2)
.
sre_constants.error: multiple repeat
modifierDans un regex, il y a des symboles de répétition consécutifs tels que **, +* ou ++.
sre_constants.error: unmatched group
modifierSurvient quand re.sub()
ne trouve pas un groupe de capture. On peut donc le rechercher pour lancer le remplacement si la condition est remplie :
#!/usr/bin/env python
if re.compile(chaine).search(regex):
re.sub(regex, chaine)
Sinon il y a peut-être une barre verticale non échappée à tort.
TypeError: maMethode() takes no arguments (1 given)
modifierUne méthode de classe doit être déclarée avec l'argument "self" à minima.
TypeError: 'module' object is not callable
modifierIl suffit d'appeler le module sous la forme : NomFichier.NomFonction.
TypeError: slice indices must be integers or None or have an __index__ method
modifierUne chaine est divisée par une variable inappropriée (ex : x dans c[x:y]).
UnicodeEncodeError: 'ascii' codec can't encode character u'\xe9' in position x: ordinal not in range(128)
modifierEn Python 2.7 sur Pywikibot, convertir la variable avec .encode(config.console_encoding, 'replace')
(passe partout en remplaçant les caractères inconnus par des ? en console).
Sinon, ne pas utiliser str(x)
, par exemple au profit de :
.encode('utf-8')
pour une stringx.__getattr__('name').encode('utf-8')
pour une classe.
Dans le cas de l'affichage du résultat d'une concaténation, la séparer en deux instructions.
Sinon, ajouter .encode('utf-8')
après la variable résultat de la concaténation.
UnicodeEncodeError: 'charmap' codec can't encode characters in position x-y: character maps to <undefined>
modifierVoir l'erreur précédente.
UnicodeWarning: Unicode unequal comparison failed to convert both arguments to Unicode - interpreting them as being unequal
modifierL'encodage ou le typage faible pose problème.
ValueError: too many values to unpack
modifierUn "for" est mal utilisé, par exemple retirer "clé" dans : "for clé, valeur in tableau".
Ressources externes
Cette partie du livre Programmation Python présente les ouvrages et les sites Web ayant permis sa rédaction et permettant au lecteur de poursuivre son étude du sujet.
Bibliographie
modifierEn français
modifier- Gérard Swinnen, Apprendre à programmer avec Python, Eyrolles (lire en ligne), (ISBN 2-84177-299-3), dont plusieurs extraits libres de droit figurent dans ce livre.
- Python en concentré, par Alex Martelli, traduction d'Éric Jacoboni, Éditions O'Reilly, Paris, 2004, 645 p., (ISBN 9782841774524). C'est le premier ouvrage de référence véritable édité en langue française. Une mine de renseignements essentielle.
- Introduction à Python, par Mark Lutz & David Ascher, traduction de Sébastien Tanguy, Olivier Berger & Jérôme Kalifa, Éditions O'Reilly, Paris, 2000, 385 p., (ISBN 2-84177-089-3). Cet ouvrage est une excellente initiation à Python pour ceux qui pratiquent déjà d'autres langages.
- L'intro Python, par Ivan Van Laningham, traduction de Denis Frère, Karine Cottereaux et Noël Renard, Éditions CampusPress, Paris, 2000, 484 p., (ISBN 2-7440-0946-6)
- Python précis & concis (il s'agit d'un petit aide-mémoire bien pratique), par Mark Lutz, traduction de James Guérin, Éditions O'Reilly, Paris, 2000, 80 p., (ISBN 2-84177-111-3) et (ISBN 2-84177-360-4)
- Programmation Python, par Tarek Ziadé, aux éditions Eyrolles. (ISBN 2-212-11677-2)
- (français) Sébastien Chazallet - Python 3 : Les fondamentaux du langage (2ème édition) - Collection Ressources informatiques, Éditions ENI - 2016 - 900 pages - (ISBN 978-2-409-00159-8)
En anglais
modifierEn langue anglaise, le choix est évidemment beaucoup plus vaste. Nous apprécions personnellement beaucoup Python : How to program, par Deitel, Liperi & Wiedermann, Prentice Hall, Upper Saddle River - NJ 07458, 2002, 1300 p., (ISBN 0-13-092361-3) , très complet, très clair, agréable à lire et qui utilise une méthodologie éprouvée, Core Python programming, par Wesley J. Chun, Prentice Hall, 2001, 770 p., (ISBN 0-13-026036-3) dont les explications sont limpides, et Learn to program using Python, par Alan Gauld, Addison-Wesley, Reading, MA, 2001, 270 p., (ISBN 0-201-70938-4) , qui est un très bon ouvrage pour débutants.
Pour aller plus loin, notamment dans l'utilisation de la bibliothèque graphique Tkinter, on pourra utilement consulter Python and Tkinter Programming, par John E. Grayson, Manning publications co., Greenwich (USA), 2000, 658 p., (ISBN 1-884777-81-3) , et surtout l'incontournable Programming Python (second edition) de Mark Lutz, Éditions O'Reilly, Paris, 2001, 1255 p., (ISBN 0-596-00085-5), qui est une extraordinaire mine de renseignements sur de multiples aspects de la programmation moderne (sur tous systèmes).
Si vous savez déjà bien programmer, et que vous souhaitez progresser encore en utilisant les concepts les plus avancés de l'algorithmique Pythonienne, procurez-vous Python Cookbook, par Alex Martelli et David Ascher, Éditions O'Reilly, Paris, 2002, 575 p., (ISBN 0-596-00167-3), dont les recettes sont savoureuses.
Si vous souhaitez plus particulièrement exploiter au mieux les ressources liées au système d'exploitation Windows, Python Programming on Win32, par Mark Hammond & Andy Robinson, Éditions O'Reilly, Paris, 2000, 654 p., (ISBN 1-56592-621-8) est un ouvrage précieux. Référence également fort utile, la Python Standard Library de Fredrik Lundh, Éditions O'Reilly, Paris, 2001, 282 p., (ISBN 0-596-00096-0)
- Jeffrey Elkner, How to think like a computer scientist
Livres en ligne
modifier- (français) Apprendre à programmer en Python
- (français) Plongez au cœur de Python
- (français) Aide LibreOffice des scripts Python (traduction en cours)
- (anglais) How to Think Like a (Python) Programmer (licence GFDL)
Site Web
modifier- (anglais) Site officiel de Python et son wiki
- (anglais) Convention de mise en forme du code
- (anglais) https://www.python.org/dev/peps/pep-0008/
- (français) Association francophone de Python
- (français) Site collaboratif français sur Python
- (français) Recueil de liens commentés
- (français) Tutoriel python
- (français) Python est mon ami
Glossaire
Voici quelques termes spécifiques au langage python
Termes
modifierM
modifier- Marshaller
- Sérialiser un objet dans un format propre à python et indépendant de l'architecture des ordinateurs. Un ordinateur A peut recevoir un objet envoyé par un ordinateur B de cette façon à travers internet ou autre. Le format est volontairement opaque et peut changer entre deux versions de python. Il est donc préférable ques les deux ordinateurs utilisent la même version. Il ne doit pas être utilisé pour la persistence, pour cela, le module pickle ou shelve doit être privilégiés[1].
Références
modifier- ↑ https: //docs.python. org/3/library/marshal.html (enlever les espaces dans l'url)
Tableau des opérateurs
Priorité des opérations
modifierLorsqu'il y a plus d'un opérateur dans une expression, l'ordre dans lequel les opérations doivent être effectuées dépend de règles de priorité. Sous Python, les règles de priorité sont les mêmes que celles qui vous ont été enseignées au cours de mathématique. Vous pouvez les mémoriser aisément à l'aide d'un « truc » mnémotechnique, l'acronyme PEMDAS
:
P
pour parenthèses. Ce sont elles qui ont la plus haute priorité. Elles vous permettent donc de « forcer » l'évaluation d'une expression dans l'ordre que vous voulez.
Ainsi2*(3-1) = 4
, et(1+1)**(5-2) = 8
.
E
pour exposants. Les exposants sont évalués ensuite, avant les autres opérations.
Ainsi2**1+1 = 3
(et non 4), et3*1**10 = 3
(et non 59049 !).
M
etD
pour multiplication et division, qui ont la même priorité. Elles sont évaluées avant l'additionA
et la soustractionS
, lesquelles sont donc effectuées en dernier lieu.
Ainsi2-2*2 renvoie -2 et non 0 !
Et2+4/2 renvoie 4.0 et non 3.0
(Rappelez-vous que / est l'opérateur de la division décimale).
Si deux opérateurs ont la même priorité, l'évaluation est effectuée de gauche à droite.
Ainsi dans l'expression59*100/60
, la multiplication est effectuée en premier, et la machine doit donc ensuite effectuer5900/60
, ce qui donne98.0
.
A
etS
pour addition et soustraction.
Dans le tableau ci-dessous :
- les opérateurs regroupés entre deux lignes épaisses ont la même priorité.
- le sens d'évaluation indique l'ordre d'évaluation des opérations dans une expression :
- → pour une évaluation de gauche à droite : a OP1 b OP2 c == (a OP1 b) OP2 c ;
- ← pour une évaluation de droite à gauche : a OP1 b OP2 c == a OP1 (b OP2 c).
- le type peut être :
- « groupe » pour un couple de caractères encadrant une ou plusieurs expressions,
- « binaire » pour un opérateur situé entre ses deux opérandes,
- « ternaire » pour un opérateur utilisant trois opérandes,
- « unaire » pour un opérateur précédant son unique opérande.
Symbole | Type | Évaluation | Nom |
---|---|---|---|
{} | Groupe | Agencement de dictionnaire | |
() | Groupe | Agencement de n-uplet | |
[] | Groupe | Agencement de liste | |
. | Binaire | → | Attribut |
() | Groupe | Argument de fonction | |
[] | Groupe | Partie (opérateur d'indiçage) | |
await | Unaire | ← | Attente de résultat |
** | Binaire | ← | Puissance |
~ | Unaire | ← | inversion de bit |
+ | Unaire | ← | Positif |
- | Unaire | ← | Négatif |
* | Binaire | → | Multiplier |
@ | Binaire | → | Multiplication de matrices |
/ | Binaire | → | Diviser |
// | Binaire | → | Résultat entier d'une division |
% | Binaire | → | Modulo |
+ | Binaire | → | Addition |
- | Binaire | → | Soustraction |
<< | Binaire | → | Décalage à gauche |
>> | Binaire | → | Décalage à droite |
& | Binaire | → | et logique |
^ | Binaire | → | ou exclusif |
| | Binaire | → | ou logique |
in | Binaire | → | Test d'appartenance |
not in | Binaire | → | Test de non appartenance |
is | Binaire | → | Test d'égalité type |
is not | Binaire | → | Test de non égalité de type |
< | Binaire | → | inférieur |
> | Binaire | → | supérieur |
<= | Binaire | → | inférieur ou égal |
>= | Binaire | → | supérieur ou égal |
== | Binaire | → | est égal |
!= | Binaire | → | est différent |
not | Unaire | ← | non booléen |
and | Binaire | → | et booléen |
or | Binaire | → | ou booléen |
if ... else ... | Ternaire | → | expression conditionnelle |
lambda | Binaire | → | expression lambda |
Tableau des types
Liste des types | |
---|---|
int | Nombre entier optimisé |
long | Nombre entier de taille arbitraire |
float | Nombre à virgule flottante |
complex | Nombre complexe |
str | Chaîne de caractère |
unicode | Chaîne de caractère unicode |
tuple | Liste de longueur fixe |
list | Liste de longueur variable |
dict | dictionnaire |
file | Fichier |
bool | Booléen |
NoneType | Absence de type |
NotImplementedType | Absence d'implementation |
function | fonction |
module | module |
Tableau des valeurs False
bool
|
False
|
int
|
0
|
float
|
0.
|
string
|
""
|
tuple
|
()
|
list
|
[]
|
dict
|
{}
|
Tableau des mots réservés
Les mots clés
modifierand
|
as
|
assert
|
break
|
class
|
continue
|
def
|
del
|
elif
|
else
|
except
|
exec *
|
finally
|
for
|
from
|
global
|
if
|
import
|
in
|
is
|
lambda
|
not
|
or
|
pass
|
print *
|
raise
|
return
|
try
|
while
|
with
|
yield
|
* Ne sont plus des mots-clés en Python 3 mais des fonctions du module builtins
.
Il faut ajouter les trois valeurs constantes True, False, None en python3 qui sont heureusement des mots clefs. Vous pouvez vous amuser en python2 à affecter True= False et Python ne dit rien ! C'est 1984 !
Autre classement :
Mot | Définition |
---|---|
and | Opérateur ET booléen logique |
as | |
assert | |
break | Sortie de boucle |
class | Définition de classe d'objet ( Programmation Orientée Objet) |
continue | |
def | Définition de fonction |
del | Suppression de |
elif | Condition contraire |
else | Contraire |
except | Sauf (à utiliser après "try") |
exec | |
finally | |
for | Boucle |
from | De |
global | Définition (ou utilisation) dans une fonction d'une variable globale |
if | Condition |
import | Importation de module |
in | Contient |
is | Est |
is not | N'est pas |
lambda | Définition d'une fonction Lambda |
not | Négation logique |
or | Opérateur de choix OU booléen logique |
pass | |
Afficher | |
raise | |
return | Stopper la fonction courante (renvoyer sa valeur) |
sort | Classer par ordre alphabétique |
try | Essayer (généralement suivi de "except" : sauf) |
while | Boucle |
yield | S'emploie uniquement dans une fonction, et renvoie son résultat régénéré |
Les fonctions
modifierCommande | Définition |
---|---|
help() | Affiche l'aide sur le paramètre |
dir() | Affiche les méthodes du paramètre |
print() | Affiche le texte en paramètre |
input() | Enregistre la saisie de l'utilisateur |
raw_input() | Équivalent à input() (sous Python 3, préférer input()) |
len() | Renvoie la taille du paramètre |
range() | Affiche la liste des entiers de l'intervalle du paramètre |
ord() | Renvoie l'ordinal associé au caractère en paramètre |
locals() | Créer un dictionnaire (objet "dict"), dont le contenu est accessible avec "[]" |
globals() | Comme locals() mais en incluant les variables globales |
str() | Convertit une variable en caractères |
int() | Convertit une variable en nombre entier |
Fichiers | |
open() | Ouvrir un fichier |
close() | Fermer un fichier |
read() | Lire un fichier |
readline() | Lire une ligne |
readlines() | Lire les lignes séparées par des "\n," |
tell() | Donne la position d'un objet |
seek() | Donne la position d'un objet |
write() | Écrire dans un fichier |
Les modules
modifierIls sont importés avant utilisation avec "import".
Module | Définition |
---|---|
anydbm | |
array | Représentation de tableaux |
atexit | Gestionnaire de fin de programme |
bisect | Outils de tri de liste par la méthode de bissection |
calendar | Ce module permet d'afficher un calendrier et d'accéder à des fonctions spécifiques |
cmath | Module pour les nombres complexes |
codecs | |
collections | |
commands | |
ConfigParser | |
copy | |
ctypes | |
datetime | |
decimal | |
dummy_thread | |
dummy_threading | |
exceptions | |
encodings.aliases | |
formatter | |
heapq | |
gettext | |
locale | |
linecache | |
marshall | |
math | Module de fonctions mathématiques (sqrt, sin, cos, tan, etc.) |
mmap | |
operator | |
os | Module de fonction concernant le système d'exploitation (OS) |
pickle | |
Queue | |
re | Regular expressions |
shelve | |
shutil | |
signal | |
stat | |
string | |
StringIO | |
struct | |
subprocess | |
sys | |
textwrap | Formatage de texte |
tempfile | |
thread | |
threading | |
time | |
timeit | |
traceback | |
unicodedata | |
xml.sax | |
warnings | |
whichdb | |
_winreg |
GFDL | Vous avez la permission de copier, distribuer et/ou modifier ce document selon les termes de la licence de documentation libre GNU, version 1.2 ou plus récente publiée par la Free Software Foundation ; sans sections inaltérables, sans texte de première page de couverture et sans texte de dernière page de couverture. |