tkinter menu accelerators and modal dialog boxes - python

I want to have a menu that shows a modal dialog box. Everything is fine, until I add an accelerator. If I do this and use the accelerator to access to dialog, it hangs. I suspect that wait_window, used inside the modal dialog box is somehow in conflict with the mainloop, when called from a "bind". Here is an example:
import tkinter
from tkinter import simpledialog
class App(tkinter.Tk):
def __init__(self):
tkinter.Tk.__init__(self)
self.bind_all("<Control-f>", lambda event: self.menu_file())
menubar = tkinter.Menu(self)
fileMenu = tkinter.Menu(menubar, tearoff=False)
fileMenu.add_command(label="File", underline=0,
command=self.menu_file, accelerator="Control+f")
# fileMenu.add_command(label="File", underline=0,
# command=self.menu_file)
menubar.add_cascade(label="File",underline=0, menu=fileMenu)
self.config(menu=menubar)
def menu_file(self):
simpledialog.Dialog(self,"Message")
app=App()
app.mainloop()
If in the above code I comment out the line that adds the accelerator and uncomment the subsequent line, there is no hangup (I can of course still use Ctrl+F to access the dialog box). The only problem is that the accelerator string is not shown next to the File menu. According to the tkinter documentations on the web that I have found, adding the accelerator should only change how the menu is shown and nothing else, so I am really puzzled. Anyone any ideas? (I could of course emulate accelerators by modifying the strings to be displayed, but I would not consider this as an elegant solution.)

As I have noticed, this is a Mac-specific bug.
Following the workaround suggested for a known Tk bug on Mac (see link), changing the line which binds the menu method to the keystroke to the following:
self.bind_all("<Command-f>", lambda event: self.after(100,self.menu_file))
is "fixing" the bug. They also suggest to increase 100 to 150 on slower systems. Hmm..

Related

Tkinter menu bindings and accelerators

I've come across a weird problem and I can't work out what is happening. I am working on a Tkinter application which utilises a menu. I have found that while creating the menu, in which all items have accelerators, some items require additional bindings to make the accelerators work (unless the menu bars are already selected) and some don't.
When an additional binding is required, I have a problem with double entry key strokes. For example, if I open a Toplevel window, I get double entries every time I type a character in an Entry box, both in the Toplevel and in the main window. This only happens if the menu item is called via the key command.
Most of the time this is not a problem, although I'd really like to know what the underlying cause is because it just seems wrong, but this particularly came to my attention recently when I implemented the built-in OS X Preferences menu, using the following code:
self.window.createcommand('::tk::mac::ShowPreferences', self._settings)
Now when I call the settings function from the build-in Preferences key command Command-,, which instantiates a new Toplevel window, this double entry is what happens. It does not happen if I navigate to the menu and open it with the mouse.
The example below recreates the problem for me. The menu bar is not strictly necessary, but the problem occurs with both the menu bar and the OS X built-in Preferences item. Interestingly, 'Settings A' which does not require a binding reproduces the problem, but 'Settings B', which does require a binding, works fine. And again, only with key commands.
import Tkinter
def settings(event = None):
top = Tkinter.Toplevel()
Tkinter.Entry(top).pack()
top.mainloop()
root = Tkinter.Tk()
root.createcommand('::tk::mac::ShowPreferences', settings)
menuBar = Tkinter.Menu(root)
fileMenu = Tkinter.Menu(menuBar)
fileMenu.add_command(label = 'Settings A', accelerator = 'Command-Shift-a', command = settings) # Does not require binding
fileMenu.add_command(label = 'Settings B', accelerator = 'Command-b', command = settings) # Requires binding
menuBar.add_cascade(label = 'File', menu = fileMenu)
root.config(menu = menuBar)
root.bind('<Command-b>', settings)
Tkinter.Entry(root).pack()
root.mainloop()
I suspected it may be a computer issue but I have tried it on another machine and I get the same result. Does anybody have any idea what is happening here and how I can prevent it?
In case anybody's interested, I think I've got to the bottom of this. I believe it was caused by the version of tkinter I had. Today I updated to Python 3 from the OS X bundled version 2.7, and the problem remained. Then I updated tkinter to ActiveTcl 8.5.18.0 and the problem seems to have disappeared.
IDLE and tkinter with Tcl/Tk on macOS

tkinter askopenfilename doubleclick passes event to parent

