Key binding does not work in tkinter - python

I made simple script:
from tkinter import *
class MyFrame(Frame):
def __init__(self, parent = None):
Frame.__init__(self, parent, bg = 'red')
self.pack(fill=BOTH, expand=YES)
self.bind('<Key>', lambda e: print("pressed any key"))
root = Tk()
root.geometry("300x200")
f = MyFrame(root)
root.mainloop()
But binding for pressing any key do not work. Nothing happens whey I press any key. Do you know why?

You need to call the bind method of parent, which is a reference to the tkinter.Tk instance that represents the main window:
parent.bind('<Key>', lambda e: print("pressed any key"))
self.bind is calling the bind method of the tkinter.Frame instance created when you did:
Frame.__init__(self, parent, bg = 'red')

The reason the binding didn't seem to work is that the frame that you attached the binding to didn't have keyboard focus. Only the widget with keyboard focus will react to the binding. It's perfectly acceptable to do what you did and bind to a frame, you just need to make sure that the widget you bind to gets the keyboard focus.
There are at least two solutions: give the frame the keyboard focus (with the focus_set method), or put the binding on the main window which is what initially gets the keyboard focus.

Related

tkinter: Unbind not working, even workarounds don't work

I bring up here a problem, that's been there for ages, but is obviously still not solved and older workarounds don't work on my Python 3.7.2 (64-bit on Win10).
I have this code:
import tkinter as tk
import tkinter.simpledialog
# message box to enter a value where to set the scale to
class EnterValueBox(tk.simpledialog.Dialog):
def body(self, master):
self.e = tk.Entry(self, width=10)
self.e.pack(pady=5)
return self.e # initial focus
def apply(self):
print(self.e.get())
# callback to open message box
def enterValue(event):
EnterValueBox(root, title="Enter Value 0..100")
# create window with scale widget
root = tk.Tk()
scale = tk.Scale(root, orient=tk.HORIZONTAL, from_=0, to=100)
scale.pack()
# unbind any button-3 events
scale.unbind("<ButtonPress-3>")
scale.unbind("<ButtonRelease-3>")
scale.unbind("<Button-3>")
# bind button-3 press event to open message box
scale.bind("<ButtonPress-3>", enterValue)
tk.mainloop()
It creates a window with a single scale widget. I want to bind ButtonPress-3 to open a little dialog to directly enter a new value. The code only prints that value to the shell, but the example shows, that the unbind is not working, because after printing the value, the dialog box is closed (when the user clicks OK) and then the default binding is executed, which sets the slider, where the user clicked in the trough of the slider widget.
I tried the workaround from Deleting and changing a tkinter event binding with a PatchedScale widget (instead of the PatchedCanvas shown there), but that didn't make any difference.
Any help would be greatly appreciated!
The default bindings are not on the widget, they are on the widget class. Calling unbind on a widget for which there is no widget-specific binding won't have any effect.
If you don't want the default binding to run after your widget-specific binding, the normal technique is to have your bound function return the string break.
def enterValue(event):
EnterValueBox(root, title="Enter Value 0..100")
return "break"

Python tkinter When Toplevel object is destroyed, With toplevel binding for <Destroy> the bound command executes multiple times

