« Programmation Python/Threads » : différence entre les versions

Contenu supprimé Contenu ajouté
Aucun résumé des modifications
Ligne 593 :
=== Programme serveur : suite et fin ===
 
Les deux classes ci-dessous complètent le script serveur. Le code implémenté dans la classe <code>ThreadClients()</code> est assez similaire à celui que nous avions développé précédemment pour le corps d'application du logiciel de « Chat ». Dans le cas présent, toutefois, nous le plaçons dans une classe dérivée de <code>Thread()</code>, parce que devons faire fonctionner ce code dans un thread indépendant de celui de l'application principale. Celui-ci est en effet déjà complètement accaparé par la boucle <code>mainloop()</code> de l'interface graphique<ref>Nous détaillerons cette question à la page {{todo}}, car elle ouvre quelques perspectives intéressantes.</ref>.
 
La classe <code>AppServeur()</code> dérive de la classe <code>AppBombardes()</code> du module ''canon04''. Nous lui avons ajouté un ensemble de méthodes complémentaires destinées à exécuter toutes les opérations qui résulteront du dialogue entamé avec les clients. Nous avons déjà signalé plus haut que les clients instancieront chacun une version dérivée de cette classe (afin de profiter des mêmes définitions de base pour la fenêtre, le canevas, etc.).
 
<source lang=python line start=138>
{{todo|num à droite}}
class ThreadClients(threading.Thread):
"""objet thread gérant la connexion de nouveaux clients"""
def __init__(self, boss, connex):
threading.Thread.__init__(self)
self.boss = boss # réf. de la fenêtre application
self.connex = connex # réf. du socket initial
def run(self):
"attente et prise en charge de nouvelles connexions clientes"
txt ="Serveur prêt, en attente de requêtes ...\n"
self.boss.afficher(txt)
self.connex.listen(5)
# Gestion des connexions demandées par les clients :
while 1:
nouv_conn, adresse = self.connex.accept()
# Créer un nouvel objet thread pour gérer la connexion :
th = ThreadConnexion(self.boss, nouv_conn)
th.start()
it = th.getName() # identifiant unique du thread
# Mémoriser la connexion dans le dictionnaire :
self.boss.enregistrer_connexion(nouv_conn, it)
# Afficher :
txt = "Client %s connecté, adresse IP %s, port %s.\n" %\
(it, adresse[0], adresse[1])
self.boss.afficher(txt)
# Commencer le dialogue avec le client :
nouv_conn.send("serveur OK")
 
class AppServeur(AppBombardes):
<pre>
"""fenêtre principale de l'application (serveur ou client)"""
138.class ThreadClients(threading.Thread):
def __init__(self, host, port, larg_c, haut_c):
139. """objet thread gérant la connexion de nouveaux clients"""
140. def __init__( self.host, bossself.port = host, connex):port
141. threading.ThreadAppBombardes.__init__(self, larg_c, haut_c)
142. self.bossactive = boss 1 # réf. de la fenêtretémoin applicationd'activité
# veiller à quitter proprement si l'on referme la fenêtre :
143. self.connex = connex # réf. du socket initial
self.bind('<Destroy>',self.fermer_threads)
144.
 
