Failure of tkinter .focus_set() - python

The code below is part of a function that produces a simple dialog with caller selected text and buttons.
The problem lies in the handling of key inputs of 's', 'a', 'd', and 'c. The code operates correctly for mouse clicks and tabbing followed by the space or enter keys. The code is written in Python 3.4 and is being tested for compliance with Windows 7.
My understanding is that Tk handles refocusing for the end user's mouse clicks. The space and enter keys are used after the user has changed focus by tabbing. For all of these interactions the keys are bound to each button by the code:
for action in ('<1>', '<space>', '<Return>'):
b.bind(action, handler)
For 'underline' key input I believe my code needs to handle the refocusing prior to calling the handler. This is the purpose of the refocus routine. The print statement ("refocussing to...") is printed with the correct value for button.winfo_name() whenever 's', 'a', 's', or 'c' is pressed . This is why I believe button.focus_set() is failing.
If it worked this would enable the handler to simply check which button was pressed by looking at event.widget.winfo_name(). As it is, the failure to refocus means that the handler is called with the wrong button in event.widget.winfo_name()
If I move focus manually by tabbing then the focus gives the name returned by event.widget.winfo_name() regardless of which of 's', 'a', 'd', or 'c' is pressed.
After reading other posts on Stack Overflow, I tried adding button.focus_force() after button.focus_set(). This had no effect on the problem.
I have tried passing the button name by changing the signature of the handler to def button_handler(event, *args) and then changing the last line of refocus to handler(event, button.winfo_name()) but *args is empty when called.
def refocus_wrapper(button):
def refocus(event):
print("refocusing to '{}'".format(button.winfo_name()))
button.focus_set()
handler(event)
return refocus
for button_text, underline, button_name in buttons:
b = ttk.Button(button_inner_frame, text=button_text, name=button_name,
underline=underline)
b.pack(padx=2, side='left')
for action in ('<1>', '<space>', '<Return>'):
b.bind(action, handler)
action = '{}'.format(button_text[underline:underline + 1].lower())
dialog.bind(action, refocus_wrapper(b))
if not default or default == button_name:
default = button_name
b.focus_set()

Your initial assumption about needing to set the focus on the button is not correct. The usual method to handle this in Tk is to bind the accelerator key event on the dialog toplevel form and call the buttons invoke method for the event binding.
In Tcl/Tk thats:
toplevel .dlg
pack [button .dlg.b -text "Save" -underline 0 -command {puts "Save pressed"}]
bind .dlg <Alt-s> {.dlg.b invoke}
So bind the key events on whatever toplevel is the parent for your buttons. If that is a dialog then its parent should be the application toplevel widget.
A python equivalent is:
from tkinter import *
main = Tk()
dialog = Toplevel(main)
button = Button(dialog, text="Send", underline="0", command=lambda: print("hello"))
button.pack()
dialog.bind('<Alt-s>', lambda event: button.invoke())
main.mainloop()
The key binding appends an event object to the callback function which we can discard using a lambda to wrap the call the the button's invoke method.

