Running a loop inside a Radio button - python

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.

Related

tkinter wont refresh widget

I want to write a program that has only a button, and after pressing that, program will start making 3 labels and then change the color of each one every 1 second only once.
It looks very simple and I wrote the following code :
import tkinter as tk
from time import sleep
def function():
mylist=list()
for i in range(3):
new_label=tk.Label(window,text='* * *',bg='yellow')
new_label.pack()
mylist.append(new_label)
print('First state finished')
sleep(1)
for label in mylist:
label.config(bg='red')
print('one label changed')
sleep(1)
window = tk.Tk()
window.geometry('300x300')
btn=tk.Button(window,text='start',command=function)
btn.pack()
tk.mainloop()
First the app is look like this (that is OK):
Second its look like this (its not OK because its print on the terminal but didn't update the lable) :
Third its look like this (at the end the app must be look like this and its OK) :
But I need to see the changes in the moment and use sleep for that reason.
Thank you All.
I would recommend to use .after(delay, callback) method of the tkinter to set the colour.
Hope this is what you want.
import tkinter as tk
def start():
global mylist
mylist = list()
for i in range(3):
new_label = tk.Label(window, text='* * *', bg='yellow')
new_label.pack()
mylist.append(new_label)
delay = 1000 # delay in seconds
for label in mylist:
# Additional delay so that next color change
# is scheduled after previous label color change
delay += 1000
schedule_color_change(delay, label)
def schedule_color_change(delay, label):
print("schedule color change for:", label)
label.after(delay, set_color, label)
def set_color(label):
print("setting color of:", label)
label.config(bg="red")
window = tk.Tk()
window.geometry('300x300')
btn = tk.Button(window, text='start', command=start)
btn.pack()
tk.mainloop()
Problem
The problem is your sleep(1), because it's a function that suspends the execution of the current thread for a set number of seconds, so it's like there is a stop to the whole script
Solution
The solution is to instantiate Thread with a target function, call start(), and let it start working. So you have to use timer which is included in the threading, then a timer from the threading module (import threading)
Inside the first "for" loop, remove your sleep(1) and write for example Time_Start_Here = threading.Timer (2, function_2) and then of course Time_Start_Here.start() to start.
start_time=threading.Timer(1,function_2)
start_time.start()
Instead you have to remove the second "for" loop and write what's inside ... inside the new function that will be called. Next you need to create the function
def function_2():
for label in mylist:
label.config(bg='red')
label.pack()
print('one label changed')
As Meritor guided me, I followed the after method and wrote the following recursive code without sleep :
import tkinter as tk
def recursive(i, listt):
lbl = listt[i]
if i >= 0:
lbl.config(bg='orange')
i -= 1
lbl.after(500, recursive, i, listt)
def function():
mylist = list()
for i in range(3):
new_label = tk.Label(window, text='* * *', bg='yellow')
new_label.pack()
mylist.append(new_label)
print('all label created')
# 2 is length of list minus 1
recursive(2, mylist)
window = tk.Tk()
window.geometry('300x300')
tk.Button(window, text='start', command=function).pack()
tk.mainloop()
Most likely my code is not optimized because it uses recursive and if you know anything better please tell me

How do I get the value in event hander function (tkinter)?

I want to get the value in the event hander function.
If clicks prints in event hander function, then it will show 1, 2, 3, 4... by every click.
import tkinter as tk
root = tk.Tk()
clicks = 0
def click(event):
global clicks
clicks += 1
print(clicks)
button = tk.Button(root, text="click me!")
button.pack()
button.bind("<Button-1>", click)
root.mainloop()
If I print it in global, it will show 0 forever. But I want it prints 1, 2, 3 by every click not always 0.
def click(event):
global clicks
clicks += 1
return clicks
print(clicks)
This case is simplified. Actually, my real case is that there are many different items(like listbox, button1, button2...) event hander function(def xxx(event)), and I need to get some value in each function after trigger their events. Then do some conditional expressions in global.
I knew the methods like class, global variable, init, (self, master),
I think global variable is the simplest method but I really don't know how to return it in event hander function and use it in global.
Thanks for Help !!!
The reason why it always prints 0 is that the program resets everything every after you close the tk window. If you want to save the values even after you close the window, maybe save it in the csv file or put it in the database. Does this help? or am I too far from what you're trying to say?
Your first set of code should work with the global variable like what TheLizzard said. Like what Matiiss said, if you place the print outside the function, it would be called once and never be called again.
As for what you are looking for, I'm assuming you want the button to be like a next button going through each item from the listbox and each time the next button hit one of the item in the listbox, it calls another function to obtain whatever that item has. Hopefully this code is what you are looking for or give you an idea.
import tkinter as tk
root = tk.Tk()
lst = {'a':'a value', 'b':'b value', 'c':'c value'}
clicks = -1
def print_text(event):
keys = list(lst.keys())
print(lst[keys[lstbx.curselection()[0]]])
def click():
global clicks
lstbx.selection_clear(0, 'end')
clicks = clicks + 1 if clicks < 2 else 0 # loop the list when reach end
lstbx.selection_set(clicks)
lstbx.event_generate('<<ListboxSelect>>') # force call function
btn = tk.Button(root, text='Next', command=click)
btn.pack()
lstbx = tk.Listbox(root, selectmode='single')
lstbx.insert(0, *lst.keys())
lstbx.pack()
lstbx.bind('<<ListboxSelect>>', print_text)
root.mainloop()

How to add a delay between 2 text messages being displayed in Tkinter Python?

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 to link multiple buttons to one text widget in tkinter?

I'm trying to learn tkinter and I wanted to write a simple rock paper scissors game, where there is a window with 3 buttons and one text widget.
I'd like to be able to press any of the buttons and for the message to appear in the text field, then click a different button, the text field to clear and display a new message associated with the second button and so on.
From the tutorials I've watched, I know that I can pass the function housing text widget as an argument in button command parameter.I know I could make 3 functions with a text field, one for each button (displaying one at a time) but that's probably not the correct way. Here's what I have so far:
import tkinter as tk
root = tk.Tk()
root.title("Rock Paper Scissors")
root.geometry("420x200")
def Rock():
rockText = "Paper!"
return rockText
def Paper():
paperText = "Scissors!"
return paperText
def Scissors():
scissorsText = "Rock!"
return scissorsText
def display():
textDisplay = tk.Text(master = root, height = 10, width = 50)
textDisplay.grid(row = 1, columnspan = 5)
textDisplay.insert(tk.END, Rock())
buttonRock = tk.Button(text = "Rock", command = display).grid(row = 0, column = 1, padx = 10)
buttonPaper = tk.Button(text = "Paper").grid(row = 0, column = 2, padx = 10)
buttonScissors = tk.Button(text = "Scissors").grid(row = 0, column = 3, padx = 10)
root.mainloop()
Any help will be appreciated.
Edit: Second thought - I can imagine I'm complicating this for myself by trying to force the game to work this way. With the random module I'd be able to get away with one function for the computer choice with a list and saving the random pick in a parameter, then returning the value into the display function.
So if I got this right you just want to make a button click change the text in the Text-widget. For that you have two easy and quite similar options. First would be to define 3 functions, as you did, and let them change the text directly. The second option would be to make one function which changes the text according to whats given. Note that in the second case we will have to use lambda which works quite well in smaller projects but decreases the efficiency of your programs when they get bigger.
First option:
import tkinter as tk
class App:
def __init__(self):
root=tk.Tk()
root.title("Rock Paper Scissors")
root.geometry("420x200")
self.text=Text(root)
self.text.grid(row=1,columnspan=5)
tk.Button(root,text="Rock",command=self.Rock).grid(row=0,column=1,padx=10)
tk.Button(root,text="Paper",command=self.Paper).grid(row=0,column=2)
tk.Button(root,text="Scissors",command=self.Scissors).grid(row=0,column=3,padx=10)
root.mainloop()
def Rock(self):
text="Paper!"
self.text.delete(0,END) #delete everything from the Text
self.text.insert(0,text) #put the text in
def Paper(self):
text="Scissors!"
self.text.delete(0,END) #delete everything from the Text
self.text.insert(0,text) #put the text in
def Scissors(self):
text="Rock!"
self.text.delete(0,END) #delete everything from the Text
self.text.insert(0,text) #put the text in
if __name__=='__main__':
App()
Second option:
import tkinter as tk
class App:
def __init__(self):
root=tk.Tk()
root.title("Rock Paper Scissors")
root.geometry("420x200")
self.text=Text(root)
self.text.grid(row=1,columnspan=5)
tk.Button(root,text="Rock",command=lambda: self.updateText('Paper!')).grid(row=0,column=1,padx=10)
tk.Button(root,text="Paper",command=lambda: self.updateText('Scissors!')).grid(row=0,column=2)
tk.Button(root,text="Scissors",command=lambda: self.updateText('Rock!')).grid(row=0,column=3,padx=10)
root.mainloop()
def updateText(self,text):
self.text.delete(0,END) #delete everything from the Text
self.text.insert(0,text) #put the text in
if __name__=='__main__':
App()
Some little side notes from me here:
If you use grid, pack or place right on the widget itself you wont assign the widget to a variable but the return of the grid, pack or place function which is None. So rather first assign the widget to an variable and then use a geometry manager on it like I did for the Text-widget.
You don't have to extra set the title with the title function afterwards. You can set it with the className-argument in Tk.
If you're working with tkinter its fine to do it functionally but rather use a class to build up GUIs.
When creating new widgets always be sure to pass them the variable for the root window first. They will get it themselves too if you don't do that but that needs more unnecessary background activity and if you have more than one Tk-window open it will automatically chooses one which may not be the one you want it to take.
And one small tip in the end: If you want to learn more about all the tkinter widgets try http://effbot.org/tkinterbook/tkinter-index.htm#class-reference.
I hope its helpfull. Have fun programming!
EDIT:
I just saw your edit with the random module. In this case I would recommend the second option. Just remove the text-argument from updateText and replace lambda: self.updateText(...) with self.updateText(). In updateText itself you add that random of list thing you mentioned. :D

How to use Tkinter after() method?

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.

Categories