Come esegui il tuo codice insieme al ciclo di eventi di Tkinter?


119

Il mio fratellino sta iniziando a programmare e per il suo progetto Science Fair sta simulando uno stormo di uccelli nel cielo. Ha scritto la maggior parte del suo codice e funziona bene, ma gli uccelli devono muoversi in ogni momento .

Tkinter, tuttavia, consuma il tempo per il proprio ciclo di eventi, quindi il suo codice non verrà eseguito. Facendo root.mainloop()corre, corre, e continua a funzionare, e l'unica cosa che funziona è i gestori di eventi.

C'è un modo per far funzionare il suo codice insieme al mainloop (senza multithreading, è fonte di confusione e questo dovrebbe essere mantenuto semplice), e se sì, che cos'è?

In questo momento, ha escogitato un brutto trucco, legando la sua move()funzione a <b1-motion>, in modo che finché tiene premuto il pulsante e muove il mouse, funziona. Ma ci deve essere un modo migliore.

Risposte:


141

Usa il aftermetodo Tksull'oggetto:

from tkinter import *

root = Tk()

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

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

Ecco la dichiarazione e la documentazione per il aftermetodo:

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

30
se si specifica che il timeout è 0, l'attività si rimetterà nel ciclo di eventi immediatamente dopo aver terminato. questo non bloccherà altri eventi, pur continuando a eseguire il codice il più spesso possibile.
Nathan

Dopo essermi strappato i capelli per ore cercando di far funzionare opencv e tkinter insieme correttamente e chiudersi in modo pulito quando è stato cliccato il pulsante [X], questo insieme a win32gui.FindWindow (Nessuno, 'titolo della finestra') ha fatto il trucco! Sono un tale noob ;-)
JxAxMxIxN

Questa non è l'opzione migliore; sebbene funzioni in questo caso, non va bene per la maggior parte degli script (viene eseguito solo ogni 2 secondi) e imposta il timeout su 0, secondo il suggerimento pubblicato da @Nathan perché viene eseguito solo quando tkinter non è occupato (il che potrebbe causare problemi in alcuni programmi complessi). Meglio restare con il threadingmodulo.
Anonimo

59

La soluzione pubblicata da Bjorn risulta in un messaggio "RuntimeError: Calling Tcl from different appartment" sul mio computer (RedHat Enterprise 5, python 2.6.1). Bjorn potrebbe non aver ricevuto questo messaggio, poiché, secondo un punto in cui ho controllato , la cattiva gestione del threading con Tkinter è imprevedibile e dipendente dalla piattaforma.

Il problema sembra essere che app.start()conta come riferimento a Tk, poiché l'app contiene elementi Tk. Ho risolto questo problema sostituendolo app.start()con un self.start()interno __init__. Ho anche fatto in modo che tutti i riferimenti Tk siano o all'interno della funzione che chiamamainloop() o sono all'interno di funzioni che sono chiamate dalla funzione che chiama mainloop()(questo è apparentemente critico per evitare l'errore "diverso apartment").

Infine, ho aggiunto un gestore di protocollo con un callback, poiché senza questo il programma esce con un errore quando la finestra di Tk viene chiusa dall'utente.

Il codice rivisto è il seguente:

# 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)

Come passereste gli argomenti al runmetodo? Non riesco a capire come ...
TheDoctor

5
tipicamente passeresti gli argomenti __init__(..), li memorizzerai selfe li userai inrun(..)
Andre Holzner

1
La radice non viene visualizzata affatto, dando l'avviso: `ATTENZIONE: le regioni di trascinamento di NSWindow dovrebbero essere invalidate solo sul thread principale! Questo creerà un'eccezione in futuro `
Bob Bobster

1
Questo commento merita molto più riconoscimento. Sorprendente.
Daniel Reyhanian

Questo è un salvavita. Il codice esterno alla GUI dovrebbe controllare che il thread tkinter sia attivo se non si vuole essere in grado di uscire dallo script python una volta usciti dalla GUI. Qualcosa di similewhile app.is_alive(): etc
m3nda

21

Quando scrivi il tuo ciclo, come nella simulazione (presumo), devi chiamare la updatefunzione che fa quello che mainloopfa: aggiorna la finestra con le tue modifiche, ma lo fai nel tuo ciclo.

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

while 1:
   task()  

10
Devi stare molto attento con questo tipo di programmazione. Se qualsiasi evento causa la taskchiamata, ti ritroverai con loop di eventi nidificati, e questo è un male. A meno che tu non comprenda appieno come funzionano i loop di eventi, dovresti evitare di chiamare updatea tutti i costi.
Bryan Oakley

Ho usato questa tecnica una volta: funziona bene ma a seconda di come lo fai, potresti avere qualche sconcertante nell'interfaccia utente.
jldupont

@Bryan Oakley Quindi l'aggiornamento è un ciclo? E come sarebbe problematico?
Green05

6

Un'altra opzione è lasciare che tkinter venga eseguito su un thread separato. Un modo per farlo è così:

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')

Attenzione però, la programmazione multithread è difficile ed è davvero facile spararsi ai piedi. Ad esempio, devi stare attento quando modifichi le variabili membro della classe di esempio sopra in modo da non interrompere il ciclo di eventi di Tkinter.


3
Non sono sicuro che possa funzionare. Ho appena provato qualcosa di simile e ottengo "RuntimeError: thread principale non è nel ciclo principale".
jldupont

5
jldupont: ho ricevuto "RuntimeError: Calling Tcl from different appartment" (forse lo stesso errore in una versione diversa). La soluzione era inizializzare Tk in run (), non in __init __ (). Ciò significa che stai inizializzando Tk nello stesso thread che chiami mainloop () in.
mgiuca

2

Questa è la prima versione funzionante di quello che sarà un lettore GPS e un presentatore di dati. tkinter è una cosa molto fragile con troppi pochi messaggi di errore. Non mette roba e non dice perché per la maggior parte del tempo. Molto difficile proveniente da un buon sviluppatore di moduli WYSIWYG. Ad ogni modo, questo esegue una piccola routine 10 volte al secondo e presenta le informazioni su un modulo. Ci è voluto un po 'per realizzarlo. Quando ho provato un valore del timer di 0, il modulo non è mai stato visualizzato. Adesso mi fa male la testa! 10 o più volte al secondo è abbastanza buono per me. Spero che aiuti qualcun altro. 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()
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.