I have this simple example of the behavior:
import tkinter as tk
from tkinter import filedialog, ttk
INITIALDIR = 'C:\\'
class MainWindow(ttk.Frame):
def __init__(self, root, *args, **kwargs):
super().__init__(root, *args, **kwargs)
self.pack()
btnoptions = {'expand':True, 'fill': 'both'}
btn = ttk.Button(self, text='Select', command=self.ask_openfile)
btn.pack(**btnoptions)
def ask_openfile(self):
self.file_opt = options = {}
options['initialdir'] = INITIALDIR
filename = filedialog.askopenfilename(**self.file_opt)
return filename
if __name__=='__main__':
root = tk.Tk()
root.geometry('600x300')
MainWindow(root).pack(expand=True, fill='both', side='top')
root.mainloop()
Basically there is one big button, which opens a open file dialog. If I select a file and press open, it works fine. However, if I double click to select a file, it selects the file, closes the dialog, and immediately opens a new open file dialog. My guess, that the second click somehow is passed to the underlying window and it clicks on the button again (button has to be under the file which is about to be selected). Is there a way to avoid this behavior? Looks like it is Windows problem, tried on windows 7 and 10 with python 3.5. On debian linux everything is fine, however, I need this to work on Windows.
Seems like this is a known issue with tk:
https://core.tcl.tk/tk/tktview?name=faf37bd379
That ticket says that the issue is fixed in tk 8.6.8.
I was having this issue with tk 8.6 and updating isn't a great option so I tried to find some other workarounds. I tried disabling the buttons and then enabling them but the rouge click just occurred after the button was enabled. I also tried adding a delay before enabling the buttons again but that didn't work either.
The two workarounds that I found that actually worked are:
1) Change the buttons to require a double-click. This can be done with bind . I don't personally like having to double-click the button but maybe other people are less picky.
2) Change the file selection to a combobox with a list of the files in a selected directory. The directory can be selected using askdirectory in place of askopenfilename. askdirectory requires the user to click the "Select Folder" button so it does not have the same issue with double-clicks.

Tkinter: Window flash when attempting to click away

Ive been trying to do this for a while now, but haven't figured out a way to do it.
I have a tkinter script, that creates a popup window when a button is pressed. However I don't want the user to be able to click away from this window to any previous windows created. I have got this working with root.grab_set(), however there is no indication to the user that they must stay on that window.
class popup(object):
def __init__(self, parent):
self.root=Toplevel(parent)
self.root.grab_set() #prevents the user clicking on the parent window
#But the window doesnt 'flash' when an attempt to click away is made
For example, when you have a window created by the filedialogue module, if you attempt to click onto another window the filedialogue window stays on top and has a 'flashing' animation to let the user know they cant click away. Is there a way I can reproduce this effect? Going through the source of filedialogue hasn't been fruitful for me, and neither have Google searches.
The simplest way i can think to do this is to use an event and the focus commands, along with the windows bell command:
#!python3
import tkinter as tk
class popup(object):
def __init__(self, parent):
self.root=tk.Toplevel(parent)
self.root.title("Popup")
self.root.bind("<FocusOut>", self.Alarm)
def Alarm(self, event):
self.root.focus_force()
self.root.bell()
main = tk.Tk()
main.title("Main")
pop = popup(main)
main.mainloop()
Here's a solution for Windows that uses FlashWindowEx from the user32 dll. You need to pass a FLASHWINFO object to it. The grab_set makes sure the popup window stays in focus and disables any widgets in the main window, making the popup transient makes sure it's always on top of the master. The <Button-1> event is used to check mouse clicks, and winfo_containing checks if another window than the popup is clicked. I then set the focus back to the popup and flash the window in focus (which then always is the popup).
You need pywin32 to use this.
import Tkinter as tk
from ctypes import *
import win32con
class popup(object):
def __init__(self, parent):
self.parent = parent
self.root=tk.Toplevel(self.parent)
self.root.title("Popup")
self.root.grab_set()
self.root.transient(self.parent)
self.root.bind("<Button-1>", self.flash)
def flash(self, event):
if self.root.winfo_containing(event.x_root, event.y_root)!=self.root:
self.root.focus_set()
number_of_flashes = 5
flash_time = 80
info = FLASHWINFO(0,
windll.user32.GetForegroundWindow(),
win32con.FLASHW_ALL,
number_of_flashes,
flash_time)
info.cbSize = sizeof(info)
windll.user32.FlashWindowEx(byref(info))
class FLASHWINFO(Structure):
_fields_ = [('cbSize', c_uint),
('hwnd', c_uint),
('dwFlags', c_uint),
('uCount', c_uint),
('dwTimeout', c_uint)]
main = tk.Tk()
main.title("Main")
pop = popup(main)
main.mainloop()
As it is now, the flash only occurs when the main window's body is clicked, so clicking the title bar just returns the focus to the popup without flashing. To make it fire also when that happens you could try using the <FocusOut> event, but you would have to make sure it only happens when the focus passes to the main window, but it never really does since the grab_set is used. You might want to figure that out, but as it is now it works quite well. So it's not perfect, but I hope it helps.

how to enable & disable tabs in a Tkinter/Tix Python GUI

