Vous êtes sur la page 1sur 53

Facult des Sciences de Luminy Master BBSG 1 anne

Programmation en langage Python Henri Garreta

digraph construction progressive dune en utilisant la bibliothque Tkinter

interface

: graphique

Lobjet de cet exercice est la prsentation de quelques lments parmi les principaux de Tkinter, la plus rpandue des bibliothques graphiques disponibles avec Python, travers la construction, tape par tape, dun (trs) modeste diteur de graphes, cest--dire un programme permettant la saisie de graphes constitus de sommets tiquets et dartes. Dans son tat final, lapplication ressemblera ceci :

Fonctionnement. Selon le choix pralablement effectu laide dun des boutons radio situs en haut gauche du cadre, on peut crer un sommet du graphe en cliquant (avec le bouton gauche) sur le point o on souhaite qu'il se trouve (fonction Cration sommet ), ou une arte en cliquant successivement sur les deux sommets qui sont ses extrmits (fonction Cration arte ) ou bien dfinir ltiquette dun sommet en cliquant sur le sommet tiqueter (fonction Dfinition tiquette ). Les tiquettes ainsi cres saffichent galement dans une liste gauche de lcran; en cliquant sur une tiquette on slectionne ou d-slectionne le sommet correspondant. Un bouton Tout effacer permet, aprs confirmation, de dtruire compltement le graphe saisi. A tout moment on peut dplacer un sommet prcdemment cr en le tranant avec le bouton droit de la souris. Enfin, des commandes Enregistrer sous... et Ouvrir... dun menu Fichier permettent de sauvegarder le graphe dit, de recharger un graphe prcdemment sauvegard ou de quitter lapplication.

Les tapes de la construction de lapplication sont les suivantes :


tape 0 : La plus petite application (avec interface) possible tape 1 : Rpartition gnrale de lespace tape 2 : Introduction dune barre dtat tape 3 : Apparition de trois boutons-radio tape 4 : Dtection des actions sur les boutons-radio tape 5 : Dtection des clics dans le canevas : cration des sommets tape 6 : Cration des artes tape 7 : Dplacement des sommets tape 8 : Liaison des sommets aux artes tape 9 : Compteurs de sommets et artes tape 10 : Un bouton pour tout effacer tape 11 : Apparition des tiquettes (figes) tape 12 : Possibilit de changer le texte des tiquettes tape 13 : Bote liste pour afficher les tiquettes tape 14 : La liste permet de mettre en vidence des sommets tape 15 : Introduction des menus tape 16 : Sauvegarde du graphe dans un fichier tape 17 : Restauration dun graphe partir dun fichier

A chaque tape nous montrons le texte source complet de lapplication, dans lequel les nouveauts (les ajouts et modifications par rapport au texte de ltape prcdente) sont crites en rouge.

Tlchargez ici une version imprimable compacte (pdf) de ce document Tlchargez ici les sources Python de tous ces programme

tape 0
But : pour dmarrer, crire la plus petite application avec interface graphique possible. videmment, elle ne fait rien ; mas elle est vivante ! (elle ragit la souris, aux actions sur la barre de titre, etc.) :

from Tkinter import * application = Tk() application.title("Edigraph 0") application.mainloop()

Explications. Dans la plupart des environnements graphiques chaque application est associe un cadre principal. La figure ci-dessus montre les lments dun tel cadre : la barre de titre, les quatre boutons (un gauche et trois droite) avec des fonctionnalits associes. Ce cadre est galement le sige de la fonction de redimensionnement manuel (dplacements des bords ou du coin). En Tkinter, un cadre principal est une instance de la classe Tk (lexpression Tk() est un appel du constructeur de cette classe). Ce cadre mrite de sappeler application puisquil est intimement li lapplication et la reprsente aux yeux de lutilisateur. Ce programme est tout fait minimal, mais prsente un trait caractristique des interfaces graphiques, assez droutant la premire fois quon le rencontre : lessentiel du travail de lapplication est enfoui dans la fonction mainloop, fournie par Tkinter, et semble donc chapper au programmeur. Ainsi, aprs un travail plus ou moins important de cration des lments de linterface graphique (ici : application=Tk()) et de configuration de ces lments (ici : application.title("Edigraph 0")), lapplication sombre dans une boucle dattente cache dans mainloop, comme si elle sendormait en attente dvnements que lutilisateur crera en agissant avec la souris, le clavier, etc. On dit que lapplication est pilote par les vnements (on parle aussi de programmation vnementielle). Par consquent, comme on le verra dans la suite de lexercice, la partie algorithmique du travail devra tre morcele en fonctions distinctes destines tre appeles en rponse aux vnements dtects par linterface utilisateur.

tape 1
But : pour mettre en place la rpartition gnrale de lespace, dcouper celui-ci en trois parties rectangulaires, provisoirement vides et colories :

from Tkinter import *

application = Tk() application.title("Edigraph 1") canevas = Canvas(application, bg="#C8FFFF", width=400, height=400) canevas.pack(side=RIGHT, fill=BOTH, expand=True, padx=2, pady=2) panneauSup = Frame(application, width=150, height = 200, padx=4, pady=4, bd=2, relief = RIDGE) panneauSup.pack(side=TOP) panneauInf = Frame(application, bg="red") panneauInf.pack(side=BOTTOM, fill=BOTH, expand=True) application.mainloop()

Explications. Nous avons cr un objet Canvas (reprsent par la variable canevas) et deux objets Frame (variables panneauSup et panneauInf). Un Canvas est quip pour supporter des collections dobjets graphiques (points, droites, courbes, etc.) et pour leur faire subir des oprations graphiques. Un Frame, en revanche, ne sert qu contenir dautres widgets ; il peut, comme ici, possder une bordure. Sauf exception, tout widget est construit par une opration qui prend comme premier argument un widget parent (ici application). Ainsi sauf exception les widgets dune interface graphique sont organiss en une arborescence unique ayant pour racine le cadre principal (lobjet Tk). Le parent dun widget donn contient celui-ci (contenance graphique : lun est dessin lintrieur de lautre) ; en outre, la destruction du parent entrane la destruction du widget en question. Parmi les autres arguments des constructeurs montrs ci-dessus :

(background) : la couleur du fond du widget, donne dans le systme RGB, cest--dire dfinie par un nombre form par la juxtaposition de trois octets (des entiers compris entre 0 et 255) qui dfinissent lintensit de rouge, de vert et de bleu. Il est particulirement commode dcrire ces nombres en hexadcimal, car la juxtaposition est alors vidente : #C8FFFF signifie : 200 (C8 en hexadcimal) dans le rouge, 255 (FF en hexa) dans le vert et 255 dans le bleu width, height : largeur et hauteur du widget padx, pady : espace de remplissage (cest--dire sparation entre le contenu et le bord) horizontal et vertical bd : paisseur de la bordure relief : forme de la bordure (RIDGE = crte)
bg

Une fois construit, chaque widget doit tre plac dans son widget parent; ce travail est assur par la mthode pack, dont quelques arguments sont :
side : ct du parent contre lequel il faut coller le widget en question expand : faut-il faire grandir lespace allou au widget lorsque le parent grandit ? fill : dans quelles directions le widget remplit-il lespace qui lui est allou ? padx, pady : espace de remplissage (cest--dire sparation entre le widget et son parent)

Notez que lordre chronologique du placement des widgets nest pas indiffrent : chaque widget est plac dans lespace laiss libre par les widgets placs avant lui.

tape 2

But : ne serait-ce que pour ne pas oublier de le faire, placer une barre dtat (pour le moment fige) en bas de la fentre principale :

from Tkinter import * application = Tk() application.title("Edigraph 2") barreEtat = Label(application, text="Ici bientt un texte daide", bd=1, relief=SUNKEN, anchor=W) barreEtat.pack(side=BOTTOM, fill=X) canevas = Canvas(application, bg="#C8FFFF", width=400, height=400) canevas.pack(side=RIGHT, fill=BOTH, expand=True, padx=2, pady=2) panneauSup = Frame(application, width=150, height=200, padx=4, pady=4, bd=2, relief=RIDGE) panneauSup.pack(side=TOP) panneauInf = Frame(application, bg="red") panneauInf.pack(side=BOTTOM, fill=BOTH, expand=True) application.mainloop()

Explications. Pas besoin de beaucoup dexplications ici. Nous crons un objet Label, avec un texte ( Ici bientt un texte daide ), une bordure troite (bd=1) et creuse (SUNKEN), et un ancrage louest (cest--dire que le texte sera cadr gauche). On a dj dit que lordre des appels de pack a une importance : chaque lment se place dans lespace laiss libre par les lments dj placs. Par exemple, en crivant lexpression barreEtat.pack(...) aprs canevas.pack(...) ou panneauSup.pack(...), on aurait obtenu d'autres configurations :

Pire

encore,

si

on

panneauInf.pack(...)

