Programmation Python/Version imprimable3

Ceci est la version imprimable de Programmation Python.
  • Si vous imprimez cette page, choisissez « Aperçu avant impression » dans votre navigateur, ou cliquez sur le lien Version imprimable dans la boîte à outils, vous verrez cette page sans ce message, ni éléments de navigation sur la gauche ou en haut.
  • Cliquez sur Rafraîchir cette page pour obtenir la dernière version du wikilivre.
  • Pour plus d'informations sur les version imprimables, y compris la manière d'obtenir une version PDF, vous pouvez lire l'article Versions imprimables.


Programmation Python

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

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. Une copie de cette licence est incluse dans l'annexe nommée « Licence de documentation libre GNU ».

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

modifier

La 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

modifier

Les 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
  1. http://cgal-python.gforge.inria.fr/
  2. http://cocos2d.org/
  3. http://infomesh.net/2001/cwm/
  4. http://directpython.sourceforge.net/
  5. http://epydoc.sourceforge.net/
  6. http://leenissen.dk/fann/index.php
  7. http://www.madiku.org/ylrt3i0sfy/?p=1291
  8. http://gmpy.sourceforge.net/
  9. http://gnuplot-py.sourceforge.net/
  10. http://pypi.python.org/pypi/guidata/
  11. http://packages.python.org/guiqwt/
  12. http://karrigell.sourceforge.net
  13. http://matplotlib.sourceforge.net/
  14. http://homepages.inf.ed.ac.uk/s0450736/maxent_toolkit.html
  15. https://svn.enthought.com/enthought/wiki/MayaVi
  16. http://www.ailab.si/orange
  17. http://www.panda3d.org/
  18. http://www.pythonware.com/products/pil/
  19. http://www.py2exe.org/ (version pour Python 2.7)
  20. http://pyclips.sourceforge.net/
  21. http://dkbza.org/pydot.html
  22. http://www.pygame.org/news.html
  23. http://www.pyglet.org/
  24. http://pygsl.sourceforge.net/
  25. http://sites.google.com/site/roguewavesoftwarefrance/produits/PyIMSL-Studio
  26. http://pyinstaller.python-hosting.com/
  27. http://www.pylonshq.com
  28. http://pymedia.org/
  29. http://pyml.sourceforge.net/
  30. http://pympi.sourceforge.net/index.html
  31. http://www.pyngl.ucar.edu/index.shtml
  32. http://www.ogre3d.org/
  33. http://datamining.anu.edu.au/~ole/pypar/
  34. http://pyserial.sourceforge.net/pyparallel.html
  35. http://pyro.sourceforge.net/
  36. https://pypi.python.org/pypi/pyRobotics/1.5
  37. http://pyserial.sourceforge.net
  38. http://www.sfml-dev.org/tutorials/1.6/start-python-fr.php
  39. http://pyusb.berlios.de/
  40. http://pyvisa.sourceforge.net/
  41. http://pyx.sourceforge.net/
  42. http://modular.math.washington.edu/sage/
  43. http://www.scipy.org/
  44. http://www.scons.org/
  45. http://simpy.sourceforge.net/
  46. http://home.gna.org/oomadness/en/soya3d/index.html
  47. http://www2.sfk.nl/svg
  48. http://twistedmatrix.com/trac/
  49. http://www.vpython.org/
  50. http://www.web2py.com
  51. http://www.wxpython.org/
  52. http://www.zope.org/



L'interface graphique

L'interface graphique pour Python

modifier

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

modifier

Si 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

