tkinter menu accelerator duplicating callbacks - python-3.x

I'm seeing some strange behavior running Python 3.7.6 and tkinter 8.6 on MacOS 10.14.6.
I've bound "Delete" to a function, and also added a menu command that triggers the same function. I've also added an accelerator option to the menu command, which only displays the text and doesn't actually create the shortcut (as noted here).
Except, in this case, it does seem to create a shortcut: pressing the "Delete" key triggers my function twice: once for the bound command, and a second time from the accelerator. Removing either the binding or the accelerator cause it to only trigger once; of course, removing both causes the keybind to no longer activate. It works fine when triggered from the menu, and this doesn't seem to happen with other keys or key combos.
I'm not sure whether I've missed a detail about Tkinter programming or I'm encountering a technical issue. I made sure to try a different keyboard, and am unsure whether it might be a bug with Tkinter on Mac.
The following code consistently exhibits my issue:
from tkinter import *
def bar(event=None):
print("Called bar")
def foo(event=None):
print("Called foo")
root = Tk()
menubar = Menu(root)
filemenu = Menu(menubar, tearoff=0)
#works fine
filemenu.add_command(label="Something else", command=bar, accelerator="Control-s")
root.bind("<Control-s>", bar)
#triggers twice when using the "Del" key
filemenu.add_command(label="Delete a thing", command=foo, accelerator="Delete")
root.bind("<Delete>", foo)
menubar.add_cascade(label="File", menu=filemenu)
root.config(menu=menubar)
root.mainloop()
Triggering each shortcut once generates the following output:
Called bar
Called foo
Called foo
So this leaves me with two questions:
Can this be replicated by other people?
How might I resolve or work around it?

I also had this issue, and looking at the code it appears that it definitely does add bindings for your accelerators. In my app I worked around it by not binding the key presses on OSX but it's hardly ideal..
There is a Tk bug for this: https://core.tcl-lang.org/tk/tktedit?name=ead70921a9665f57d2fbcfb3cdee51391858bc96

Your code worked fine on my windows system running Python 3.7, but believing there are some issues with tkinter on MacOS, you can try the using the themed tk ttk Menubutton widget and see if it works any better. The look of the ttk Menu might slightly differ from the tk Menu design.
Here is your posted code with the ttk Menubutton:
import tkinter as tk
from tkinter import ttk
def bar(event=None):
print("Called bar")
def foo(event=None):
print("Called foo")
root = tk.Tk()
# create a ttk menubutton
mb = ttk.Menubutton(root, text="File")
# create a cascade of menu options for this menubutton mb
mb.menu = tk.Menu(mb, tearoff=0)
mb.menu.add_command(label="Something else", command=bar, accelerator="Control-s")
root.bind("<Control-s>", bar)
mb.menu.add_command(label="Delete a thing",command=foo, accelerator="Delete")
root.bind("<Delete>", foo)
mb.configure(menu=mb.menu)
mb.pack(side='left')
root.mainloop()
I hope it works!

Related

Hide tk window when using filedialog

I'm trying to use filedialog.asksavefilename to get a save file path. I am running this code in the IDLE shell and it's a text based interface. This is the function to get the save path:
def getPath():
root=tk.Tk()
root.lift()
root.attributes('-topmost',True)
root.after_idle(root.attributes,'-topmost',False)
path = filedialog.asksaveasfilename(defaultextension=".txt", filetypes=(("Text Documents", "*.txt"),))
root.destroy()
The dialog opened behind other windows, so I used this to make the dialog appear at the front. This works, but there is still an empty window behind it which I don't want. I've tried using root.withdraw() but this just hides everything. I'd like to have only the file dialog open without the empty tk window. Any ideas as to how to do this?
I've found a way to achieve the desired effect:
def getPath():
root=tk.Tk()
root.overrideredirect(True)
root.attributes("-alpha", 0)
path = filedialog.asksaveasfilename(defaultextension=".txt", filetypes=(("Text Documents", "*.txt"),))
root.destroy()
I've removed all of the unnecessary lift and topmost parts - they didn't help. I used root.overrideredirect(True) to remove the title bar and root.attributes("-alpha", 0) to make the window 100% transparent, so you can't see it. The only drawback is that the file dialog window flashes when it opens, but that's not too much of a problem.
from tkinter import Tk
from tkinter.filedialog import asksaveasfilename
def get_path():
root = Tk()
root.withdraw()
path = asksaveasfilename()
root.destroy()
return(path)
print(get_path()) # to verify expected result
Is this the behavior you're looking for? Hope this helps.

tkinter top-level menues not showing with macOS

Here is the classic code from effbot, which informed:
Toplevel menus are displayed just under the title bar of the root or any other toplevel windows (or on Macintosh, along the upper edge of the screen). To create a toplevel menu, create a new Menu instance, and use add methods to add commands and other menu entries to it.
But not working, No menues found. I expect there are Welcome and Quit to the right side of python. Any thing missed?
System info
Python 3.7.5
macOS Catalina
TkVersion 8.6
import tkinter as tk
def hello():
print('Hi~')
root = tk.Tk()
root.title("MacOS Catalina")
menubar = tk.Menu(root)
root.configure(menu=menubar)
menubar.add_command(label='Welcome', command=hello)
menubar.add_command(label='Quit', command=root.quit)
root.mainloop()
On OSX, you cannot put commands on the root menu. Apple simply doesn't allow it. You can only put other menus (cascades). For example:
...
menubar = tk.Menu(root)
root.configure(menu=menubar)
menu = tk.Menu(menubar)
menu.add_command(label="Welcome", command=hello)
menu.add_command(label="Quit", command=root.quit)
menubar.add_cascade(label="My Menu", menu=menu)

