How to disable tkinter listbox during a function - python-3.x

I have a GUI created in Python 3.x using tkinter from Visual Studio 2017 Community. A top widget is created and within it a frame and listbox are added. When the user clicks on an entry in the listbox the index of the selection is passed to twoSecondFunction() which takes around 2 seconds to complete.
How can I stop the user from making another selection during this time?
I have tried using the line myListbox.config(state=tkinter.DISABLED) to disable the listbox when a selection has been made and only enabling the listbox again once the twoSecondFunction() has completed.
When I run the code from Visual Studio myListbox click events are handled by myListboxSelection and 'twoSecondFunction()' and print() are called. The window appears to be unresponsive for the duration of the myListboxSelection().
if the user clicks on another entry before
Function finished
has been output myListboxSelection() is called another time.
I can queue over 10 commands by clicking quickly before "Function finished" is output. The GUI appears unresponsive for the duration of all queued events and only responds to other commands after executing all the queued myListboxSelection() calls.
I have tried removing the line myListbox.config(state=tkinter.NORMAL) and this then registers only one click for the duration of the program, so myListbox.config(state=tkinter.DISABLED) is working as it should when it is called. myListbox also greys out.
I have also added extra print() lines throughout the program to ensure all operations happen in the expected order which they do.
It seems as though the logic executes a lot more quickly than the GUI itself responds, so myListbox becomes enabled far more quickly than the GUI responds. I do not see myListbox grey out during the execution of 'twoSecondFunction()'.
Is it because the myListbox.config(state=tkinter.DISABLED) only takes effect after the event handler completes its execution? I.e. myListbox.config(state=tkinter.DISABLED) never takes effect because myListbox.config(state=tkinter.NORMAL) is set before myListbox is actually disabled?
import tkinter #For the GUI tools
#Event handler for mouse click of myListbox
def myListboxSelection(event):
myListbox.config(state=tkinter.DISABLED) #Disable myListbox
myListboxIndex = int(myListbox.curselection()[0]) #Get selection index
twoSecondFunction(myListboxIndex) #Call function that takes 2 seconds
print("Function finished") #Output to console on completion
myListbox.config(state=tkinter.NORMAL) #Enable Listbox
#Create GUI
GUITopWidget = tkinter.Tk(screenName = "myListboxGUI") #Create Top Level Widget
myFrame = tkinter.Frame(GUITopWidget, name = "myFrame") #Create frame
myFrame.pack() #Pass to Geometry Manager
myListbox = tkinter.Listbox(authoritiesListFrame) #Create myListbox
myListbox.bind('<<ListboxSelect>>', myListboxSelection) #Bind mouse click event
populateListbox(myListbox) #Add entries to the listbox
myListbox.pack() #Pass to Geometry Manager
#Run the GUI loop
GUITopWidget.mainloop()

It appears that if you have an event handler and use mainloop() it does indeed stop any logic from executing during the callback. Instead define a loop which can handle the event calls, other logic and call update() on the GUI manually:
import tkinter #For the GUI tools
rootWindowClosed = False
requestQueued = False #True request is queued
myListboxIndex= -1 #Set to -1 between requests
#Event handler for mouse click of myListbox
def myListboxSelection(event):
myListbox.config(state=tkinter.DISABLED) #Disable myListbox
myListboxIndex = int(myListbox.curselection()[0]) #Get selection index
#On closing of root window
def closeGUIHandler():
#Set the rootWindowClosed flag to True
global rootWindowClosed
rootWindowClosed = True
#Destroy GUI Root
GUITopWidget.destroy()
#Create GUI
GUITopWidget = tkinter.Tk(screenName = "myListboxGUI") #Create Top Level Widget
GUITopWidget.protocol("WM_DELETE_WINDOW", closeGUIHandler) #Set delete window protocol
myFrame = tkinter.Frame(GUITopWidget, name = "myFrame") #Create frame
myFrame.pack() #Pass to Geometry Manager
myListbox = tkinter.Listbox(authoritiesListFrame) #Create myListbox
myListbox.bind('<<ListboxSelect>>', myListboxSelection) #Bind mouse click event
populateListbox(myListbox) #Add entries to the listbox
myListbox.pack() #Pass to Geometry Manager
#Run the GUI loop
while rootWindowClosed == False:
#If requestQueued then execute queued request
if requestQueued == True:
twoSecondFunction(myListboxIndex) #Call function that takes 2 seconds
myListbox.config(state=tkinter.NORMAL) #Enable Listbox
myListboxIndex = -1
#Update GUI window if not closed
time.sleep(0.05) #Sleep for 50 ms to allow event handling before executing code
if rootWindowClosed == False:
GUIRootWidget.update()