In the following code, When the toplevel window is destroyed, the command in the
bind statement is executed multiple times. Probably once for each child widget within the Top Level. When I change the toplevel to a Frame, the bind command only executes once.
In the example, quit() or raise SystemExit are deferred until the command finishes its looping. Why is this happening?
import tkinter as tk
from tkinter import ttk
from tkinter.messagebox import showinfo
class PlainFrame(tk.Frame):
def __init__(self,parent):
super().__init__(parent)
self.butts = []
for i in range(20) :
# button
self.butts.append(ttk.Button(self, text=f'Click Me {i}'))
self.butts[i]['command'] = self.button_clicked
self.butts[i].grid(column=0,row=i)
self.pack()
def button_clicked(self):
showinfo(title='Information',
message='Hello, Tkinter!')
class MainFrame(tk.Toplevel):
#class MainFrame(tk.Frame):
def __init__(self, container,*args,**kwargs):
super().__init__(container,kwargs)
options = {'padx': 5, 'pady': 5}
# label
self.label = ttk.Label(self, text='Hello, Tkinter!')
self.label.pack(**options)
self.quit_button = ttk.Button(self,text='Quit',
command = self._quit)
self.quit_button.pack()
self.frame = PlainFrame(self)
# add when frame
#self.pack()
def button_clicked(self):
showinfo(title='Information',
message='Hello, Tkinter!')
def _quit(self):
self.destroy()
class App(tk.Tk):
def __init__(self):
super().__init__()
# configure the root window
self.title('My Awesome App')
self.geometry('600x100')
def quitting(self,event):
print ('just passing through')
quit()
raise SystemExit
if __name__ == "__main__":
app = App()
frame = MainFrame(app)
# app.withdraw()
frame.bind('<Destroy>',app.quitting)
app.mainloop()
With toplevel binding for the bound command executes multiple times
Yes, this is how tkinter is designed to work.
When you bind to something, you don't bind to a widget. Rather, you bind to a binding tag. Every widget has a set of binding tags. When a widget receives an event, tkinter will check each of its binding tags to see if there's a function bound to it for the given event.
So, what are the widget binding tags? Every widget gets the binding tag "all". Each widget also gets a binding tag named after the widget itself. It gets a third binding tag that is the name of the widget class (eg: "Button", "Label", etc). The fourth tag -- the one causing you trouble -- is the name of the window that contains the widget. The order goes from most to least specific: widget, widget class, window, "all".
You can see this by printing out the binding tags for a widget. Consider the following code:
import tkinter as tk
root = tk.Tk()
toplevel = tk.Toplevel(root)
label = tk.Label(toplevel)
print(f"binding tags for label: {label.bindtags()}")
When run, the above code produces this output:
binding tags for label: ('.!toplevel.!label', 'Label', '.!toplevel', 'all')
The first string, .!toplevel.!label is the internal name of the label widget. Label is the widget class, .!toplevel is the name of the toplevel widget, and then there's the string all.
What happens when you click on the label? First, tkinter will check to see if there is a binding for the button click on the tag .!toplevel.!label. It will have one if you bound to the widget itself. Next, it will check to see if there is a binding on the widget class. Widgets like scrollbars and buttons will have a binding on the widget class, but label won't. Next, tkinter will see if there's a binding on the window itself for the event. And finally, it will see if there is a binding on the special tag all.
You can alter the bindtags for a widget by passing the list of binding tags to the bindtags command. For example, if you want every widget to have a bind tag for the frame, you could set the bindtags to include the frame, and then every widget will respond to an event that is bound to the frame.
You can use the same technique to remove bindings as well. For example, if you wanted to remove all default bindings from a Text widget, you could remove Text from its list of bind tags. Once you do that, the widget will not respond to any key presses or key releases.
The canonical description of how binding tags works is in the bindtags man page for tcl/tk.

How to sound a bell when the user clicks outside a modal window?

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.

Simple bind function doesn't work in python

I'm currently creating an adventure game and I want to bind alt+a to my callback. It doesn't do what I want, so I have two questions:
Is it possible to bind a function to a Label, too?
Why does this (simplyfied) code doesn't work?
Here is the code:
import tkinter as tk
dw = tk.Tk()
dw.title('Hearts')
def play(event):
print('This is the test.')
areal = tk.Frame(master=dw, width=1200, height=600, bg='blue')
areal.pack_propagate(0)
areal.pack(fill=tk.BOTH, expand=bool(dw))
areal.bind("<Alt-A>", play)
dw.mainloop()
It doesn't give me an error, but it doesn't do anything when I click the Frame and afterwards press alt+a. What is wrong here?
EDIT:
import tkinter as tk
def go_fwd(event):
areal.focus_set()
print(event.x, event.y)
dw = tk.Tk()
dw.title('Adventure')
areal = tk.Frame(master=dw, width=20000, height=600, bg='blue')
areal.pack_propagate(0)
areal.pack(fill=tk.BOTH, expand=bool(dw)-100)
areal.focus_set()
dw.bind("<Alt-A>", go_fwd)
enter = tk.Frame(master=dw, width=20000, height=100, bg='cyan')
enter.pack(fill=tk.X)
enterentry = tk.Text(master=enter, width=100, height=4, bg='white')
enterentry.pack()
enterbutton = tk.Button(master=enter, text='Senden', bg='red')
enterbutton.pack()
dw.mainloop()
Here is the complete code.
Is it possible to bind a function to a Label, too?
You can bind to any widget you want. However, if you bind key events, the bindings will only work if the widget has focus. By default, most widgets other than Entry and Text don't get focus unless you explicitly set the focus to them.
Note: only one widget can have keyboard focus at a time.
You can also set a binding to the root window, which will cause it to fire no matter what widget has focus.
For a more thorough explanation of how key bindings are processed, see this answer: https://stackoverflow.com/a/11542200/7432
Why does this (simplyfied) code doesn't work?
It doesn't work the way you expect because the binding is on a Frame widget, but that widget doesn't have the keyboard focus. You could give it focus with something like this:
areal.focus_set()
Or, you could only give it focus after you click on the frame, by creating a binding on a mouse click:
areal.bind("<1>", lambda event: areal.focus_set())
Note: you are binding to a capital "A", so make sure when you test that you're pressing control-alt-a
You need to bind to dw instead of your frame.
So, you can do dw.bind("<Alt-A>", play).
A minor note, Alt-A will bind to the uppercase A as expected, so you'd have to click Alt+Shift+A on your keyboard. Doing Alt+A on your keyboard won't work, you'd have to bind to Alt-a for this to work.
The main window has keyboard focus. Or, alternatively you can leave the bind on the frame and just do areal.focus_set() to set the focus to the frame.

