Como você executa seu próprio código junto com o loop de eventos do Tkinter?

119

Meu irmão mais novo está começando a programar e, para seu projeto da Feira de Ciências, está fazendo uma simulação de um bando de pássaros no céu. Ele escreveu a maior parte de seu código e funciona muito bem, mas os pássaros precisam se mover a cada momento .

O Tkinter, no entanto, consome tempo para seu próprio loop de eventos e, portanto, seu código não será executado. Fazendo root.mainloop()execuções, execuções e continuamente rodando, e a única coisa que executa são os manipuladores de eventos.

Existe uma maneira de ter seu código executado junto com o mainloop (sem multithreading, é confuso e deve ser mantido simples) e, se sim, qual é?

Agora mesmo, ele apareceu com um hack feio, vinculando sua move()função a <b1-motion>, de modo que, enquanto ele mantiver o botão pressionado e mexer o mouse, ele funcionará. Mas deve haver uma maneira melhor.

Allan S
fonte

Respostas:

141

Use o aftermétodo no Tkobjeto:

from tkinter import *

root = Tk()

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

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

Aqui está a declaração e a documentação do aftermétodo:

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
fonte
30
se você especificar o tempo limite como 0, a tarefa se colocará de volta no loop de eventos imediatamente após terminar. isso não bloqueará outros eventos, enquanto executa seu código com a maior freqüência possível.
Nathan,
Depois de puxar meu cabelo por horas tentando fazer o opencv e o tkinter funcionarem juntos de maneira adequada e bem fechados quando o botão [X] foi clicado, isso junto com win32gui.FindWindow (None, 'window title') resolveu o problema! Eu sou um noob ;-)
JxAxMxIxN
Esta não é a melhor opção; embora funcione neste caso, não é bom para a maioria dos scripts (ele só roda a cada 2 segundos), e configurando o tempo limite para 0, conforme a sugestão postada por @Nathan porque ele só roda quando o tkinter não está ocupado (o que poderia causar problemas em alguns programas complexos). Melhor ficar com o threadingmódulo.
Anônimo de
59

A solução postada por Bjorn resulta em uma mensagem "RuntimeError: Calling Tcl from different appartment" no meu computador (RedHat Enterprise 5, python 2.6.1). Bjorn pode não ter recebido esta mensagem, pois, de acordo com um lugar que verifiquei , o manuseio incorreto do threading com o Tkinter é imprevisível e depende da plataforma.

O problema parece ser que app.start()conta como uma referência a Tk, uma vez que app contém elementos Tk. Consertei isso substituindo app.start()por um self.start()interior __init__. Eu também fiz isso para que todas as referências Tk estejam dentro da função que chamamainloop() ou dentro de funções que são chamadas pela função que chama mainloop()(isso é aparentemente crítico para evitar o erro de "apartamento diferente").

Finalmente, adicionei um manipulador de protocolo com um retorno de chamada, pois sem ele o programa sai com um erro quando a janela Tk é fechada pelo usuário.

O código revisado é o seguinte:

# 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
fonte
Como você passaria argumentos para o runmétodo? Não consigo descobrir como ...
TheDoctor
5
normalmente você passaria argumentos __init__(..), os armazenaria selfe os usaria emrun(..)
Andre Holzner,
1
A raiz não aparece, dando o aviso: `AVISO: as regiões de arrasto NSWindow só devem ser invalidadas no thread principal! Isso lançará uma exceção no futuro `
Bob Bobster
1
Este comentário merece muito mais reconhecimento. Surpreendente.
Daniel Reyhanian
Este é um salva-vidas. O código fora da GUI deve verificar se o thread do tkinter está ativo, se você não quiser sair do script python ao sair da gui. Algo comowhile app.is_alive(): etc
m3nda
21

Ao escrever seu próprio loop, como na simulação (presumo), você precisa chamar a updatefunção que faz o que o mainloopfaz: atualiza a janela com suas alterações, mas você o faz em seu loop.

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

while 1:
   task()  
jma
fonte
10
Você tem que ter muito cuidado com esse tipo de programação. Se algum evento taskfor chamado, você acabará com loops de evento aninhados, e isso é ruim. A menos que você entenda completamente como funcionam os loops de evento, você deve evitar chamar updatea todo custo.
Bryan Oakley
Eu usei essa técnica uma vez - funciona bem, mas dependendo de como você faz isso, você pode ter alguns problemas na IU.
jldupont
@Bryan Oakley Então a atualização é um loop? E como isso seria problemático?
Green05
6

Outra opção é permitir que o tkinter seja executado em uma thread separada. Uma maneira de fazer isso é assim:

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

Porém, tenha cuidado, a programação multithread é difícil e é realmente fácil atirar no próprio pé. Por exemplo, você deve ter cuidado ao alterar as variáveis ​​de membro da classe de exemplo acima, para não interromper com o loop de evento do Tkinter.


fonte
3
Não tenho certeza se isso pode funcionar. Tentei algo semelhante e recebo "RuntimeError: o thread principal não está no loop principal".
jldupont
5
jldupont: Recebi "RuntimeError: Calling Tcl from different appartment" (possivelmente o mesmo erro em uma versão diferente). A correção era inicializar o Tk em run (), não em __init __ (). Isso significa que você está inicializando o Tk no mesmo segmento em que chama mainloop () em.
mgiuca
2

Esta é a primeira versão funcional do que será um leitor GPS e apresentador de dados. tkinter é uma coisa muito frágil, com poucas mensagens de erro. Na maior parte do tempo, não armazena coisas e não diz o porquê. Muito difícil vindo de um bom desenvolvedor de formulários WYSIWYG. De qualquer forma, ele executa uma pequena rotina 10 vezes por segundo e apresenta as informações em um formulário. Demorou um pouco para acontecer. Quando tentei um valor de timer de 0, o formulário nunca apareceu. Minha cabeça agora dói! 10 ou mais vezes por segundo é suficiente para mim. Espero que ajude outra pessoa. 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
fonte