Jak uruchomić własny kod razem z pętlą zdarzeń Tkintera?

119

Mój młodszy brat dopiero zaczyna programować, a w ramach swojego projektu Science Fair przeprowadza symulację stada ptaków na niebie. Napisał większość swojego kodu i działa dobrze, ale ptaki muszą się poruszać w każdej chwili .

Jednak Tkinter poświęca czas na własną pętlę zdarzeń, więc jego kod nie będzie działać. Wykonywanie root.mainloop(), uruchamianie i kontynuowanie działania, a jedyne, co uruchamia, to programy obsługi zdarzeń.

Czy istnieje sposób, aby jego kod działał obok pętli głównej (bez wielowątkowości, jest to mylące i powinno być proste), a jeśli tak, to co to jest?

W tej chwili wymyślił brzydki hack, do którego przypisał swoją move()funkcję <b1-motion>, więc tak długo, jak przytrzyma przycisk i porusza myszą, działa. Ale musi być lepszy sposób.

Allan S.
źródło

Odpowiedzi:

141

Użyj aftermetody na Tkobiekcie:

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 dokumentacja aftermetody:

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
źródło
30
jeśli określisz limit czasu na 0, zadanie wróci do pętli zdarzeń natychmiast po zakończeniu. nie spowoduje to zablokowania innych zdarzeń, podczas gdy kod będzie wykonywany tak często, jak to możliwe.
Nathan
Po kilku godzinach wyciągania włosów z głowy, próbując zmusić opencv i tkinter do poprawnej współpracy i czystego zamknięcia po kliknięciu przycisku [X], razem z win32gui.FindWindow (brak, 'tytuł okna') załatwiło sprawę! Jestem takim noobem ;-)
JxAxMxIxN
To nie jest najlepsza opcja; chociaż działa w tym przypadku, nie jest dobre dla większości skryptów (działa tylko co 2 sekundy) i ustawiając limit czasu na 0, zgodnie z sugestią wysłaną przez @Nathan, ponieważ działa tylko wtedy, gdy tkinter nie jest zajęty (co może powodować problemy w niektórych złożonych programach). Najlepiej trzymać się threadingmodułu.
Anonimowy
59

Plik Rozwiązanie wysłane przez Bjorn wyników w „RuntimeError: Wywoływanie Tcl z innym mieszkaniu” wiadomości na moim komputerze (RedHat Enterprise 5, Python 2.6.1). Bjorn mógł nie otrzymać tej wiadomości, ponieważ według jednego z miejsc, które sprawdziłem , niewłaściwa obsługa wątków w Tkinter jest nieprzewidywalna i zależna od platformy.

Wydaje się, że problem polega na tym, że app.start()liczy się jako odniesienie do Tk, ponieważ aplikacja zawiera elementy Tk. Naprawiłem to poprzez zastąpienie app.start()z self.start()wewnątrz __init__. Zrobiłem też to tak, że wszystkie odwołania Tk znajdują się wewnątrz funkcji, która wywołujemainloop() albo są wewnątrz funkcji, które są wywoływane przez funkcję, która wywołuje mainloop()(jest to najwyraźniej krytyczne, aby uniknąć błędu „inne mieszkanie”).

Na koniec 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.

Zmieniony kod jest następujący:

# 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
źródło
Jak przekazałbyś argumenty do runmetody? Wydaje się, że nie wiem, jak ...
TheDoctor
5
zazwyczaj przekazujesz argumenty __init__(..), przechowujesz je selfi używaszrun(..)
Andre Holzner
1
Korzeń w ogóle się nie pojawia, dając ostrzeżenie: `` OSTRZEŻENIE: regiony przeciągania NSWindow powinny być unieważniane tylko w głównym wątku! Spowoduje to w przyszłości wyjątek ”
Bob Bobster
1
Ten komentarz zasługuje na znacznie większe uznanie. Niesamowity.
Daniel Reyhanian
To ratuje życie. Kod poza GUI powinien sprawdzać, czy wątek tkinter jest aktywny, jeśli nie chcesz mieć możliwości wyjścia ze skryptu Pythona po wyjściu z GUI. Coś w rodzajuwhile app.is_alive(): etc
m3nda
21

Pisząc własną pętlę, tak jak w symulacji (zakładam), musisz wywołać updatefunkcję, która robi to, co mainlooprobi: aktualizuje okno twoimi zmianami, ale robisz to w swojej pętli.

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

while 1:
   task()  
jma
źródło
10
Przy tego rodzaju programowaniu trzeba być bardzo ostrożnym. Jeśli jakieś zdarzenia powodują taskwywołanie, otrzymasz zagnieżdżone pętle zdarzeń, a to źle. Jeśli nie rozumiesz w pełni, jak działają pętle zdarzeń, powinieneś unikać dzwonienia updateza wszelką cenę.
Bryan Oakley,
Użyłem tej techniki raz - działa OK, ale w zależności od tego, jak to zrobisz, możesz mieć pewne oszołomienie w interfejsie użytkownika.
jldupont,
@Bryan Oakley Czy zatem aktualizacja to pętla? A jakie to byłoby problematyczne?
Green 05
6

Inną opcją jest zezwolenie tkinterowi na wykonanie w osobnym wątku. Można to zrobić 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')

Uważaj jednak, programowanie wielowątkowe jest trudne i naprawdę łatwo jest strzelić sobie w stopę. Na przykład, musisz być ostrożny, kiedy zmieniasz zmienne składowe klasy przykładowej powyżej, aby nie przerywać pętli zdarzeń Tkintera.


źródło
3
Nie jestem pewien, czy to zadziała. Właśnie spróbowałem czegoś podobnego i otrzymałem komunikat „RuntimeError: główny wątek nie znajduje się w głównej pętli”.
jldupont,
5
jldupont: Otrzymałem „RuntimeError: Calling Tcl from another appartment” (prawdopodobnie ten sam błąd w innej wersji). Poprawka polegała na zainicjowaniu Tk w run (), a nie w __init __ (). Oznacza to, że inicjalizujesz Tk w tym samym wątku, w którym wywołujesz mainloop () w.
mgiuca
2

To pierwsza działająca wersja tego, co będzie czytnikiem GPS i prezenterem danych. tkinter jest bardzo delikatną rzeczą ze zbyt małą liczbą komunikatów o błędach. Nie umieszcza rzeczy i nie mówi dlaczego przez większość czasu. Bardzo trudne od dobrego programisty formularzy WYSIWYG. W każdym razie uruchamia to małą procedurę 10 razy na sekundę i przedstawia informacje w formularzu. Zajęło trochę czasu, zanim to się stało. Kiedy wypróbowałem wartość licznika czasu równą 0, formularz nigdy się nie pojawił. Boli mnie teraz głowa! 10 lub więcej razy na sekundę wystarczy 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()
Micheal Morrow
źródło