Как запустить свой собственный код вместе с циклом событий Tkinter?

119

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

Однако Tkinter тратит время на собственный цикл обработки событий, поэтому его код не запускается. Выполнение 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, 'window title') помогло! Я такой нуб ;-)
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)
Kevin
источник
Как передать аргументы runметоду? Кажется, я не могу понять, как ...
TheDoctor
5
обычно вы передаете аргументы __init__(..), сохраняете их selfи используете вrun(..)
Андре Хольцнер
1
Корень вообще не отображается, выдавая предупреждение: `ПРЕДУПРЕЖДЕНИЕ: области перетаскивания NSWindow должны быть недействительными только в основном потоке! Это вызовет исключение в будущем '
Боб Бобстер
1
Этот комментарий заслуживает большего признания. Удивительный.
Даниэль Рейханян
Это спасатель жизни. Код вне графического интерфейса должен проверять, что поток tkinter жив, если вы не можете выйти из скрипта python после выхода из графического интерфейса. Что-то вроде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()
Майкл Морроу
источник