focus_set() was, of course, working perfectly. My expectation that refocusing would rewrite the event object was at fault. The following code (which has been extensively revised to incorporate Brian Oakley's suggestions) produces the expected results.:
def _make_buttons(dialog, buttons, buttonbox, default, handler):
def bcommand_wrapper(button):
def bcommand():
name = button.winfo_name()
dialog.destroy()
handler(name)
return bcommand
for button_text, underline, button_name in buttons:
b = ttk.Button(buttonbox, text=button_text, underline=underline,
name=button_name)
b.configure(command=bcommand_wrapper(b))
b.pack(padx=2, side='left')
action = button_text[underline:underline + 1].lower()
try:
dialog.bind(action, lambda event, b=b: b.invoke())
except tk.TclError:
raise ValueError(
"Invalid underline of '{}' in position {}. Character '{}'.".
format(button_text, underline, action))
b.bind('<Return>', lambda event, b=b: b.invoke())
if not default or default == button_name:
default = button_name
b.focus_set()
I gave up my original idea of returning a Tk event to the caller. This is a dialog so the caller isn't going to need anything more than the name of the button that was clicked.
Note that I am not trapping accelerator keys with the 'Alt' modifier. The 'Alt' key, at least on MS Windows, is a functional key when used with accelerator keys: It causes the display of underlines on menus. Here the underlines are static and so the use of the 'Alt' key would be inappropriate.

Might I suggest using root.bind('c', ) to just have the shortcuts perform what you want?
Just make sure to only bind them when the window pops up, and unbind them when you're done.

Related

Python 3, Keyboard module -How to stop the default action when adding a hotkey (workaround found)

I'm trying to overwrite an existing keyboard function on the enter key with a custom hotkey. The problem is, I cannot stop the default action from occurring also. Worse yet, it occurs after the custom action, so I don't have the chance to retroactively correct it as well.
Here are the relevant parts of the code:
from tkinter import *
from tkinter import ttk
from keyboard import *
root = Tk()
root.geometry('400x300')
'''This is the problematic function called by the hotkey'''
def entr_key(text):
'''Enter key results in printing of the line to the right of cursor'''
curs_pos = text.index(INSERT)
right_hand = text.get(curs_pos,END)
right_hand = right_hand.split('\n')[:1] #lines -> strs in a list, selects the first
print (right_hand)
return
'''THIS IS THE MAIN WIDGET FOR THIS FRAME'''
text_view = ttk.Frame(root, padding=10)
text_view.grid()
text_box = Text(text_view, width=45, wrap=WORD)
text_box.grid(column=0, row=1)
text_box.insert('1.0', 'This is a Text widget demo,\nThis text is aimed at testing the enter key function')
add_hotkey("enter", lambda: entr_key(text_box))
root.mainloop()
(I've changed up some variable names to be more understandable, apologies if I missed any but it's not the source of the problem!)
I've also (unsuccesfully) tried other ways of doing this, eg:
while True:
if is_pressed('enter'):
entr_key(text_box)
'''AND ALSO'''
on_press_key("enter", lambda x=None: entr_key(text_box))
Just to be clear, I don't want the default action of enter key moving the text to a new line.
I need either a way to "break" the key event so that only the custom action takes place, or a way for the custom action to occur after default action, so I can retroactively edit it out
EDIT!!!
I've found a workaround: at the start of entr_key() I call time.sleep(0.01). During this, the default action of the enter key occurs first, and I can retroactively edit it out when the custom function resumes. The delay is slight enough to not be noticeable at all.
But still, if anyone knows how to prevent the default action from occurring completely, I would really appreciate it.

Is there a way to read keypresses from the Enter key?

I have been fooling around in python (v3.9.10) and I have found no way to read keypresses from the enter key and have tkinter do something with that keypress. I am trying to make a dumb little program to show some numbers on my screen for school and want to be able to do it quickly. this is my code so far:
from tkinter import *
#DEFS
def submit():
submitted=entrybox.get()
display.config(text=submitted)
app=Tk()
entrybox=Entry(width=10)
entrybox.place(x=10,y=10)
#display label
display=Label(text=' ')
display.place(x=50,y=50)
display.config(font='Arial 72')
#submit button
subbutton=Button(text='SUBMIT',command=submit)
subbutton.place(x=70,y=10)
(Sorry if my code looks really bad I am quite new to python and everything it has to offer)
(I am also sorry for sounding repetitive and/or dumb. Forums such as this aren't familiar to me either)
is there any way to read key presses from the enter key while allowing the rest of my tkinter stuff to run as intended? thanks in advance.
You can use .bind() to do this like so:
app.bind('<Return>', function)
This will call function when enter is pressed. '<return>' denotes the return or enter key, and it calls function when the event of a return keypress occurs.
You could add it to your code like so:
from tkinter import *
def submit():
submitted=entrybox.get()
display.config(text=submitted)
app=Tk()
entrybox=Entry(width=10)
entrybox.place(x=10,y=10)
#display label
display=Label(text=' ')
display.place(x=50,y=50)
display.config(font='Arial 72')
#submit button
subbutton=Button(text='SUBMIT',command=submit)
subbutton.place(x=70,y=10)
app.bind('<Return>', lambda x: submit())
app.mainloop()
This will do what you are looking for...
How to bind and handle events
Use bind(sequence, func) method on your widget (like Button or even the root app Tk).
The parameter sequence is one of the predefined events, a string name for keys, mouse-events, etc.
The parameter func is a callback or handler function, given by name only (without parentheses or arguments), which must have one positional parameter for Event.
Demo
See this minimal example:
from tkinter import *
def callback(event):
print(event)
def quit(): # did we miss something ?
print("Escape was pressed. Quitting.. Bye!")
exit()
app = Tk()
app.bind('<Return>', callback) # on keypress of Enter or Return key
app.bind('<Enter>', callback) # on mouse-pointer entering the widget - here the app's root window (not confuse with Enter key of keyboard)
app.bind('<Escape>', quit) # on keypress of Escape key
app.mainloop() # to start the main loop listening for events
Prints after following keys pressed:
Enter key was pressed
Escape key was pressed
<KeyPress event keysym=Return keycode=36 char='\r' x=-583 y=-309>
Exception in Tkinter callback
Traceback (most recent call last):
File "/usr/lib/python3.6/tkinter/__init__.py", line 1705, in __call__
return self.func(*args)
TypeError: quit() takes 0 positional arguments but 1 was given
Note:
The Enter key was caught and bound callback/handler function (callback) invoked which prints the passed event.
The Escape key was caught and bound callback/handler function (quit) invoked. But since it hasn't the required parameter (like event) the invocation failed with a TypeError.
When adding the parameter def quit(event): it succeeds:
<Enter event focus=True x=6 y=188>
<Enter event focus=True x=182 y=45>
<KeyPress event keysym=Return keycode=36 char='\r' x=-583 y=-309>
<Enter event state=Button1 focus=True x=123 y=68>
Escape was pressed. Quitting.. Bye!
Note:
The <Enter> event is when the mouse-pointer enters the (visible) widget frame, here of the application's root window.
The <Escape> event exits the application
Further reading
RealPython: Python GUI Programming With Tkinter is a rich tutorial to learn more, especially on Using .bind().

Button behaviour

I work with Python 3.5 and TKinter.
I defined a label and file dialog that updates this label.
A button is responsible to launch this dialog.
self.sel_folder_val = the label that will be updated.
The code:
self.sel_folder_val['text']=filedialog.askdirectory()
After pressing the button in order to launch this dialog, the button stays pressed. Any dialog that a button is responsible to open cause the button to stay low (pressed) after closing this dialog.
I have tried this also with no help...:
self.select_folder_btn.config(relief=RAISED)
Code example:
self.select_folder_btn = Button(self.top)
self.select_folder_btn.place(relx=0.07, rely=0.57, height=34, width=187)
self.select_folder_btn.configure(activebackground="#d9d9d9")
self.select_folder_btn.configure(activeforeground="#000000")
self.select_folder_btn.configure(background="#d9d9d9")
self.select_folder_btn.configure(disabledforeground="#a3a3a3")
self.select_folder_btn.configure(font=self.font3)
self.select_folder_btn.configure(foreground="#000000")
self.select_folder_btn.configure(highlightbackground="#d9d9d9")
self.select_folder_btn.configure(highlightcolor="black")
self.select_folder_btn.configure(pady="0")
self.select_folder_btn.configure(text='''Select destination folder''')
self.select_folder_btn.bind('<Button-1>',self.update_folder_value)
def update_folder_value(self,event):
self.sel_folder_val['text']=filedialog.askdirectory()
return
After executing update_folder_value() function, self.select_folder_btn stays down.
I used the command:
self.select_folder_btn.configure(command=self.update_folder_value)
Instead of bind:
self.select_folder_btn.bind('<Button-1>',self.update_folder_value)
It solved my problem.
Thanks
First for future reference this is a minimal working example:
from Tkinter import *
import tkFileDialog as filedialog
class app:
def __init__(self):
self.top = Tk()
self.select_folder_btn = Button(self.top)
self.select_folder_btn.place(relx=0.07, rely=0.57, height=34, width=187)
self.select_folder_btn.configure(activebackground="#d9d9d9")
self.select_folder_btn.configure(activeforeground="#000000")
self.select_folder_btn.configure(background="#d9d9d9")
self.select_folder_btn.configure(disabledforeground="#a3a3a3")
#self.select_folder_btn.configure(font=self.font3)
self.select_folder_btn.configure(foreground="#000000")
self.select_folder_btn.configure(highlightbackground="#d9d9d9")
self.select_folder_btn.configure(highlightcolor="black")
self.select_folder_btn.configure(pady="0")
self.select_folder_btn.configure(text='''Select destination folder''')
self.select_folder_btn.configure(command=self.update_folder_value)
self.sel_folder_val = {}
self.top.mainloop()
def update_folder_value(self):
self.sel_folder_val['text']=filedialog.askdirectory()
self.top.update_idletasks()
app()
and even that's not minimal. Second your problem is hard to find since this isn't minimal- you're doing something really weird - binding the button to a click. You're overriding the built-in binding, and apparently it still affects the state of the button on press, but not going back. What you wanted is:
self.select_folder_btn.configure(command=self.update_folder_value)
instead of your:
self.select_folder_btn.bind('<Button-1>',self.update_folder_value)
You could also define that in the Button command. What you did is bypassed the button mechanism, so apparently only half of it is executed, and the relief is not raised. Note you have to remove the event parameter your method accepts.

Tkinter binding mouse double click

I'm trying to bind my mouse double click to a function which for now just prints the current selection in a Tkinter list box. To be clear the function should only print when the user double clicks on one of the items of a Tkinter list box. What event binding should I use?
You can bind to <Double-Button-1>:
widget.bind('<Double-Button-1>', handler)
There is also <Button-1> for normal mouse clicks and <Triple-Button-1> for a triple mouse click.
For more information on bindings in Tkinter, see Events and Bindings.
You have to realize that there is a hierarchy to all widgets, and this means that for each widget you click, multiple bindings are possible. If you don't override the default action, each hierarchy's default handler gets called, starting at the lowest level (such as your Listbox) and going all the way up to the Tk() or Toplevel() widget. For you, since you want to print only when a listbox item is clicked, you can bind to the listbox widget, as follows:
listboxWidget.bind('<Double-Button-1>', listboxWidget_leftclick_handler)
Then, when you enter the def listboxWidget_leftclick_handler(event) function, you don't have to check the event.widget value to see if it's the name of your Listbox widget. But you could also check at a higher level (bind a handler to a higher-level widget) and check event.widget to see which widget was clicked.
Also note that the only way to prevent the entire hierarchy of event handlers from triggering is by using a return 'break' from your custom handler, but you usually only need to do this if later handlers corrupt what your custom handler has done.
Additional info about default handlers
The other part which I left out is that there is also a "default" handler for most events. If you bind your own handler, once it's finished, if you don't return 'break', the default handler will be called next.
For example, say you want to make your own Entry box into a password entry. By default, when you type alphanumeric chars when the Entry has focus (which means it's getting input from the keyboard), the chars will appear in the Entry. You can bind:
myEntry.bind('<KeyPress>', passworder)
where passworder is your custom handler which grabs the event holding your inputted char and then outputs an asterisk into the Entry instead. But, if you don't use a return "break" at the end of your handler, the Entry widget is still going to see that char that you didn't want shown, because once your handler is done inserting the asterisk, the default handler will simply insert the typed char (like it would normally). But, if you do the return 'break', the default handler won't get called, and the typed char(s) won't appear in the Entry.
As an add-on. In order to distinguish action between a single click and a double click, delay the call to mouse action for a brief period to allow for the double click flag to be set. See below example:
from tkinter import *
def mouse_click(event):
''' delay mouse action to allow for double click to occur
'''
aw.after(300, mouse_action, event)
def double_click(event):
''' set the double click status flag
'''
global double_click_flag
double_click_flag = True
def mouse_action(event):
global double_click_flag
if double_click_flag:
print('double mouse click event')
double_click_flag = False
else:
print('single mouse click event')
root = Tk()
aw = Canvas(root, width=200, height=100, bg='grey')
aw.place(x=0, y=0)
double_click_flag = False
aw.bind('<Button-1>', mouse_click) # bind left mouse click
aw.bind('<Double-1>', double_click) # bind double left clicks
aw.mainloop()

disable tkinter keyboard shortcut (2)

I'm proposing a continuation of the discussion in disable tkinter keyboard shortcut: I have an event handler for an event that Tkinter also uses, so that my prog & Tkinter interact badly.
Since it is a problem that I've been unable to solve I'm re-proposing here, where I tried to boil it down to the simplest form in the following code:
#!/usr/bin/env python
from Tkinter import *
import tkFont
def init():
global root,text
root = Tk()
root.geometry("500x500+0+0")
dFont=tkFont.Font(family="Arial", size=10)
text=Text(root, width=16, height=5, font=dFont)
text.pack(side=LEFT, fill=BOTH, expand = YES)
root.bind("<Control-b>", setbold)
text.tag_config("b",font=('Verdana', '10', 'bold' ))
text.tag_config("i",font=('Verdana', '10', 'italic' ))
def removeformat(event=None):
text.tag_remove('b',SEL_FIRST,SEL_LAST)
text.tag_remove('i',SEL_FIRST,SEL_LAST)
def setbold(event=None):
removeformat()
text.tag_add('b', SEL_FIRST,SEL_LAST)
text.edit_modified(True)
def main():
init()
mainloop()
if __name__ == '__main__':
main()
What it should do is simply to produce a text window where you write into.
Selecting some text and pressing Ctrl+B the program should remove any preexisting tag, then assign to it the 'b' tag that sets the text to bold.
What instead happens is an exception at the first tag_remove, telling me that text doesn't contain any characters tagged with "sel".
The suggestion to use a return 'break' is of no use, since the selection disappears before setbold() has any chance to act...
Set your binding on the text widget, not on the root. (Whole toplevel bindings are processed after widget class bindings – where the standard <Control-Key-b> binding is – and those are processed after the widget instance bindings, which is what you want to use here.) And you need to do that 'break'; it inhibits the subsequent bindings. (If you're having any problems after that, it's probably that the focus is wrong by default, but that's easy to fix.)
The only other alternative is to reconfigure the bindtags so that class bindings are processed after toplevel bindings, but the consequences of doing that are very subtle and far-reaching; you should use the simpler approach from my first paragraph instead as that's the normal way of handling these things.
Bindings are handled in a specific order, defined by the bindtags of that widget. By default this order is:
The specific widget
The widget class
The toplevel window
The special class "all"
If there are conflicting bindings -- for example, a control-b binding on both the widget and class -- they both will fire (in the described order) unless you break the chain by returning "break".
In the case of the code you posted, however, you are binding to the toplevel window (ie: the root window), and the conflicting binding is on the class. Therefore, the binding will fire for the class before it is processed by the toplevel, so even if your binding returned "break" it wouldn't matter since the class binding happens first.
The most straight-forward solution is to move your binding to the actual widget and return "break". That will guarantee your binding fires first, and the return "break" guarantees that the class binding does not fire.
If you really want your binding on the root window, you can remove the binding for the class using the bind_class method with the value of "Text" for the class.
You might find the Events and Bindings page on effbot.org to be useful.

Categories

Resources