modifier

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

  1. 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.
  2. À la deuxième ligne de notre exemple : fen1 = Tk(), nous utilisons l'une des classes du module Tkinter, la classe Tk(), et nous en créons une instance (autre terme désignant un objet spécifique), à savoir la fenêtre fen1. 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 classe Tk() , 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 de Tk(), 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].
  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 classe Label(). 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 classe Label(). 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'objet fen1 est le widget maître de l'objet tex1. (On pourra dire aussi que l'objet tex1 est un widget esclave de l'objet fen1).
    • 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.
    Nous pourrions encore préciser bien d'autres caractéristiques : la police à utiliser, ou la couleur d'arrière-plan, par exemple. Toutes ces caractéristiques ont cependant une valeur par défaut dans les définitions internes de la classe Label(). Nous ne devons indiquer des options que pour les caractéristiques que nous souhaitons différentes du modèle standard.
  4. À la quatrième ligne de notre exemple : tex1.pack() , nous activons une méthode associée à l'objet tex1 : la méthode pack(). 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.
    La méthode pack() fait partie d'un ensemble de méthodes qui sont applicables non seulement aux widgets de la classe Label(), 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éthode pack() 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.
  5. À 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 classe Button() 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'option command ce qui devra se passer lorsque l'utilisateur effectuera un clic sur le bouton. Dans ce cas précis, nous actionnerons la méthode destroy associée à l'objet fen1, ce qui devrait provoquer l'effacement de la fenêtre.
  6. 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.
  7. 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'objet fen1, 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

modifier

Vous 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

modifier
 

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

  1. Comment faut-il modifier le programme pour ne plus avoir que des lignes de couleur cyan, maroon et green ?
  2. Comment modifier le programme pour que toutes les lignes tracées soient horizontales et parallèles ?
  3. 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.
  4. 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.
  5. Reprenez le programme initial. Remplacez la méthode create_line par create_rectangle. Que se passe-t-il ?
De la même façon, essayez aussi create_arc, create_oval, et create_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 fonction drawline 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 fonction drawline, 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 de global x1, y1, ..., que se passe-t-il ? Pourquoi ? Quelle conclusion pouvez-vous tirer de tout cela ?
  1. Anneaux olympiques :
    1. 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.
    2. Modifiez le programme ci-dessus en y ajoutant cinq boutons. Chacun de ces boutons provoquera le tracé de chacun des cinq anneaux.

Solution

  1. Réfléchissez !
  2. Réfléchissez !
  3. Réfléchissez !
  4. Réfléchissez !
  5. Réfléchissez !
  6. Réfléchissez !
  7. 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()
    
     
    Fenêtre avec les cinq anneaux olympiques

Exemple graphique : deux dessins alternés

modifier

Cet 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 ou RIGHT, pour « pousser » le widget du côté correspondant dans la fenêtre.
  • Les options padx et pady 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

  1. 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 :
     
  2. À 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

  1. Voir ci dessous
  2. # 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

modifier
 

Bien 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

modifier

Dans 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

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

modifier
 Au long de ce cours, nous vous présenterons petit à petit le mode d'utilisation d'un certain nombre de widgets. Comprenez bien cependant qu'il n'entre pas dans nos intentions de fournir ici un manuel de référence complet sur Tkinter. Nous limiterons nos explications aux widgets qui nous semblent les plus intéressants d'un point de vue didactique, c'est-à-dire ceux qui pourront nous aider à mettre en évidence des concepts importants, tel le concept de classe.

Il 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

modifier
 

Jusqu'à 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 :

modifier
 

Cette 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
  1. 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ètre bg ='white' par bg ='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 classe PhotoImage()[8].
  2. 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éthode create_image() associée à l'objet can1 (lequel objet est lui-même une instance de la classe Canvas). 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).
  3. 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.
  4. 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).
  5. 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

modifier

Du 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

  1. É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.
  2. 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é.
  3. 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 !).
  4. Même exercice avec des charges électriques (loi de Coulomb). Donner cette fois une possibilité de choisir le signe des charges.
  5. É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.
  6. É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.
  7. 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.
  8. 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.
  9. Modifiez le programme ci-dessus de manière à tracer d'autres figures. Consultez votre professeur pour des suggestions (courbes de Lissajous).
  10. É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 :
     
  11. É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

  1. # 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()
    
     
    capture d'écran du résultat final
  2. Réfléchissez !
  3. Réfléchissez !
  4. Réfléchissez !
  5. # 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()
    
     
    capture d'écran du résultat final
  6. Réfléchissez !
  7. Voir ci dessous.
  8. Voir ci dessous.
  9. # 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()
    
     
    capture d'écran du résultat final
  10. Réfléchissez !
  11. Réfléchissez !

Animation automatique - Récursivité

modifier