How to pass an event to parent tkinter widget?

Is it possible in Tkinter to pass an event directly to the parent widget?
I have a canvas wich is covered by a grid of other canvases (is that the plural?), which I added using the parent_canvas.create_window() method. I want some of the events, e.g mouse release events, to be handled by the parent canvas.
If I only bind the event a parent method, the event.x and event.y coordinates come out relative to the child canvas which catches the event.
Tkinter does not pass events to parent widgets. However, you can simulate the effect through the use of bind tags (or "bindtags").
The shortest explanation I can give is this: when you add bindings to a widget, you aren't adding a binding to a widget, you are binding to a "bind tag". This tag has the same name as the widget, but it's not actually the widget.
Widgets have a list of bind tags, so when an event happens on a widget, the bindings for each tag are processed in order. Normally the order is:
bindings on the actual widget
bindings on the widget class
bindings on the toplevel widget that contains the widget
bindings on "all"
Notice that nowhere in that list is "bindings on the parent widget".
You can insert your own bindtags into that order. So, for example, you can add the main canvas to the bind tags of each sub-canvas. When you bind to either, the function will get called. Thus, it will appear that the event is passed to the parent.
Here's some example code written in python 2.7. If you click on a gray square you'll see two things printed out, showing that both the binding on the sub-canvas and the binding on the main canvas fire. If you click on a pink square you'll see that the sub-canvas binding fires, but it prevents the parent binding from firing.
With that, all button clicks are in effect "passed" to the parent. The sub-canvas can control whether the parent should handle the event or not, by returning "break" if it wants to "break the chain" of event processing.
import Tkinter as tk
class Example(tk.Frame):
def __init__(self, parent):
tk.Frame.__init__(self, parent)
self.main = tk.Canvas(self, width=400, height=400,
borderwidth=0, highlightthickness=0,
background="bisque")
self.main.pack(side="top", fill="both", expand=True)
# add a callback for button events on the main canvas
self.main.bind("<1>", self.on_main_click)
for x in range(10):
for y in range(10):
canvas = tk.Canvas(self.main, width=48, height=48,
borderwidth=1, highlightthickness=0,
relief="raised")
if ((x+y)%2 == 0):
canvas.configure(bg="pink")
self.main.create_window(x*50, y*50, anchor="nw", window=canvas)
# adjust the bindtags of the sub-canvas to include
# the parent canvas
bindtags = list(canvas.bindtags())
bindtags.insert(1, self.main)
canvas.bindtags(tuple(bindtags))
# add a callback for button events on the inner canvas
canvas.bind("<1>", self.on_sub_click)
def on_sub_click(self, event):
print "sub-canvas binding"
if event.widget.cget("background") == "pink":
return "break"
def on_main_click(self, event):
print "main widget binding"
if __name__ == "__main__":
root = tk.Tk()
Example(root).pack (fill="both", expand=True)
root.mainloop()

Categories