I'm trying to make a tabbed GUI in Python and I want to be able to toggle the enabled/disabled state of the tabs (i.e. prevent the user from switching tabs, and ghost non-active tabs out to make this fact obvious). So far I've been unable to figure out how to do this state toggling.
I've decided to go with Tkinter and/or Tix because they come built into Python distros on Windows, (guiding my users through installing extra third-party dependencies will be more trouble than it's worth). I've worked with Tkinter a bit but never Tix until now-tabs seem to require it. So I've built a two-tabbed Tix.NoteBook based on the demo at http://svn.python.org/projects/python/trunk/Demo/tix/samples/NoteBook.py
For disabling a tab, the only relevant attribute of the Tix tab instance (e.g. nb.hard_disk in the demo code) seems to be configure() but naively doing something Tkinter-like, i.e. nb.hard_disk.configure(state=Tix.DISABLED), results in TclError: unknown option "-state"
Searches for "disable Tix notebook tab" yield nothing, and even the more general "disable Tix widget" yields nothing I can understand/use. Grateful for any pointers in the right direction.
In general how you disable widgets in Tkinter is by setting the "state" option to Tk.DISABLED or more foolproof just setting it to a string saying "disabled". The following grays out and disables your tab:
notebook.tab(0, state="disabled")
with 0 being the index of the tab you want to disable, and notebook being your notebook object. Does that answer your question?
Below is a simple notebook example to demonstrate:
import Tkinter
import ttk
window = Tkinter.Tk()
notebook = ttk.Notebook(window)
notebook.pack()
subframe = Tkinter.Frame(window)
subframe.pack()
notebook.add(subframe, text="tab", state="normal")
def buttonaction():
notebook.tab(0, state="disabled")
button = Tkinter.Button(subframe, command=buttonaction, text="click to disable tab")
button.pack()
if __name__ == "__main__":
window.mainloop()
This might be what you are looking for:
nb.pageconfigure('hard_disk', state=Tix.DISABLED)
http://tix.sourceforge.net/dist/current/man/html/TixCmd/tixNoteBook.htm#M27

wxPython: how to make taskbar icon respond to left-click

Using wxPython, I created a taskbar icon and menu.
Everything works fine (in Windows at least) upon right-click of the icon: i.e., the menu is displayed, and automatically hidden when you click somewhere else, like on Windows' taskbar.
Now I do want to have the menu appear when the icon is left-clicked as well.
So I inserted a Bind() to a left-click in the Frame class wrapper, calling the CreatePopupMenu() of the taskbar icon:
import wx
class BibTaskBarIcon(wx.TaskBarIcon):
def __init__(self, frame):
wx.TaskBarIcon.__init__(self)
self.frame = frame
icon = wx.Icon('test_icon.ico', wx.BITMAP_TYPE_ICO)
self.SetIcon(icon, "title")
def CreatePopupMenu(self):
self.menu = wx.Menu()
self.menu.Append(wx.NewId(), "dummy menu ")
self.menu.Append(wx.NewId(), "dummy menu 2")
return self.menu
class TaskBarFrame(wx.Frame):
def __init__(self, parent, title):
wx.Frame.__init__(self, parent, style=wx.FRAME_NO_TASKBAR)
...
self.tbicon = BibTaskBarIcon(self)
wx.EVT_TASKBAR_LEFT_UP(self.tbicon, self.OnTaskBarLeftClick)
...
def OnTaskBarLeftClick(self, evt):
self.PopupMenu(self.tbicon.CreatePopupMenu())
...
def main(argv=None):
app = wx.App(False)
TaskBarFrame(None, "testing frame")
app.MainLoop()
This works fine, except that the menu does not disappear automatically when you click somewhere else on your screen. In fact, left-clicking multiple times on the icon creates multiple menus. The only way to hide the menu(s) is to click on one of its items (which you don't always want). I've looked at the available methods of TaskbarIcon, but I failed to be clear about which one to use to hide the menu (.Destroy() didn't work). Moreover, I don't know which event to bind it to (there is a EVT_SET_FOCUS, but I couldn't find any EVT_LOOSE_FOCUS or similar).
So, how to hide the menu upon losing focus?
EDIT: I've inserted a bit more code, to make it more clear
Ah, I've discovered what went wrong. In the statement
self.PopupMenu(self.tbicon.CreatePopupMenu())
I had bound the popup menu to the frame, instead of to the taskbar icon.
By changing it to:
self.tbicon.PopupMenu(self.tbicon.CreatePopupMenu())
all is working well now.
Thanks for all remarks
I think the problem here is that the PopupMenu is usually used in a program's context, not a little icon in the system tray. What that means is that in a normal frame, the popup menu would detect the click the you clicked off of it. Here, you are clicking outside of the wxPython program. Also, the PopupMenu is usually used with EVT_CONTEXT_MENU, not this taskbar event.
You can try wx.EVT_KILL_FOCUS and see if that works since it should theoretically fire when you click off the menu. Or you could ask on the official wxPython forum here: http://groups.google.com/group/wxpython-users/topics
Mike Driscoll
Blog: http://blog.pythonlibrary.org

Categories