Як запускати власний код поряд із циклом подій Tkinter?


119

Мій маленький брат просто займається програмуванням, і для проекту Science Fair він займається моделюванням зграї птахів у небі. Він отримав більшу частину свого коду, написаного, і він прекрасно працює, але птахам потрібно рухатись кожну мить .

Тинкір, однак, підбирає час для власного циклу подій, і тому його код не працюватиме. Виконуйте root.mainloop(), пробігає та продовжує працювати, і єдине, що він працює, - це обробники подій.

Чи є спосіб, щоб його код запустився поруч з мейнполу (без багатопотокових запитів, це заплутано, і це має бути просто), і якщо так, то що це?

Ось зараз він придумав некрасивий злом, пов'язуючи свою move()функцію <b1-motion>, так що, поки він утримує кнопку вниз і помахує мишкою, це працює. Але має бути кращий спосіб.

Відповіді:


141

Використовуйте afterметод на 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()

Ось декларація та документація щодо 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."""

30
якщо ви вказали час очікування, який дорівнюватиме 0, завдання повернеться до циклу подій відразу після закінчення. це не заблокує інші події, при цьому все одно запускайте свій код якомога частіше.
Натан

Після витягування волосся на години, намагаючись змусити opencv і tkinter працювати разом належним чином і чисто закрити при натисканні кнопки [X], це разом з win32gui.FindWindow (None, "назва вікна") зробила трюк! Я такий noob ;-)
JxAxMxIxN

Це не найкращий варіант; хоча це працює в цьому випадку, це не добре для більшості сценаріїв (він працює лише кожні 2 секунди) та встановлення тайм-ауту становить 0, за пропозицією @Nathan, оскільки він працює лише тоді, коли tkinter не зайнятий (що може викликати проблеми в деяких складних програмах). Найкраще дотримуватися threadingмодуля.
Анонім

59

Рішення опубліковано Bjorn результатів в «RuntimeError: Виклик Tcl з різною кухні об'єднані» повідомлення на моєму комп'ютері (RedHat Enterprise 5, пітон 2.6.1). Бьорн, можливо, не отримав це повідомлення, оскільки, згідно з одним місцем, яке я перевірив , неправильне нанизування різьби з Tkinter є непередбачуваним і залежить від платформи.

Здається, проблема полягає в тому, що вона app.start()вважається посиланням на Tk, оскільки додаток містить елементи Tk. Я зафіксував це шляхом заміни app.start()з self.start()внутрішньої сторони __init__. Я також зробив це так, що всі посилання на Tk або знаходяться всередині функції, яка викликає,mainloop() або всередині функцій, які викликаються функцією, яка викликає mainloop()(це, мабуть, критично, щоб уникнути помилки "іншої квартири").

Нарешті, я додав обробник протоколів із зворотним викликом, оскільки без цього програма виходить із помилкою, коли вікно Tk користувачем закрито.

Перероблений код такий:

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

Як би ви передали аргументи runметоду? Я не можу зрозуміти, як це зробити ...
TheDoctor

5
як правило, ви передаватимете аргументи __init__(..), зберігати їх selfі використовувати в нихrun(..)
Андре Хольцнер

1
Корінь взагалі не відображається, попереджаючи: `УВАГА: області перетягування вікон повинні бути визнані недійсними лише на головній нитці! Це призведе до виключення в майбутньому `
Боб Бобстер

1
Цей коментар заслуговує на значно більше визнання. Дивовижний.
Даніель Рейханян

Це рятівник життя. Код поза графічним інтерфейсом повинен перевіряти наявність потоку tkinter живим, якщо ви не зможете вийти зі сценарію python після виходу з GUI. Щось на кшталтwhile app.is_alive(): etc
m3nda

21

Коли ви пишете власний цикл, як у моделюванні (я припускаю), вам потрібно викликати updateфункцію, яка робить те, що mainloopробить: оновлює вікно зі своїми змінами, але ви робите це у своєму циклі.

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

while 1:
   task()  

10
Ви повинні бути дуже обережними з таким видом програмування. Якщо будь-які події викликають taskвиклик, ви закінчите вкладеними петлями подій, і це погано. Якщо ви повністю не розумієте, як працюють петлі подій, вам слід уникати дзвінків updateза будь-яку ціну.
Брайан Оуклі

Я використовував цю техніку один раз - працює нормально, але залежно від того, як ви це зробите, у вас може виникнути непохитний інтерфейс користувача.
jldupont

@Bryan Oakley Тоді оновити цикл? І як це було б проблематично?
Green05

6

Інший варіант - дозволити tkinter виконуватись на окремій потоці. Один із способів зробити це такий:

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

Будьте обережні, проте багатопотокове програмування важко, і вистрілити себе в ногу дуже легко. Наприклад, ви повинні бути обережними при зміні змінних учасників зразкового класу вище, щоб не переривати цикл подій Tkinter.


3
Не впевнений, що це може спрацювати. Просто спробував щось подібне, і я отримав "RuntimeError: головна нитка не в основному циклі".
jldupont

5
jldupont: Я отримав "RuntimeError: Виклик Tcl з іншого приміщення" (можливо однакова помилка в іншій версії). Виправлення полягало в ініціалізації Tk у run (), а не в __init __ (). Це означає, що ви ініціалізуєте Tk у тій самій нитці, що і ви називаєте mainloop () дюйма
mgiuca

2

Це перша робоча версія того, що буде GPS-зчитувачем та презентатором даних. tkinter - дуже крихка річ, у якій занадто мало повідомлень про помилки. Він не викладає речі і не розповідає, чому багато часу. Дуже важко отримати хорошого розробника форми WYSIWYG. У будь-якому випадку, це виконує невеликий розпорядок 10 разів на секунду та подає інформацію на бланку. Знадобився час, щоб це сталося. Коли я спробував значення таймера 0, форма так і не з’явилася. В мене зараз болить голова! 10 і більше разів за секунду для мене досить добре. Я сподіваюся, що це допоможе комусь іншому. Майк Морроу

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()
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.