154 votes

Comment exécuter votre propre code avec la boucle d'événement de Tkinter ?

Mon petit frère commence tout juste à programmer, et pour son projet de foire scientifique, il fait une simulation d'une volée d'oiseaux dans le ciel. Il a écrit la plus grande partie de son code, et ça marche bien, mais les oiseaux doivent bouger chaque instant .

Tkinter, cependant, monopolise le temps pour sa propre boucle d'événement, 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 qui s'exécute est le gestionnaire d'événements.

Existe-t-il un moyen de faire en sorte que son code s'exécute en même temps que la boucle principale (sans multithreading, c'est confus et cela doit rester simple), et si oui, quel est-il ?

Pour l'instant, il est venu avec un affreux piratage, attachant son move() à la fonction <b1-motion> de sorte que tant qu'il maintient le bouton enfoncé et remue la souris, ça fonctionne. Mais il doit y avoir un meilleur moyen.

178voto

Dave Ray Points 20873

Utilisez le after sur la méthode Tk objet :

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 le after método:

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

41 votes

Si vous spécifiez que le délai est de 0, la tâche se remettra dans la boucle d'événement immédiatement après avoir terminé. cela ne bloquera pas d'autres événements, tout en exécutant votre code aussi souvent que possible.

2 votes

Après m'être arraché les cheveux pendant des heures en essayant de faire fonctionner opencv et tkinter ensemble correctement et de les fermer proprement lorsque le bouton [X] était cliqué, ceci avec win32gui.FindWindow(None, 'window title') a fait l'affaire ! Je suis un tel noob ;-)

2 votes

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 fixant le timeout à 0, selon la suggestion postée par @Nathan parce qu'il ne s'exécute que lorsque tkinter n'est pas occupé (ce qui pourrait causer des problèmes dans certains programmes complexes). Le mieux est de s'en tenir à l'option threading module.

73voto

Kevin Points 131

El solution affichée par Bjorn résulte en un "RuntimeError : Calling Tcl from different appartment" sur mon ordinateur (RedHat Enterprise 5, python 2.6.1). Bjorn n'a peut-être pas eu ce message, puisque, selon un endroit où j'ai vérifié la mauvaise gestion du threading 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 app contient des éléments Tk. J'ai corrigé cela en remplaçant app.start() con un self.start() à l'intérieur de __init__ . J'ai également fait en sorte que toutes les références à Tk se trouvent soit à l'intérieur du fichier qui appelle mainloop() ou sont à l'intérieur qui sont appelées par la fonction qui appelle mainloop() (ceci est apparemment essentiel pour éviter l'erreur "appartement différent").

Enfin, j'ai ajouté un gestionnaire de protocole avec un callback, car sans cela le programme sort 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)

0 votes

Comment passer des arguments à la run méthode ? Je n'arrive pas à trouver comment...

6 votes

En général, vous passez des arguments à __init__(..) et les stocker dans self et les utiliser dans run(..)

2 votes

La racine ne s'affiche pas du tout, donnant l'avertissement suivant : ` AVERTISSEMENT : Les régions de déplacement de NSWindow ne doivent être invalidées que sur le fil principal ! Ceci lancera une exception dans le futur `

28voto

jma Points 177

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

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

while 1:
   task()

14 votes

Vous devez être très prudent avec ce genre de programmation. Si un événement provoque task pour être 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 devriez éviter d'appeler update à tout prix.

0 votes

J'ai utilisé cette technique une fois. Elle fonctionne bien, mais selon la façon dont vous la réalisez, vous pouvez avoir un certain décalage dans l'interface utilisateur.

0 votes

@Bryan Oakley La mise à jour est une boucle alors ? Et en quoi cela poserait-il problème ?

8voto

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 très facile de se tirer une balle dans le pied. Par exemple, vous devez faire attention lorsque vous changez les variables membres de la classe d'exemple ci-dessus afin de ne pas interrompre la boucle d'événement de Tkinter.

3voto

Micheal Morrow Points 73

Ceci est 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 pas de choses en place et ne dit pas pourquoi la plupart du temps. Très difficile venant d'un bon développeur de formulaire WYSIWYG. Bref, ceci exécute une petite routine 10 fois par seconde et présente l'information sur un formulaire. Il a fallu un certain temps pour y arriver. Lorsque j'ai essayé une valeur de timer de 0, le formulaire n'est jamais apparu. J'ai maintenant mal à la tête ! 10 fois par seconde ou plus, c'est suffisant pour moi. J'espère que cela aidera 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()

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X