2009-01-19 7 views
83

Mon petit frère commence tout juste à programmer, et pour son projet de Science Fair, il fait une simulation d'une volée d'oiseaux dans le ciel. Il a écrit la plupart de son code, et cela fonctionne bien, mais les oiseaux doivent bouger à chaque instant. Tkinter, cependant, fait du temps pour sa propre boucle d'événements, et donc son code ne fonctionnera pas. Faire root.mainloop() s'exécute, s'exécute et continue à fonctionner, et la seule chose qu'il exécute est les gestionnaires d'événements.Comment lancez-vous votre propre code à côté de la boucle d'événement de Tkinter?

Existe-t-il un moyen de faire fonctionner son code à côté de la boucle principale (sans multithreading, c'est déroutant et cela devrait être simple), et si oui, qu'est-ce que c'est?

À l'heure actuelle, il est venu avec un hack laid, attachant sa fonction move() à <b1-motion>, de sorte que tant qu'il appuie sur le bouton et se tortille la souris, cela fonctionne. Mais il doit y avoir un meilleur moyen.

Répondre

105

Utilisez la méthode after sur l'objet Tk:

from tkinter import * 

root = Tk() 

def task(): 
    print("hello") 
    root.after(2000, task) # reschedule event in 2 seconds 

root.after(2000, task) 
root.mainloop() 

Voici la déclaration et la documentation pour la méthode after:

def after(self, ms, func=None, *args): 
    """Call function once after given time. 

    MS specifies the time in milliseconds. FUNC gives the 
    function which shall be called. Additional parameters 
    are given as parameters to the function call. Return 
    identifier to cancel scheduling with after_cancel.""" 
+19

Si vous spécifiez le délai d'expiration à 0, la tâche se replacera sur la boucle d'événement immédiatement après la fin. Cela ne bloque pas les autres événements, tout en exécutant votre code aussi souvent que possible. – Nathan

+0

Après avoir tiré mes cheveux pendant des heures en essayant de faire fonctionner opencv et tkinter ensemble correctement et fermer correctement lorsque le bouton [X] a été cliqué, cela avec win32gui.FindWindow (None, 'window title') a fait l'affaire! Je suis un tel noob ;-) – JxAxMxIxN

2

Une autre option est de laisser tkinter exécuter sur un thread séparé. Une façon de le faire est comme ceci:

import Tkinter 
import threading 

class MyTkApp(threading.Thread): 
    def __init__(self): 
     self.root=Tkinter.Tk() 
     self.s = Tkinter.StringVar() 
     self.s.set('Foo') 
     l = Tkinter.Label(self.root,textvariable=self.s) 
     l.pack() 
     threading.Thread.__init__(self) 

    def run(self): 
     self.root.mainloop() 


app = MyTkApp() 
app.start() 

# Now the app should be running and the value shown on the label 
# can be changed by changing the member variable s. 
# Like this: 
# app.s.set('Bar') 

Attention cependant, la programmation multithread est difficile et il est vraiment facile de tirer sur votre auto dans le pied. Par exemple, vous devez faire attention lorsque vous modifiez les variables membres de la classe d'échantillon ci-dessus afin de ne pas interrompre avec la boucle d'événement de Tkinter.

+2

Utilisez simplement une file d'attente pour communiquer avec le fil. – jldupont

+2

Je ne sais pas si cela peut fonctionner. Juste essayé quelque chose de similaire et j'ai "RuntimeError: le thread principal n'est pas dans la boucle principale". – jldupont

+4

jldupont: J'ai "RuntimeError: Appeler Tcl de différents appartements" (peut-être la même erreur dans une version différente). Le correctif consistait à initialiser Tk dans run(), pas dans __init __(). Cela signifie que vous initialisez Tk dans le même thread que vous appelez mainloop(). – mgiuca

35

La solution postée par Bjorn aboutit à un message "RuntimeError: Calling Tcl from different appartment" sur mon ordinateur (RedHat Enterprise 5, python 2.6.1). Bjorn n'a peut-être pas reçu ce message, car, selon one place I checked, le traitement incorrect du thread avec Tkinter est imprévisible et dépend de la plate-forme.

Le problème semble être que app.start() compte comme une référence à Tk, puisque l'application contient des éléments Tk. J'ai corrigé ceci en remplaçant app.start() par un self.start() à l'intérieur __init__. J'ai également fait en sorte que toutes les références Tk sont soit dans la fonction qui appelle mainloop() ou sont à l'intérieur fonctions qui sont appelées par la fonction qui appelle mainloop() (ceci est apparemment critique pour éviter l'erreur "appartement différent").

Enfin, j'ai ajouté un gestionnaire de protocole avec un rappel, car sans cela le programme se termine avec une erreur lorsque la fenêtre Tk est fermée par l'utilisateur.

Le code révisé est comme suit:

# Run tkinter code in another thread 

import tkinter as tk 
import threading 

class App(threading.Thread): 

    def __init__(self): 
     threading.Thread.__init__(self) 
     self.start() 

    def callback(self): 
     self.root.quit() 

    def run(self): 
     self.root = tk.Tk() 
     self.root.protocol("WM_DELETE_WINDOW", self.callback) 

     label = tk.Label(self.root, text="Hello World") 
     label.pack() 

     self.root.mainloop() 


app = App() 
print('Now we can continue running code while mainloop runs!') 

for i in range(100000): 
    print(i) 
+0

Comment passeriez-vous des arguments à la méthode 'run'? Je n'arrive pas à comprendre comment ... – TheDoctor

+1

typiquement vous passeriez des arguments à '__init __ (..)', les stockez dans 'self' et les utiliserez dans' run (..) ' –

15

