I have a TKinter application (Python) that reacts to button presses and keyboard input. However, it is possible that a function that is called after one of these events may take some time to execute. I would like for the GUI to reject all user input while the function is executing. [EDIT: Just want to clarify: all user input refers to any and all buttons/key bindings that exist in the app - I am not just trying to reject the event from a particular button.]
Here is a very simple example to show that TKinter currently seems to add events to a queue that gets executed after each event is handled. Each time the button is pressed, the text in the button has a zero appended to it. However, if the button is pressed when the text reads 00, the function takes a while to execute (I put in a self.after(3000)), and if the button is pressed while that function is executing, then each of those presses will register. So if I press the button 5 times in under the 3 seconds that I have it "stall", then each of those clicks registers, and I end up seeing 0000000 on the button.
import tkinter as tk
# To demonstrate that keystrokes/button clicks do register while a function is executing (and then subsequently fire)
class Application(tk.Frame):
def __init__(self, master=None):
super().__init__(master)
# Build the app
self.text = tk.StringVar()
self.pack()
self.create_widgets()
def create_widgets(self):
self.btn = tk.Button(self, textvariable=self.text, command=self.DoSomething, padx=30, pady=30)
self.btn.grid(row=0, column=0)
self.text.set('0')
def DoSomething(self):
# self.RejectEventsWhileThisFuncExecutes()
old_msg = self.text.get()
if old_msg == '00':
self.after(3000)
self.text.set(old_msg + '0')
# self.BeginAcceptingEventsAgain()
root = tk.Tk()
app = Application(master=root)
app.mainloop()
Basically, what I'd like is maybe something I can call in DoSomething() at the beginning and end of the function, say self.RejectEventsWhileThisFuncExecutes() and self.BeginAcceptingEventsAgain(), that will make sure that no clicks are registered while the function is executing.
A common strategy is to create an invisible widget (eg: a 1x1 frame in the corner of the window), and do a grab (ie: call grab_set) on that widget. That causes all events to be funneled to that widget. As long as there are no bindings on that window, the net effect is that the events get ignored. You just need to flush the event queue (call the update method) before removing the grab.
You can remove the command from the button at the start of DoSomething:
self.btn.config(command=lambda: None)
And then reset it at the end. Just make sure to update to process all queued events before rebinding:
self.update()
self.btn.config(command=self.DoSomething)
Related
The situation is simple. I have a main window with a Help - About menu.
When this menu item is clicked, a modal window is opened (let's say it's an About-window).
With self.grab_set() I disabled the main-window (although the modal window does flicker when you click the main title bar).
So far, so good.
Here is the question: I really like to sound a bell when the user clicks outside the modal window on the main window.
This is what I could find about grab_set(), really not that much:
[effbot] ...a method called grab_set, which makes sure that no mouse or keyboard
events are sent to the wrong window.
[effbot] Routes all events for this application to this widget.
[kite.com] A grab directs all events to this and descendant widgets in the application.
[google books] grab_set() ensures that all of the application's events are sent to w until a corresponding call is made to grab_release ([Me:] or till the window is destroyed?)
I'm not quite sure how to understand this: does it mean you can handle an event on the main window within the modal window (like sounding my bell)?
So I tried things like:
self.bind('<Button-1>', self.bell) Exception in Tkinter callback: _tkinter.TclError: bad window path name
parent.bind('<Button-1>', self.bell) Nothing happens
So, how to sound a bell like when clicked outside the modal window on the main window, like in so many other applications?
Derived questions:
Is it still possible to cature events from the main window after using
grab_set for the modal window?
Is there a way to prevent the flickering?
I really like to understand this mysterious grab_set() method.
Stripped code:
import tkinter as tk
class About(tk.Toplevel):
def __init__(self, parent):
tk.Toplevel.__init__(self, parent)
self.geometry('200x150')
#--- OK button
btn_ok = tk.Button(self, text='OK', command=self.destroy) # destroy with OK
btn_ok.pack(side=tk.TOP)
btn_ok.focus() # destroy with spacebar
#--- Make window modal
self.grab_set()
# self.wait_window() # is this necessary in this case?
# self.bind('<Button-1>', self.bell) ??? The question
class MenuBar(tk.Menu):
def __init__(self, parent):
tk.Menu.__init__(self)
helpmenu = tk.Menu(self, tearoff=0)
helpmenu.add_command(label='About', command=lambda: About(parent))
self.add_cascade(label='Help', menu=helpmenu)
class MainApp():
def __init__(self, parent):
parent.configure(background='#000000')
parent.geometry('800x600')
menubar = MenuBar(parent)
parent.configure(menu=menubar)
if __name__ == '__main__':
root = tk.Tk()
MainApp(root)
root.mainloop()
When you set a grab, all button clicks will go to the window with the grab. You capture them the way you capture any other event. In the case of a button click you do that by binding a function to <1>.
It's important to know that a binding on a root window or a Toplevel window will apply to all widgets in that window. For example, binding to self in your code will fire even when you click on the "Ok" button. Therefore, the callback should probably only do work when the widget associated with the event is the same as the toplevel.
Example:
class About(tk.Toplevel):
def __init__(self, parent):
...
self.bind("<1>", self.capture_click)
...
def capture_click(self, event):
if event.widget == self:
<your logic here>
In the case of wanting to know if the user clicked outside the window, you can use the coordinates of the event object to compare against the window to see if the click is inside or outside.
def on_click(self, event):
if event.widget == self:
if (event.x < 0 or event.x > self.winfo_width() or
event.y < 0 or event.y > self.winfo_height()):
self.bell()
I found a second solution. Though my question was explicitly about using grab_set(), this method does the same for me: making the window as modal as possible and sound a bell.
Instead of using self.grab(), you can also disable the parent window:
parent.attributes('-disabled', True)
Of course it needs to be enabled again when the OK button is clicked (and when the windows is closed with the [X] close control. However, my original About-window has no window decoration). The command for the OK-button becomes:
btn_ok = tk.Button(self, text='OK', command=lambda: self.closeme(parent))
...which calls the closeme function:
def closeme(self, parent):
parent.attributes('-disabled', False)
self.destroy()
The bell sounds automatically when clicking a disabled window.
Method 1: Keeps you in full control of the main window but does not 'freeze' the main window: you can still move it around.
Method 2: Completely freezes the main window, but if it happens to be (partially) covered by another window (not of this application), then you can only bring back to top using Alt+Tab (windows).
I'm sure I will use both techniques in the future depending on my needs.
I know similar things have been asked a lot, but I've tried to figure this out for two hours now and I'm not getting anywhere. I want to have a button in a Tkinter window that is only visible on mouseover. So far I failed at making the button invisible in the first place (I'm familiar with events and stuff, that's not what this question is about) pack_forget() won't work, because I want the widget to stay in place. I'd like some way to do it like I indicated in the code below:
import tkinter as tki
class MyApp(object):
def __init__(self, root_win):
self.root_win = root_win
self.create_widgets()
def create_widgets(self):
self.frame1 = tki.Frame(self.root_win)
self.frame1.pack()
self.btn1 = tki.Button(self.frame1, text='I\'m a button')
self.btn1.pack()
self.btn1.visible=False #This doesnt't work
def main():
root_win = tki.Tk()
my_app = MyApp(root_win)
root_win.mainloop()
if __name__ == '__main__':
main()
Is there any way to set the visibility of widgets directly? If not, what other options are there?
Use grid as geometry manager and use:
self.btn1.grid_remove()
which will remember its place.
You can try using event to call function.
If "Enter" occurs for button then call a function that calls pack()
and if "Leave" occurs for button then call a function that calls pack_forget().
Check this link for event description:List of All Tkinter Events
If you wish your button to stay at a defined place then you can use place(x,y) instead of pack()
I'm quite new to programming and trying to write a fairly simple program that records the times in between different keyboard button presses (a bit like multiple reaction time tests) and prints these times on screen in an array, then terminates and saves the array after a certain period of time is up.
I've already written most of the program in pygame after giving up on Tkinter because it seemed to be the best thing for responding to keyboard input in real time. However, now that I'm wanting the text to scroll automatically once the screen fills up, add more columns to the array, and export to Excel, I'm starting to wonder whether I'd be better off with a module more suited to text handling.
Can anyone advise me on whether I'm making a mistake attempting this in pygame and whether responding immediately to multiple keyboard inputs in Tkinter is possible? I can provide more detail if necessary.
Using Tkinter, you can bind to <Any-KeyPress>. The function that is called is passed an event object that has a timestamp. You can use that to compute the time between events.
Here's a quick example that shows how to display the time between keypresses. You can of course add your own logic to count and track and display however you want.
import Tkinter as tk
class Example(tk.Frame):
def __init__(self, parent):
self.last_event = None
tk.Frame.__init__(self, parent)
self.label = tk.Label(self, text="")
self.text = tk.Text(self, wrap="word")
self.label.pack(side="top", fill="x")
self.text.pack(fill="both", expand=True)
self.text.bind("<Any-KeyRelease>", self.on_key_release)
def on_key_release(self, event):
if self.last_event is not None:
delta = event.time - self.last_event.time
self.label.configure(text="time since last event: %s ms" % delta)
self.last_event = event
if __name__ == "__main__":
root = tk.Tk()
Example(root).pack(fill="both", expand=True)
root.mainloop()
I got the following code from a tutorial. I then modified main() so that two windows are created as seperate threads. When I run it, only one window is created. Then when I press the Quit button in that window, a second window appears. In this new window the button has a different look than the first one (a look which I like better) and then if I press either of the two Quit buttons, both windows close and the program exits.
Why does the second window not appear until the first Quit button is pressed, and why does it look different when it does appear?
EDIT: This happens when no threads are used as well, where only one window is created at a time.
EDIT: This is a screenshot of the two windows that are created. The one on the left is created with the program is run, the one on the right is created after clicking the "Quit" button on the first.
from Tkinter import Tk, BOTH
from ttk import Frame, Button, Style
class Example(Frame):
def __init__(self, parent):
Frame.__init__(self, parent)
self.parent = parent
self.initUI()
def initUI(self):
self.parent.title("Quit button")
self.style = Style()
self.style.theme_use("default")
self.pack(fill=BOTH, expand=1)
quitButton = Button(self, text="Quit",
command=self.quit)
quitButton.place(x=50, y=50)
from threading import Thread
def main():
for i in range(2):
root = Tk()
root.geometry("250x150+300+300")
app = Example(root)
Thread(target=root.mainloop()).start()
if __name__ == '__main__':
main()
You cannot use tkinter this way. Tkinter isn't thread safe, you can only ever access tk widgets and commands except from the thread that created the root window.
As for one window only showing after the other is destroyed even without threading, it's hard to say since you don't show the code. If you're creating more than one instance of Tk, and calling mainloop more than once, that's the problem. Tkinter is designed to work when you create precisely one instance of Tk, and call mainloop precisely once.
If you want more than one window, create a single instance of Tk for the first window, and instances of Toplevel for additional windows.
So I have this button thats made to start stuff, like this
self.buttontext = StringVar()
self.buttontext.set("Start")
self.button = Button(self.bottomframe, textvariable=self.buttontext, command=self.start)
And when it starts, I want the user to be able to cut it short if they need to by changing the same button to a stop button right after it starts
def start(self):
self.button.config(command=self.stop)
self.buttontext.set("Stop")
permission = True
for ...
if update:
run properly
else:
end prematurely
self.button.config(command = self.start)
self.buttontext.set("Start")
That considers a boolean in every iteration of the loop. The stop function will change update to false so that the loop
def stop(self):
permission = False
However, after I click 'Start' I guess control is no longer in the mainloop and the buttons are unresponsive, despite the button changing its properties for the duration of the runtime. How can I make the button responsive so that it can be interrupted?
call self.update() on every iteration of the loop so that the application can service both screen-refresh events and button-press events (assuming self refers to a tkinter widget)