Pour 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

  1. 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.
  2. Modifiez le programme de telle façon que la balle change de couleur à chaque « virage ».
  3. 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 »).
  4. Modifiez le programme de manière à obtenir d'autres mouvements. Tâchez par exemple d'obtenir un mouvement circulaire.
  5. 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 !
  6. À 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.
  7. 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).
  8. Écrivez un programme dans lequel évoluent plusieurs balles de couleurs différentes, qui rebondissent les unes sur les autres ainsi que sur les parois.
  9. 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.
  10. É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...).
  11. É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.
  12. Perfectionnement du jeu précédent : la partie s'arrête également si le serpent « se recoupe ».

Solution

  1. Réfléchissez !
  2. Réfléchissez !
  3. Réfléchissez !
  4. Réfléchissez !
  5. # 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()
    
     
    capture d'écran du résultat final
  6. Réfléchissez !
  7. Réfléchissez !
  8. Réfléchissez !
  9. Réfléchissez !
  10. Réfléchissez !
  11. Réfléchissez !
  12. Réfléchissez !
  1. 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.
  2. 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.
  3. 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 classe Cadillac, référencé dans la variable maVoiture)
  4. 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.
  5. Au sens strict, une telle fonction qui ne devra renvoyer aucune valeur est donc plutôt une procédure.
  6. La méthode configure() peut s'appliquer à n'importe quel widget préexistant, pour en modifier les propriétés.
  7. En anglais, le mot bind signifie « lier »
  8. 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

modifier

Que faut-il posséder d'abord ?

modifier

Tkinter (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

modifier

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

modifier

Propriétés et méthodes de l'objet fenêtre

modifier

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

modifier

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

modifier

Chaque 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

modifier

L'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

modifier

Propriétés et méthodes de l'objet Entry

modifier

L'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

modifier

Le 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

modifier

Les 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

modifier

Python 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 »

modifier

Les 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(), ou place()) 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éthode get() permet de récupérer la chaîne entière. La méthode delete() permet d'en effacer tout ou partie. La méthode insert() 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 et stylePoliceTk : 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éthode self.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

modifier
 

Vous 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. Ainsi 80 correspond à 128, et c0 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'option padx =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 boucle for 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 variables n, col, rel et txt (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 cadre f1). Les options padx et pady permettent de réserver un petit espace autour de chaque étiquette, et un autre autour de chaque petit cadre. L'option side =TOP positionne les 6 petits cadres les uns en dessous des autres dans le cadre conteneur f1.
  • 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 options padx et pady 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

modifier

Le 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
À faire... 


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

modifier

Les 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 »

modifier
 

Les 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 classe Tk(). 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éthode get(), comme nous le faisons à la ligne 8 pour actualiser le libellé.

Remarque concernant l'entrée de caractères accentués

modifier
Nous vous avons déjà signalé précédemment que Python est tout à fait capable de prendre en charge les alphabets du monde entier (grec, cyrillique, arabe, japonais, etc. - voir notamment page
À faire... 


). Il en va de même pour Tkinter. En tant que francophone, vous souhaiterez certainement que les utilisateurs de vos scripts puissent entrer des caractères accentués dans les widgets Entry, Text et leurs dérivés (ComboBox, ScrolledText).

Veuillez 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 »

modifier

Ce 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 balise cible. 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éthode tag_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éthode see(). 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 composite ScrolledText. 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éfixe label_ 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 (option pady).
  • 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é dans ScrolledText. Il suffit cette fois d'associer aux noms d'option le préfixe text_.
  • Lignes 18 à 20 : Il est prévu un cadre (un widget Frame) autour du widget Text. L'option borderframe = 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 pour label_ et text_.
  • 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 que text_width et text_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 et fill = BOTH de la méthode pack() 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 et cible ainsi que le formatage du texte qui leur sera associé. La ligne 29 précise en outre que le texte associé à la balise lien 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 »

modifier

Le 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
À faire... 


.

  • 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 composant Label, un composant Canvas et deux composants Scrollbar. 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 composant Canvas intégré dans le méga-widget ScrolledCanvas. 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
À faire... 


).

  • Ligne 35 : C'est la méthode create_window() du widget Canvas 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éthode create_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

modifier

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

modifier