Lorsque vous rédigez votre propre boucle, comme dans la simulation (je suppose), vous devez appeler la fonction update qui fait ce que le mainloop fait: met à jour la fenêtre avec vos changements, mais vous le faites dans votre boucle.

def task(): 
    # do something 
    root.update() 

while 1: 
    task() 
+8

Vous devez être _very_ attention à ce genre de programmation. Si des événements provoquent l'appel de 'task', vous finirez avec des boucles d'événements imbriquées, et c'est mauvais. À moins que vous compreniez parfaitement comment les boucles d'événements fonctionnent, vous devriez éviter d'appeler 'update' à tout prix. –

+0

J'ai utilisé cette technique une fois - fonctionne bien mais selon la façon dont vous le faites, vous pourriez avoir une certaine stupéfaction dans l'interface utilisateur. – jldupont

2

Ceci est la première version de travail de ce qui sera un lecteur GPS et un présentateur de données. tkinter est une chose très fragile avec trop peu de messages d'erreur. Il ne met pas les choses en place et ne dit pas pourquoi la plupart du temps. Très difficile venant d'un bon développeur de forme WYSIWYG. Quoi qu'il en soit, cela exécute une petite routine 10 fois par seconde et présente l'information sur un formulaire. Ça a pris du temps pour y arriver. Lorsque j'ai essayé une valeur de minuterie de 0, le formulaire ne s'est jamais présenté. Ma tête me fait mal maintenant! 10 fois ou plus par seconde est assez bon pour moi. J'espère que ça aide quelqu'un d'autre. Mike Morrow

import tkinter as tk 
import time 

def GetDateTime(): 
    # Get current date and time in ISO8601 
    # https://en.wikipedia.org/wiki/ISO_8601 
    # https://xkcd.com/1179/ 
    return (time.strftime("%Y%m%d", time.gmtime()), 
      time.strftime("%H%M%S", time.gmtime()), 
      time.strftime("%Y%m%d", time.localtime()), 
      time.strftime("%H%M%S", time.localtime())) 

class Application(tk.Frame): 

    def __init__(self, master): 

    fontsize = 12 
    textwidth = 9 

    tk.Frame.__init__(self, master) 
    self.pack() 

    tk.Label(self, font=('Helvetica', fontsize), bg = '#be004e', fg = 'white', width = textwidth, 
      text='Local Time').grid(row=0, column=0) 
    self.LocalDate = tk.StringVar() 
    self.LocalDate.set('waiting...') 
    tk.Label(self, font=('Helvetica', fontsize), bg = '#be004e', fg = 'white', width = textwidth, 
      textvariable=self.LocalDate).grid(row=0, column=1) 

    tk.Label(self, font=('Helvetica', fontsize), bg = '#be004e', fg = 'white', width = textwidth, 
      text='Local Date').grid(row=1, column=0) 
    self.LocalTime = tk.StringVar() 
    self.LocalTime.set('waiting...') 
    tk.Label(self, font=('Helvetica', fontsize), bg = '#be004e', fg = 'white', width = textwidth, 
      textvariable=self.LocalTime).grid(row=1, column=1) 

    tk.Label(self, font=('Helvetica', fontsize), bg = '#40CCC0', fg = 'white', width = textwidth, 
      text='GMT Time').grid(row=2, column=0) 
    self.nowGdate = tk.StringVar() 
    self.nowGdate.set('waiting...') 
    tk.Label(self, font=('Helvetica', fontsize), bg = '#40CCC0', fg = 'white', width = textwidth, 
      textvariable=self.nowGdate).grid(row=2, column=1) 

    tk.Label(self, font=('Helvetica', fontsize), bg = '#40CCC0', fg = 'white', width = textwidth, 
      text='GMT Date').grid(row=3, column=0) 
    self.nowGtime = tk.StringVar() 
    self.nowGtime.set('waiting...') 
    tk.Label(self, font=('Helvetica', fontsize), bg = '#40CCC0', fg = 'white', width = textwidth, 
      textvariable=self.nowGtime).grid(row=3, column=1) 

    tk.Button(self, text='Exit', width = 10, bg = '#FF8080', command=root.destroy).grid(row=4, columnspan=2) 

    self.gettime() 
    pass 

    def gettime(self): 
    gdt, gtm, ldt, ltm = GetDateTime() 
    gdt = gdt[0:4] + '/' + gdt[4:6] + '/' + gdt[6:8] 
    gtm = gtm[0:2] + ':' + gtm[2:4] + ':' + gtm[4:6] + ' Z' 
    ldt = ldt[0:4] + '/' + ldt[4:6] + '/' + ldt[6:8] 
    ltm = ltm[0:2] + ':' + ltm[2:4] + ':' + ltm[4:6] 
    self.nowGtime.set(gdt) 
    self.nowGdate.set(gtm) 
    self.LocalTime.set(ldt) 
    self.LocalDate.set(ltm) 

    self.after(100, self.gettime) 
    #print (ltm) # Prove it is running this and the external code, too. 
    pass 

root = tk.Tk() 
root.wm_title('Temp Converter') 
app = Application(master=root) 

w = 200 # width for the Tk root 
h = 125 # height for the Tk root 

# get display screen width and height 
ws = root.winfo_screenwidth() # width of the screen 
hs = root.winfo_screenheight() # height of the screen 

# calculate x and y coordinates for positioning the Tk root window 

#centered 
#x = (ws/2) - (w/2) 
#y = (hs/2) - (h/2) 

#right bottom corner (misfires in Win10 putting it too low. OK in Ubuntu) 
x = ws - w 
y = hs - h - 35 # -35 fixes it, more or less, for Win10 

#set the dimensions of the screen and where it is placed 
root.geometry('%dx%d+%d+%d' % (w, h, x, y)) 

root.mainloop() 
Questions connexes