avait crit lexpression barreEtat.pack(...) en dernier, aprs (cest--dire aprs que ce panneau ait pris le bas de l'espace restant) on

aurait obtenu ceci :

tape 3
But : ajouter les trois boutons-radio Cration sommet, Cration arte et Dfinition tiquette, synchroniss par la variable tatCourant :

from Tkinter import * CREATION_SOMMET = "Cration sommet : cliquez pour dsigner lemplacement" DEBUT_CREATION_ARETE = "Cration arte : cliquez pour dsigner lorigine" DEFINITION_ETIQUETTE = "Dfinition dtiquette : dsignez le sommet" application = Tk() application.title("Edigraph 3") etatCourant = StringVar(application) etatCourant.set(CREATION_SOMMET) barreEtat = Label(application, text="Ici bientt un texte daide", bd=1, relief=SUNKEN, anchor=W) barreEtat.pack(side=BOTTOM, fill=X) canevas = Canvas(application, bg="#C8FFFF", width=400, height=400) canevas.pack(side=RIGHT, fill=BOTH, expand=True, padx=2, pady=2) panneauSup = Frame(application, width=150, height = 200, padx=4, pady=4, bd=2, relief = RIDGE) panneauSup.pack(side=TOP) lab = Label(panneauSup, text="Bouton gauche :") lab.grid(row=0, sticky=W) rb = Radiobutton(panneauSup, text="Cration sommet", variable=etatCourant, value=CREATION_SOMMET) rb.grid(row=1, sticky=W) rb = Radiobutton(panneauSup, text="Cration arte", variable=etatCourant, value=DEBUT_CREATION_ARETE) rb.grid(row=2, sticky=W) rb = Radiobutton(panneauSup, text="Dfinition tiquette", variable=etatCourant, value=DEFINITION_ETIQUETTE) rb.grid(row=3, sticky=W) panneauInf = Frame(application, bg="red") panneauInf.pack(side=BOTTOM, fill=BOTH, expand=True) application.mainloop()

Explications. Il y a ici plusieurs nouveauts intressantes. Dune part, la cration de quatre nouveaux widgets : un Label (affect la variable lab) et trois Radiobutton (les valeurs successives de la variable rb). Notez que ces widgets nont pas pour parent le cadre gnral (lobjet Tk valeur de la variable application) mais lobjet Frame dsign par la variable panneauSup. Par consquent, les oprations de placement de ces widgets ne seront pas relatives au cadre de lapplication, mais relatives au petit panneau suprieur gauche. Ces oprations sont effectues par le gestionnaire grid, qui ici dfinit une grille de quatre lignes (row=0, ... row=3) et une colonne, puisquaucune indication de colonne nest donne. Le paramtre sticky=W (W pour West) indique que lorsque la cellule de la grille est plus grande que le widget quelle contient, ce dernier dont tre plac contre la paroi gauche de la cellule. Un groupe de boutons radio est un ensemble de cases cocher coordonnes de telle manire que lorsquon en coche une, celle qui tait alors coche cesse de ltre (comme les boutons de slection sur un poste radio). Cette coordination se fait ici travers lobjet StringVar dsign par la variable etatCourant, pass comme argument dans la construction de chaque bouton radio : lorsquun bouton est slectionn, sa valeur associe (dfinie la construction par le paramtre value) est affecte lobjet StringVar, lequel ragit ce changement en teignant le bouton qui tait alors coch. Bien noter que, malgr son nom, un objet StringVariable nest pas une variable ordinaire de Python, mais un vrai widget qui encapsule une variable chane de caractres, dtecte les changements que cette variable subit et les notifie dautres widgets (quand on coche un bouton radio, cela produit laffectation dune valeur convenue la StringVariable associe, ce qui provoque lextinction du bouton radio qui tait alors coch).

tape 4
But : dtecter les actions sur les boutons radio et, pour commencer, changer le texte affich dans la barre dtat (ce nest pas le plus important, mais cela montrera quon entend bien les boutons) :

from Tkinter import * CREATION_SOMMET = "Cration sommet : cliquez pour dsigner lemplacement" DEBUT_CREATION_ARETE = "Cration arte : cliquez pour dsigner lorigine"

DEFINITION_ETIQUETTE = "Dfinition dtiquette : dsignez le sommet" application = Tk() application.title("Edigraph 4") etatCourant = StringVar(application) etatCourant.set(CREATION_SOMMET) def cocheCaseCreationSommet(): barreEtat.config(text=CREATION_SOMMET) def cocheCaseCreationArete(): barreEtat.config(text=DEBUT_CREATION_ARETE) def cocheCaseDefEtiquette(): barreEtat.config(text=DEFINITION_ETIQUETTE) barreEtat = Label(application, text=CREATION_SOMMET, bd=1, relief=SUNKEN, anchor=W) barreEtat.pack(side=BOTTOM, fill=X) canevas = Canvas(application, bg="#C8FFFF", width=400, height=400) canevas.pack(side=RIGHT, fill=BOTH, expand=True, padx=2, pady=2) panneauSup = Frame(application, width=150, height = 200, padx=4, pady=4, bd=2, relief = RIDGE) panneauSup.pack(side=TOP) lab = Label(panneauSup, text="Bouton gauche :") lab.grid(row=0, sticky=W) rb = Radiobutton(panneauSup, text="Cration sommet", command=cocheCaseCreationSommet, variable=etatCourant, value=CREATION_SOMMET) rb.grid(row=1, sticky=W) rb = Radiobutton(panneauSup, text="Cration arte", command=cocheCaseCreationArete, variable=etatCourant, value=DEBUT_CREATION_ARETE) rb.grid(row=2, sticky=W) rb = Radiobutton(panneauSup, text="Dfinition tiquette", command=cocheCaseDefEtiquette, variable=etatCourant, value=DEFINITION_ETIQUETTE) rb.grid(row=3, sticky=W) panneauInf = Frame(application, bg="red") panneauInf.pack(side=BOTTOM, fill=BOTH, expand=True) application.mainloop()

Explications. Ce nest pas trs compliqu : lors de la cration de chaque bouton radio, un paramtre supplmentaire, nomm command, spcifie une fonction qui devra tre appele lorsque le bouton sera actionn. Cela fait trois fonctions, une par bouton, qui pour le moment se rduisent changer la valeur de la proprit text de la barre dtat. A ce propos on notera lemploi de la mthode config, que tout widget possde, et qui permet de spcifier la valeur de pratiquement nimporte quelle proprit (la documentation emploie le mot option) du widget. En dfinitive, il y a trois manires principales dinitialiser ou de modifier ces proprits :

les paramtres du constructeur, lors de la construction du widget. Exemple : lab=Label(panneauSup, text="Bouton gauche :"), un appel ultrieur de la mthode config, dont les arguments sont pratiquement les mmes que ceux du constructeur. Exemple : barreEtat.config(text=CREATION_SOMMET),

enfin, certaines proprits particulirement importantes peuvent tre dfinies par une mthode propre. Exemple : application.title("Edigraph 4").

tape 5
But : dtecter les clics faits sur le canevas avec le bouton gauche et sen servir pour crer des sommets :

from Tkinter import * CREATION_SOMMET = "Cration sommet : cliquez pour dsigner lemplacement" DEBUT_CREATION_ARETE = "Cration arte : cliquez pour dsigner lorigine" DEFINITION_ETIQUETTE = "Dfinition dtiquette : dsignez le sommet" R = 3 COULEUR_SOMMET = "black" # rayon des sommets # couleur des sommets

application = Tk() application.title("Edigraph 5") etatCourant = StringVar(application) etatCourant.set(CREATION_SOMMET) def creationSommet(x, y): sommet = canevas.create_oval(x - R, y - R, x + R, y + R, fill = COULEUR_SOMMET, width = 0) def cocheCaseCreationSommet(): barreEtat.config(text=CREATION_SOMMET) def cocheCaseCreationArete(): barreEtat.config(text=DEBUT_CREATION_ARETE) def cocheCaseDefEtiquette(): barreEtat.config(text=DEFINITION_ETIQUETTE) def pressionBoutonGauche(event): if etatCourant.get() == CREATION_SOMMET: creationSommet(event.x, event.y)

elif etatCourant.get() == DEBUT_CREATION_ARETE: print "Cration dartes pas encore en place" else: # etatCourant.get() == DEFINITION_ETIQUETTE print "Dfinition tiquette pas encore en place" barreEtat = Label(application, text=CREATION_SOMMET, bd=1, relief=SUNKEN, anchor=W) barreEtat.pack(side=BOTTOM, fill=X) canevas = Canvas(application, bg="#C8FFFF", width=400, height=400) canevas.pack(side=RIGHT, fill=BOTH, expand=True, padx=2, pady=2) canevas.bind("<Button-1>", pressionBoutonGauche) panneauSup = Frame(application, width=150, height = 200, padx=4, pady=4, bd=2, relief = RIDGE) panneauSup.pack(side=TOP) lab = Label(panneauSup, text="Bouton gauche :") lab.grid(row=0, sticky=W) rb = Radiobutton(panneauSup, text="Cration sommet", command=cocheCaseCreationSommet, variable=etatCourant, value=CREATION_SOMMET) rb.grid(row=1, sticky=W) rb = Radiobutton(panneauSup, text="Cration arte", command=cocheCaseCreationArete, variable=etatCourant, value=DEBUT_CREATION_ARETE) rb.grid(row=2, sticky=W) rb = Radiobutton(panneauSup, text="Dfinition tiquette", command=cocheCaseDefEtiquette, variable=etatCourant, value=DEFINITION_ETIQUETTE) rb.grid(row=3, sticky=W) panneauInf = Frame(application, bg="red") panneauInf.pack(side=BOTTOM, fill=BOTH, expand=True) application.mainloop()

Explications. Un sommet sera reprsent par un disque noir dun rayon de 3 pixels (voir les paramtres R et COULEUR_SOMMET). La cration de tels sommets avec la souris demande trois niveaux dintervention dans le programme :

dabord, prendre note que lorsque le pointeur est au-dessus du canevas, chaque pression sur le premier bouton de la souris doit provoquer lappel dune certaine fonction. Cest linstruction canevas.bind("<button-1>", pressionBoutonGauche) qui fait un tel enregistrement. La chane "<button-1>", spcifie dans Tkinter, est la reprsentation conventionnelle de lvnement pression sur le premier bouton (cest--dire le bouton de gauche) ; pressionBoutonGauche est le nom que nous avons donn la fonction en question ; ensuite, crire la fonction pressionBoutonGauche en tenant compte du fait que le mme vnement (pression sur le bouton gauche) aura des effets diffrents selon le bouton radio couramment slectionn ; puisque cest lobjet etatCourant qui renseigne sur ltat des boutons radio, on a le code montr ci-dessus enfin, crire la fonction qui soccupe spcifiquement de construire un sommet ; cette fonction est appele avec les coordonnes du point o laction avec la souris sest produite, et se contente dappeler la mthode createOval du canevas.

On notera que dans les arguments de la mthode createOval on utilise, pour dfinir une ellipse, les coordonnes du coin suprieur gauche et du coin infrieur droit du rectangle dans lequel lellipse sinscrit :

Puisquon veut un disque, le rectangle est un carr de cte 2 R pixels, dont (x,y) est le centre ; ces coordonnes sont donc (x R, y R) pour un coin et (x + R, y + R) pour lautre. Enfin, largument FILL=COULEUR_SOMMET dfinit la couleur de remplissage (puisquil sagit dune courbe ferme) et width=0 dfinit lpaisseur du bord (pas de bord ici).

tape 6
But : mettre en place la cration des artes. Le protocole est le suivant : 1. Lutilisateur slectionne le bouton radio Cration arte (immdiatement la barre dtat affiche donc Cration arte : cliquez pour dsigner lorigine) 2. Ensuite lutilisateur dsigne, en cliquant dessus, un des sommets visibles sur le canevas ; sil y arrive, laffichage de la barre dtat devient Cration arte : cliquez pour dsigner lextrmit 3. Lutilisateur doit alors essayer de cliquer sur un deuxime sommet ; sil y arrive, larte sera cr et la barre dtat reviendra laffichage Cration arte : cliquez pour dsigner lorigine Notez que origine et extrmit sont des expressions conventionnelles pour dsigner les deux extrmits dune arte qui, en ralit, nest pas oriente.

from Tkinter import * CREATION_SOMMET = "Cration sommet : cliquez pour dsigner lemplacement" DEBUT_CREATION_ARETE = "Cration arte : cliquez pour dsigner lorigine"

SUITE_CREATION_ARETE = "Cration arte : cliquez pour dsigner lextremit" DEFINITION_ETIQUETTE = "Dfinition dtiquette : dsignez le sommet" R = 3 COULEUR_SOMMET = "black" # rayon des sommets # couleur des sommets

application = Tk() application.title("Edigraph 6") etatCourant = StringVar(application) etatCourant.set(CREATION_SOMMET) def sommetVoisin(x, y): # on recherche un objet dessin dans le canevas prs de (x, y) pres = 10 objets = canevas.find_enclosed(x - pres, y - pres, x + pres, y + pres) if len(objets) > 0: # sil y en a... return objets[0] # ...on rend le premier else: return None

def creationSommet(x, y): sommet = canevas.create_oval(x - R, y - R, x + R, y + R, fill = COULEUR_SOMMET, width = 0) def cocheCaseCreationSommet(): barreEtat.config(text=CREATION_SOMMET) def cocheCaseCreationArete(): barreEtat.config(text=DEBUT_CREATION_ARETE) def cocheCaseDefEtiquette(): barreEtat.config(text=DEFINITION_ETIQUETTE) def centre(sommet): a, b, c, d = canevas.coords(sommet) return ((a + c) / 2, (b + d) / 2) def creationArete(orig, extr): x0, y0 = centre(orig) x1, y1 = centre(extr) arete = canevas.create_line(x0, y0, x1, y1) def pressionBoutonGauche(event): global sommetSelectionne if etatCourant.get() == CREATION_SOMMET: creationSommet(event.x, event.y) elif etatCourant.get() == DEBUT_CREATION_ARETE: s = sommetVoisin(event.x, event.y) if s != None: sommetSelectionne = s etatCourant.set(SUITE_CREATION_ARETE) barreEtat.config(text=SUITE_CREATION_ARETE) elif etatCourant.get() == SUITE_CREATION_ARETE: s = sommetVoisin(event.x, event.y) if s != None: creationArete(sommetSelectionne, s) etatCourant.set(DEBUT_CREATION_ARETE) barreEtat.config(text=DEBUT_CREATION_ARETE) else: # etatCourant.get() == DEFINITION_ETIQUETTE print "Dfinition tiquette pas encore en place"

barreEtat = Label(application, text=CREATION_SOMMET, bd=1, relief=SUNKEN, anchor=W) barreEtat.pack(side=BOTTOM, fill=X) canevas = Canvas(application, bg="#C8FFFF", width=400, height=400) canevas.pack(side=RIGHT, fill=BOTH, expand=True, padx=2, pady=2) canevas.bind("<Button-1>", pressionBoutonGauche) panneauSup = Frame(application, width=150, height = 200, padx=4, pady=4, bd=2, relief = RIDGE) panneauSup.pack(side=TOP) lab = Label(panneauSup, text="Bouton gauche :") lab.grid(row=0, sticky=W) rb = Radiobutton(panneauSup, text="Cration sommet", command=cocheCaseCreationSommet, variable=etatCourant, value=CREATION_SOMMET) rb.grid(row=1, sticky=W) rb = Radiobutton(panneauSup, text="Cration arte", command=cocheCaseCreationArete, variable=etatCourant, value=DEBUT_CREATION_ARETE) rb.grid(row=2, sticky=W) rb = Radiobutton(panneauSup, text="Dfinition tiquette", command=cocheCaseDefEtiquette, variable=etatCourant, value=DEFINITION_ETIQUETTE) rb.grid(row=3, sticky=W) panneauInf = Frame(application, bg="red") panneauInf.pack(side=BOTTOM, fill=BOTH, expand=True) application.mainloop()

Explications. Il faut introduire un nouvel tat, SUITE_CREATION_ARETE (qui ne correspond pas un bouton radio) pour caractriser le fait quon a dj slectionn une extrmit de larte, et donc que le prochain clic va servir dsigner lautre extrmit. Ensuite, on doit crire deux cas nouveaux dans la cascade de if...elif...else qui constituent la fonction pressionBoutonGauche, correspondant respectivement au choix de lorigine et celui de lextrmit. Ces deux cas ont une partie commune : la recherche du sommet sur lequel le clic est tomb ; cest la fonction sommetVoisin qui fait ce travail, dune manire particulirement commode. Alors que dans dautres bibliothques graphiques ce serait au programmeur de parcourir la liste des sommets existants un instant donn, en Tkinter il ny en a pas besoin du moins pour le moment : le canevas connat les constructions graphiques quon lui a faites (ici on na fait que des ovales) et peut nous donner, par la mthode find_enclosed, la liste de celles qui sont incluses dans un rectangle donn. Pour tolrer un certain niveau derreur de vise, nous demandons la mthode find_enclosed la liste des objets graphiques (il sagit de cercles de rayon 3) inclus dans un carr de cte 20, puisque pres vaut 10 (si cela se rvle trop grossier on pourra toujours raffiner ultrieurement). Si la mthode find_enclosed renvoie une liste non vide (probablement rduite un objet), on prend son premier lment pour rsultat de la fonction sommetVoisin ; si cette liste est vide, la rsultat est None pour indiquer que le clic na dsign aucun sommet. Attention, subtilit. Lors de la slection de la premire extrmit on dtermine un sommet dont on ne peut pour le moment rien faire, part le garder pour plus tard ; lorsquon aura slectionn la deuxime extrmit il nous faudra les deux sommets pour dfinir une arte. Do lintroduction de

la variable sommetSelectionne, dont il faut veiller ce quelle ne soit pas locale car elle serait dans ce cas perdue entre les deux vnements ; la dclaration global sommetSelectionn fait cela. Enfin, la cration effective des artes est effectue par la fonction creationArete, qui prend deux sommets et cre un segment par un appel de la mthode create_line. Une fonction centre auxiliaire est requise, pour calculer les coordonnes du centre du disque qui reprsente graphiquement chaque sommet (le canevas ne connat que les coordonnes du rectangle englobant).

tape 7
But : permettre le dplacement des sommets crs en les tranant avec le bouton droit de la souris. Cela fonctionne ainsi : 1. tout instant, lutilisateur peut cliquer sur un sommet avec le bouton droit de la souris ; lapplication passe alors dans un tat particulier un sommet est en train dtre dplac , manifest par le fait que la variable sommetEnDeplacement (qui habituellement vaut None) prend pour valeur le sommet dont il sagit, 2. sans relcher le bouton droit, lutilisateur bouge la souris : le sommet en question suit cette dernire et est ainsi tran jusqu une nouvelle place, 3. quand lutilisateur lche le bouton droit de la souris, lopration se termine : la variable sommetEnDeplacement reprend la valeur None et plus aucun sommet ne bouge. N.B. Pour le moment, les artes ne suivront pas les dplacements de leurs extrmits, on soccupera de cela ltape suivante.

from Tkinter import * CREATION_SOMMET = "Cration sommet : cliquez pour dsigner lemplacement" DEBUT_CREATION_ARETE = "Cration arte : cliquez pour dsigner lorigine" SUITE_CREATION_ARETE = "Cration arte : cliquez pour dsigner lextremit" DEFINITION_ETIQUETTE = "Dfinition dtiquette : dsignez le sommet" R = 3 # rayon des sommets

COULEUR_SOMMET = "black" sommetEnDeplacement = None

# couleur des sommets # sommet en cours dtre dplac

application = Tk() application.title("Edigraph 7") etatCourant = StringVar(application) etatCourant.set(CREATION_SOMMET) def sommetVoisin(x, y): # on recherche un objet dessin dans le canevas prs de (x, y) pres = 10 objets = canevas.find_enclosed(x - pres, y - pres, x + pres, y + pres) if len(objets) > 0: # sil y en a... return objets[0] # ...on rend le premier else: return None def creationSommet(x, y): sommet = canevas.create_oval(x - R, y - R, x + R, y + R, fill = COULEUR_SOMMET, width = 0) def cocheCaseCreationSommet(): barreEtat.config(text=CREATION_SOMMET) def cocheCaseCreationArete(): barreEtat.config(text=DEBUT_CREATION_ARETE) def cocheCaseDefEtiquette(): barreEtat.config(text=DEFINITION_ETIQUETTE) def centre(sommet): a, b, c, d = canevas.coords(sommet) return ((a + c) / 2, (b + d) / 2) def creationArete(orig, extr): x0, y0 = centre(orig) x1, y1 = centre(extr) arete = canevas.create_line(x0, y0, x1, y1) def pressionBoutonGauche(event): global sommetSelectionne if etatCourant.get() == CREATION_SOMMET: creationSommet(event.x, event.y) elif etatCourant.get() == DEBUT_CREATION_ARETE: s = sommetVoisin(event.x, event.y) if s != None: sommetSelectionne = s etatCourant.set(SUITE_CREATION_ARETE) barreEtat.config(text=SUITE_CREATION_ARETE) elif etatCourant.get() == SUITE_CREATION_ARETE: s = sommetVoisin(event.x, event.y) if s != None: creationArete(sommetSelectionne, s) etatCourant.set(DEBUT_CREATION_ARETE) barreEtat.config(text=DEBUT_CREATION_ARETE) else: # etatCourant.get() == DEFINITION_ETIQUETTE print "Dfinition tiquette pas encore en place" def deplacementSommet(x, y): canevas.coords(sommetEnDeplacement, x - R, y - R, x + R, y + R)

def pressionBoutonDroit(event): global sommetEnDeplacement sommetEnDeplacement = sommetVoisin(event.x, event.y) def mouvementBoutonDroit(event): if sommetEnDeplacement != None: deplacementSommet(event.x, event.y) def relachementBoutonDroit(event): global sommetEnDeplacement sommetEnDeplacement = None barreEtat = Label(application, text=CREATION_SOMMET, bd=1, relief=SUNKEN, anchor=W) barreEtat.pack(side=BOTTOM, fill=X) canevas = Canvas(application, bg="#C8FFFF", width=400, height=400) canevas.pack(side=RIGHT, fill=BOTH, expand=True, padx=2, pady=2) canevas.bind("<Button-1>", pressionBoutonGauche) canevas.bind("<Button-3>", pressionBoutonDroit) canevas.bind("<Motion>", mouvementBoutonDroit) canevas.bind("<ButtonRelease-3>", relachementBoutonDroit) panneauSup = Frame(application, width=150, height = 200, padx=4, pady=4, bd=2, relief = RIDGE) panneauSup.pack(side=TOP) lab = Label(panneauSup, text="Bouton gauche :") lab.grid(row=0, sticky=W) rb = Radiobutton(panneauSup, text="Cration sommet", command=cocheCaseCreationSommet, variable=etatCourant, value=CREATION_SOMMET) rb.grid(row=1, sticky=W) rb = Radiobutton(panneauSup, text="Cration arte", command=cocheCaseCreationArete, variable=etatCourant, value=DEBUT_CREATION_ARETE) rb.grid(row=2, sticky=W) rb = Radiobutton(panneauSup, text="Dfinition tiquette", command=cocheCaseDefEtiquette, variable=etatCourant, value=DEFINITION_ETIQUETTE) rb.grid(row=3, sticky=W) panneauInf = Frame(application, bg="red") panneauInf.pack(side=BOTTOM, fill=BOTH, expand=True) application.mainloop()

Explications. Trois nouveaux vnements souris nous intressent ici, lorsquils se produisent sur le canevas, et doivent tre lies des fonctions qui les traitent :
<Button-3> signale

la pression du bouton de droite (<Button-2> dsigne le bouton du milieu, dans les souris trois boutons), <Motion> reprsente le mouvement de la souris (cest un vnement qui narrive pas seul, mais par rafales), <ButtonRelease-3> indique que le bouton droit a t relch.

Trois fonctions spcifiques ont t crites pour traiter ces vnements :


pressionBoutonDroit recherche le sommet sur lequel on a cliqu et, sil le trouve, en valeur de la variable sommetEnDeplacement, sinon, cette variable garde la valeur None,

fait la

modifie les coordonnes du sommet en cours de dplacement, si cet sommet existe (cest--dire si sommetEnDeplacement nest pas None) ; sinon, lvnement est ignor. Le dplacement effectif du sommet est ralis par la mthode coords du canevas, qui permet de donner de nouvelles valeurs aux coordonnes dun objet graphique du canevas. relachementBoutonDroit termine lopration en redonnant sommetEnDeplacement la valeur None.
mouvementBoutonDroit

Remarquez que la variable sommetEnDeplacement est dclare global dans les fonctions pressionBoutonDroit et relachementBoutonDroit, sans quoi elle serait locale ces fonctions et son affectation naurait pas deffet durable. Sa dclaration global dans la fonction mouvementBoutonDroit nest pas ncessaire car elle ny subit pas une affectation.

tape 8
But : lier les sommets et les artes afin que les dplacements des premiers entranent les modifications ncessaires des secondes.

from Tkinter import * CREATION_SOMMET = "Cration sommet : cliquez pour dsigner lemplacement" DEBUT_CREATION_ARETE = "Cration arte : cliquez pour dsigner lorigine" SUITE_CREATION_ARETE = "Cration arte : cliquez pour dsigner lextremit" DEFINITION_ETIQUETTE = "Dfinition dtiquette : dsignez le sommet" R = 3 COULEUR_SOMMET = "black" sommetEnDeplacement = None attributs = { } attenantes extremites = { } # rayon des sommets # couleur des sommets # sommet en cours dtre dplac # deux dictionnaires: # associe chaque sommet les artes # (avec un premier lment expliqu plus tard) # associe chaque arte ses deux extremits

application = Tk() application.title("Edigraph 8") etatCourant = StringVar(application) etatCourant.set(CREATION_SOMMET) def sommetVoisin(x, y): # on recherche un objet dessin dans le canevas prs de (x, y) pres = 10 objets = canevas.find_enclosed(x - pres, y - pres, x + pres, y + pres) if len(objets) > 0: # sil y en a... return objets[0] # ...on rend le premier else: return None def creationSommet(x, y): sommet = canevas.create_oval(x - R, y - R, x + R, y + R, fill = COULEUR_SOMMET, width = 0) attributs[sommet] = [ "garde-place" ] def creationArete(orig, extr): x0, y0 = centre(orig)

x1, y1 = centre(extr) arete = canevas.create_line(x0, y0, x1, y1) extremites[arete] = (orig, extr) attributs[orig].append(arete) attributs[extr].append(arete) def cocheCaseCreationSommet(): barreEtat.config(text=CREATION_SOMMET) def cocheCaseCreationArete(): barreEtat.config(text=DEBUT_CREATION_ARETE) def cocheCaseDefEtiquette(): barreEtat.config(text=DEFINITION_ETIQUETTE) def centre(sommet): a, b, c, d = canevas.coords(sommet) return ((a + c) / 2, (b + d) / 2) def pressionBoutonGauche(event): global sommetSelectionne if etatCourant.get() == CREATION_SOMMET: creationSommet(event.x, event.y) elif etatCourant.get() == DEBUT_CREATION_ARETE: s = sommetVoisin(event.x, event.y) if s != None: sommetSelectionne = s etatCourant.set(SUITE_CREATION_ARETE) barreEtat.config(text=SUITE_CREATION_ARETE) elif etatCourant.get() == SUITE_CREATION_ARETE: s = sommetVoisin(event.x, event.y) if s != None: creationArete(sommetSelectionne, s) etatCourant.set(DEBUT_CREATION_ARETE) barreEtat.config(text=DEBUT_CREATION_ARETE) else: # etatCourant.get() == DEFINITION_ETIQUETTE print "Dfinition tiquette pas encore en place" def replacer(arete): sommet0, sommet1 = extremites[arete] x0, y0 = centre(sommet0) x1, y1 = centre(sommet1) canevas.coords(arete, x0, y0, x1, y1) def deplacementSommet(x, y): canevas.coords(sommetEnDeplacement, x - R, y - R, x + R, y + R) liste = attributs[sommetEnDeplacement] for arete in liste[1:]: replacer(arete) def pressionBoutonDroit(event): global sommetEnDeplacement sommetEnDeplacement = sommetVoisin(event.x, event.y) def mouvementBoutonDroit(event): if sommetEnDeplacement != None: deplacementSommet(event.x, event.y) def relachementBoutonDroit(event): global sommetEnDeplacement sommetEnDeplacement = None

barreEtat = Label(application, text=CREATION_SOMMET, bd=1, relief=SUNKEN, anchor=W) barreEtat.pack(side=BOTTOM, fill=X) canevas = Canvas(application, bg="#C8FFFF", width=400, height=400) canevas.pack(side=RIGHT, fill=BOTH, expand=True, padx=2, pady=2) canevas.bind("<Button-1>", pressionBoutonGauche) canevas.bind("<Button-3>", pressionBoutonDroit) canevas.bind("<Motion>", mouvementBoutonDroit) canevas.bind("<ButtonRelease-3>", relachementBoutonDroit) panneauSup = Frame(application, width=150, height = 200, padx=4, pady=4, bd=2, relief = RIDGE) panneauSup.pack(side=TOP) lab = Label(panneauSup, text="Bouton gauche :") lab.grid(row=0, sticky=W) rb = Radiobutton(panneauSup, text="Cration sommet", command=cocheCaseCreationSommet, variable=etatCourant, value=CREATION_SOMMET) rb.grid(row=1, sticky=W) rb = Radiobutton(panneauSup, text="Cration arte", command=cocheCaseCreationArete, variable=etatCourant, value=DEBUT_CREATION_ARETE) rb.grid(row=2, sticky=W) rb = Radiobutton(panneauSup, text="Dfinition tiquette", command=cocheCaseDefEtiquette, variable=etatCourant, value=DEFINITION_ETIQUETTE) rb.grid(row=3, sticky=W) panneauInf = Frame(application, bg="red") panneauInf.pack(side=BOTTOM, fill=BOTH, expand=True) application.mainloop()

Explications. Dans ce programme on lie les sommets aux artes et rciproquement. Cela se fait par deux tables associatives (ou dictionnaires) :

une table associative, nomme extremites, dont les cls sont les artes et les valeurs des couples de sommets ; tant donne une arte, cette table permet de retrouver ses deux extrmits, une autre table associative, nomme attributs, dont les cls sont les sommets et les valeurs des listes dartes : tant donn un sommet, elle permet de retrouver toutes les artes dont ce sommet est une extrmit. On profitera plus loin de cette table pour y ranger aussi ltiquette de chaque sommet, elle sera le premier lment de la liste ; cest pourquoi nous mettons dores et dj une chane conventionnelle ("garde-place") en tte de chacune de ces listes.

Les modifications faites dans les fonctions creationSommet et creationArete sont videntes. Lors de la cration dun sommet on initialise sa liste dattributs avec une liste rduite la valeur conventionnelle ; lors de la cration dune arte, on doit lajouter aux listes dattributs de ses deux extrmits. Enfin, la fonction deplacementSommet doit tre modifie de sorte quun tel dplacement produise galement la modification de toutes les artes qui sappuient sur le sommet dplac.

tape 9

But : introduire deux petits gadgets : les compteurs de sommets et dartes

from Tkinter import * CREATION_SOMMET = "Cration sommet : cliquez pour dsigner lemplacement" DEBUT_CREATION_ARETE = "Cration arte : cliquez pour dsigner lorigine" SUITE_CREATION_ARETE = "Cration arte : cliquez pour dsigner lextremit" DEFINITION_ETIQUETTE = "Dfinition dtiquette : dsignez le sommet" R = 3 COULEUR_SOMMET = "black" sommetEnDeplacement = None attributs = { } attenantes extremites = { } nombreSommets = 0 nombreAretes = 0 # rayon des sommets # couleur des sommets # sommet en cours dtre dplac # deux dictionnaires: # associe chaque sommet les artes # (avec un premier lment expliqu plus tard) # associe chaque arte ses deux extremits # nombre de sommets crs # nombre dartes cres

application = Tk() application.title("Edigraph 9") etatCourant = StringVar(application) etatCourant.set(CREATION_SOMMET) def sommetVoisin(x, y): # on recherche un objet dessin dans le canevas prs de (x, y) pres = 10 objets = canevas.find_enclosed(x - pres, y - pres, x + pres, y + pres) if len(objets) > 0: # sil y en a... return objets[0] # ...on rend le premier else: return None def rafraichirCompteurs(nSommets, nAretes): global nombreSommets, nombreAretes

nombreSommets = nSommets nombreAretes = nAretes etiqNbrSommets.config(text = str(nombreSommets) + " sommets") etiqNbrAretes.config(text = str(nombreAretes) + " artes") def creationSommet(x, y): sommet = canevas.create_oval(x - R, y - R, x + R, y + R, fill = COULEUR_SOMMET, width = 0) attributs[sommet] = [ "garde-place" ] rafraichirCompteurs(nombreSommets + 1, nombreAretes) def creationArete(orig, extr): x0, y0 = centre(orig) x1, y1 = centre(extr) arete = canevas.create_line(x0, y0, x1, y1) extremites[arete] = (orig, extr) attributs[orig].append(arete) attributs[extr].append(arete) rafraichirCompteurs(nombreSommets, nombreAretes + 1) def cocheCaseCreationSommet(): barreEtat.config(text=CREATION_SOMMET) def cocheCaseCreationArete(): barreEtat.config(text=DEBUT_CREATION_ARETE) def cocheCaseDefEtiquette(): barreEtat.config(text=DEFINITION_ETIQUETTE) def centre(sommet): a, b, c, d = canevas.coords(sommet) return ((a + c) / 2, (b + d) / 2) def pressionBoutonGauche(event): global sommetSelectionne if etatCourant.get() == CREATION_SOMMET: creationSommet(event.x, event.y) elif etatCourant.get() == DEBUT_CREATION_ARETE: s = sommetVoisin(event.x, event.y) if s != None: sommetSelectionne = s etatCourant.set(SUITE_CREATION_ARETE) barreEtat.config(text=SUITE_CREATION_ARETE) elif etatCourant.get() == SUITE_CREATION_ARETE: s = sommetVoisin(event.x, event.y) if s != None: creationArete(sommetSelectionne, s) etatCourant.set(DEBUT_CREATION_ARETE) barreEtat.config(text=DEBUT_CREATION_ARETE) else: # etatCourant.get() == DEFINITION_ETIQUETTE print "Dfinition tiquette pas encore en place" def replacer(arete): sommet0, sommet1 = extremites[arete] x0, y0 = centre(sommet0) x1, y1 = centre(sommet1) canevas.coords(arete, x0, y0, x1, y1)

def deplacementSommet(x, y): canevas.coords(sommetEnDeplacement, x - R, y - R, x + R, y + R) liste = attributs[sommetEnDeplacement] for arete in liste[1:]: replacer(arete) def pressionBoutonDroit(event): global sommetEnDeplacement sommetEnDeplacement = sommetVoisin(event.x, event.y) def mouvementBoutonDroit(event): if sommetEnDeplacement != None: deplacementSommet(event.x, event.y) def relachementBoutonDroit(event): global sommetEnDeplacement sommetEnDeplacement = None barreEtat = Label(application, text=CREATION_SOMMET, bd=1, relief=SUNKEN, anchor=W) barreEtat.pack(side=BOTTOM, fill=X) canevas = Canvas(application, bg="#C8FFFF", width=400, height=400) canevas.pack(side=RIGHT, fill=BOTH, expand=True, padx=2, pady=2) canevas.bind("<Button-1>", pressionBoutonGauche) canevas.bind("<Button-3>", pressionBoutonDroit) canevas.bind("<Motion>", mouvementBoutonDroit) canevas.bind("<ButtonRelease-3>", relachementBoutonDroit) panneauSup = Frame(application, width=150, height = 200, padx=4, pady=4, bd=2, relief = RIDGE) panneauSup.pack(side=TOP) lab = Label(panneauSup, text="Bouton gauche :") lab.grid(row=0, sticky=W) rb = Radiobutton(panneauSup, text="Cration sommet", command=cocheCaseCreationSommet, variable=etatCourant, value=CREATION_SOMMET) rb.grid(row=1, sticky=W) rb = Radiobutton(panneauSup, text="Cration arte", command=cocheCaseCreationArete, variable=etatCourant, value=DEBUT_CREATION_ARETE) rb.grid(row=2, sticky=W) rb = Radiobutton(panneauSup, text="Dfinition tiquette", command=cocheCaseDefEtiquette, variable=etatCourant, value=DEFINITION_ETIQUETTE) rb.grid(row=3, sticky=W) panneauInf = Frame(application, bg="red") panneauInf.pack(side=BOTTOM, fill=BOTH, expand=True) etiqNbrAretes = Label(panneauInf) etiqNbrAretes.pack(side=BOTTOM, anchor=W) etiqNbrSommets = Label(panneauInf) etiqNbrSommets.pack(side=BOTTOM, anchor=W) rafraichirCompteurs(0, 0) application.mainloop()

Explications. Voyez les cinq lignes la fin du programme, juste avant lappel de mainloop : on cre deux nouveaux widgets de type Label, placs dans le panneau infrieur gauche ( panneauInf est leur parent),colls contre le ct du bas (side=BOTTOM) et ancrs gauche (anchor=W).

Ces labels exhibent des textes produits par la fonction rafraichirCompteurs qui dune part met jour les valeurs des variables globales nombreSommets et nombreAretes et dautre part produit laffichage, par des expressions comme etiqNbrSommets.config(text=str(nombreSommets)+" sommets").

tape 10
But : ajouter un bouton pour effacer tous les sommets et toutes les artes. Avant cela, afficher une bote de confirmation pour sassurer que lutilisateur souhaite bien tout supprimer.

from Tkinter import * import tkMessageBox CREATION_SOMMET = "Cration sommet : cliquez pour dsigner lemplacement" DEBUT_CREATION_ARETE = "Cration arte : cliquez pour dsigner lorigine" SUITE_CREATION_ARETE = "Cration arte : cliquez pour dsigner lextremit" DEFINITION_ETIQUETTE = "Dfinition dtiquette : dsignez le sommet" R = 3 COULEUR_SOMMET = "black" sommetEnDeplacement = None attributs = { } attenantes extremites = { } nombreSommets = 0 nombreAretes = 0 # rayon des sommets # couleur des sommets # sommet en cours dtre dplac # deux dictionnaires: # associe chaque sommet les artes # (avec un premier lment expliqu plus tard) # associe chaque arte ses deux extremits # nombre de sommets crs # nombre dartes cres

application = Tk() application.title("Edigraph 10") etatCourant = StringVar(application) etatCourant.set(CREATION_SOMMET)

def sommetVoisin(x, y): # on recherche un objet dessin dans le canevas prs de (x, y) pres = 10 objets = canevas.find_enclosed(x - pres, y - pres, x + pres, y + pres) if len(objets) > 0: # sil y en a... return objets[0] # ...on rend le premier else: return None def rafraichirCompteurs(nSommets, nAretes): global nombreSommets, nombreAretes nombreSommets = nSommets nombreAretes = nAretes etiqNbrSommets.config(text = str(nombreSommets) + " sommets") etiqNbrAretes.config(text = str(nombreAretes) + " artes") def creationSommet(x, y): sommet = canevas.create_oval(x - R, y - R, x + R, y + R, fill = COULEUR_SOMMET, width = 0) attributs[sommet] = [ "garde-place" ] rafraichirCompteurs(nombreSommets + 1, nombreAretes) def creationArete(orig, extr): x0, y0 = centre(orig) x1, y1 = centre(extr) arete = canevas.create_line(x0, y0, x1, y1) extremites[arete] = (orig, extr) attributs[orig].append(arete) attributs[extr].append(arete) rafraichirCompteurs(nombreSommets, nombreAretes + 1) def cocheCaseCreationSommet(): barreEtat.config(text=CREATION_SOMMET) def cocheCaseCreationArete(): barreEtat.config(text=DEBUT_CREATION_ARETE) def cocheCaseDefEtiquette(): barreEtat.config(text=DEFINITION_ETIQUETTE) def centre(sommet): a, b, c, d = canevas.coords(sommet) return ((a + c) / 2, (b + d) / 2) def pressionBoutonGauche(event): global sommetSelectionne if etatCourant.get() == CREATION_SOMMET: creationSommet(event.x, event.y) elif etatCourant.get() == DEBUT_CREATION_ARETE: s = sommetVoisin(event.x, event.y) if s != None: sommetSelectionne = s etatCourant.set(SUITE_CREATION_ARETE) barreEtat.config(text=SUITE_CREATION_ARETE) elif etatCourant.get() == SUITE_CREATION_ARETE: s = sommetVoisin(event.x, event.y) if s != None: creationArete(sommetSelectionne, s)

etatCourant.set(DEBUT_CREATION_ARETE) barreEtat.config(text=DEBUT_CREATION_ARETE) else: # etatCourant.get() == DEFINITION_ETIQUETTE print "Dfinition tiquette pas encore en place" def replacer(arete): sommet0, sommet1 = extremites[arete] x0, y0 = centre(sommet0) x1, y1 = centre(sommet1) canevas.coords(arete, x0, y0, x1, y1) def deplacementSommet(x, y): canevas.coords(sommetEnDeplacement, x - R, y - R, x + R, y + R) liste = attributs[sommetEnDeplacement] for arete in liste[1:]: replacer(arete) def pressionBoutonDroit(event): global sommetEnDeplacement sommetEnDeplacement = sommetVoisin(event.x, event.y) def mouvementBoutonDroit(event): if sommetEnDeplacement != None: deplacementSommet(event.x, event.y) def relachementBoutonDroit(event): global sommetEnDeplacement sommetEnDeplacement = None def toutEffacer(): canevas.addtag_all("a_effacer") canevas.delete("a_effacer") rafraichirCompteurs(0, 0) def pressionBoutonEffacer(): if tkMessageBox.askyesno("Attention", "Vous voulez vraiment tout dtruire?"): toutEffacer() barreEtat = Label(application, text=CREATION_SOMMET, bd=1, relief=SUNKEN, anchor=W) barreEtat.pack(side=BOTTOM, fill=X) canevas = Canvas(application, bg="#C8FFFF", width=400, height=400) canevas.pack(side=RIGHT, fill=BOTH, expand=True, padx=2, pady=2) canevas.bind("<Button-1>", pressionBoutonGauche) canevas.bind("<Button-3>", pressionBoutonDroit) canevas.bind("<Motion>", mouvementBoutonDroit) canevas.bind("<ButtonRelease-3>", relachementBoutonDroit) panneauSup = Frame(application, width=150, height = 200, padx=4, pady=4, bd=2, relief = RIDGE) panneauSup.pack(side=TOP) lab = Label(panneauSup, text="Bouton gauche :") lab.grid(row=0, sticky=W) rb = Radiobutton(panneauSup, text="Cration sommet", command=cocheCaseCreationSommet, variable=etatCourant, value=CREATION_SOMMET) rb.grid(row=1, sticky=W) rb = Radiobutton(panneauSup, text="Cration arte", command=cocheCaseCreationArete, variable=etatCourant, value=DEBUT_CREATION_ARETE)

rb.grid(row=2, sticky=W) rb = Radiobutton(panneauSup, text="Dfinition tiquette", command=cocheCaseDefEtiquette, variable=etatCourant, value=DEFINITION_ETIQUETTE) rb.grid(row=3, sticky=W) bouton = Button(panneauSup, command=pressionBoutonEffacer) bouton.grid(row=4) text="Tout effacer",

panneauInf = Frame(application, bg="red") panneauInf.pack(side=BOTTOM, fill=BOTH, expand=True) etiqNbrAretes = Label(panneauInf) etiqNbrAretes.pack(side=BOTTOM, anchor=W) etiqNbrSommets = Label(panneauInf) etiqNbrSommets.pack(side=BOTTOM, anchor=W) rafraichirCompteurs(0, 0) application.mainloop()

Explications. On constate lajout dun bouton simple (widget Button) aux trois boutons radio qui occupent dj le panneau suprieur gauche. La fonction associe, pressionBoutonEffacer, provoque laffichage dune boite Oui-Non par la fonction askyesno du module tkMessageBox. Cette fonction boolenne renvoie True si et seulement si lutilisateur a quitt la bote Oui-Non en appuyant sur le bouton Oui. Il faut alors effacer les constructions graphiques faites dans le canevas. Pour cela il ny a quune mthode, canevas.delete, dont on dcouvre cette occasion quelle nefface que les objets pralablement marqus dune certaine marque, ou tag. Cela explique les deux appels successifs : canevas.addtag_all("a_effacer"), pour mettre la marque "a_effacer" sur tous les objets graphiques du canevas, puis canevas.delete("a_effacer") pour supprimer effectivement tous les objets portant la marque "a_effacer".

tape 11
But : associer chaque sommet une tiquette (qui, pour commencer, sera banale et fige lors de la cration du sommet).

from Tkinter import * import tkMessageBox CREATION_SOMMET = "Cration sommet : cliquez pour dsigner lemplacement" DEBUT_CREATION_ARETE = "Cration arte : cliquez pour dsigner lorigine" SUITE_CREATION_ARETE = "Cration arte : cliquez pour dsigner lextremit" DEFINITION_ETIQUETTE = "Dfinition dtiquette : dsignez le sommet" R = 3 COULEUR_SOMMET = "black" sommetEnDeplacement = None attributs = { } attenantes extremites = { } nombreSommets = 0 nombreAretes = 0 # rayon des sommets # couleur des sommets # sommet en cours dtre dplac # deux dictionnaires: # associe chaque sommet les artes # (avec un premier lment expliqu plus tard) # associe chaque arte ses deux extremits # nombre de sommets crs # nombre dartes cres

application = Tk() application.title("Edigraph 11") etatCourant = StringVar(application) etatCourant.set(CREATION_SOMMET) def sommetVoisin(x, y): # on recherche un objet dessin dans le canevas prs de (x, y) pres = 10 objets = canevas.find_enclosed(x - pres, y - pres, x + pres, y + pres) if len(objets) > 0: # sil y en a... return objets[0] # ...on rend le premier else: return None def rafraichirCompteurs(nSommets, nAretes): global nombreSommets, nombreAretes

nombreSommets = nSommets nombreAretes = nAretes etiqNbrSommets.config(text = str(nombreSommets) + " sommets") etiqNbrAretes.config(text = str(nombreAretes) + " artes") def creationSommet(x, y): sommet = canevas.create_oval(x - R, y - R, x + R, y + R, fill = COULEUR_SOMMET, width = 0) s = str(nombreSommets) etiquette = canevas.create_text(x + 2 * R, y - R, text = s, anchor=W) attributs[sommet] = [ etiquette ] # au lieu de "garde-place" rafraichirCompteurs(nombreSommets + 1, nombreAretes) def creationArete(orig, extr): x0, y0 = centre(orig) x1, y1 = centre(extr) arete = canevas.create_line(x0, y0, x1, y1) extremites[arete] = (orig, extr) attributs[orig].append(arete) attributs[extr].append(arete) rafraichirCompteurs(nombreSommets, nombreAretes + 1) def cocheCaseCreationSommet(): barreEtat.config(text=CREATION_SOMMET) def cocheCaseCreationArete(): barreEtat.config(text=DEBUT_CREATION_ARETE) def cocheCaseDefEtiquette(): barreEtat.config(text=DEFINITION_ETIQUETTE) def centre(sommet): a, b, c, d = canevas.coords(sommet) return ((a + c) / 2, (b + d) / 2) def pressionBoutonGauche(event): global sommetSelectionne if etatCourant.get() == CREATION_SOMMET: creationSommet(event.x, event.y) elif etatCourant.get() == DEBUT_CREATION_ARETE: s = sommetVoisin(event.x, event.y) if s != None: sommetSelectionne = s etatCourant.set(SUITE_CREATION_ARETE) barreEtat.config(text=SUITE_CREATION_ARETE) elif etatCourant.get() == SUITE_CREATION_ARETE: s = sommetVoisin(event.x, event.y) if s != None: creationArete(sommetSelectionne, s) etatCourant.set(DEBUT_CREATION_ARETE) barreEtat.config(text=DEBUT_CREATION_ARETE) else: # etatCourant.get() == DEFINITION_ETIQUETTE print "Dfinition tiquette pas encore en place" def replacer(arete): sommet0, sommet1 = extremites[arete] x0, y0 = centre(sommet0) x1, y1 = centre(sommet1) canevas.coords(arete, x0, y0, x1, y1)

def deplacementSommet(x, y): canevas.coords(sommetEnDeplacement, x - R, y - R, x + R, y + R) liste = attributs[sommetEnDeplacement] canevas.coords(liste[0], x + 2 * R, y - R) for arete in liste[1:]: replacer(arete) def pressionBoutonDroit(event): global sommetEnDeplacement sommetEnDeplacement = sommetVoisin(event.x, event.y) def mouvementBoutonDroit(event): if sommetEnDeplacement != None: deplacementSommet(event.x, event.y) def relachementBoutonDroit(event): global sommetEnDeplacement sommetEnDeplacement = None def toutEffacer(): canevas.addtag_all("a_effacer") canevas.delete("a_effacer") rafraichirCompteurs(0, 0) def pressionBoutonEffacer(): if tkMessageBox.askyesno("Attention", "Vous voulez vraiment tout dtruire?"): toutEffacer() barreEtat = Label(application, text=CREATION_SOMMET, bd=1, relief=SUNKEN, anchor=W) barreEtat.pack(side=BOTTOM, fill=X) canevas = Canvas(application, bg="#C8FFFF", width=400, height=400) canevas.pack(side=RIGHT, fill=BOTH, expand=True, padx=2, pady=2) canevas.bind("<Button-1>", pressionBoutonGauche) canevas.bind("<Button-3>", pressionBoutonDroit) canevas.bind("<Motion>", mouvementBoutonDroit) canevas.bind("<ButtonRelease-3>", relachementBoutonDroit) panneauSup = Frame(application, width=150, height = 200, padx=4, pady=4, bd=2, relief = RIDGE) panneauSup.pack(side=TOP) lab = Label(panneauSup, text="Bouton gauche :") lab.grid(row=0, sticky=W) rb = Radiobutton(panneauSup, text="Cration sommet", command=cocheCaseCreationSommet, variable=etatCourant, value=CREATION_SOMMET) rb.grid(row=1, sticky=W) rb = Radiobutton(panneauSup, text="Cration arte", command=cocheCaseCreationArete, variable=etatCourant, value=DEBUT_CREATION_ARETE) rb.grid(row=2, sticky=W) rb = Radiobutton(panneauSup, text="Dfinition tiquette", command=cocheCaseDefEtiquette, variable=etatCourant, value=DEFINITION_ETIQUETTE) rb.grid(row=3, sticky=W) bouton = Button(panneauSup, command=pressionBoutonEffacer) bouton.grid(row=4) text="Tout effacer",

panneauInf = Frame(application, bg="red") panneauInf.pack(side=BOTTOM, fill=BOTH, expand=True) etiqNbrAretes = Label(panneauInf) etiqNbrAretes.pack(side=BOTTOM, anchor=W) etiqNbrSommets = Label(panneauInf) etiqNbrSommets.pack(side=BOTTOM, anchor=W) rafraichirCompteurs(0, 0) application.mainloop()

Explications. Pour avoir des sommets tiquets il faut assurer deux services : dune part, la cration dun sommet sur le canevas doit provoquer laffichage dun texte dans le voisinage du sommet ; dautre part, tout dplacement ultrieur du sommet doit traner le texte pour le garder dans la mme position relative par rapport au sommet. Le texte (pour commencer, simplement le numro du sommet) sera cr par un appel de la mthode create_text du canevas. Afin de pouvoir ensuite le retrouver partir du sommet, le texte sera mmoris dans la liste des attributs (dont on avait gard libre la premire place, cet effet). Ainsi, dans la fonction deplacementSommet, les deux lignes liste = attributs[sommetEnDeplacement] et canevas.coords(liste[0], x + 2 * R, y - R) produisent le dplacement de ltiquette lors de chaque dplacement du sommet.

tape 12
But : pouvoir donner aux sommets, pralablement crs, des tiquettes de son choix. Lutilisateur coche le troisime bouton radio (Dfinition tiquette) puis slectionne un sommet avec la souris. Une bote de dialogue lui permet alors dindiquer la nouvelle tiquette. Pour compliquer les choses on dcide, bien que ce ne soit pas trs justifi, que ltiquette dun sommet ne peut tre dfinie plus dune fois : lorsque ltiquette nest plus un nombre, on ne peut plus la changer.

from Tkinter import *

import tkMessageBox import tkSimpleDialog CREATION_SOMMET = "Cration sommet : cliquez pour dsigner lemplacement" DEBUT_CREATION_ARETE = "Cration arte : cliquez pour dsigner lorigine" SUITE_CREATION_ARETE = "Cration arte : cliquez pour dsigner lextremit" DEFINITION_ETIQUETTE = "Dfinition dtiquette : dsignez le sommet" R = 3 COULEUR_SOMMET = "black" sommetEnDeplacement = None attributs = { } attenantes extremites = { } nombreSommets = 0 nombreAretes = 0 # rayon des sommets # couleur des sommets # sommet en cours dtre dplac # deux dictionnaires: # associe chaque sommet les artes # (avec un premier lment expliqu plus tard) # associe chaque arte ses deux extremits # nombre de sommets crs # nombre dartes cres

application = Tk() application.title("Edigraph 12") etatCourant = StringVar(application) etatCourant.set(CREATION_SOMMET) def sommetVoisin(x, y): # on recherche un objet dessin dans le canevas prs de (x, y) pres = 10 objets = canevas.find_enclosed(x - pres, y - pres, x + pres, y + pres) if len(objets) > 0: # sil y en a... return objets[0] # ...on rend le premier else: return None def rafraichirCompteurs(nSommets, nAretes): global nombreSommets, nombreAretes nombreSommets = nSommets nombreAretes = nAretes etiqNbrSommets.config(text = str(nombreSommets) + " sommets") etiqNbrAretes.config(text = str(nombreAretes) + " artes") def creationSommet(x, y): sommet = canevas.create_oval(x - R, y - R, x + R, y + R, fill = COULEUR_SOMMET, width = 0) s = str(nombreSommets) etiquette = canevas.create_text(x + 2 * R, y - R, text = s, anchor=W) attributs[sommet] = [ etiquette ] # au lieu de "garde-place" rafraichirCompteurs(nombreSommets + 1, nombreAretes) def creationArete(orig, extr): x0, y0 = centre(orig) x1, y1 = centre(extr) arete = canevas.create_line(x0, y0, x1, y1) extremites[arete] = (orig, extr) attributs[orig].append(arete) attributs[extr].append(arete) rafraichirCompteurs(nombreSommets, nombreAretes + 1)

def cocheCaseCreationSommet(): barreEtat.config(text=CREATION_SOMMET) def cocheCaseCreationArete(): barreEtat.config(text=DEBUT_CREATION_ARETE) def cocheCaseDefEtiquette(): barreEtat.config(text=DEFINITION_ETIQUETTE) def centre(sommet): a, b, c, d = canevas.coords(sommet) return ((a + c) / 2, (b + d) / 2) def definitionEtiquette(sommet): etiquette = attributs[sommet][0] texte = canevas.itemcget(etiquette, "text") if not texte.isdigit(): tkMessageBox.showwarning("Dfinition tiquette", "Ltiquette de ce sommet a dj t dfinie") else: texte = tkSimpleDialog.askstring("Etiquette", "Donner la nouvelle valeur :", initialvalue = texte) if texte != None: canevas.itemconfigure(etiquette, text = texte) def pressionBoutonGauche(event): global sommetSelectionne if etatCourant.get() == CREATION_SOMMET: creationSommet(event.x, event.y) elif etatCourant.get() == DEBUT_CREATION_ARETE: s = sommetVoisin(event.x, event.y) if s != None: sommetSelectionne = s etatCourant.set(SUITE_CREATION_ARETE) barreEtat.config(text=SUITE_CREATION_ARETE) elif etatCourant.get() == SUITE_CREATION_ARETE: s = sommetVoisin(event.x, event.y) if s != None: creationArete(sommetSelectionne, s) etatCourant.set(DEBUT_CREATION_ARETE) barreEtat.config(text=DEBUT_CREATION_ARETE) else: # etatCourant.get() == DEFINITION_ETIQUETTE s = sommetVoisin(event.x, event.y) if s != None: definitionEtiquette(s) def replacer(arete): sommet0, sommet1 = extremites[arete] x0, y0 = centre(sommet0) x1, y1 = centre(sommet1) canevas.coords(arete, x0, y0, x1, y1) def deplacementSommet(x, y): canevas.coords(sommetEnDeplacement, x - R, y - R, x + R, y + R) liste = attributs[sommetEnDeplacement] canevas.coords(liste[0], x + 2 * R, y - R) for arete in liste[1:]: replacer(arete) def pressionBoutonDroit(event): global sommetEnDeplacement

sommetEnDeplacement = sommetVoisin(event.x, event.y) def mouvementBoutonDroit(event): if sommetEnDeplacement != None: deplacementSommet(event.x, event.y) def relachementBoutonDroit(event): global sommetEnDeplacement sommetEnDeplacement = None def toutEffacer(): canevas.addtag_all("a_effacer") canevas.delete("a_effacer") rafraichirCompteurs(0, 0) def pressionBoutonEffacer(): if tkMessageBox.askyesno("Attention", "Vous voulez vraiment tout dtruire?"): toutEffacer() barreEtat = Label(application, text=CREATION_SOMMET, bd=1, relief=SUNKEN, anchor=W) barreEtat.pack(side=BOTTOM, fill=X) canevas = Canvas(application, bg="#C8FFFF", width=400, height=400) canevas.pack(side=RIGHT, fill=BOTH, expand=True, padx=2, pady=2) canevas.bind("<Button-1>", pressionBoutonGauche) canevas.bind("<Button-3>", pressionBoutonDroit) canevas.bind("<Motion>", mouvementBoutonDroit) canevas.bind("<ButtonRelease-3>", relachementBoutonDroit) panneauSup = Frame(application, width=150, height = 200, padx=4, pady=4, bd=2, relief = RIDGE) panneauSup.pack(side=TOP) lab = Label(panneauSup, text="Bouton gauche :") lab.grid(row=0, sticky=W) rb = Radiobutton(panneauSup, text="Cration sommet", command=cocheCaseCreationSommet, variable=etatCourant, value=CREATION_SOMMET) rb.grid(row=1, sticky=W) rb = Radiobutton(panneauSup, text="Cration arte", command=cocheCaseCreationArete, variable=etatCourant, value=DEBUT_CREATION_ARETE) rb.grid(row=2, sticky=W) rb = Radiobutton(panneauSup, text="Dfinition tiquette", command=cocheCaseDefEtiquette, variable=etatCourant, value=DEFINITION_ETIQUETTE) rb.grid(row=3, sticky=W) bouton = Button(panneauSup, command=pressionBoutonEffacer) bouton.grid(row=4) text="Tout effacer",

panneauInf = Frame(application, bg="red") panneauInf.pack(side=BOTTOM, fill=BOTH, expand=True) etiqNbrAretes = Label(panneauInf) etiqNbrAretes.pack(side=BOTTOM, anchor=W) etiqNbrSommets = Label(panneauInf) etiqNbrSommets.pack(side=BOTTOM, anchor=W) rafraichirCompteurs(0, 0)

application.mainloop()

Explications. Les quelques ajouts faits au programme se passent dexplications. Notez la mthode itemcget qui permet dobtenir la valeur dune proprit dun objet graphique. Dans lexpression texte = canevas.itemcget(etiquette, "text"), lobjet graphique est etiquette (un objet graphique text), la proprit est "text" et sa valeur est le texte affich, recueilli dans la variable texte. Cela nous permet dune part de vrifier que la valeur courante nest pas dj une vraie tiquette (cest--dire autre chose quun nombre) et dautre part de lafficher comme valeur initiale dans la bote de dialogue. Au retour de la bote de dialogue, si elle a russi, la chane obtenue est donne pour nouvelle valeur de la proprit text de lobjet etiquette en question.

tape 13
But : introduire un widget Listbox pour afficher les tiquettes expressment dfinies.

from Tkinter import * import tkMessageBox import tkSimpleDialog CREATION_SOMMET = "Cration sommet : cliquez pour dsigner lemplacement" DEBUT_CREATION_ARETE = "Cration arte : cliquez pour dsigner lorigine" SUITE_CREATION_ARETE = "Cration arte : cliquez pour dsigner lextremit" DEFINITION_ETIQUETTE = "Dfinition dtiquette : dsignez le sommet" R = 3 COULEUR_SOMMET = "black" sommetEnDeplacement = None attributs = { } attenantes # rayon des sommets # couleur des sommets # sommet en cours dtre dplac # deux dictionnaires: # associe chaque sommet les artes

extremites = { } nombreSommets = 0 nombreAretes = 0

# (avec un premier lment expliqu plus tard) # associe chaque arte ses deux extremits # nombre de sommets crs # nombre dartes cres

application = Tk() application.title("Edigraph 13") etatCourant = StringVar(application) etatCourant.set(CREATION_SOMMET) def sommetVoisin(x, y): # on recherche un objet dessin dans le canevas prs de (x, y) pres = 10 objets = canevas.find_enclosed(x - pres, y - pres, x + pres, y + pres) if len(objets) > 0: # sil y en a... return objets[0] # ...on rend le premier else: return None def rafraichirCompteurs(nSommets, nAretes): global nombreSommets, nombreAretes nombreSommets = nSommets nombreAretes = nAretes etiqNbrSommets.config(text = str(nombreSommets) + " sommets") etiqNbrAretes.config(text = str(nombreAretes) + " artes") def creationSommet(x, y): sommet = canevas.create_oval(x - R, y - R, x + R, y + R, fill = COULEUR_SOMMET, width = 0) s = str(nombreSommets) etiquette = canevas.create_text(x + 2 * R, y - R, text = s, anchor=W) attributs[sommet] = [ etiquette ] # au lieu de "garde-place" rafraichirCompteurs(nombreSommets + 1, nombreAretes) def creationArete(orig, extr): x0, y0 = centre(orig) x1, y1 = centre(extr) arete = canevas.create_line(x0, y0, x1, y1) extremites[arete] = (orig, extr) attributs[orig].append(arete) attributs[extr].append(arete) rafraichirCompteurs(nombreSommets, nombreAretes + 1) def cocheCaseCreationSommet(): barreEtat.config(text=CREATION_SOMMET) def cocheCaseCreationArete(): barreEtat.config(text=DEBUT_CREATION_ARETE) def cocheCaseDefEtiquette(): barreEtat.config(text=DEFINITION_ETIQUETTE) def centre(sommet): a, b, c, d = canevas.coords(sommet) return ((a + c) / 2, (b + d) / 2) def definitionEtiquette(sommet): etiquette = attributs[sommet][0] texte = canevas.itemcget(etiquette, "text") if not texte.isdigit():

tkMessageBox.showwarning("Dfinition tiquette", "Ltiquette de ce sommet a dj t dfinie") else: texte = tkSimpleDialog.askstring("Etiquette", "Donner la nouvelle valeur :", initialvalue = texte) if texte != None: canevas.itemconfigure(etiquette, text = texte) listeEtiquettes.insert(END, texte) def pressionBoutonGauche(event): global sommetSelectionne if etatCourant.get() == CREATION_SOMMET: creationSommet(event.x, event.y) elif etatCourant.get() == DEBUT_CREATION_ARETE: s = sommetVoisin(event.x, event.y) if s != None: sommetSelectionne = s etatCourant.set(SUITE_CREATION_ARETE) barreEtat.config(text=SUITE_CREATION_ARETE) elif etatCourant.get() == SUITE_CREATION_ARETE: s = sommetVoisin(event.x, event.y) if s != None: creationArete(sommetSelectionne, s) etatCourant.set(DEBUT_CREATION_ARETE) barreEtat.config(text=DEBUT_CREATION_ARETE) else: # etatCourant.get() == DEFINITION_ETIQUETTE s = sommetVoisin(event.x, event.y) if s != None: definitionEtiquette(s) def replacer(arete): sommet0, sommet1 = extremites[arete] x0, y0 = centre(sommet0) x1, y1 = centre(sommet1) canevas.coords(arete, x0, y0, x1, y1) def deplacementSommet(x, y): canevas.coords(sommetEnDeplacement, x - R, y - R, x + R, y + R) liste = attributs[sommetEnDeplacement] canevas.coords(liste[0], x + 2 * R, y - R) for arete in liste[1:]: replacer(arete) def pressionBoutonDroit(event): global sommetEnDeplacement sommetEnDeplacement = sommetVoisin(event.x, event.y) def mouvementBoutonDroit(event): if sommetEnDeplacement != None: deplacementSommet(event.x, event.y) def relachementBoutonDroit(event): global sommetEnDeplacement sommetEnDeplacement = None def toutEffacer(): canevas.addtag_all("a_effacer") canevas.delete("a_effacer") rafraichirCompteurs(0, 0) def pressionBoutonEffacer(): if tkMessageBox.askyesno("Attention",

"Vous voulez vraiment tout dtruire?"): toutEffacer() barreEtat = Label(application, text=CREATION_SOMMET, bd=1, relief=SUNKEN, anchor=W) barreEtat.pack(side=BOTTOM, fill=X) canevas = Canvas(application, bg="#C8FFFF", width=400, height=400) canevas.pack(side=RIGHT, fill=BOTH, expand=True, padx=2, pady=2) canevas.bind("<Button-1>", pressionBoutonGauche) canevas.bind("<Button-3>", pressionBoutonDroit) canevas.bind("<Motion>", mouvementBoutonDroit) canevas.bind("<ButtonRelease-3>", relachementBoutonDroit) panneauSup = Frame(application, width=150, height = 200, padx=4, pady=4, bd=2, relief = RIDGE) panneauSup.pack(side=TOP) lab = Label(panneauSup, text="Bouton gauche :") lab.grid(row=0, sticky=W) rb = Radiobutton(panneauSup, text="Cration sommet", command=cocheCaseCreationSommet, variable=etatCourant, value=CREATION_SOMMET) rb.grid(row=1, sticky=W) rb = Radiobutton(panneauSup, text="Cration arte", command=cocheCaseCreationArete, variable=etatCourant, value=DEBUT_CREATION_ARETE) rb.grid(row=2, sticky=W) rb = Radiobutton(panneauSup, text="Dfinition tiquette", command=cocheCaseDefEtiquette, variable=etatCourant, value=DEFINITION_ETIQUETTE) rb.grid(row=3, sticky=W) bouton = Button(panneauSup, command=pressionBoutonEffacer) bouton.grid(row=4) text="Tout effacer",