Related

How to make a time limit for answer in python game with GUI?

I am making a game that requires from a user to type a proper phylum, based on a given photo of a species. I provide a GUI for my game. Currently, I am struggling to limit the time that user have to give his answer. I tried with Timer (threading), but idk how to cancel a thread when user doesn't exceed a maximum time.
Here is my code for the button that is used to confirm answer:
time_answer = 10
def confirm_action():
global img_label, answer, n
from_answer = answer.get().lower()
if n == len(files) - 1:
answer_check(from_answer, round_up(end - start, 2))
confirm.configure(text="Done")
answer.configure(state=DISABLED)
n += 1
elif n == len(files):
root.quit()
else:
answer_check(from_answer, "(Press shift)")
answer.bind("<Shift_L>", insert_text)
n += 1
img_f = ImageTk.PhotoImage(Image.open(f"program_ib_zdjecia/faces/{directories[n]}/{files[n]}"))
img_label.configure(image=img_f)
img_label.image = img_f
t = Timer(time_answer, confirm_action)
t.start()
confirm = Button(root, text="Confirm", command=confirm_action)
As #Bryan Oakley said, you can use tkinter's after() method to set a timer that disables user input, but only if the user doesn't submit input within the certain amount of time.
I'll have the explanations here, and a simple example will be at the bottom.
Setting a timer using after()
First, how to set a timer using after(). It takes two arguments:
The number of milliseconds to wait before calling the function, and
The function to call when it's time.
For example, if you wanted to change a label's text after 1 second, you could do something like this:
root.after(1000, lambda: label.config(text="Done!")
Canceling the timer using after_cancel()
Now, if the user does submit the input within the given amount of time, you'll want some way of cancelling the timer. That's what after_cancel() is for. It takes one argument: the string id of the timer to cancel.
To get the id of a timer, you need to assign the return value of after() to a variable. Like this:
timer = root.after(1000, some_function)
root.after_cancel(timer)
Example
Here's a simple example of a cancel-able timer using a button. The user has 3 seconds to press the button before it becomes disabled, and if they press the button before time is up, the timer gets canceled so that the button never gets disabled.
import tkinter
# Create the window and the button
root = tkinter.Tk()
button = tkinter.Button(root, text="Press me before time runs out!")
button.pack()
# The function that disables the button
def disable_button():
button.config(state="disabled", text="Too late!")
# The timer that disables the button after 3 seconds
timer = root.after(3000, disable_button)
# The function that cancels the timer
def cancel_timer():
root.after_cancel(timer)
# Set the button's command so that it cancels the timer when it's clicked
button.config(command=cancel_timer)
root.mainloop()
If you have any more questions, let me know!

popup in tkinter with pygame that doest go away unless closed/pressed ok

I am trying to get a popup for my pygame screen using tkinter, but i just want a simple messagebox that i pass in the message, and message type (like: "error"). What i don't know how to do is make it so that they can't avoid not answering it, if they click somewhere else it will not let the user do anything till they answer it, not even go to desktop sort of thing.
what i have so far:
def popUp(self, message, messagetype='Error'):
#Tk().wm_withdraw() #to hide the main window
messagebox.showinfo(messagetype, message)
For this to work your games's mainloop must be in a function (let's call it play). Let's say you have very simple code.
When a condition is met you can access the new function popUp. Which can have your tkinter window. When the button is pressed you can have its command as ...command=play) if the player wants to restart. As the popUp function is not inside the mainloop the game will be unresponsive. An example:
def play():
while carryOn:
for event in pygame.event.get():
if event.type==pygame.QUIT:
carryOn=False
#Checking if a key is pressed and then responding e=with function in the sprite class
keys = pygame.key.get_pressed()
if keys[pygame.K_LEFT]:
player.moveLeft(5)
if keys[pygame.K_RIGHT]:
player.moveRight(5)
if keys[pygame.K_UP]:
player.moveUp(5)
if keys[pygame.K_DOWN]:
player.moveDown(5)
if keys[pygame.K_a]:
player.moveLeft(5)
if keys[pygame.K_d]:
player.moveRight(5)
if keys[pygame.K_w]:
player.moveUp(5)
if keys[pygame.K_s]:
player.moveDown(5)
#yourcondition
# if ... :
# carryOn = False
# popUp()
#Game Logic
all_sprites_list.update()
#Setting the background
screen.fill(GREEN)
#Now let's draw all the sprites
all_sprites_list.draw(screen)
#Refresh Screen
pygame.display.flip()
#Number of frames per second
clock.tick(60)
This is not full code - missing sprite class and code before that. You could then have your tkinter window (after importing tkinter) like:
global window #so it can be destroyed in the other function
window = Tk()
#message = ...
#message.grid ...
button = Button(window, text="restart", width=5, command=play)
button.grid # where you want it
The game will be frozen until the user presses restart (you may also want a window .destroy() in your code somewhere. You can adapt this to pretty much whatever you want but it is no where close to complete code. I hope this was useful.

PySimpleGUI - not show background when window change

In my application I have multiple windows that change based on events(one close and another open) and show only one window at a time. During one window close and another open its take some time since fetch data from database and prepare for window.
Here problem is that during the time of one window close and another open user can see and feel that one is being open and another is being close by seeing the background.
What I want, until second screen is not fully loaded, first window be visible on the screen.
My current code is something like,
import PySimpleGUI as sg
layout = [[sg.Button('Users', key='show_user_list')]]
window = sg.Window('users').Layout(layout)
while True:
event, values = window.Read()
if event == 'show_user_list':
window.Close()
# code ommited here for simplicity
# do mysql stuff to fetch data
# layout2 = ...
# window2 = sg.Window('user listing').Layout(layout2)
# while True:
# event, values = window2.Read()
# ...
# like that I have multiple windows
else:
pass
How I can give users feel like the window content is changing not one window closes and another opens?
Sure, you can make sure the background is not seen by first opening your Window 2, which will be created on top of Window 1, THEN closing your Window 1.
To do this, add a .Finalize() onto the Window 2 creation. This will cause the window to immediate show up. Then on the next line, close Window 1.
import PySimpleGUI as sg
layout = [[sg.Button('Users', key='show_user_list')]]
window = sg.Window('users').Layout(layout)
while True:
event, values = window.Read()
if event == 'show_user_list':
# code ommited here for simplicity
# do mysql stuff to fetch data
# layout2 = ...
# window2 = sg.Window('user listing').Layout(layout2).Finalize()
# window.Close()
# while True:
# event, values = window2.Read()
# ...
# like that I have multiple windows
else:
pass
The key to making this kind of window update work is to create the windows at the same location. The default is to make windows that are centered on the screen. This means if your windows are not the same size then you'll likely notice a small "blip" as you change from one to the other. But it shouldn't look bad because it'll happen so quickly.
If you really want to get fancy, you can add another step which will make the switch between the windows even smoother. This new step involves creating window 2 with Alpha=0, meaning that it's invisible, then after it's fully formed (using Finalize()) you change the Alpha to 1 which will make the window appear.
import PySimpleGUI as sg
layout = [[sg.Text('Example of window-replacement')],
[sg.Combo(['abdeffg', 'rrrfwwew'], size=(10, 4))],
[sg.B('Enable Filter'), sg.B('Warning'), sg.B('Reopen')],]
window = sg.Window('My Text Editor', layout)
while True: # Event Loop
event, values = window.Read()
if event is None:
break
print(event, values)
if event == 'Reopen':
layout2 = [[sg.Text('This is a completely different window')],
[sg.Combo(['abdeffg', 'rrrfwwew'], size=(10, 4))],
[sg.B('Enable Filter'), sg.B('Warning'), sg.B('Reopen')], ]
window2 = sg.Window('My Text Editor', layout2, alpha_channel=0).Finalize()
window2.SetAlpha(1)
window.Close()
window = window2
window.Close()
This removed some of the "painting" of the window that I was seeing. That shouldn't happen because I use this same trick when creating the window to begin with. Alpha is used to hide the window while it's being created.

How to programmatically quit mainloop via a tkinter canvas Button

My program generates several graphs one at a time and each has a quit button.
The program pauses in mainloop until I press the button, then generates the next graph.
I would like a way to programmatically press or invoke the action associated to that button, in this case root.quit()
I have tried calling invoke() on the button but this doesn't work. My feeling is that the event is lost before mainloop is started.
from tkinter import *
pause = False # passed in as an arg
root = Tk()
root.title(name)
canvas = Canvas(root, width=canvas_width, height=canvas_height, bg = 'white')
canvas.pack()
quit = Button(root, text='Quit', command=root.quit)
quit.pack()
# make sure everything is drawn
canvas.update()
if not pause:
# Invoke the button event so we can draw the next graph or exit
quit.invoke()
root.mainloop()
I realised that the problem was with the event being lost and mainloop blocking so I used the pause arg to determine when to run mainloop, i.e. on the last graph.
See Tkinter understanding mainloop
All graphs are displayed and when you press Quit on any window all windows disappear and the program ends.
If there is a better way to do this please let me know, but this works.
root = Tk()
root.title(name) # name passed in as an arg
# Creation of the canvas and elements moved into another function
draw( root, ... )
if not pause:
root.update_idletasks()
root.update()
else:
mainloop()

How to check if toplevel exists after destroying it in tkinter?

I'm trying to check if a particular Toplevel has been destroyed, which happens after a certain button is pressed, so that I can then do something else in the program (i.e. create a NEW Toplevel).
Supposedly, after the initial toplevel is closed by the user, the output on the shell should be "N". This would indicate that the program understood that the initial toplevel no longer existed, allowing me to carry onto the next stage in that particular if not t1.winfo_exists(): clause (see below).
This output does not happen. Nothing happens on the output. I used 'winfo_exists()' and I cannot find what I have done incorrectly.
from tkinter import *
root = Tk()
t1 = Toplevel(root)
t1.title('REG')
def GetREG():
global e, reg
reg = e.get() # find out the user input
# Destroy the toplevel:
t1.destroy() # after the user presses the SubmitButton
label = Label(t1, text="Enter your REG:")
label.pack()
e = Entry(t1) # for the user to input their REG
e.pack()
SubmitButton = Button(t1,text='Submit',command=GetREG) # button to submit entry
SubmitButton.pack(side='bottom')
if not t1.winfo_exists(): # TRYING TO CHECK when the does not exist
# supposedly, this should occur after the SubmitButton is pressed
# which shold allow me to then carry out the next step in the program
print("No")
root.mainloop()
Could it be that when the user destroys a window, this is not recognized as a non-existent state? It doesn't work either when I 'delete' the t1 toplevel with the cross, or when it gets deleted via the SubmitButton (with t1.destroy() in GetREG()).
Any suggestions?
At the time you check for t1.winfo_exists(), the window still exists because you're calling that function about a millisecond after it has been created. How is tkinter supposed to know that you want that if statement to wait for the window to be destroyed?
If you want to wait for it to be destroyed before executing more code you can use the method wait_window, which is like mainloop in that it processes events until the window is destroyed.
from tkinter import *
root = Tk()
t1 = Toplevel(root)
...
root.wait_window(t1)
# there is now no need to check, since it's not possible to get
# here until the window has been destroyed.
root.mainloop()

Resources