Comment exécutez-vous votre propre code avec la boucle d'événements de Tkinter?

119

Mon petit frère se lance juste dans la programmation et pour son projet d'expo-sciences, il fait une simulation d'une volée d'oiseaux dans le ciel. Il a écrit la majeure partie de son code, et cela fonctionne bien, mais les oiseaux doivent bouger à chaque instant .

Tkinter, cependant, monopolise le temps de sa propre boucle d'événements, et donc son code ne s'exécutera pas. Faire root.mainloop()s'exécute, s'exécute et continue de s'exécuter, et la seule chose qu'il exécute est les gestionnaires d'événements.

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

En ce moment, il a inventé un horrible hack, liant sa move()fonction à <b1-motion>, de sorte que tant qu'il maintient le bouton enfoncé et agite la souris, cela fonctionne. Mais il doit y avoir un meilleur moyen.

Allan S
la source

Réponses:

142

Utilisez la afterméthode sur l' Tkobjet:

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 de la afterméthode:

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."""
Dave Ray
la source
30
si vous spécifiez le délai d'expiration à 0, la tâche se remettra sur la boucle d'événements immédiatement après la fin. cela ne bloquera pas les autres événements, tout en exécutant votre code aussi souvent que possible.
Nathan
Après m'être arraché les cheveux pendant des heures en essayant de faire fonctionner correctement opencv et tkinter ensemble et se fermer proprement lorsque le bouton [X] a été cliqué, ceci avec win32gui.FindWindow (None, 'window title') a fait l'affaire! Je suis un tel noob ;-)
JxAxMxIxN
Ce n'est pas la meilleure option; bien que cela fonctionne dans ce cas, ce n'est pas bon pour la plupart des scripts (il ne s'exécute que toutes les 2 secondes), et en définissant le délai d'expiration sur 0, selon la suggestion publiée par @Nathan car il ne s'exécute que lorsque tkinter n'est pas occupé (ce qui pourrait causer des problèmes dans certains programmes complexes). Mieux vaut s'en tenir au threadingmodule.
Anonyme
59

La solution publié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 un endroit que j'ai vérifié , une mauvaise gestion des threads avec Tkinter est imprévisible et dépend de la plate-forme.

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

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 le suivant:

# 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)
Kevin
la source
Comment passeriez-vous des arguments à la runméthode? Je n'arrive pas à comprendre comment ...
TheDoctor
5
typiquement, vous passiez des arguments __init__(..), les stockiez selfet les run(..)
utilisiez
1
La racine n'apparaît pas du tout, ce qui donne l'avertissement: `AVERTISSEMENT: les régions de glissement NSWindow ne doivent être invalidées que sur le fil principal! Cela jettera une exception à l'avenir `
Bob Bobster
1
Ce commentaire mérite beaucoup plus de reconnaissance. Incroyable.
Daniel Reyhanian
C'est une bouée de sauvetage. Le code en dehors de l'interface graphique doit vérifier si le thread tkinter est actif si vous ne voulez pas pouvoir quitter le script python une fois que vous avez quitté l'interface graphique. Quelque chose commewhile app.is_alive(): etc
m3nda
21

Lorsque vous écrivez votre propre boucle, comme dans la simulation (je suppose), vous devez appeler la updatefonction qui fait ce que mainloopfait: met à jour la fenêtre avec vos modifications, mais vous le faites dans votre boucle.

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

while 1:
   task()  
jma
la source
10
Vous devez être très prudent avec ce type de programmation. Si des événements provoquent taskl'appel, vous vous retrouverez avec des boucles d'événements imbriquées, et c'est mauvais. À moins que vous ne compreniez parfaitement le fonctionnement des boucles d'événements, vous devez éviter d'appeler updateà tout prix.
Bryan Oakley le
J'ai utilisé cette technique une fois - cela fonctionne bien, mais selon la façon dont vous le faites, vous pourriez avoir des effets stupéfiants dans l'interface utilisateur.
jldupont
@Bryan Oakley La mise à jour est-elle donc une boucle? Et comment cela serait-il problématique?
Green05
6

Une autre option est de laisser tkinter s'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 se tirer une balle dans le pied. Par exemple, vous devez être prudent lorsque vous modifiez les variables membres de la classe exemple ci-dessus afin de ne pas interrompre avec la boucle d'événements de Tkinter.


la source
3
Je ne suis pas sûr que cela puisse fonctionner. J'ai juste essayé quelque chose de similaire et j'obtiens "RuntimeError: le thread principal n'est pas dans la boucle principale".
jldupont
5
jldupont: J'ai "RuntimeError: Calling Tcl from different appartment" (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 () dans.
mgiuca
2

Il s'agit de la première version fonctionnelle de ce qui sera un lecteur GPS et un présentateur de données. tkinter est une chose très fragile avec beaucoup trop peu de messages d'erreur. Il ne met rien en place et ne dit pas pourquoi la plupart du temps. Très difficile venant d'un bon développeur de formulaires WYSIWYG. Quoi qu'il en soit, cela exécute une petite routine 10 fois par seconde et présente les informations sur un formulaire. Il a fallu un certain 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, c'est assez bien pour moi. J'espère que cela 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()
Micheal Morrow
la source