binding keyboard events tkinter

Still quite new at python , i'm learning tkinter.
I would like to use keyboard events rather than mouse events for some fonction.
However keyboard events do not work although mouse events do.
here is a very simple example of what is not working. Using button 1 with the mouse and pressing key 'z on the keyboard should do the same, but the keyboard does nothing. I have tried to read tkinter documentation but didn't find the answer.
thanks for your help
from tkinter import *
class Pipi(Frame):
def __init__(self,master=None):
Frame.__init__(self,width=400,height=400,bg='red')
self.pack()
self.bind("<z>",self.do)
self.bind("<Button-1>", self.do)
def do(self,event):
print('la vie est belle')
root=Tk()
Pipi(root)
root.mainloop()
This is due to Frame widget not having the focus. With the mouse event, it works seemingly different. There can be a couple of workarounds:
Grabbing the focus before the event happens
binding to something else
To achieve the first, simply add:
self.focus_set()
somewhere inside __init__.
To achieve the second, replace:
self.bind("<z>",self.do)
with:
self.master.bind('<z>', self.do)

abaqus thread tkinter Tk window refreshing never

I have run into a problem durring abaqus programming with phyton 2.7.
I'm using Tkinter and making a window with a lot of controls on it.
My main problem is that durring my plugin window is opened, the user needs to click on abaqus menuitems, browse modells, etc. So using the main program while my plugin still works on screen.
If I do create my Tk window without thread, than when the user clicks on abaqus main windo functions while my plugin is opened, then abaqus will not respond or crash with "LoadlibraryA error 193"
(example: while plugin runs and user clicks on Viewvport menĂ¼/ViewPort Annotation Options then he/she wont be able to change tabs)
If i do create my Tk window inside a thread, then the al the Tk window controls will only responds the mouse events after I leave the Tk window with my cursor.
(example: I make 2 notebook page and after start i click on the not selected one. then nothing happens until my mous inside the Tk window, but as soon as i move it out, the click takes effect and the tab changes...)
The threaded version of my code:
import threading
class pilotDB(threading.Thread):
def shutdown_ttk_repeat(self):
self.root.eval('::ttk::CancelRepeat')
self.root.destroy()
def __init__(self):
import threading
threading.Thread.__init__(self)
def refresh(self):
self.root.after(50, self.refresh)
def tabpage(self):
import ttk
import sys
self.notebook = ttk.Notebook(self.root)
self.tabpage_tab1 = ttk.Frame(self.notebook,width=400,height=500)
self.tabpage_tab2 = ttk.Frame(self.notebook,width=400,height=500)
self.notebook.add(self.tabpage_tab1, text='Tab1')
self.notebook.add(self.tabpage_tab2, text='Tab2')
self.notebook.place(x=30, y=40)
def run(self):
import Tkinter
self.root = Tkinter.Tk()
self.root.protocol("WM_DELETE_WINDOW", self.shutdown_ttk_repeat)
self.tabpage()
self.root.after(1000, self.refresh())
self.root.mainloop()
app = pilotDB()
app.start()
app/pilotDB has no function named "start" (last line of the code posted). If I put a call to self.run() in init, and delete threading, then the program works as expected, i.e. opens a window and displays 2 tabs and the user can switch between tabs, all the time the mouse is in the window as is normal. Note also that the refresh() function does nothing but call itself repeatedly. You should try to find the offending code by first commenting the lines for the "WM_DELETE_WINDOW", shutdown_ttk_repeat and the call to execute the function tabpage() which leaves a basic window. Then uncomment one and run it, and repeat until you find the error.

Python Tkinter Clickable Text?

I'm wondering if there's a way to make clickable text in Tkinter. Maybe like you would see on a title screen of a game, and where you hover your mouse over the text and it changes color/hightlights itself. All I need the click to do is execute another function.
Are either of these things possible? Thanks!
you are looking for tkinter's events:
tk_widget.bind("<Button-1>",CALLBACK)
The call back needs to take an event argument which is a dictionary containing information about what triggered the event.
This can run into issues with widgets that overlap such as windows in a canvas or labels sometimes triggering the callback for the window behind it.
For hovering the mouse over a widget the event is called "<Enter>" and moving mouse out of widget region is called "<Leave>" for highlighting text effect, if you just want to capture a click anywhere on a window then on the root call root.bind_all("<Button-1>",CALLBACK)
source: http://infohost.nmt.edu/tcc/help/pubs/tkinter/web/index.html
http://infohost.nmt.edu/tcc/help/pubs/tkinter/web/events.html
example:
try:
import tkinter as tk
except ImportError:
import Tkinter as tk
def change_case(event=None):
new_text = str.swapcase(lab["text"])
lab.config(text=new_text)
def red_text(event=None):
lab.config(fg="red")
def black_text(event=None):
lab.config(fg="black")
root = tk.Tk()
lab = tk.Label(root,text="this is a test")
lab.bind("<Button-1>",change_case)
lab.bind("<Enter>",red_text)
lab.bind("<Leave>",black_text)
lab.grid()
root.mainloop()
hope this helps :)

Resources