panneauInf = Frame(application) panneauInf.pack(side=BOTTOM, fill=BOTH, expand=True) etiqNbrAretes = Label(panneauInf) etiqNbrAretes.pack(side=BOTTOM, anchor=W) etiqNbrSommets = Label(panneauInf) etiqNbrSommets.pack(side=BOTTOM, anchor=W) rafraichirCompteurs(0, 0) lab = Label(panneauInf, text="Etiquettes :") lab.pack(side=TOP, anchor=W) listeEtiquettes = Listbox(panneauInf) listeEtiquettes.pack(side=BOTTOM, fill=Y, expand=True) application.mainloop()

Explications. Il sagit de continuer peupler le panneau en bas gauche : il y avait dj deux compteurs (n sommets et m artes) on y ajoute un Label Etiquettes : en haut (side=TOP) et une Bote liste en bas (side=BOTTOM, mais elle sera au-dessus des deux compteurs, car ceux-ci ont t placs avant elle. La seule autre modification faire concerne la saisie dune tiquette : il faut penser lajouter dans la bote liste, en fin de liste (END).

tape 14
But : pouvoir utiliser la liste dtiquette pour mettre en vidence un ou plusieurs sommets.

from Tkinter import * import tkMessageBox import tkSimpleDialog CREATION_SOMMET = "Cration sommet : cliquez pour dsigner lemplacement" DEBUT_CREATION_ARETE = "Cration arte : cliquez pour dsigner lorigine" SUITE_CREATION_ARETE = "Cration arte : cliquez pour dsigner lextremit" DEFINITION_ETIQUETTE = "Dfinition dtiquette : dsignez le sommet" R = 3 COULEUR_SOMMET = "black" COULEUR_SOMMET2 = "red" sommetEnDeplacement = None attributs = { } attenantes extremites = { } sommetAssocie = { } correspondant nombreSommets = 0 nombreAretes = 0 # rayon des sommets # couleur des sommets # couleur des sommets mis en vidence # sommet en cours dtre dplac # trois dictionnaires: # associe chaque sommet les artes # (avec un premier lment expliqu plus tard) # associe chaque arte ses deux extremits # associe chaque tiquette le sommet # nombre de sommets crs # nombre dartes cres

application = Tk() application.title("Edigraph 14") etatCourant = StringVar(application) etatCourant.set(CREATION_SOMMET) def sommetVoisin(x, y):

# on recherche un objet dessin dans le canevas prs de (x, y) pres = 10 objets = canevas.find_enclosed(x - pres, y - pres, x + pres, y + pres) if len(objets) > 0: # sil y en a... return objets[0] # ...on rend le premier else: return None def rafraichirCompteurs(nSommets, nAretes): global nombreSommets, nombreAretes nombreSommets = nSommets nombreAretes = nAretes etiqNbrSommets.config(text = str(nombreSommets) + " sommets") etiqNbrAretes.config(text = str(nombreAretes) + " artes") def creationSommet(x, y): sommet = canevas.create_oval(x - R, y - R, x + R, y + R, fill = COULEUR_SOMMET, width = 0) s = str(nombreSommets) etiquette = canevas.create_text(x + 2 * R, y - R, text = s, anchor=W) attributs[sommet] = [ etiquette ] # au lieu de "garde-place" rafraichirCompteurs(nombreSommets + 1, nombreAretes) def creationArete(orig, extr): x0, y0 = centre(orig) x1, y1 = centre(extr) arete = canevas.create_line(x0, y0, x1, y1) extremites[arete] = (orig, extr) attributs[orig].append(arete) attributs[extr].append(arete) rafraichirCompteurs(nombreSommets, nombreAretes + 1) def cocheCaseCreationSommet(): barreEtat.config(text=CREATION_SOMMET) def cocheCaseCreationArete(): barreEtat.config(text=DEBUT_CREATION_ARETE) def cocheCaseDefEtiquette(): barreEtat.config(text=DEFINITION_ETIQUETTE) def centre(sommet): a, b, c, d = canevas.coords(sommet) return ((a + c) / 2, (b + d) / 2) def definitionEtiquette(sommet): etiquette = attributs[sommet][0] texte = canevas.itemcget(etiquette, "text") if not texte.isdigit(): tkMessageBox.showwarning("Dfinition tiquette", "Ltiquette de ce sommet a dj t dfinie") else: texte = tkSimpleDialog.askstring("Etiquette", "Donner la nouvelle valeur :", initialvalue = texte) if texte != None: canevas.itemconfigure(etiquette, text = texte) listeEtiquettes.insert(END, texte) sommetAssocie[texte] = sommet def pressionBoutonGauche(event): global sommetSelectionne

if etatCourant.get() == CREATION_SOMMET: creationSommet(event.x, event.y) elif etatCourant.get() == DEBUT_CREATION_ARETE: s = sommetVoisin(event.x, event.y) if s != None: sommetSelectionne = s etatCourant.set(SUITE_CREATION_ARETE) barreEtat.config(text=SUITE_CREATION_ARETE) elif etatCourant.get() == SUITE_CREATION_ARETE: s = sommetVoisin(event.x, event.y) if s != None: creationArete(sommetSelectionne, s) etatCourant.set(DEBUT_CREATION_ARETE) barreEtat.config(text=DEBUT_CREATION_ARETE) else: # etatCourant.get() == DEFINITION_ETIQUETTE s = sommetVoisin(event.x, event.y) if s != None: definitionEtiquette(s) def replacer(arete): sommet0, sommet1 = extremites[arete] x0, y0 = centre(sommet0) x1, y1 = centre(sommet1) canevas.coords(arete, x0, y0, x1, y1) def deplacementSommet(x, y): canevas.coords(sommetEnDeplacement, x - R, y - R, x + R, y + R) liste = attributs[sommetEnDeplacement] canevas.coords(liste[0], x + 2 * R, y - R) for arete in liste[1:]: replacer(arete) def pressionBoutonDroit(event): global sommetEnDeplacement sommetEnDeplacement = sommetVoisin(event.x, event.y) def mouvementBoutonDroit(event): if sommetEnDeplacement != None: deplacementSommet(event.x, event.y) def relachementBoutonDroit(event): global sommetEnDeplacement sommetEnDeplacement = None def toutEffacer(): canevas.addtag_all("a_effacer") canevas.delete("a_effacer") rafraichirCompteurs(0, 0) def pressionBoutonEffacer(): if tkMessageBox.askyesno("Attention", "Vous voulez vraiment tout dtruire?"): toutEffacer() def actionSurListe(evt): selections = listeEtiquettes.curselection() selection = selections[0] i = int(selection) texte = listeEtiquettes.get(i) sommet = sommetAssocie[texte] # # # # # ex.: ("3", "5", "9") ex.: "3" ex.: 3 le texte correspondant le sommet correspondant

x, y = centre(sommet) if canevas.itemcget(sommet, "fill") == COULEUR_SOMMET:

canevas.itemconfigure(sommet, fill=COULEUR_SOMMET2) else: canevas.itemconfigure(sommet, fill=COULEUR_SOMMET) barreEtat = Label(application, text=CREATION_SOMMET, bd=1, relief=SUNKEN, anchor=W) barreEtat.pack(side=BOTTOM, fill=X) canevas = Canvas(application, bg="#C8FFFF", width=400, height=400) canevas.pack(side=RIGHT, fill=BOTH, expand=True, padx=2, pady=2) canevas.bind("<Button-1>", pressionBoutonGauche) canevas.bind("<Button-3>", pressionBoutonDroit) canevas.bind("<Motion>", mouvementBoutonDroit) canevas.bind("<ButtonRelease-3>", relachementBoutonDroit) panneauSup = Frame(application, width=150, height = 200, padx=4, pady=4, bd=2, relief = RIDGE) panneauSup.pack(side=TOP) lab = Label(panneauSup, text="Bouton gauche :") lab.grid(row=0, sticky=W) rb = Radiobutton(panneauSup, text="Cration sommet", command=cocheCaseCreationSommet, variable=etatCourant, value=CREATION_SOMMET) rb.grid(row=1, sticky=W) rb = Radiobutton(panneauSup, text="Cration arte", command=cocheCaseCreationArete, variable=etatCourant, value=DEBUT_CREATION_ARETE) rb.grid(row=2, sticky=W) rb = Radiobutton(panneauSup, text="Dfinition tiquette", command=cocheCaseDefEtiquette, variable=etatCourant, value=DEFINITION_ETIQUETTE) rb.grid(row=3, sticky=W) bouton = Button(panneauSup, command=pressionBoutonEffacer) bouton.grid(row=4) text="Tout effacer",

panneauInf = Frame(application) panneauInf.pack(side=BOTTOM, fill=BOTH, expand=True) etiqNbrAretes = Label(panneauInf) etiqNbrAretes.pack(side=BOTTOM, anchor=W) etiqNbrSommets = Label(panneauInf) etiqNbrSommets.pack(side=BOTTOM, anchor=W) rafraichirCompteurs(0, 0) lab = Label(panneauInf, text="Etiquettes :") lab.pack(side=TOP, anchor=W) listeEtiquettes = Listbox(panneauInf) listeEtiquettes.pack(side=BOTTOM, fill=Y, expand=True) listeEtiquettes.bind("<Double-Button-1>", actionSurListe) application.mainloop()

Explications. Compte tenu de nos intentions, nous introduisons une nouvelle table associative, sommetAssocie, qui associe chaque tiquette de la bote liste le sommet correspondant. Bien entendu, cette table est augmente dun nouveau couple (tiquette, sommet) chaque fois quune tiquette est dfinie (fonction definitionEtiquette).

Dautre part nous indiquons tre intresss par les vnements <Double-Button-1> (cest--dire double-clic avec le premier bouton de la souris) se produisant sur la bote liste. Regardez la fonction actionSurListe correspondante : lexpression listeEtiquettes.curselection() obtient la liste des items slectionns (en thorie il pourrait y en avoir plusieurs ; en pratique il ny en aura quun). Bizarrement, cette liste est fate de chanes exprimant des nombres ; nous prenons la premire (selection=selections[0]), la convertissons en nombre (i=int(selection)) et nous en servons pour retrouver ltiquette correspondante (texte=listeEtiquettes.get(i)) puis le sommet associ (sommet=sommetAssocie[texte]). Ouf ! La suite du travail de cette fonction consiste changer la couleur du sommet en question : on rcupre la couleur courante (nouvelle apparition de la fonction itemcget dj rencontre) et si la couleur est COULEUR_SOMMET alors, laide de la fonction itemconfigure, on donne au sommet la couleur COULEUR_SOMMET2, sinon on lui donne la couleur COULEUR_SOMMET.

tape 15
But : faire apparatre les menus.

from Tkinter import * import tkMessageBox import tkSimpleDialog import sys CREATION_SOMMET = "Cration sommet : cliquez pour dsigner lemplacement" DEBUT_CREATION_ARETE = "Cration arte : cliquez pour dsigner lorigine" SUITE_CREATION_ARETE = "Cration arte : cliquez pour dsigner lextremit" DEFINITION_ETIQUETTE = "Dfinition dtiquette : dsignez le sommet" R = 3 COULEUR_SOMMET = "black" COULEUR_SOMMET2 = "red" sommetEnDeplacement = None # rayon des sommets # couleur des sommets # couleur des sommets mis en vidence # sommet en cours dtre dplac # trois dictionnaires:

attributs = { } attenantes extremites = { } sommetAssocie = { } correspondant nombreSommets = 0 nombreAretes = 0

# associe chaque sommet les artes # (avec un premier lment expliqu plus tard) # associe chaque arte ses deux extremits # associe chaque tiquette le sommet # nombre de sommets crs # nombre dartes cres

application = Tk() application.title("Edigraph 15") etatCourant = StringVar(application) etatCourant.set(CREATION_SOMMET) def sommetVoisin(x, y): # on recherche un objet dessin dans le canevas prs de (x, y) pres = 10 objets = canevas.find_enclosed(x - pres, y - pres, x + pres, y + pres) if len(objets) > 0: # sil y en a... return objets[0] # ...on rend le premier else: return None def rafraichirCompteurs(nSommets, nAretes): global nombreSommets, nombreAretes nombreSommets = nSommets nombreAretes = nAretes etiqNbrSommets.config(text = str(nombreSommets) + " sommets") etiqNbrAretes.config(text = str(nombreAretes) + " artes") def creationSommet(x, y): sommet = canevas.create_oval(x - R, y - R, x + R, y + R, fill = COULEUR_SOMMET, width = 0) s = str(nombreSommets) etiquette = canevas.create_text(x + 2 * R, y - R, text = s, anchor=W) attributs[sommet] = [ etiquette ] # au lieu de "garde-place" rafraichirCompteurs(nombreSommets + 1, nombreAretes) def creationArete(orig, extr): x0, y0 = centre(orig) x1, y1 = centre(extr) arete = canevas.create_line(x0, y0, x1, y1) extremites[arete] = (orig, extr) attributs[orig].append(arete) attributs[extr].append(arete) rafraichirCompteurs(nombreSommets, nombreAretes + 1) def cocheCaseCreationSommet(): barreEtat.config(text=CREATION_SOMMET) def cocheCaseCreationArete(): barreEtat.config(text=DEBUT_CREATION_ARETE) def cocheCaseDefEtiquette(): barreEtat.config(text=DEFINITION_ETIQUETTE) def centre(sommet): a, b, c, d = canevas.coords(sommet) return ((a + c) / 2, (b + d) / 2)

def definitionEtiquette(sommet): etiquette = attributs[sommet][0] texte = canevas.itemcget(etiquette, "text") if not texte.isdigit(): tkMessageBox.showwarning("Dfinition tiquette", "Ltiquette de ce sommet a dj t dfinie") else: texte = tkSimpleDialog.askstring("Etiquette", "Donner la nouvelle valeur :", initialvalue = texte) if texte != None: canevas.itemconfigure(etiquette, text = texte) listeEtiquettes.insert(END, texte) sommetAssocie[texte] = sommet def pressionBoutonGauche(event): global sommetSelectionne if etatCourant.get() == CREATION_SOMMET: creationSommet(event.x, event.y) elif etatCourant.get() == DEBUT_CREATION_ARETE: s = sommetVoisin(event.x, event.y) if s != None: sommetSelectionne = s etatCourant.set(SUITE_CREATION_ARETE) barreEtat.config(text=SUITE_CREATION_ARETE) elif etatCourant.get() == SUITE_CREATION_ARETE: s = sommetVoisin(event.x, event.y) if s != None: creationArete(sommetSelectionne, s) etatCourant.set(DEBUT_CREATION_ARETE) barreEtat.config(text=DEBUT_CREATION_ARETE) else: # etatCourant.get() == DEFINITION_ETIQUETTE s = sommetVoisin(event.x, event.y) if s != None: definitionEtiquette(s) def replacer(arete): sommet0, sommet1 = extremites[arete] x0, y0 = centre(sommet0) x1, y1 = centre(sommet1) canevas.coords(arete, x0, y0, x1, y1) def deplacementSommet(x, y): canevas.coords(sommetEnDeplacement, x - R, y - R, x + R, y + R) liste = attributs[sommetEnDeplacement] canevas.coords(liste[0], x + 2 * R, y - R) for arete in liste[1:]: replacer(arete) def pressionBoutonDroit(event): global sommetEnDeplacement sommetEnDeplacement = sommetVoisin(event.x, event.y) def mouvementBoutonDroit(event): if sommetEnDeplacement != None: deplacementSommet(event.x, event.y) def relachementBoutonDroit(event): global sommetEnDeplacement sommetEnDeplacement = None def toutEffacer():

canevas.addtag_all("a_effacer") canevas.delete("a_effacer") rafraichirCompteurs(0, 0) def pressionBoutonEffacer(): if tkMessageBox.askyesno("Attention", "Vous voulez vraiment tout dtruire?"): toutEffacer() def actionSurListe(evt): selections = listeEtiquettes.curselection() selection = selections[0] i = int(selection) texte = listeEtiquettes.get(i) sommet = sommetAssocie[texte] # # # # # ex.: ("3", "5", "9") ex.: "3" ex.: 3 le texte correspondant le sommet correspondant

x, y = centre(sommet) if canevas.itemcget(sommet, "fill") == COULEUR_SOMMET: canevas.itemconfigure(sommet, fill=COULEUR_SOMMET2) else: canevas.itemconfigure(sommet, fill=COULEUR_SOMMET) def comMenuQuitter(): if tkMessageBox.askyesno("Attention", "Vous voulez vraiment quitter ce programme?"): sys.exit(0) def comMenuAPropos(): tkMessageBox.showinfo( "A propos de...", "Edigraph\n\n" + "un diteur de graphes purement dmonstratif de la\n" + "ralisation des interfaces graphiques avec Tkinter + "(C) H. Garreta, 2005") def enregistrerGraphe(): tkMessageBox.showwarning("Excuses...", "La fonction enregistrer implmente")

\n\n"

graphe

nest

pas

encore

def restaurerGraphe(): tkMessageBox.showwarning("Excuses...", "La fonction restaurer graphe nest pas encore implmente") barreEtat = Label(application, text=CREATION_SOMMET, bd=1, relief=SUNKEN, anchor=W) barreEtat.pack(side=BOTTOM, fill=X) canevas = Canvas(application, bg="#C8FFFF", width=400, height=400) canevas.pack(side=RIGHT, fill=BOTH, expand=True, padx=2, pady=2) canevas.bind("<Button-1>", pressionBoutonGauche) canevas.bind("<Button-3>", pressionBoutonDroit) canevas.bind("<Motion>", mouvementBoutonDroit) canevas.bind("<ButtonRelease-3>", relachementBoutonDroit) panneauSup = Frame(application, width=150, height = 200, padx=4, pady=4, bd=2, relief = RIDGE) panneauSup.pack(side=TOP) lab = Label(panneauSup, text="Bouton gauche :") lab.grid(row=0, sticky=W) rb = Radiobutton(panneauSup, command=cocheCaseCreationSommet, text="Cration sommet",

variable=etatCourant, value=CREATION_SOMMET) rb.grid(row=1, sticky=W) rb = Radiobutton(panneauSup, text="Cration arte", command=cocheCaseCreationArete, variable=etatCourant, value=DEBUT_CREATION_ARETE) rb.grid(row=2, sticky=W) rb = Radiobutton(panneauSup, text="Dfinition tiquette", command=cocheCaseDefEtiquette, variable=etatCourant, value=DEFINITION_ETIQUETTE) rb.grid(row=3, sticky=W) bouton = Button(panneauSup, command=pressionBoutonEffacer) bouton.grid(row=4) text="Tout effacer",

panneauInf = Frame(application) panneauInf.pack(side=BOTTOM, fill=BOTH, expand=True) etiqNbrAretes = Label(panneauInf) etiqNbrAretes.pack(side=BOTTOM, anchor=W) etiqNbrSommets = Label(panneauInf) etiqNbrSommets.pack(side=BOTTOM, anchor=W) rafraichirCompteurs(0, 0) lab = Label(panneauInf, text="Etiquettes :") lab.pack(side=TOP, anchor=W) listeEtiquettes = Listbox(panneauInf) listeEtiquettes.pack(side=BOTTOM, fill=Y, expand=True) listeEtiquettes.bind("<Double-Button-1>", actionSurListe) barreDeMenus = Menu(application) application.config(menu = barreDeMenus) menuFichier = Menu(barreDeMenus) barreDeMenus.add_cascade(label="Fichier", menu=menuFichier) menuFichier.add_command(label="Nouveau", command=pressionBoutonEffacer) menuFichier.add_command(label="Ouvrir...", command=restaurerGraphe) menuFichier.add_command(label="Enregistrer sous...", command=enregistrerGraphe) menuFichier.add_separator() menuFichier.add_command(label="Quitter", command=comMenuQuitter) menuAide = Menu(barreDeMenus) barreDeMenus.add_cascade(label="Aide", menu=menuAide) menuAide.add_command(label="A propos", command=comMenuAPropos) application.mainloop()

Explications. Pour commencer, regardez les lignes rouges la fin du programme, ci-dessus : on y cre une barre de menus (variable barreDeMenus) et deux menus (variables menuFichier et menuAide). Ces trois objets sont des widgets, chacun a donc un parent : application est le parent de la barre de menus, cette dernire est le parent des deux autres menus. Mais il faut encore prciser de qui ces menus sont les menus (vous suivez ?) : linstruction application.config(menu=barreDeMenus) indique que barreDeMenus est la barre de menus de notre application, cela la place automatiquement en haut du cadre principal. Les deux lignes barreDeMenus.add_cascade(label="Fichier", menu=menuFichier) et barreDeMenus.add_cascade(label="Aide", menu=menuAide) accrochent les deux menus droulants

Fichier et Aide la barre de menus. On na pas encore mis des commandes dans ces menus, cela peut tre fait plus tard : les instructions menu.add_command(label=item, command=fonction) accrochent des couples (item, action) aux menus. Il y a donc quatre fonctions crire correspondant aux commandes des menus. Pour commencer, on sen tire avec deux panneaux dexcuse (fonctions enregistrerGraphe et restaurerGraphe), un panneau dinformation (fonction comMenuAPropos) et une fonction comMenuQuitter qui, en passant par une bote de confirmation, termine le programme par un moyen radical : sys.exit(0).

tape 16
But : crire la fonction de sauvegarde dans un fichier de texte du graphe cr. Pour illustrer le format du fichier cr, disons que la sauvegarde du graphe :

produit le fichier suivant, o chaque sommet est associ ses coordonnes, puis la liste de ses voisins immdiats (les sommets qui lui sont relis par une arte) :

"0" (225.0, 39.0) "1" "1" (68.0, 111.0) "0" "Deux" "Trois" "Deux" (367.0, 112.0) "1" "Trois" "Trois" (229.0, 170.0) "1" "Deux"

Bien entendu, seule la fonction enregistrerGraphe est reprendre :

from Tkinter import * import tkMessageBox import tkSimpleDialog import tkFileDialog import sys CREATION_SOMMET = "Cration sommet : cliquez pour dsigner lemplacement" DEBUT_CREATION_ARETE = "Cration arte : cliquez pour dsigner lorigine" SUITE_CREATION_ARETE = "Cration arte : cliquez pour dsigner

lextremit" DEFINITION_ETIQUETTE = "Dfinition dtiquette : dsignez le sommet" R = 3 COULEUR_SOMMET = "black" COULEUR_SOMMET2 = "red" sommetEnDeplacement = None attributs = { } attenantes extremites = { } sommetAssocie = { } correspondant nombreSommets = 0 nombreAretes = 0 # rayon des sommets # couleur des sommets # couleur des sommets mis en vidence # sommet en cours dtre dplac # trois dictionnaires: # associe chaque sommet les artes # (avec un premier lment expliqu plus tard) # associe chaque arte ses deux extremits # associe chaque tiquette le sommet # nombre de sommets crs # nombre dartes cres

application = Tk() application.title("Edigraph 16") etatCourant = StringVar(application) etatCourant.set(CREATION_SOMMET) def sommetVoisin(x, y): # on recherche un objet dessin dans le canevas prs de (x, y) pres = 10 objets = canevas.find_enclosed(x - pres, y - pres, x + pres, y + pres) if len(objets) > 0: # sil y en a... return objets[0] # ...on prend le premier else: return None def rafraichirCompteurs(nSommets, nAretes): global nombreSommets, nombreAretes nombreSommets = nSommets nombreAretes = nAretes etiqNbrSommets.config(text = str(nombreSommets) + " sommets") etiqNbrAretes.config(text = str(nombreAretes) + " artes") def creationSommet(x, y): sommet = canevas.create_oval(x - R, y - R, x + R, y + R, fill = COULEUR_SOMMET, width = 0) s = str(nombreSommets) etiquette = canevas.create_text(x + 2 * R, y - R, text = s, anchor=W) attributs[sommet] = [ etiquette ] # au lieu de "garde-place" rafraichirCompteurs(nombreSommets + 1, nombreAretes) def creationArete(orig, extr): x0, y0 = centre(orig) x1, y1 = centre(extr) arete = canevas.create_line(x0, y0, x1, y1) extremites[arete] = (orig, extr) attributs[orig].append(arete) attributs[extr].append(arete) rafraichirCompteurs(nombreSommets, nombreAretes + 1) def cocheCaseCreationSommet(): barreEtat.config(text=CREATION_SOMMET)

def cocheCaseCreationArete(): barreEtat.config(text=DEBUT_CREATION_ARETE) def cocheCaseDefEtiquette(): barreEtat.config(text=DEFINITION_ETIQUETTE) def centre(sommet): a, b, c, d = canevas.coords(sommet) return ((a + c) / 2, (b + d) / 2) def definitionEtiquette(sommet): etiquette = attributs[sommet][0] texte = canevas.itemcget(etiquette, "text") if not texte.isdigit(): tkMessageBox.showwarning("Dfinition tiquette", "Ltiquette de ce sommet a dj t dfinie") else: texte = tkSimpleDialog.askstring("Etiquette", "Donner la nouvelle valeur :", initialvalue = texte) if texte != None: canevas.itemconfigure(etiquette, text = texte) listeEtiquettes.insert(END, texte) sommetAssocie[texte] = sommet def pressionBoutonGauche(event): global sommetSelectionne if etatCourant.get() == CREATION_SOMMET: creationSommet(event.x, event.y) elif etatCourant.get() == DEBUT_CREATION_ARETE: s = sommetVoisin(event.x, event.y) if s != None: sommetSelectionne = s etatCourant.set(SUITE_CREATION_ARETE) barreEtat.config(text=SUITE_CREATION_ARETE) elif etatCourant.get() == SUITE_CREATION_ARETE: s = sommetVoisin(event.x, event.y) if s != None: creationArete(sommetSelectionne, s) etatCourant.set(DEBUT_CREATION_ARETE) barreEtat.config(text=DEBUT_CREATION_ARETE) else: # etatCourant.get() == DEFINITION_ETIQUETTE s = sommetVoisin(event.x, event.y) if s != None: definitionEtiquette(s) def replacer(arete): sommet0, sommet1 = extremites[arete] x0, y0 = centre(sommet0) x1, y1 = centre(sommet1) canevas.coords(arete, x0, y0, x1, y1) def deplacementSommet(x, y): canevas.coords(sommetEnDeplacement, x - R, y - R, x + R, y + R) liste = attributs[sommetEnDeplacement] canevas.coords(liste[0], x + 2 * R, y - R) for arete in liste[1:]: replacer(arete) def pressionBoutonDroit(event): global sommetEnDeplacement sommetEnDeplacement = sommetVoisin(event.x, event.y)

def mouvementBoutonDroit(event): if sommetEnDeplacement != None: deplacementSommet(event.x, event.y) def relachementBoutonDroit(event): global sommetEnDeplacement sommetEnDeplacement = None def toutEffacer(): canevas.addtag_all("a_effacer") canevas.delete("a_effacer") rafraichirCompteurs(0, 0) def pressionBoutonEffacer(): if tkMessageBox.askyesno("Attention", "Vous voulez vraiment tout dtruire?"): toutEffacer() def actionSurListe(evt): selections = listeEtiquettes.curselection() selection = selections[0] i = int(selection) texte = listeEtiquettes.get(i) sommet = sommetAssocie[texte] # # # # # ex.: ("3", "5", "9") ex.: "3" ex.: 3 le texte correspondant le sommet correspondant

x, y = centre(sommet) if canevas.itemcget(sommet, "fill") == COULEUR_SOMMET: canevas.itemconfigure(sommet, fill=COULEUR_SOMMET2) else: canevas.itemconfigure(sommet, fill=COULEUR_SOMMET) def comMenuQuitter(): if tkMessageBox.askyesno("Attention", "Vous voulez vraiment quitter ce programme?"): sys.exit(0) def comMenuAPropos(): tkMessageBox.showinfo( "A propos de...", "Edigraph\n\n" + "un diteur de graphes purement dmonstratif de la\n" + "ralisation des interfaces graphiques avec Tkinter + "(C) H. Garreta, 2005") def enregistrerGraphe(): fichier = tkFileDialog.asksaveasfile() if fichier == None: return liste = canevas.find_all() for item in liste: if canevas.type(item) == "oval": dep = attributs[item] print >> fichier, " + canevas.itemcget(dep[0], "text") + ", print >> fichier, centre(item), for arete in dep[1:]: voisin = extremites[arete][0] if voisin == item: voisin = extremites[arete][1] s = canevas.itemcget(attributs[voisin][0], "text") print >> fichier, " + s + ", print >> fichier

\n\n"

fichier.close() def restaurerGraphe(): tkMessageBox.showwarning("Excuses...", "La fonction restaurer graphe nest pas encore implmente") barreEtat = Label(application, text=CREATION_SOMMET, bd=1, relief=SUNKEN, anchor=W) barreEtat.pack(side=BOTTOM, fill=X) canevas = Canvas(application, bg="#C8FFFF", width=400, height=400) canevas.pack(side=RIGHT, fill=BOTH, expand=True, padx=2, pady=2) canevas.bind("<Button-1>", pressionBoutonGauche) canevas.bind("<Button-3>", pressionBoutonDroit) canevas.bind("<Motion>", mouvementBoutonDroit) canevas.bind("<ButtonRelease-3>", relachementBoutonDroit) panneauSup = Frame(application, width=150, height = 200, padx=4, pady=4, bd=2, relief = RIDGE) panneauSup.pack(side=TOP) lab = Label(panneauSup, text="Bouton gauche :") lab.grid(row=0, sticky=W) rb = Radiobutton(panneauSup, text="Cration sommet", command=cocheCaseCreationSommet, variable=etatCourant, value=CREATION_SOMMET) rb.grid(row=1, sticky=W) rb = Radiobutton(panneauSup, text="Cration arte", command=cocheCaseCreationArete, variable=etatCourant, value=DEBUT_CREATION_ARETE) rb.grid(row=2, sticky=W) rb = Radiobutton(panneauSup, text="Dfinition tiquette", command=cocheCaseDefEtiquette, variable=etatCourant, value=DEFINITION_ETIQUETTE) rb.grid(row=3, sticky=W) bouton = Button(panneauSup, command=pressionBoutonEffacer) bouton.grid(row=4) text="Tout effacer",

panneauInf = Frame(application) panneauInf.pack(side=BOTTOM, fill=BOTH, expand=True) etiqNbrAretes = Label(panneauInf) etiqNbrAretes.pack(side=BOTTOM, anchor=W) etiqNbrSommets = Label(panneauInf) etiqNbrSommets.pack(side=BOTTOM, anchor=W) rafraichirCompteurs(0, 0) lab = Label(panneauInf, text="Etiquettes :") lab.pack(side=TOP, anchor=W) listeEtiquettes = Listbox(panneauInf) listeEtiquettes.pack(side=BOTTOM, fill=Y, expand=True) listeEtiquettes.bind("<Double-Button-1>", actionSurListe) barreDeMenus = Menu(application) application.config(menu = barreDeMenus) menuFichier = Menu(barreDeMenus) barreDeMenus.add_cascade(label="Fichier", menu=menuFichier) menuFichier.add_command(label="Nouveau", command=pressionBoutonEffacer)

menuFichier.add_command(label="Ouvrir...", command=restaurerGraphe) menuFichier.add_command(label="Enregistrer sous...", command=enregistrerGraphe) menuFichier.add_separator() menuFichier.add_command(label="Quitter", command=comMenuQuitter) menuAide = Menu(barreDeMenus) barreDeMenus.add_cascade(label="Aide", menu=menuAide) menuAide.add_command(label="A propos", command=comMenuAPropos) application.mainloop()

Explications. Pour commencer, la ligne fichier=tkFileDialog.asksaveasfile() produit laffichage dune bote de dialogue douverture de fichier en criture (le genre de dialogue quon obtient dasn toutes les applications quand on actionne la commande Save as... du menu File...). Si le dialogue se termine par le choix russi dun fichier, cette fonction renvoie le fichier en question, sinon elle renvoie None. Le reste de la fonction consiste en ceci :

obtention de la liste de tous les objets graphiques tracs sur le canevas (liste=canevas.find_all()), parcours des lments de cette liste, en ne sintressant qu ceux qui sont des ovales (sur le canevas on a trac des ovales en fait des cercles et des segments de droite), chacun de ces lments reprsente donc un sommet du graphe, et dep est la liste de ses attributs (ltiquette, suivie des artes touchant ce sommet). On commence par crire ltiquette (canevas.itemcget(dep[0], "text")), encadre par des guillemets, ensuite on crit les coordonnes du centre du sommet (ne pas oublier que la fonction centre renvoie un doublet, cela provoque lcriture dune virgule entre les coordonnes et de parenthses autour), ensuite on parcourt les artes (dep[1:] est la liste des attributs prive du premier lment). Pour chaque arte, on obtient lautre extrmit (le sommet qui nest pas celui dont on est en train dcrire les attributs) et on en affiche ltiquette, galement entre guillemets. A la fin, on va la ligne (instruction print >> fichier , tous les autres print se terminent par une virgule qui empche le retour la ligne).

Cest tout. Pas mal, hein...?

tape 17
But : crire la fonction de restauration, qui reconstruit un graphe partir de sa description enregistre dans un fichier laide de la fonction prcdente. Cette tape est laisse au lecteur titre dexercice...