Vous 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)
  • Tout ce qui précède peut être simplifié en faisant appel à une expression lambda. Ce mot réservé Python désigne une expression qui renvoie un objet fonction, similaire à ceux que vous créez avec l'instruction 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

    modifier

    Nous 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é.

     Il reste également impératif de commenter convenablement le code produit, au fur et à mesure de son élaboration. S'efforcer de rédiger de bons commentaires est en effet nécessaire, non seulement pour que votre code soit facile à lire (et donc à maintenir plus tard, par d'autres ou par vous-même), mais aussi pour que vous soyez forcés d'exprimer ce que vous souhaitez vraiment que la machine fasse (cf. Erreurs sémantiques).
    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 :

    modifier

    Lorsque 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 » de self (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 widget Menubutton 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éthode add_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ètre boss. La méthode effacer(), que nous définissons nous-même plus loin, servira à vider le canevas. La méthode prédéfinie quit() provoque la sortie de la boucle mainloop() 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 widget Menubutton, puisqu'à ce stade le Menu n'existait pas encore. Nous ne pouvions pas non plus définir le widget Menu en premier lieu, puisque celui-ci doit être défini comme un « esclave » du widget Menubutton. Il faut donc bien procéder en trois étapes comme nous l'avons fait, en faisant appel à la méthode configure(). (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 primordiale Tk(). 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 type Tk() 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 »

    modifier

    Continuez 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 » :

    modifier

    Cette 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 » :

    modifier

    La 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é dans self.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'objet checkbutton à 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é !

    1. Voir page : Recherche des erreurs et expérimentation (  À faire : Créer la page)
    2. Voir page : Modules contenant des bibliothèques de classes (  À faire : Créer la page)
    3. Pour écrire dans une variable Tkinter, il faudrait utiliser la méthode set(). Exemple :
      self.actMusi.set(45)


    Turtle

    Un peu de détente avec le module turtle

    modifier

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

    Consultez également ces pages dans d’autres projets Wikimedia :

    Article encyclopédique sur Wikipédia.
    Définition sur Wiktionnaire.


    Fonctions disponibles

    modifier

    Pour 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

    modifier
    • forward(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 fonction up() avant d'utiliser goto() car sinon il tracera le parcours effectué.

    Gestion de l'écran, de l'affichage

    modifier
    • reset() : 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

    modifier
    • color("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

    modifier
    • circle(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

    Tracer une spirale quelconque

    modifier
    from 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

    modifier
    from 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

    modifier
    from 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

    modifier

    Les 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

    modifier

    Appel de fonction

    modifier

    Pour 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()
    
    #!/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

    modifier

    Cette 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

    modifier

    Le module se nomme xml.dom[1] :

    import xml.dom
    

    Références

    modifier



    Tests

    Tests unitaires

    modifier

    La bibliothèque unittest[1] permet d'automatiser les tests unitaires.

    Syntaxe

    modifier

    La 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

    modifier

    Si 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

    modifier

    Exemple 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

    modifier

    Le framework Pywikipedia propose toute une série de tests basés sur unittest.

    1. Le télécharger sur http://tools.wmflabs.org/pywikibot/
    2. Conformément au manuel tests/README.rst, lancer python pwb.py tests/api_tests.py -v.

    Tests fonctionnels

    modifier

    Il 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

    modifier

    La 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

    modifier
    from 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

    modifier

    Installation officielle[1] :

    pip install Django
    

    Utilisation

    modifier
     
    Interface d'administration.

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

    modifier

    Tout 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

    modifier

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

    modifier

    Pour 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

    modifier

    Dans 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

    modifier

    Dans 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

    modifier

    Dans 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

    modifier

    Dans 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

    modifier

    Voir aussi

    modifier


    Aiohttp

    Aiohttp est un framework web pour le Python qui reposer sur la boucle d'évènements asyncio.

    À savoir

    modifier

    La 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
    1. 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

    modifier

    Une introduction

    modifier
    ageDeEricPraline      = 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

    modifier
    import 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')
    

    Jython : utilisation de Java en Python

    modifier
    #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()
    

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

    modifier

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

    modifier

    Ce 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

    modifier

    Tkinter

    modifier

    Cet 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

    modifier

    Cet 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

    modifier

    Ce 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 »

    modifier

    En 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

    modifier

    Notre 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é.

     Tkinter renvoie ces valeurs sous la forme de chaînes de caractères. Il faut donc les convertir dans un type numérique si nous voulons pouvoir les utiliser dans un calcul.

    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 variable self.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éthode move(), 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 variable self.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.
     L'opérateur += 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

    modifier

    Disposant 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éthode animer_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 classe Frame(), 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éthodes tirer() et orienter() communiquent avec l'objet Canon() 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 dictionnaires self.guns et self.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

    modifier

    Tel 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

    modifier

    Dans 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

    modifier

    Le « 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)

     
     Vous trouverez la discussion complète du jeu de Ping, sa théorie et ses extensions, dans la revue « Pour la science » n° 298 - Août 2002, pages 98 à 102.

    Programmation

    modifier

    Lorsque 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()
    
     Rappel : Si vous souhaitez expérimenter ces programmes sans avoir à les réécrire, vous pouvez trouver leur code source à l'adresse :
    http://www.ulg.ac.be/cifen/inforef/swi/python.htm
    1. 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 ».
    2. 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

    modifier

    Ajouter l'encodage sous le shebang : # coding: utf-8.

    Le regex ajoute le symbole � à la place des pipes (|)

    modifier

    Encoder 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()

    modifier

    Dans 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

    modifier

    ImportError: bad magic number in...

    modifier

    Si 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

    modifier

    Il 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

    modifier

    Ajouter 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

    modifier

    Si les imports sont fait avec *, les spécifier.

    sre_constants.error: bad character range

    modifier

    Arrive quand on inverse les paramètres dans re.compile(paramètre1).search(paramètre2).

    sre_constants.error: multiple repeat

    modifier

    Dans un regex, il y a des symboles de répétition consécutifs tels que **, +* ou ++.

    sre_constants.error: unmatched group

    modifier

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

    modifier

    Une méthode de classe doit être déclarée avec l'argument "self" à minima.

    TypeError: 'module' object is not callable

    modifier

    Il suffit d'appeler le module sous la forme : NomFichier.NomFonction.

    TypeError: slice indices must be integers or None or have an __index__ method

    modifier

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

    modifier

    En 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 string
    • x.__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>

    modifier

    Voir l'erreur précédente.

    UnicodeWarning: Unicode unequal comparison failed to convert both arguments to Unicode - interpreting them as being unequal

    modifier

    L'encodage ou le typage faible pose problème.

    ValueError: too many values to unpack

    modifier

    Un "for" est mal utilisé, par exemple retirer "clé" dans : "for clé, valeur in tableau".



    Ressources externes

    Consultez également ces pages dans d’autres projets Wikimedia :

    Ressources éducatives sur Wikiversité.

    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

    modifier

    En 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

    modifier

    En 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

    Site Web

    modifier


    Glossaire

    Voici quelques termes spécifiques au langage python

    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
    1. https: //docs.python. org/3/library/marshal.html (enlever les espaces dans l'url)


    Tableau des opérateurs

    Priorité des opérations

    modifier

    Lorsqu'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.
      Ainsi 2*(3-1) = 4, et (1+1)**(5-2) = 8.
    • E pour exposants. Les exposants sont évalués ensuite, avant les autres opérations.
      Ainsi 2**1+1 = 3 (et non 4), et 3*1**10 = 3 (et non 59049 !).
    • M et D pour multiplication et division, qui ont la même priorité. Elles sont évaluées avant l'addition A et la soustraction S, lesquelles sont donc effectuées en dernier lieu.
      Ainsi 2-2*2 renvoie -2 et non 0 !Et 2+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'expression 59*100/60, la multiplication est effectuée en premier, et la machine doit donc ensuite effectuer 5900/60, ce qui donne 98.0.
    • A et S 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.
    Précédence des opérateurs du plus au moins prioritaire[1]
    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 2 : Liste des types prédéfinis en Python


    Tableau des valeurs False

    Les valeurs False
    bool False
    int 0
    float 0.
    string ""
    tuple ()
    list []
    dict {}
    Tableau 3 : Liste des valeurs évaluées à False


    Tableau des mots réservés

    Les mots clés

    modifier
    Liste des mots réservés
    and 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
    print 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

    modifier
    Commande 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

    modifier

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


    1. https://docs.python.org/fr/3.6/reference/expressions.html#operator-precedence