¿Cómo ejecuta su propio código junto con el bucle de eventos de Tkinter?

119

Mi hermano pequeño recién se está iniciando en la programación y, para su proyecto de la Feria de Ciencias, está haciendo una simulación de una bandada de pájaros en el cielo. Ha escrito la mayor parte de su código y funciona bien, pero los pájaros necesitan moverse. todo momento .

Tkinter, sin embargo, acapara el tiempo de su propio ciclo de eventos, por lo que su código no se ejecutará. Hacer se root.mainloop()ejecuta, se ejecuta y se sigue ejecutando, y lo único que se ejecuta son los controladores de eventos.

¿Hay alguna manera de que su código se ejecute junto con el bucle principal (sin subprocesos múltiples, es confuso y esto debería mantenerse simple), y si es así, cuál es?

En este momento, se le ocurrió un truco feo, vinculando su move()función a <b1-motion>, de modo que mientras mantenga presionado el botón y mueva el mouse, funcione. Pero tiene que haber una forma mejor.

Allan S
fuente

Respuestas:

141

Utilice el aftermétodo en el 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()

Aquí está la declaración y la documentación del 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
fuente
30
si especifica que el tiempo de espera sea 0, la tarea se volverá a colocar en el ciclo de eventos inmediatamente después de finalizar. esto no bloqueará otros eventos, mientras sigue ejecutando su código con la mayor frecuencia posible.
Nathan
Después de tirarme de los pelos durante horas tratando de que opencv y tkinter funcionen juntos correctamente y se cierren limpiamente cuando se hizo clic en el botón [X], esto junto con win32gui.FindWindow (Ninguno, 'título de la ventana') funcionó. Soy un novato ;-)
JxAxMxIxN
Ésta no es la mejor opción; aunque funciona en este caso, no es bueno para la mayoría de los scripts (solo se ejecuta cada 2 segundos), y establecer el tiempo de espera en 0, según la sugerencia publicada por @Nathan porque solo se ejecuta cuando tkinter no está ocupado (lo que podría causar problemas en algunos programas complejos). Lo mejor es seguir con el threadingmódulo.
Anónimo
59

La solución publicada por Bjorn da como resultado un mensaje "RuntimeError: Calling Tcl from different appartment" en mi computadora (RedHat Enterprise 5, python 2.6.1). Es posible que Bjorn no haya recibido este mensaje, ya que, según un lugar que verifiqué , el manejo incorrecto del subproceso con Tkinter es impredecible y depende de la plataforma.

El problema parece ser que app.start()cuenta como una referencia a Tk, ya que la aplicación contiene elementos Tk. Arreglé esto reemplazando app.start()con un self.start()inside __init__. También lo hice para que todas las referencias Tk estén dentro de la función que llamamainloop() o dentro de funciones que son llamadas por la función que llama mainloop()(esto es aparentemente crítico para evitar el error de "apartamento diferente").

Finalmente, agregué un controlador de protocolo con una devolución de llamada, ya que sin esto, el programa sale con un error cuando el usuario cierra la ventana Tk.

El código revisado es el siguiente:

# 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
fuente
¿Cómo pasarías argumentos al runmétodo? Parece que no puedo entender cómo ...
TheDoctor
5
Por lo general, le pasaría argumentos __init__(..), los almacenaría selfy los usaría enrun(..)
Andre Holzner
1
La raíz no aparece en absoluto, dando la advertencia: `ADVERTENCIA: ¡Las regiones de arrastre de NSWindow solo deben invalidarse en el hilo principal! Esto arrojará una excepción en el futuro '
Bob Bobster
1
Este comentario merece mucho más reconocimiento. Asombroso.
Daniel Reyhanian
Este es un salvavidas. El código fuera de la GUI debe verificar que el subproceso tkinter esté activo si no desea poder salir del script de Python una vez que salga de la interfaz gráfica de usuario. Algo comowhile app.is_alive(): etc
m3nda
21

Al escribir su propio ciclo, como en la simulación (supongo), necesita llamar a la updatefunción que hace lo que mainloophace: actualiza la ventana con sus cambios, pero lo hace en su ciclo.

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

while 1:
   task()  
jma
fuente
10
Hay que tener mucho cuidado con este tipo de programación. Si taskse llama a cualquier evento , terminará con bucles de eventos anidados, y eso es malo. A menos que comprenda completamente cómo funcionan los bucles de eventos, debe evitar llamar updatea toda costa.
Bryan Oakley
Usé esta técnica una vez; funciona bien, pero dependiendo de cómo lo hagas, es posible que tengas algo de asombro en la interfaz de usuario.
jldupont
@Bryan Oakley ¿Entonces la actualización es un bucle? ¿Y cómo sería eso problemático?
Green05
6

Otra opción es dejar que tkinter se ejecute en un hilo separado. Una forma de hacerlo es así:

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

Sin embargo, tenga cuidado, la programación multiproceso es difícil y es muy fácil dispararse a sí mismo en el pie. Por ejemplo, debe tener cuidado cuando cambia las variables miembro de la clase de muestra anterior para no interrumpir con el ciclo de eventos de Tkinter.


fuente
3
No estoy seguro de que esto funcione. Intenté algo similar y me sale "RuntimeError: el hilo principal no está en el bucle principal".
jldupont
5
jldupont: Obtuve "RuntimeError: Calling Tcl from different appartment" (posiblemente el mismo error en una versión diferente). La solución fue inicializar Tk en run (), no en __init __ (). Esto significa que está inicializando Tk en el mismo hilo que llama mainloop () en.
mgiuca
2

Esta es la primera versión funcional de lo que será un lector de GPS y presentador de datos. tkinter es una cosa muy frágil con muy pocos mensajes de error. No pone cosas y no dice por qué la mayor parte del tiempo. Muy difícil viniendo de un buen desarrollador de formularios WYSIWYG. De todos modos, esto ejecuta una pequeña rutina 10 veces por segundo y presenta la información en un formulario. Me tomó un tiempo hacerlo realidad. Cuando probé un valor de temporizador de 0, el formulario nunca apareció. ¡Me duele la cabeza ahora! 10 o más veces por segundo es suficiente para mí. Espero que ayude a alguien más. 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
fuente