I have a problem using the after method in Tkinter.
The plan is to print i with interval of one second. I checked whether the after method is suitable, but I don't know exactly.
Here is the code.
# -*- coding: utf-8 -*-
from Tkinter import *
import time
root = Tk()
root.title("Program")
root['background'] ='gray'
def command_Print():
for i in range(0, 10, 1):
time.sleep(1)
Label0.after(1)
Labelvar.set(i)
Labelvar = StringVar()
Labelvar.set(u'original value')
Frame0 = Frame(root)
Frame0.place(x=0, y=0, width=100, height=50)
Label0 = Label(Frame0, textvariable=Labelvar, anchor='w')
Label0.pack(side=LEFT)
Frame_I = Frame(root)
Frame_I.place(x = 100, y = 0, width=100, height=70)
Button_I = Button(Frame_I, text = "Button" , width = 100, height=70, command = command_Print)
Button_I.place(x=0, y=0)
Button_I.grid(row=0, column=0, sticky=W, pady=4)
Button_I.pack()
root.mainloop()
after with a single argument (eg: after(10)) is the same as calling time.sleep, and should generally be avoided. Since it puts your GUI to sleep, your GUI won't be able to respond to any events (including requests by the user or OS to refresh or resize the window)
When you call after with two or more arguments, the second argument is a reference to a function you want to call in the future. All remaining arguments will be passed to that function.
Tkinter maintains a queue of events. mainloop is the function that watches that queue and runs events as they come in. When you call after, the function you requested is simply added to the queue with a timestamp. When it is ready to be processed, tkinter will call the function and pass in the arguments. It's as simple as that.
Don't use time.sleep() at all in Tkinter applications. Have the callback schedule a call to itself with after().
def command_Print(counter=0):
Labelvar.set(counter)
if counter < 10:
root.after(1000, lambda: command_Print(counter+1))
Also, range(0, 10, 1) is just range(10). There's no need to repeat the defaults.
Related
Hey I am new to python and am using tkinter for my gui. I am having trouble using the "after" method.
The goal is to make a random letter appear every 5 seconds.
Here is my code:
import random
import time
from tkinter import *
root = Tk()
w = Label(root, text="GAME")
w.pack()
frame = Frame(root, width=300, height=300)
frame.pack()
L1 = Label(root, text="User Name")
L1.pack(side=LEFT)
E1 = Entry(root, bd =5)
E1.pack(side=LEFT)
tiles_letter = ['a', 'b', 'c', 'd', 'e']
while len(tiles_letter) > 0:
rand = random.choice(tiles_letter)
tile_frame = Label(frame, text=rand)
tile_frame.pack()
frame.after(500)
tiles_letter.remove(rand) # remove that tile from list of tiles
root.mainloop()
can someone please help me --- the problem is definitely frame.after(500):
i'm not sure if it is correct to use "frame" and I don't know what which argument follows the 500.
Thanks
You need to give a function to be called after the time delay as the second argument to after:
after(delay_ms, callback=None, *args)
Registers an alarm callback that is called after a given time.
So what you really want to do is this:
tiles_letter = ['a', 'b', 'c', 'd', 'e']
def add_letter():
rand = random.choice(tiles_letter)
tile_frame = Label(frame, text=rand)
tile_frame.pack()
root.after(500, add_letter)
tiles_letter.remove(rand) # remove that tile from list of tiles
root.after(0, add_letter) # add_letter will run as soon as the mainloop starts.
root.mainloop()
You also need to schedule the function to be called again by repeating the call to after inside the callback function, since after only executes the given function once. This is also noted in the documentation:
The callback is only called once for each call to this method. To keep
calling the callback, you need to reregister the callback inside
itself
Note that your example will throw an exception as soon as you've exhausted all the entries in tiles_letter, so you need to change your logic to handle that case whichever way you want. The simplest thing would be to add a check at the beginning of add_letter to make sure the list isn't empty, and just return if it is:
def add_letter():
if not tiles_letter:
return
rand = random.choice(tiles_letter)
tile_frame = Label(frame, text=rand)
tile_frame.pack()
root.after(500, add_letter)
tiles_letter.remove(rand) # remove that tile from list of tiles
Live-Demo: repl.it
I believe, the 500ms run in the background, while the rest of the code continues to execute and empties the list.
Then after 500ms nothing happens, as no function-call is implemented in the after-callup (same as frame.after(500, function=None))
after is used to delay execution of the program or to execute a command in background sometime in the future. But you can build a loop inside the mainloop by calling itself.
import tkinter as tk #import tkinter
import datetime #import datetime for our clock
def tick(): #function to update the clock
showed_time = clock['text'] #current showed time
current_time = datetime.datetime.now().strftime("%H:%M:%S") #real time
if showed_time != current_time: #if the showed time is not the real time
clock.configure(text=current_time) #update the label with the current time
clock.after(1000, tick) #call yourself in 1000ms (1sec.) again to update the clock
return None
root=tk.Tk()
clock = tk.Label(root)
clock.pack()
tick()
root.mainloop()
In the above script we had built a digital clock and get in touch with the after method. The after method is nothing but an interval and on the end of that interval we want that something happen.
To learn more about this basic widget method [click]
after(delay_ms, callback=None, args)
This method registers a callback function that will be called after a
given number of milliseconds. Tkinter only guarantees that the
callback will not be called earlier than that; if the system is busy,
the actual delay may be much longer.
import tkinter as tk
import datetime
def tick():
showed_time = clock['text']
current_time = datetime.datetime.now().strftime("%H:%M:%S")
if showed_time != current_time:
clock.configure(text=current_time)
global alarm #make sure the alarm is reachable
alarm = clock.after(1000, tick)#assign the alarm to a variable
return None
def stop():
stop.after_cancel(alarm) #cancel alarm
root=tk.Tk()
clock = tk.Label(root)
clock.pack()
stop = tk.Button(root, text='Stop it!', command=stop)
stop.pack()
tick()
root.mainloop()
Here we have the same code but with the ability to cancel our loop with the after_cancel method of tkinter. You dont need to global the alarm inside a class. self.alarm = self.clock.after(...) works fine.
after_cancel(id)
Cancels an alarm callback.
id
Alarm identifier.
So my aim is to use a single function to show a text message upon a button click. Then there should be a delay and then another text message should be displayed.
The game is a dice game that should show 'Rolling...' upon a button click. And then after a while, it should display a random number.
I tried both .sleep() and .after() and both of them resulted in my program not showing the before delay text. Here's my code:
# Imports
import tkinter as tk
from random import randrange
import time
# Global variables
# SIDES is a constant
SIDES = 12
# Functions
def func():
display["text"] = "Rolling..."
window.after(2000)
display["text"] = str(randrange(SIDES) + 1)
# Main program loop
window = tk.Tk()
display = tk.Label(window, text="Press the button \nto roll the dice.", width=20, height=3)
button = tk.Button(window, text="Roll", command=func)
display.pack()
button.pack(pady=10)
window.mainloop()
Any help would be much appreciated!
Try:
window.after(2000, lambda: display.config(text=randrange(SIDES) + 1))
instead of the:
window.after(2000)
display["text"] = str(randrange(SIDES) + 1)
The problem is that when you sleep in the function, the tkinter main loop is interrupted and the screen isn't updated. (window.after() is just a gloified sleep here). The correct solution is to pass a callback to after, which will make it immediately return and call the callback later:
def func():
display["text"] = "Rolling..."
window.after(2000, lambda: display.__setitem__("text", str(randrange(SIDES) + 1)))
(Note that the call to __setitem__ is a direct one-liner lambda translation. This is not good design.)
How would I adjust my code to run the following loop while a radio button is selected? The idea is to create a program that clicks the left mouse button every 20 seconds or so to prevent idle. I think I'm so close I just don't quite understand how the mainloop() event stuff works. Does my loop code need to go in the main loop or something? Also, what is value=val).pack(anchor=tk.W)? I have a feeling that is a piece I don't get as well. Here is my current code:
import tkinter as tk, pyautogui, time # This is the prefered way to call tkinter, don't use wildcards.
my_ui_window = tk.Tk() # TK
my_ui_window.title('Radio Button Example')
v = tk.IntVar()
v.set(1) # initializing the choice
on_or_off = [
("Enabled"),
("Disabled")
]
def ExecuteChoice():
choice = (v.get())
while choice == 0:
time.sleep(20)
pyautogui.click()
else:
print ('waiting...')
time.sleep(3)
for val, i in enumerate(on_or_off):
tk.Radiobutton(my_ui_window,
text=i,
borderwidth = 2,
indicatoron= 0,
width = 20,
padx = 50,
variable=v,
command=ExecuteChoice(),
value=val).pack(anchor=tk.W)
my_ui_window.mainloop()
Here is my code re-written appropriate to tkinter. I was making several mistakes. The main one is you typically do not run loops inside tkinter and you definitely don't use sleep. Instead we use the .after class. Here is a better way. It's now heavily commented for anyone lost.
import tkinter as tk # This is the prefered way to call tkinter, don't use wildcards.
import pyautogui # This allows mouse stuff
import time # This allows sleep commands
my_ui_window = tk.Tk() # make tk.Tk() into just a single object.
my_ui_window.title('Radio Button Example')
v = tk.IntVar() # This becomes the index of sorts for our radio elements.
v.set(1) # initializing radio button to off
on_or_off = [ # Creates an array to represent the radio buttons needed.
("Enabled"),
("Disabled")
]
def every_20_seconds(): # Calls this function which clicks if the radio button is set to index 0. It then tells my_ui_window to wait 20 seconds using the .after method before calling itself again. In the meantime, it gives control back to the mainloop() which is always searching for an event change on the UI.
if v.get() == 0:
pyautogui.click()
my_ui_window.after(20000, every_20_seconds)
for val, i in enumerate(on_or_off): # This builds the UI for us.
tk.Radiobutton(my_ui_window,
text=i,
borderwidth = 2,
indicatoron= 0,
width = 20,
padx = 50,
variable=v,
value=val).pack(anchor=tk.W)
every_20_seconds()
my_ui_window.mainloop()
This should do pretty much what you want, even though it's not perfect yet:
import tkinter as tk,pyautogui,time
import threading
my_ui_window = tk.Tk() # TK
my_ui_window.title('Radio Button Example')
v = tk.IntVar()
v.set(0) # initializing the choice
on_or_off = [
(1, "Enabled"),
(0, "Disabled")
]
def ExecuteChoice():
choice = v.get()
if choice == 1:
print("CLICK")
threading.Timer(5.0, ExecuteChoice).start()
else:
print ('waiting...')
pass
for val, name in on_or_off:
tk.Radiobutton(my_ui_window,
text=name,
borderwidth = 2,
indicatoron= 0,
width = 20,
padx = 50,
variable=v,
command=ExecuteChoice,
value=val).pack(anchor=tk.W)
my_ui_window.mainloop()
There were two issues with your code:
You used command=ExecuteChoice() instead of command=ExecuteChoice. Thus, you call the function when initializing your RadioButtons instead of setting this function as a parameter
Yourwhile loop in ExecuteChoice was blocking, i.e. it is the only thing running. The GUI will not update anymore. Hence, you need to call my_ui_window.update() and choice = v.get() in the loop so to update the GUI and check whether the user has changed his choice of Radio-Buttons Thus, we exchange it with an if and an asynchronous timer instead of sleep()
/e: Comment is right. As mentioned, this is not best practice but the closest to the code of the poster to still make it work. I've made a little additional adjustment, to not block anymore. That doesn't mean its best practice. This would include rewriting the code more.
from tkinter import *
from random import *
root = Tk()
#A function to create the turn for the current player. The current player isnt in this code as it is not important
def turn():
window = Tk()
dice = Button(window, text="Roll the dice!", bg= "white", command=lambda:diceAction(window))
dice.pack()
window.mainloop()
#a function to simulate a dice. It kills the function turn.
def diceAction(window):
result = Tk()
y = randint(1, 6)
quitButton = Button(result, text="Ok!", bg="white", command=result.destroy)
quitButton.pack()
window.destroy()
result.mainloop()
#A function to create the playing field and to start the game
def main():
label1 = Button(root, text="hi", bg="black")
label1.pack()
while 1:
turn()
print("Hi")
turn()
main()
root.mainloop()
My problem is that the code in the while function after the first turn() the code isnt executed until i close the root window(which i dont want because it represents the playing field). You can copy this code and execute it yourself if you want.
I have no idea what causes this and havent found anything online. Sorry for the long code but i wrote it so that it is executeable.
I don't know why this particular problem is occurring, but there are a couple of things in your code that are considered bad practice.
Instead of creating multiple instances of Tk(), you should use Toplevel widgets for any pop-up windows needed. Also, it's better to use root.mainloop() to run the program rather than a while loop.
I've made some edits to your code so that it uses a Toplevel widget and discards of the while loop.
from tkinter import *
from random import *
#A function to create the turn for the current player. The current player isnt in this code as it is not important
def turn(prev=None):
# destroy the previous turn
if prev:
prev.destroy()
# pop up with dice
window = Toplevel()
dice = Button(window, text="Roll the dice!", bg= "white")
dice.config(command=lambda b=dice, w=window:diceAction(b, w))
dice.pack()
#a function to simulate a dice, reconfigures the pop-up
def diceAction(button, window):
# roll dice
y = randint(1, 6)
# use dice result here?
print(y)
# reconfigure button, the command now starts a new turn
button.config(text='ok', command=lambda w=window:turn(prev=w))
root = Tk()
# I hijacked this button to use as a start button
label1 = Button(root, text="hi", bg="black", command=turn)
label1.pack()
root.mainloop()
I don't know if this is what you need, but it functions as the code in the question would if it worked.
Sorry I couldn't help with the cause of the error.
I'm having issues passing variables between two scripts in python without ending the imported script. I would like for the status bar in my tkinter GUI to update to reflect what number the counter in script2 is on. I've included the code below:
from tkinter import *
import script2
root = Tk()
canvas = Canvas(root, width=300, height=300)
canvas.pack()
def runScript():
var = script2.information()
button1 = Button(root, text="Click Me!", command=runScript)
button1.pack()
var = StringVar()
var.set('Waiting for Input...')
status = Label(root, textvariable=var, bd=1, relief=SUNKEN, anchor=W)
status.pack(side=BOTTOM, fill=X)
root.mainloop()
script2.py would be:
#script2
import time
def information():
variable = 0
while variable < 500:
yield variable
variable += 1
print(variable)
time.sleep(1)
I understand that this has to do with generators, but my understanding was that yield would pause the loop, send the data to the original program, let it process it, and then un-pause the loop. Is there a better way of understanding this process?
For one thing, script2.information() is a generator function which means you will have to manually iterate the generator object returned on the first call it to get successive values.
For another, tkinter doesn't support multithreading. One thing you can do is schedule for a function to be called after a certain amount of time using the universal widget method after(). In this case, it can be used to schedule a call to an (added) function that iterates the generator object after it's been created by calling script2.information() and updates the StringVar() widget accordingly each time it's called.
You also need to change script2.py so it doesn't call time.sleep(). Doing so will make your tkinter GUI program hang whenever it's called (since it temporarily interrupts execution of the tkinter mainloop()).
Modified script2.py:
import time
def information():
variable = 0
while variable < 60: # changed from 500 for testing
yield variable
variable += 1
print(variable)
# time.sleep(1) # don't call in tkinter programs
main script:
from tkinter import *
import script2
DELAY = 100 # in millisecs
root = Tk()
canvas = Canvas(root, width=300, height=300)
canvas.pack()
def do_update(gen, var):
try:
next_value = next(gen)
except StopIteration:
var.set('Done!')
else:
var.set(next_value)
root.after(DELAY, do_update, gen, var) # call again after delay
def run_script(var):
gen = script2.information() # create generator object
do_update(gen, var) # start iterating generator and updating var
var = StringVar()
var.set('Waiting for Input...')
button1 = Button(root, text="Run script!", command=lambda: run_script(var))
button1.pack()
status = Label(root, textvariable=var, bd=1, relief=SUNKEN, anchor=W)
status.pack(side=BOTTOM, fill=X)
root.mainloop()
Your information generator returns an iterator, you will need to save the iterator instance and call next() on it to get the next value. To change the var, you will need to use .set() to change it.
from tkinter import *
import script2
root = Tk()
canvas = Canvas(root, width=300, height=300)
canvas.pack()
def runScript():
var.set(next(info)) # set the variable as the "next" result of the info generator
info = script2.information() # the info is an iterator
button1 = Button(root, text="Click Me!", command=runScript)
button1.pack()
var = StringVar()
var.set('Waiting for Input...')
status = Label(root, textvariable=var, bd=1, relief=SUNKEN, anchor=W)
status.pack(side=BOTTOM, fill=X)
root.mainloop()
The script2 can stay the same. But I recommend you to not use time.sleep, it wouldn't work as you expect it to! It will just make your program not respond for the given seconds.
You will need to use root.after if you want to increment to continue till StopIteration, just change runScript to this::
def runScript():
global info
try:
var.set(next(info)) # set the variable as the "next" result of the info generator
except StopIteration:
var.set('Finished')
info = script2.information()
else:
root.after(1, runScript)