145. def run(self):
def specificites(self):
146. "attente et prise en charge de nouvelles connexions clientes"
147. txt ="Serveurpréparer prêt,les enobjets attentespécifiques de requêtesla ...\npartie serveur"
148. self.bossmaster.affichertitle(txt'<<< Serveur pour le jeu des bombardes >>>')
149. self.connex.listen(5)
150. # Gestionwidget desText, connexionsassocié demandéesà parune lesbarre clientsde défilement :
151. while 1: st =Frame(self)
self.avis =Text(st, width =65, height =5)
152. nouv_conn, adresse = self.connex.accept()
self.avis.pack(side =LEFT)
153. # Créer un nouvel objet thread pour gérer la connexion :
154. scroll =Scrollbar(st, thcommand = ThreadConnexion(self.boss, nouv_connavis.yview)
self.avis.configure(yscrollcommand =scroll.set)
155. th.start()
scroll.pack(side =RIGHT, fill =Y)
156. it = th.getName() # identifiant unique du thread
st.pack()
157. # Mémoriser la connexion dans le dictionnaire :
158. self.boss.enregistrer_connexion(nouv_conn, it)
159. # partie serveur # Afficherréseau :
self.conn_client = {} # dictionn. des connexions clients
160. txt = "Client %s connecté, adresse IP %s, port %s.\n" %\
self.verrou =threading.Lock() # verrou pour synchroniser threads
161. (it, adresse[0], adresse[1])
# Initialisation du serveur - Mise en place du socket :
162. self.boss.afficher(txt)
connexion = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
163. # Commencer le dialogue avec le client :
try:
164. nouv_conn.send("serveur OK")
connexion.bind((self.host, self.port))
165.
except socket.error:
166.class AppServeur(AppBombardes):
txt ="La liaison du socket à l'hôte %s, port %s a échoué.\n" %\
167. """fenêtre principale de l'application (serveur ou client)"""
168. def __init__ (self, .host, self.port, larg_c, haut_c):
169. self.host, self.port = hostavis.insert(END, porttxt)
self.accueil =None
170. AppBombardes.__init__(self, larg_c, haut_c)
else:
171. self.active =1 # témoin d'activité
172. # veiller à quitter proprement# sidémarrage l'ondu refermethread guettant la fenêtreconnexion des clients :
173. self.bindaccueil = ThreadClients('<Destroy>',self.fermer_threads, connexion)
self.accueil.start()
174.
 
175. def specificites(self):
def depl_aleat_canon(self, id):
176. "préparer les objets spécifiques de la partie serveur"
"déplacer aléatoirement le canon <id>"
177. self.master.title('<<< Serveur pour le jeu des bombardes >>>')
x, y = AppBombardes.depl_aleat_canon(self, id)
178.
179. # widgetsignaler Text,ces associénouvelles àcoord. uneà barretous deles défilementclients :
180. st =Frame(self.verrou.acquire()
for cli in self.conn_client:
181. self.avis =Text(st, width =65, height =5)
message = "mouvement_de,%s,%s,%s," % (id, x, y)
182. self.avis.pack(side =LEFT)
183. scroll =Scrollbar(st, command = self.avisconn_client[cli].yviewsend(message)
184. self.avisverrou.configurerelease(yscrollcommand =scroll.set)
185. scroll.pack(side =RIGHT, fill =Y)
186. def goal(self, i, st.pack(j):
"le canon <i> signale qu'il a atteint l'adversaire <j>"
187.
188. # partie serveurAppBombardes.goal(self, réseaui, :j)
# Signaler les nouveaux scores à tous les clients :
189. self.conn_client = {} # dictionn. des connexions clients
190. self.verrou =threading.Lockacquire() # verrou pour synchroniser threads
for cli in self.conn_client:
191. # Initialisation du serveur - Mise en place du socket :
msg ='scores,'
192. connexion = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
193. try for id in self.pupi:
194. connexion.bind((self.host, sc = self.port)pupi[id].valeur_score()
msg = msg +"%s;%s," % (id, sc)
195. except socket.error:
self.conn_client[cli].send(msg)
196. txt ="La liaison du socket à l'hôte %s, port %s a échoué.\n" %\
197 time.sleep(.5) # pour mieux (self.host,séparer les messages self.port)
198. self.avisverrou.insertrelease(END, txt)
 
199. self.accueil =None
def ajouter_canon(self, id):
200. else:
"instancier un canon et un pupitre de nom <id> dans 2 dictionn."
201. # démarrage du thread guettant la connexion des clients :
# on alternera ceux des 2 camps :
202. self.accueil = ThreadClients(self, connexion)
203. n = len(self.accueil.start(guns)
if n %2 ==0:
204.
sens = -1
205. def depl_aleat_canon(self, id):
else:
206. "déplacer aléatoirement le canon <id>"
sens = 1
207. x, y = AppBombardes.depl_aleat_canon(self, id)
x, y = self.coord_aleat(sens)
208. # signaler ces nouvelles coord. à tous les clients :
coul =('dark blue', 'dark red', 'dark green', 'purple',
209. self.verrou.acquire()
'dark cyan', 'red', 'cyan', 'orange', 'blue', 'violet')[n]
210. for cli in self.conn_client:
211. messageself.guns[id] = "mouvement_deCanon(self.jeu,%s,%s,%s," % (id, x, y, sens, coul)
212. self.pupi[id] = Pupitre(self, self.conn_clientguns[cliid].send(message)
213. self.verroupupi[id].releaseinactiver()
return (x, y, sens, coul)
214.
215. def goal(self, i, j):
def enlever_canon(self, id):
216. "le canon <i> signale qu'il a atteint l'adversaire <j>"
"retirer le canon et le pupitre dont l'identifiant est <id>"
217. AppBombardes.goal(self, i, j)
if self.active == 0: # la fenêtre a été refermée
218. # Signaler les nouveaux scores à tous les clients :
return
219. self.verrou.acquire()
220. for cli in self.conn_client:guns[id].effacer()
221. del msg ='scores,'self.guns[id]
222. for id in self.pupi:[id].destroy()
223. sc =del self.pupi[id].valeur_score()
224. msg = msg +"%s;%s," % (id, sc)
def orienter_canon(self, id, angle):
225. self.conn_client[cli].send(msg)
"régler la hausse du canon <id> à la valeur <angle>"
226. time.sleep(.5) # pour mieux séparer les messages
227. self.verrouguns[id].releaseorienter(angle)
self.pupi[id].reglage(angle)
228.
229. def ajouter_canon(self, id):
def tir_canon(self, id):
230. "instancier un canon et un pupitre de nom <id> dans 2 dictionn."
231. #"déclencher onle alterneratir ceuxdu descanon 2 camps :<id>"
232. n = len(self.guns[id].feu()
 
233. if n %2 ==0:
def enregistrer_connexion(self, conn, it):
234. sens = -1
"Mémoriser la connexion dans un dictionnaire"
235. else:
236. sensself.conn_client[it] = 1conn
 
237. x, y = self.coord_aleat(sens)
def afficher(self, txt):
238. coul =('dark blue', 'dark red', 'dark green', 'purple',
"afficher un message dans la zone de texte"
239. 'dark cyan', 'red', 'cyan', 'orange', 'blue', 'violet')[n]
240. self.guns[id] = Canonavis.insert(self.jeu, id, x, y, sensEND, coultxt)
 
241. self.pupi[id] = Pupitre(self, self.guns[id])
def fermer_threads(self, evt):
242. self.pupi[id].inactiver()
"couper les connexions existantes et fermer les threads"
243. return (x, y, sens, coul)
# couper les connexions établies avec tous les clients :
244.
for id in self.conn_client:
245. def enlever_canon(self, id):
self.conn_client[id].send('fin')
246. "retirer le canon et le pupitre dont l'identifiant est <id>"
# forcer la terminaison du thread serveur qui attend les requêtes :
247. if self.active == 0: # la fenêtre a été refermée
if self.accueil != None:
248. return
249. self.guns[id]accueil.effacer_Thread__stop()
self.active =0 # empêcher accès ultérieurs à Tk
250. del self.guns[id]
 
251. self.pupi[id].destroy()
if __name__ =='__main__':
252. del self.pupi[id]
AppServeur(host, port, largeur, hauteur).mainloop()
253.
</source>
254. def orienter_canon(self, id, angle):
255. "régler la hausse du canon <id> à la valeur <angle>"
256. self.guns[id].orienter(angle)
257. self.pupi[id].reglage(angle)
258.
259. def tir_canon(self, id):
260. "déclencher le tir du canon <id>"
261. self.guns[id].feu()
262.
263. def enregistrer_connexion(self, conn, it):
264. "Mémoriser la connexion dans un dictionnaire"
265. self.conn_client[it] = conn
266.
267. def afficher(self, txt):
268. "afficher un message dans la zone de texte"
269. self.avis.insert(END, txt)
270.
271. def fermer_threads(self, evt):
272. "couper les connexions existantes et fermer les threads"
273. # couper les connexions établies avec tous les clients :
274. for id in self.conn_client:
275. self.conn_client[id].send('fin')
276. # forcer la terminaison du thread serveur qui attend les requêtes :
277. if self.accueil != None:
278. self.accueil._Thread__stop()
279. self.active =0 # empêcher accès ultérieurs à Tk
280.
281.if __name__ =='__main__':
282. AppServeur(host, port, largeur, hauteur).mainloop()
</pre>
 
;Commentaires
Ligne 751 ⟶ 749 :
* Ligne 173 : Il vous arrivera de temps à autre de vouloir « intercepter » l'ordre de fermeture de l'application que l'utilisateur déclenche en quittant votre programme, par exemple parce que vous voulez forcer la sauvegarde de données importantes dans un fichier, ou fermer aussi d'autres fenêtres, etc. Il suffit pour ce faire de détecter l'événement <Destroy>, comme nous le faisons ici pour forcer la terminaison de tous les threads actifs.
 
* Lignes 179 à 186 : Au passage, voici comment vous pouvez associer une barre de défilement (widget ''Scrollbar'') à un widget ''Text'' (vous pouvez faire de même avec un widget ''Canvas''), sans faire appel à la bibliothèque ''Pmw''<ref>Voir : [[../Tkinter#Installation_des_Python_m.C3.A9ga-widgets|Python Mega Widgets, page {{todo}}.]]</ref>.
 
* Ligne 190 : Instanciation de l'obet « verrou » permettant de synchroniser les threads.
Ligne 769 ⟶ 767 :
C'est dans la méthode <code>run()</code> de la classe <code>ThreadSocket()</code> (lignes 86 à 126) que se trouve le code traitant les messages échangés avec le serveur. Nous y avons d'ailleurs laissé une instruction print (à la ligne 88) afin que les messages reçus du serveur apparaissent sur la sortie standard. Si vous réalisez vous-même une forme plus définitive de ce jeu, vous pourrez bien évidemment supprimer cette instruction.
 
<source lang=python line>
{{todo|num à droite}}
#######################################################
# Jeu des bombardes - partie cliente #
# (C) Gérard Swinnen, Liège (Belgique) - Juillet 2004 #
# Licence : GPL #
# Avant d'exécuter ce script, vérifiez que l'adresse, #
# le numéro de port et les dimensions de l'espace de #
# jeu indiquées ci-dessous correspondent exactement #
# à ce qui a été défini pour le serveur. #
#######################################################
 
from Tkinter import *
<pre>
import socket, sys, threading, time
1.#######################################################
from canon_serveur import Canon, Pupitre, AppServeur
2.# Jeu des bombardes - partie cliente #
 
3.# (C) Gérard Swinnen, Liège (Belgique) - Juillet 2004 #
host, port = '192.168.0.235', 35000
4.# Licence : GPL #
largeur, hauteur = 700, 400 # dimensions de l'espace de jeu
5.# Avant d'exécuter ce script, vérifiez que l'adresse, #
 
6.# le numéro de port et les dimensions de l'espace de #
class AppClient(AppServeur):
7.# jeu indiquées ci-dessous correspondent exactement #
def __init__(self, host, port, larg_c, haut_c):
8.# à ce qui a été défini pour le serveur. #
AppServeur.__init__(self, host, port, larg_c, haut_c)
9.#######################################################
10.
def specificites(self):
11.from Tkinter import *
"préparer les objets spécifiques de la partie client"
12.import socket, sys, threading, time
self.master.title('<<< Jeu des bombardes >>>')
13.from canon_serveur import Canon, Pupitre, AppServeur
self.connex =ThreadSocket(self, self.host, self.port)
14.
self.connex.start()
15.host, port = '192.168.0.235', 35000
self.id =None
16.largeur, hauteur = 700, 400 # dimensions de l'espace de jeu
 
17.
def ajouter_canon(self, id, x, y, sens, coul):
18.class AppClient(AppServeur):
"instancier 1 canon et 1 pupitre de nom <id> dans 2 dictionnaires"
19. def __init__(self, host, port, larg_c, haut_c):
20. AppServeurself.__init__guns[id] = Canon(self.jeu, hostid, portint(x), larg_cint(y),int(sens), haut_ccoul)
self.pupi[id] = Pupitre(self, self.guns[id])
21.
self.pupi[id].inactiver()
22. def specificites(self):
23. "préparer les objets spécifiques de la partie client"
def activer_pupitre_personnel(self, id):
24. self.master.title('<<< Jeu des bombardes >>>')
self.id =id # identifiant reçu du serveur
25. self.connex =ThreadSocket(self, self.host, self.port)
26. self.connexpupi[id].startactiver()
27. self.id =None
def tir_canon(self, id):
28.
r = self.guns[id].feu() # renvoie False si enrayé
29. def ajouter_canon(self, id, x, y, sens, coul):
if r and id == self.id:
30. "instancier 1 canon et 1 pupitre de nom <id> dans 2 dictionnaires"
31. self.guns[id] = Canon( self.jeu, id, intconnex.signaler_tir(x),int(y),int(sens), coul)
32. self.pupi[id] = Pupitre(self, self.guns[id])
def imposer_score(self, id, sc):
33. self.pupi[id].inactiver()
self.pupi[id].valeur_score(int(sc))
34.
35. def activer_pupitre_personnel(self, id):
def deplacer_canon(self, id, x, y):
36. self.id =id # identifiant reçu du serveur
"note: les valeurs de x et y sont reçues en tant que chaînes"
37. self.pupi[id].activer()
self.guns[id].deplacer(int(x), int(y))
38.
 
39. def tir_canon(self, id):
def orienter_canon(self, id, angle):
40. r = self.guns[id].feu() # renvoie False si enrayé
41. if"régler rla andhausse iddu ==canon self.<id:> à la valeur <angle>"
42. self.connexguns[id].signaler_tirorienter(angle)
43. if id == self.id:
self.connex.signaler_angle(angle)
44. def imposer_score(self, id, sc):
else:
45. self.pupi[id].valeur_score(int(sc))
self.pupi[id].reglage(angle)
46.
47. def deplacer_canon(self, id, x, y):
def fermer_threads(self, evt):
48. "note: les valeurs de x et y sont reçues en tant que chaînes"
"couper les connexions existantes et refermer les threads"
49. self.guns[id].deplacer(int(x), int(y))
self.connex.terminer()
50.
self.active =0 # empêcher accès ultérieurs à Tk
51. def orienter_canon(self, id, angle):
 
52. "régler la hausse du canon <id> à la valeur <angle>"
def depl_aleat_canon(self, id):
53. self.guns[id].orienter(angle)
pass # => méthode inopérante
54. if id == self.id:
 
55. self.connex.signaler_angle(angle)
56. def goal(self, a, elseb):
pass # => méthode inopérante
57. self.pupi[id].reglage(angle)
 
58.
 
59. def fermer_threads(self, evt):
class ThreadSocket(threading.Thread):
60. "couper les connexions existantes et refermer les threads"
"""objet thread gérant l'échange de messages avec le serveur"""
61. self.connex.terminer()
def __init__(self, boss, host, port):
62. self.active =0 # empêcher accès ultérieurs à Tk
threading.Thread.__init__(self)
63.
self.app = boss # réf. de la fenêtre application
64. def depl_aleat_canon(self, id):
# Mise en place du socket - connexion avec le serveur :
65. pass # => méthode inopérante
self.connexion = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
66.
67. def goal(self, a, b) try:
self.connexion.connect((host, port))
68. pass # => méthode inopérante
except socket.error:
69.
print "La connexion a échoué."
70.
sys.exit()
71.class ThreadSocket(threading.Thread):
72. """objet thread gérant l'échange deprint messages"Connexion établie avec le serveur""."
 
73. def __init__(self, boss, host, port):
74. def threading.Thread.__init__run(self):
while 1:
75. self.app = boss # réf. de la fenêtre application
76. # Mise en place dumsg_recu socket -= self.connexion avec le serveur :.recv(1024)
print "*%s*" % msg_recu
77. self.connexion = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
# le message reçu est d'abord converti en une liste :
78. try:
79. selft =msg_recu.connexion.connectsplit((host', port)')
if t[0] =="" or t[0] =="fin":
80. except socket.error:
81. print "La connexion a échoué."# fermer le présent thread :
82. sys.exit() break
83. print "Connexion établie avec leelif t[0] =="serveur. OK":
self.connexion.send("client OK")
84.
elif t[0] =="canons":
85. def run(self):
self.connexion.send("OK") # accusé de réception
86. while 1:
# éliminons le 1er et le dernier élément de la liste.
87. msg_recu = self.connexion.recv(1024)
# ceux qui restent sont eux-mêmes des listes :
88. print "*%s*" % msg_recu
89. # le message reçu estlc d'abord converti en une liste= t[1:-1]
# chacune est la description complète d'un canon :
90. t =msg_recu.split(',')
91. if t[0] =="" or t[0]for =="fin"g in lc:
92. # fermer le présent threads := g.split(';')
93. break self.app.ajouter_canon(s[0], s[1], s[2], s[3], s[4])
94. elif t[0] =="serveur OKnouveau_canon":
95. self.connexionapp.sendajouter_canon("clientt[1], t[2], t[3], t[4], OK"t[5])
96. elif if len(t[0]) =="canons">6:
97. self.connexionapp.sendactiver_pupitre_personnel("OK"t[1]) # accusé de réception
elif t[0] =='angle':
98. # éliminons le 1er et le dernier élément de la liste.
99. # ceuxil quise restentpeut sontque eux-mêmesl'on desait listesreçu :plusieurs infos regroupées.
100. lc# =on ne considère alors que la première t[1:-1]
101. #self.app.orienter_canon(t[1], t[2]) chacune est la description complète d'un canon :
102. elif t[0] for g in lc=="tir_de":
103. s = gself.app.splittir_canon(';'t[1])
104. elif self.app.ajouter_canon(st[0], s[1], s[2], s[3], s[4])=="scores":
# éliminons le 1er et le dernier élément de la liste.
105. elif t[0] =="nouveau_canon":
106. self.app.ajouter_canon(t[1],# t[2],ceux t[3],qui t[4],restent t[5])sont eux-mêmes des listes :
107. iflc len(t)= >6t[1:-1]
108. # chaque élément est self.app.activer_pupitre_personnel(t[1])la description d'un score :
109. elif t[0] =='angle' for g in lc:
110. # il se peut ques l'on= ait reçu plusieurs infos regroupéesg.split(';')
111. # on ne considère alors que la premièreself.app.imposer_score(s[0], :s[1])
elif t[0] =="mouvement_de":
112. self.app.orienter_canon(t[1], t[2])
113. elif self.app.deplacer_canon(t[01] =="tir_de":,t[2],t[3])
114. elif self.app.tir_canon(t[10]) =="départ_de":
115. elif self.app.enlever_canon(t[01] =="scores":)
 
116. # éliminons le 1er et le dernier élément de la liste.
# Le thread <réception> se termine ici.
117. # ceux qui restent sont eux-mêmes des listes :
print "Client arrêté. Connexion interrompue."
118. lc = t[1:-1]
self.connexion.close()
119. # chaque élément est la description d'un score :
120. for g in lc:
def signaler_tir(self):
121. s = g.split(';')
122. self.appconnexion.imposer_scoresend(s[0], s[1]'feu')
 
123. elif t[0] =="mouvement_de":
def signaler_angle(self, angle):
124. self.app.deplacer_canon(t[1],t[2],t[3])
self.connexion.send('orienter,%s,' % angle)
125. elif t[0] =="départ_de":
126. self.app.enlever_canon(t[1])
def terminer(self):
127.
self.connexion.send('fin')
128. # Le thread <réception> se termine ici.
 
129. print "Client arrêté. Connexion interrompue."
# Programme principal :
130. self.connexion.close()
if __name__ =='__main__':
131.
AppClient(host, port, largeur, hauteur).mainloop()
132. def signaler_tir(self):
</source>
133. self.connexion.send('feu')
134.
135. def signaler_angle(self, angle):
136. self.connexion.send('orienter,%s,' % angle)
137.
138. def terminer(self):
139. self.connexion.send('fin')
140.
141.# Programme principal :
142.if __name__ =='__main__':
143. AppClient(host, port, largeur, hauteur).mainloop()
144.
</pre>
 
;Commentaires
Ligne 933 ⟶ 928 :
{{Exercices}}
#Simplifiez le script correspondant au client de « chat » décrit à la page 287 <ref>{{todo}}</ref>, en supprimant l'un des deux objets threads. Arrangez-vous par exemple pour traiter l'émission de messages au niveau du thread principal.
#Modifiez le jeu des bombardes (version monoposte) du chapitre 15 (voir pages 229 <ref>{{todo}}</ref> et suivantes), en ne gardant qu'un seul canon et un seul pupitre de pointage. Ajoutez-y une cible mobile, dont le mouvement sera géré par un objet thread indépendant (de manière à bien séparer les portions de code qui contrôlent l'animation de la cible et celle du boulet).
{{solution}}
<ol>
Ligne 1 149 ⟶ 1 144 :
Vous devez bien comprendre que pendant l'écoulement de l'intervalle de temps programmé à l'aide de la méthode <code>after()</code>, votre application n'est pas du tout « figée ». Vous pouvez par exemple pendant ce temps : cliquer sur un bouton, redimensionner la fenêtre, effectuer une entrée clavier, etc. Comment cela est-il rendu possible ?
 
Nous avons mentionné déjà à plusieurs reprises le fait que les applications graphiques modernes comportent toujours une sorte de moteur qui « tourne » continuellement en tâche de fond : ce dispositif se met en route lorsque vous activez la méthode <code>mainloop()</code> de votre fenêtre principale. Comme son nom l'indique fort bien, cette méthode met en oeuvre une boucle répétitive perpétuelle, du même type que les boucles <code>while</code> que vous connaissez bien. De nombreux mécanismes sont intégrés à ce « moteur ». L'un d'entre eux consiste à réceptionner tous les événements qui se produisent, et à les signaler ensuite à l'aide de messages appropriés aux programmes qui en font la demande (voir : Programmes pilotés par des événements, page <ref>{{todo|page}}</ref>), d'autres contrôlent les actions à effectuer au niveau de l'affichage, etc. Lorsque vous faites appel à la méthode <code>after()</code> d'un widget, vous utilisez en fait un mécanisme de chronométrage qui est intégré lui aussi à <code>mainloop()</code>, et c'est donc ce gestionnaire central qui déclenche l'appel de fonction que vous souhaitez, après un certain intervalle de temps.
 
La technique d'animation utilisant la méthode <code>after()</code> est la seule possible pour une application fonctionnant toute entière sur un seul thread, parce que c'est la boucle <code>mainloop()</code> qui dirige l'ensemble du comportement d'une telle application de manière absolue. C'est notamment elle qui se charge de redessiner tout ou partie de la fenêtre chaque fois que cela s'avère nécessaire. Pour cette raison, vous ne pouvez pas imaginer de construire un moteur d'animation qui redéfinirait les coordonnées d'un objet graphique à l'intérieur d'une simple boucle <code>while</code>, par exemple, parce que pendant tout ce temps l'exécution de <code>mainloop()</code> resterait suspendue, ce qui aurait pour conséquence que pendant tout ce temps aucun objet graphique ne serait redessiné (en particulier celui que vous souhaitez mettre en mouvement !). En fait, toute l'application apparaîtrait figée, aussi longtemps que la boucle while ne serait pas interrompue.
Ligne 1 173 ⟶ 1 168 :
[[Image:Apprendre à programmer avec Python 71.png|center|capture d'écran de l'application]]
 
<source lang=python line>
{{todo|num à droite}}
from Tkinter import *
from math import sin, cos
import time, threading
 
class App(Frame):
<pre>
def __init__(self):
1.from Tkinter import *
Frame.__init__(self)
2.from math import sin, cos
self.pack()
3.import time, threading
can =Canvas(self, width =400, height =400,
4.
bg ='ivory', bd =3, relief =SUNKEN)
5.class App(Frame):
can.pack(padx =5, pady =5)
6. def __init__(self):
cercle = can.create_oval(185, 355, 215, 385, fill ='red')
7. Frame.__init__(self)
8. self.packtb = Thread_balle(can, cercle)
9. can =CanvasButton(self, widthtext =400'Marche', heightcommand =400,tb.start).pack(side =LEFT)
10. # Button(self, bgtext ='ivoryArrêt', bdcommand =3, relieftb.stop).pack(side =SUNKENRIGHT)
# arrêter l'autre thread si l'on ferme la fenêtre :
11. can.pack(padx =5, pady =5)
self.bind('<Destroy>', tb.stop)
12. cercle = can.create_oval(185, 355, 215, 385, fill ='red')
13. tb = Thread_balle(can, cercle)
class Thread_balle(threading.Thread):
14. Button(self, text ='Marche', command =tb.start).pack(side =LEFT)
def __init__(self, canevas, dessin):
15. # Button(self, text ='Arrêt', command =tb.stop).pack(side =RIGHT)
threading.Thread.__init__(self)
16. # arrêter l'autre thread si l'on ferme la fenêtre :
17. self.bind('<Destroy>'can, tbself.stop)dessin = canevas, dessin
self.anim =1
18.
19.class Thread_balle(threading.Thread):
20. def __init__run(self, canevas, dessin):
21. threading.Threada = 0.__init__(self)0
22. self.can,while self.dessinanim == canevas, dessin1:
23. self.anim a +=1 .01
x, y = 200 + 170*sin(a), 200 +170*cos(a)
24.
self.can.coords(self.dessin, x-15, y-15, x+15, y+15)
25. def run(self):
26. a = 0 time.sleep(0.010)
 
27. while self.anim == 1:
28. def stop(self, evt a += .010):
self.anim =0
29. x, y = 200 + 170*sin(a), 200 +170*cos(a)
 
30. self.can.coords(self.dessin, x-15, y-15, x+15, y+15)
App().mainloop()
31. time.sleep(0.010)
</source>
32.
33. def stop(self, evt =0):
34. self.anim =0
35.
36.App().mainloop()
</pre>
 
;Commentaires
Ligne 1 220 ⟶ 1 213 :
* Ligne 15 : Vous ne pouvez par redémarrer un thread qui s'est terminé. De ce fait, vous ne pouvez lancer cette animation qu'une seule fois (tout au moins sous la forme présentée ici). Pour vous en convaincre, activez la ligne n° 15 en enlevant le caractère <code>#</code> situé au début (et qui fait que Python considère qu'il s'agit d'un simple commentaire) : lorsque l'animation est lancée, un clic de souris sur le bouton ainsi mis en place provoque la sortie de la boucle <code>while</code> des lignes 27-31, ce qui termine la méthode <code>run()</code>. L'animation s'arrête, mais le thread qui la gérait s'est terminé lui aussi. Si vous essayez de le relancer à l'aide du bouton « Marche », vous n'obtenez rien d'autre qu'un message d'erreur.
 
* Lignes 26 à 31 : Pour simuler un mouvement circulaire uniforme, il suffit de faire varier continuellement la valeur d'un angle a. Le sinus et le cosinus de cet angle permettent alors de calculer les coordonnées x et y du point de la circonférence qui correspond à cet angle<ref>Vous pouvez trouver quelques explications complémentaires à ce sujet, à la page {{todo}}.</ref>. À chaque itération, l'angle ne varie que d'un centième de radian seulement (environ 0,6°), et il faudra donc 628 itérations pour que le mobile effectue un tour complet. La temporisation choisie pour ces itérations se trouve à la ligne 31 : 10 millisecondes. Vous pouvez accélérer le mouvement en diminuant cette valeur, mais vous ne pourrez guère descendre en dessous de 1 milliseconde (0.001 s), ce qui n'est déjà pas si mal.
 
== Notes ==