2009-01-19 12 views
83

Mój młodszy brat właśnie zaczyna programować, a dla jego projektu Science Fair przeprowadza symulację stada ptaków na niebie. Napisał większość swojego kodu i działa dobrze, ale ptaki muszą przenosić w każdej chwili.Jak uruchomić własny kod wraz z pętlą zdarzeń Tkintera?

Tkinter, jednak, hogs czas dla własnej pętli zdarzeń, a więc jego kod nie będzie działać. Wykonanie root.mainloop() działa, uruchamia się i działa, a jedyne co uruchamia to programy obsługi zdarzeń.

Czy istnieje sposób na to, aby jego kod był uruchamiany równolegle z krachem (bez wielowątkowości, jest mylący i powinien być prosty), a jeśli tak, to co to jest?

W tej chwili wpadł na brzydkie hakowanie, wiążąc funkcję move() z <b1-motion>, tak więc dopóki przytrzymuje przycisk i porusza myszą, działa. Ale musi być lepszy sposób.

Odpowiedz

105

Użyj metody after na obiekcie 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() 

Oto deklaracja i dokumentacji dla metody 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

jeśli określisz timeout na 0, zadanie powróci do pętli zdarzeń natychmiast po zakończeniu. nie blokuje to innych zdarzeń, a kod jest tak często uruchamiany, jak to tylko możliwe. – Nathan

+0

Po wyciągnięciu włosów na kilka godzin, próbując otworzyć opencv i tkinter, aby prawidłowo współpracowały i czyściły się po kliknięciu przycisku [X], to wraz z win32gui.FindWindow (None, "tytuł okna") zmyśliło! Jestem taki noob ;-) – JxAxMxIxN

2

Innym rozwiązaniem jest umożliwienie tkinter wykonać w osobnym wątku. Jednym ze sposobów to jest tak:

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

Bądź jednak ostrożny, programowanie wielowątkowe jest trudne i jest to naprawdę proste, aby strzelać siebie w stopę. Na przykład musisz zachować ostrożność, zmieniając zmienne składowe powyższej klasy próbek, aby nie przerywać pętli zdarzeń Tkintera.

+2

Wystarczy użyć kolejki, aby komunikować się z wątkiem. – jldupont

+2

Nie wiesz, czy to zadziała. Po prostu wypróbowałem coś podobnego i dostaję "RuntimeError: główny wątek nie jest w głównej pętli". – jldupont

+4

jldupont: Dostałem "RuntimeError: wywołanie Tcl z innego mieszkania" (prawdopodobnie ten sam błąd w innej wersji). Rozwiązaniem było zainicjowanie Tk w run(), a nie w __init __(). Oznacza to, że uruchamiasz Tk w tym samym wątku, w którym wywołujesz funkcję mainloop(). – mgiuca

35

Rozwiązanie opublikowane przez Bjorn powoduje wyświetlenie komunikatu "RuntimeError: Calling Tcl z innego mieszkania" na moim komputerze (RedHat Enterprise 5, python 2.6.1). Bjorn mógł nie otrzymać tej wiadomości, ponieważ, zgodnie z one place I checked, niewłaściwe obchodzenie się z Tkinter jest nieprzewidywalne i zależne od platformy.

Problem polega na tym, że numer app.start() liczy się jako odniesienie do Tk, ponieważ aplikacja zawiera elementy Tk. Naprawiłem to, zastępując app.start() z self.start() wewnątrz __init__. Udało mi się również, aby wszystkie odwołania TK były wewnątrz funkcji , która wywołuje mainloop() lub są w ramach funkcji , które są wywoływane przez funkcji, która wywołuje mainloop() (jest to najwyraźniej ważne, aby uniknąć błędu "inny apartament").

W końcu dodałem obsługę protokołu z wywołaniem zwrotnym, ponieważ bez tego program kończy pracę z błędem, gdy okno Tk jest zamykane przez użytkownika.

Zmodyfikowany kod wygląda następująco:

# 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

W jaki sposób przekażesz argumenty do metody 'run'? Nie mogę wymyślić, jak ... – TheDoctor

+1

zwykle przekazywałbyś argumenty do '__init __ (..)', przechowuj je w 'self' i używaj ich w' run (..) ' –

15

Pisząc swoją pętlę, jak w symulacji (zakładam), należy wywołać funkcję update który robi co mainloop robi: uaktualnia okno ze zmianami, ale robisz to w swojej pętli.

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

while 1: 
    task() 
+8

Musisz być Bardzo ostrożnie z tego rodzaju programowaniem. Jeśli jakiekolwiek zdarzenia powodują wywołanie 'task', otrzymasz zagnieżdżone pętle zdarzeń, a to jest złe. Dopóki w pełni nie wiesz, jak działają pętle zdarzeń, powinieneś unikać wywoływania 'update' za wszelką cenę. –

+0

Użyłem tej techniki raz - działa dobrze, ale w zależności od tego, jak to robisz, możesz mieć oszałamiające wrażenie w interfejsie użytkownika. – jldupont

2

To jest pierwsza działająca wersja tego, co będzie czytnikiem GPS i prezentera danych. tkinter jest bardzo delikatną rzeczą o zbyt małej liczbie komunikatów o błędach. Nie umieszcza rzeczy i nie wyjaśnia, dlaczego tak wiele czasu. Bardzo trudne z dobrego programisty formularzy WYSIWYG. W każdym razie, to uruchamia małą rutynę 10 razy na sekundę i przedstawia informacje na formularzu. Zajęło to trochę czasu, aby to się stało. Kiedy próbowałem wartość licznika czasu 0, formularz nigdy nie pojawił się. Teraz boli mnie głowa! 10 lub więcej razy na sekundę jest wystarczająco dobre dla mnie. Mam nadzieję, że pomoże to komuś innemu. 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() 
Powiązane problemy