Wie führen Sie Ihren eigenen Code neben der Ereignisschleife von Tkinter aus?

119

Mein kleiner Bruder beginnt gerade mit dem Programmieren und für sein Science Fair-Projekt simuliert er einen Vogelschwarm am Himmel. Er hat den größten Teil seines Codes geschrieben und es funktioniert gut, aber die Vögel müssen sich jeden Moment bewegen .

Tkinter beansprucht jedoch die Zeit für eine eigene Ereignisschleife, sodass sein Code nicht ausgeführt wird. Läuft root.mainloop(), läuft und läuft weiter, und das einzige, was ausgeführt wird, sind die Ereignishandler.

Gibt es eine Möglichkeit, seinen Code neben dem Mainloop auszuführen (ohne Multithreading ist dies verwirrend und sollte einfach gehalten werden), und wenn ja, was ist das?

Im Moment hat er sich einen hässlichen Hack ausgedacht, an den er seine move()Funktion gebunden hat <b1-motion>, so dass es funktioniert , solange er den Knopf gedrückt hält und mit der Maus wackelt. Aber es muss einen besseren Weg geben.

Allan S.
quelle

Antworten:

140

Verwenden Sie die afterMethode für das TkObjekt:

from tkinter import *

root = Tk()

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

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

Hier ist die Deklaration und Dokumentation für die afterMethode:

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
quelle
29
Wenn Sie das Zeitlimit auf 0 setzen, wird die Aufgabe unmittelbar nach Abschluss wieder in die Ereignisschleife versetzt. Dadurch werden andere Ereignisse nicht blockiert, während Ihr Code so oft wie möglich ausgeführt wird.
Nathan
Nachdem ich mir stundenlang die Haare ausgezogen hatte, um zu versuchen, opencv und tkinter richtig und sauber zu schließen, als auf die Schaltfläche [X] geklickt wurde, hat dies zusammen mit win32gui.FindWindow (Keine, 'Fenstertitel') den Trick getan! Ich bin so ein Noob ;-)
JxAxMxIxN
Dies ist nicht die beste Option. Obwohl es in diesem Fall funktioniert, ist es für die meisten Skripte nicht gut (es wird nur alle 2 Sekunden ausgeführt) und setzt das Zeitlimit gemäß dem von @Nathan veröffentlichten Vorschlag auf 0, da es nur ausgeführt wird, wenn tkinter nicht beschäftigt ist (was möglich ist) Probleme in einigen komplexen Programmen verursachen). Am besten beim threadingModul bleiben .
Anonym
59

Die von Björn veröffentlichte Lösung führt auf meinem Computer (RedHat Enterprise 5, Python 2.6.1) zu der Meldung "RuntimeError: Tcl aus einer anderen Wohnung aufrufen". Björn hat diese Nachricht möglicherweise nicht erhalten, da laut einer von mir überprüften Stelle das falsche Behandeln von Threading mit Tkinter unvorhersehbar und plattformabhängig ist.

Das Problem scheint zu sein, dass dies app.start()als Referenz auf Tk gilt, da die App Tk-Elemente enthält. Ich habe dies behoben, indem ich es durch app.start()ein self.start()Inneres ersetzt habe __init__. Ich habe es auch so gemacht, dass sich alle Tk-Referenzen entweder innerhalb der aufrufenden Funktionmainloop() oder innerhalb von Funktionen befinden, die von der aufrufenden Funktion aufgerufen werdenmainloop() (dies ist anscheinend kritisch, um den Fehler "andere Wohnung" zu vermeiden).

Schließlich habe ich einen Protokollhandler mit einem Rückruf hinzugefügt, da das Programm ohne diesen mit einem Fehler beendet wird, wenn das Tk-Fenster vom Benutzer geschlossen wird.

Der überarbeitete Code lautet wie folgt:

# 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
quelle
Wie würden Sie Argumente an die runMethode übergeben? Ich kann nicht herausfinden, wie man ...
TheDoctor
5
Normalerweise übergibt man Argumente an __init__(..), speichert sie selfund verwendet sie inrun(..)
Andre Holzner
1
Die Wurzel wird überhaupt nicht angezeigt und gibt die Warnung aus: `WARNUNG: NSWindow-Drag-Regionen sollten nur im Haupt-Thread ungültig gemacht werden! Dies wird in Zukunft eine Ausnahme
auslösen
1
Dieser Kommentar verdient viel mehr Anerkennung. Tolle.
Daniel Reyhanian
Dies ist ein Lebensretter. Code außerhalb der GUI sollte prüfen, ob der tkinter-Thread aktiv ist, wenn Sie das Python-Skript nach dem Beenden der GUI nicht beenden können. So etwas wiewhile app.is_alive(): etc
m3nda
20

Wenn Sie Ihre eigene Schleife schreiben, wie in der Simulation (ich nehme an), müssen Sie die updateFunktion aufrufen, die das tut, was sie mainlooptut: Aktualisiert das Fenster mit Ihren Änderungen, aber Sie tun es in Ihrer Schleife.

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

while 1:
   task()  
jma
quelle
10
Bei dieser Art der Programmierung muss man sehr vorsichtig sein. Wenn Ereignisse taskaufgerufen werden, werden verschachtelte Ereignisschleifen angezeigt, und das ist schlecht. Wenn Sie nicht vollständig verstehen, wie Ereignisschleifen funktionieren, sollten Sie um updatejeden Preis einen Anruf vermeiden .
Bryan Oakley
Ich habe diese Technik einmal verwendet - funktioniert einwandfrei, aber je nachdem, wie Sie es tun, kann es zu Staffelungen in der Benutzeroberfläche kommen.
Jldupont
6

Eine andere Möglichkeit besteht darin, tkinter in einem separaten Thread ausführen zu lassen. Eine Möglichkeit ist wie folgt:

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

Seien Sie jedoch vorsichtig, Multithread-Programmierung ist schwierig und es ist wirklich einfach, sich selbst in den Fuß zu schießen. Zum Beispiel müssen Sie vorsichtig sein, wenn Sie Mitgliedsvariablen der obigen Beispielklasse ändern, damit Sie nicht mit der Ereignisschleife von Tkinter unterbrechen.


quelle
3
Ich bin mir nicht sicher, ob das funktionieren kann. Ich habe gerade etwas Ähnliches ausprobiert und bekomme "RuntimeError: Hauptthread befindet sich nicht in der Hauptschleife".
Jldupont
5
jldupont: Ich habe "RuntimeError: Aufruf von Tcl aus einer anderen Wohnung" (möglicherweise der gleiche Fehler in einer anderen Version). Das Update bestand darin, Tk in run () zu initialisieren, nicht in __init __ (). Dies bedeutet, dass Sie Tk in demselben Thread initialisieren, in dem Sie mainloop () in
aufrufen
2

Dies ist die erste funktionierende Version eines GPS-Lesegeräts und Datenpräsentators. tkinter ist eine sehr fragile Sache mit viel zu wenigen Fehlermeldungen. Es stellt keine Sachen auf und sagt nicht, warum die meiste Zeit. Sehr schwierig von einem guten WYSIWYG-Formularentwickler zu kommen. Auf jeden Fall wird 10 Mal pro Sekunde eine kleine Routine ausgeführt, und die Informationen werden auf einem Formular angezeigt. Es hat eine Weile gedauert, bis es soweit war. Als ich einen Timer-Wert von 0 versuchte, wurde das Formular nie angezeigt. Mein Kopf tut jetzt weh! 10 oder mehr Mal pro Sekunde ist gut genug für mich. Ich hoffe es hilft jemand anderem. 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
quelle