In Tkinter for constructing the menubar with the <menu_item>.add_command() we need a string for the accelerator argument which will create the hotkey binding for a command.
I created a method, which is checking if the user's platform is Mac or other, and if it is, then returns the Command key string combined with the other keys.
But it doesn't work -> the menu is building, if I click on the menu-item it is working, but not working with the hot-keys. ALthough I can see the ⌘ + N in the menu..
My first thought is, that the self.hot_key() method is not called while passed as an argument..
import sys
import Tkinter
class app(object):
def __init__(self):
self.gui = Tkinter.Tk()
self.gui.minsize(width=640, height=320)
menu = Tkinter.Menu(self.gui)
filemenu = Tkinter.Menu(menu, tearoff=0)
filemenu.add_command(
label = 'New',
command = self.New,
accelerator = self.hot_key('n')
)
menu.add_cascade(
label = 'File',
menu = filemenu
)
self.gui.config(menu=menu)
self.text = Tkinter.Text(self.gui)
self.text.pack(expand=Tkinter.YES, fill=Tkinter.BOTH)
def hot_key(self, *keys):
super_key = 'Command' if sys.platform == 'darwin' else 'Control'
return '{super}+{keys}'.format(super=super_key, keys='+'.join(keys))
def New(self):
print "I'm working!"
App = app()
App.gui.mainloop()
According to this page,
The "accelerator" option is used to indicate the menu accelerator that
should be associated with this menu. This does not actually create the
accelerator, but only displays what it is next to the menu item. You
still need to create a binding for the accelerator yourself.
So your accelerator keyword argument is working as designed -- the Command-N symbol appears in your menu.
As mgilson suggests in a comment, you can use bind_all to get the keyboard combination to actually do something.
self.gui.bind_all("<Command-n>", lambda event: self.New())
Related
window6.after(1,lambda:window6.destroy())
is what I've been using to close my windows, is there any way to get them back after doing this?
basically, is there something that is the opposite of this?
ps. these are the libraries that I've imported, if it helps in any way
import tkinter as tk
from tkinter import *
import time
from tkinter import ttk
Is there a way to reopen a window after closing it using destroy() in tkinter?
The short answer is "no". Once it has been destroyed, it is impossible to get back. You should either create the window via a frame or class so that it's easy to recreate, or hide the window by calling .withdraw() rather than .destroy().
If you put the window code into a class or a function then after destroying it you can create a new instance of it by
1: creating a new instance of the class with the window code in the init function
2: call the function the has the code for the window
By doing this you are essentially creating a new instance of the program, but without initiating the script.
from tkinter impot *
from tkinter import ttk
#creating window function, not class
def main_window():
#window code here
root = Tk()
Label(root, text = "Hello World").pack()
#destroying main window
root.destroy()
root.mainloop()
main_window()
Of course, there are a few hurdles such as the window shutting down as soon as it opens, but this is to show that you can create a new instance of a window from your program.
You can wait for user input to see whether or not the window will open or close.
If you took an OOP approach, you can pass a reference to the Parent Widget as argument to the New_Window and store it in a class attribute.
You´ll have a two way reference: Parent knows child and child knows parent.
Then you can set the Parent Reference to the New_Window to None, from within the child Widget self.parent.new_window = None in a close_me() method right after you call self.destroy() on the New_Window:
1st Bonus: this code prevents the opening of more than 1 instance of a Window at a time. You won´t get more than 1 New_Window on the screen. I don´t think having two loggin windows opened or two equal options window makes sense.
2nd Bonus: It is possible to close the window from other parts of the code, as in a MVC patter, the Controller can close the window after doing some processing.
Here´s a working example:
import tkinter as tk
class Toolbar(tk.Frame):
'''Toolbar '''
def __init__(self, master, *args, **kwargs):
super().__init__(master, *args, **kwargs)
# to store the New Window reference
self.new_window = None
self.button_new_window = tk.Button(self, text = 'New Window', command = lambda : self.get_window(self))
self.configure_grid()
def configure_grid(self):
'''Configures the Grid layout'''
self.grid(row=1, column=0, columnspan=3, sticky=(tk.N,tk.S,tk.E,tk.W))
self.button_new_window.grid(row = 2, column = 2, padx=5, pady=5)
def get_window(self, parent):
''' If window exists, return it, else, create it'''
self.new_window = self.new_window if self.new_window else Window(parent)
return self.new_window
class Window(tk.Toplevel):
'''Opens a new Window.
#param parent -- tk.Widget that opens/reference this window
'''
def __init__ (self, parent : tk.Widget):
# Stores reference to the Parent Widget, so you can set parent.new_window = None
self.parent = parent
super().__init__(master = parent.master)
self.title('New Window')
self.button_dummy = tk.Button(self, text = 'Do the thing', width = 25, command = lambda : print("Button pressed on window!"))
self.button_close = tk.Button(self, text = 'Close', width = 25, command = self.close_me)
self.configure_grid()
def configure_grid(self):
'''Grid'''
self.button_dummy.grid(row = 1, column = 0)
self.button_close.grid(row = 2, column = 0)
def close_me(self):
'''Tkinter widgets are made of two parts. 1. The python Object and 2. The GUI Widget.
The destroy() method gets rid of the widget part, but leaves the object in memory.
To also destroy the object, you need to set all of its references count to ZERO on
the Parent Widget that created the new Window, so the Garbage Collector can collect it.
'''
# Destroys the Widget
self.destroy()
# Decreasses the reference count on the Parent Widget so the Garbage Collector can destroy the python object
self.parent.new_window = None
if __name__ == '__main__':
root = tk.Tk()
toolbar = Toolbar(root)
root.mainloop()
I don´t know if this: .destroy() and re-instantiate approach is more efficient than the .withdraw() and .deiconify(). Maybe if you have a program that runs for long periods of time and opens a lot of windows it can be handy to avoid stackoverflow or heapoverflow.
It sure frees up the object reference from memory, but it has the additional cost of the re-instantiation, and that is processing time.
But as David J. Malan would say on CS50, “There´s always a tradeoff”.
I often see Tkinter applications initialize Menu widgets using tearoff=0 in the constructor.
import tkinter as tk
root = tk.Tk()
menubar = tk.Menu(root)
filemenu = tk.Menu(menubar, tearoff=0)
effbot.org's documentation for Menu specifies that the default value for tearoff is 1, but it doesn't explain what the value is used for.
tearoff=
Default value is 1. (tearOff/TearOff)
tearoffcommand=
No default value. (tearOffCommand/TearOffCommand)
What does the tearoff attribute do when initializing a tkinter Menu widget?
The official python docs admit that they're a little light on details:
The tkinter package is a thin object-oriented layer on top of Tcl/Tk. To use tkinter, you don’t need to write Tcl code, but you will need to consult the Tk documentation, and occasionally the Tcl documentation.
The Tk documentation for tearoff gives you what you're looking for:
tearoff allows you to detach menus for the main window creating floating menus. If you create a menu you will see dotted lines at the top when you click a top menu item. If you click those dotted lines the menu tears off and becomes floating.
Here you can see a tkinter Menu tear-off with the code for it in the background. I'm not sure how useful this is going to be but according to New Mexico Tech:
Normally, a menu can be torn off: the first position (position 0) in the list of choices is occupied by the tear-off element, and the additional choices are added starting at position 1. If you set tearoff=0, the menu will not have a tear-off feature, and choices will be added starting at position 0.
Try this if you want to test the floating menu if you are using Windows.
from tkinter import *
import re
class HoverInfo(Menu):
def __init__(self, parent, text, command=None):
self._com = command
Menu.__init__(self,parent, tearoff=1)
if not isinstance(text, str):
raise TypeError('Trying to initialise a Hover Menu with a non string type: ' + text.__class__.__name__)
toktext=re.split('\n', text)
for t in toktext:
self.add_command(label = t)
self._displayed=False
self.master.bind("<Enter>",self.Display )
self.master.bind("<Leave>",self.Remove )
def __del__(self):
self.master.unbind("<Enter>")
self.master.unbind("<Leave>")
def Display(self,event):
if not self._displayed:
self._displayed=True
self.post(event.x_root, event.y_root)
if self._com != None:
self.master.unbind_all("<Return>")
self.master.bind_all("<Return>", self.Click)
def Remove(self, event):
if self._displayed:
self._displayed=False
self.unpost()
if self._com != None:
self.unbind_all("<Return>")
def Click(self, event):
self._com()
class MyApp(Frame):
def __init__(self, parent=None):
Frame.__init__(self, parent)
self.grid()
self.lbl = Label(self, text='testing')
self.lbl.grid()
self.hover = HoverInfo(self, 'while hovering press return \n for an exciting msg', self.HelloWorld)
def HelloWorld(self):
print('Hello World')
app = MyApp()
app.master.title('test')
app.mainloop()
This example is Hover Class by Gogo. Display message when hovering over something with mouse cursor in Python
I Just set tear-off to 1 to see the floating effect.
By default, the choices in the menu start taking place from position 1. If we set the tearoff = 1, then it will start taking place from 0th position
from tkinter import *
main = Tk()
var = StringVar()
options = OptionMenu(main, var, 'option1')
options.grid()
options['menu'].add_command(label='option2')
main.mainloop()
When you select 'option1' in the menu it shows that string on the widget. If you select 'option2' it does not show that string on the widget. If you set a command for option2 it will run. Why is 'option2' not showing in the widget when it is selected?
python 3.5
UPDATE:
By adding strings through a for loop I run into a problem where the command for each string uses the same variable.
from tkinter import *
def _print(var, string):
print(string)
var.set(string)
lst = ['hello', 'bob', 'testing']
main = Tk()
var = StringVar()
options = OptionMenu(main, var, 'option1')
options.grid()
for string in lst:
options['menu'].add_command(label=string, command=lambda: _print(var, string))
main.mainloop()
I tried using lambda event, i=string: _print(var, i)) in the loop but lamdba doesnt like it. Says its missing 'event'. I've put event in the _print function and the call to _print but I get the same error that lamdba is missing event. Any idea how I can pass the right variable for each command in the loop?
It works when you declared 'option2' in the Optionmenu() widget. More examples can be read from here and here.
from tkinter import *
main = Tk()
var = StringVar()
options = OptionMenu(main, var, 'option1', 'option2')
options.grid()
#options['menu'].add_command(label='option2')
main.mainloop()
I found that the menu option is used for creating items in a Menu() widget but not for a Optionmenu() widget. This is stated here. You can read more about how to declare a .Menu() widget here. All the best to your tkinter developments.
Edit1:
The reason why it was failing was because you did not declare the command option associated with the .add_command method. Having done this alone, you will notice that the Optionmenu still does not get updated with the new label of the added menu item. To resolve this issue, you have to use the .set() method of the Control Variable StringVar to do the updating. See revised script below.
from tkinter import *
def _print1(value):
# By default, callback to OptionMenu has a positional argument for the
# menu item's label.
print(value)
print(var.get())
def _print2():
var.set('option2')
print(var.get())
main = Tk()
var = StringVar()
options = OptionMenu(main, var, 'option1',command=_print1)
options.grid()
options['menu'].add_command(label='option2', command=_print2)
# To add more clickable menu items, the 'add_command' method requires you to
# to declare it's options 'label' and 'command'.
main.mainloop()
Edit2:
Remark, I have added the command aspect to options in Edit1. I
did not address earlier but thought it needed to be shown for
completeness.
Responding to your question in your UPDATE, to do what you want, I
rewrote the script as a class object. Also I used the internal class
_setit found in tkinter, which the OptionMenu widget had used to
do configure the callback used by command. This approach overcame the issue you encountered.
Revised Code:
from tkinter import *
class App(Tk):
def __init__(self, parent=None):
Tk.__init__(self, parent)
self.parent=parent
self.createOM()
self.grid()
def createOM(self):
# Create OptionMenu
omlist=['option1']
self.var = StringVar()
self.options = OptionMenu(self, self.var, *omlist,
command=self._print1)
self.options.grid()
values = ['hello', 'bob', 'testing']
for v in values:
self.options['menu'].add_command(
label=v, command=_setit(self.var, v, self._print2))
# To add more clickable menu items, the 'add_command' method requires you to
# to declare it's options 'label' and 'command'.
def _print1(self, value, *args):
#callback for OptionMenu has these arguments because inherently it
#uses the _setit class to configure the callback with these arguments.
print()
print(value)
#self.var.set('option10') #Uncomment to change OptionMenu display
print(self.var.get())
def _print2(self, value, *args):
print()
print(value)
#self.var.set('option20') #Uncomment to change OptionMenu display
print(self.var.get())
#The following class is extracted from tkinter.
class _setit:
"""Internal class. It wraps the command in the widget OptionMenu."""
def __init__(self, var, value, callback=None):
self.__value = value
self.__var = var
self.__callback = callback
def __call__(self, *args):
self.__var.set(self.__value)
if self.__callback:
self.__callback(self.__value, *args)
if __name__ == "__main__":
app = App()
app.mainloop()
It doesn't select anything because it's not supposed to. You're basically hacking OptionMenu's visual part. The items you pass as values are first added to the menu and then are given the command option to modify the selection. When you call options['menu'].add_command(label='option2') you're basically only modifying the visual part of the widget, 'option2' is not added to values, and thus command option isn't set. See for yourself in the source code here.
I have a object oriented tkinter program set up.
I have initialized a variable to store Toplevel() in as
self.toplevel = None
Then when I create the actual Toplevel window I simply assign it to the variable:
self.toplevel = Toplevel()
The thing is...when the Toplevel() window is closed, the value still remains in the variable self.toplevel. How can I reset the variable back to None after closing the window so that I can perform a check:
if (self.toplevel == None):
self.toplevel = Toplevel()
Or are there any other methods to prevent multiple Toplevel Windows from opening?
Check this How do I handle the window close event in Tkinter?
Assign the value None to self.toplevel after the Toplevelcloses usign a callback function TopCloses. For this, write a method within the GUI class to access the toplevel attribute and set it's value to None inside the callback function.
In your main program,
def TopCloses():
top.destroy()
#Call the setTopLevel method and assign the attribute toplevel value None
guiObject.setTopLevel(None)
top.protocol("WM_DELETE_WINDOW", TopCloses)
root.mainloop()
Here is my solution:
#somewhere in __init__ make
self.window = None
#I took this piece of code from my bigger app and I have a function
#self.makevariables(), which is called in init, which contains the line above.
def instructions(self):
if self.window == None: #here I check whether it exists, if not make it, else give focus to ok button which can close it
self.window = Toplevel(takefocus = True)
#some optional options lol
self.window.geometry("200x200")
self.window.resizable(0, 0)
#widgets in the toplevel
Label(self.window, text = "NOPE").pack()
self.window.protocol("WM_DELETE_WINDOW", self.windowclosed) #this overrides the default behavior when you press the X in windows and calls a function
self.okbutton = Button(self.window, text = "Ok", command = self.windowclosed, padx = 25, pady = 5)
self.okbutton.pack()
self.okbutton.focus()
self.okbutton.bind("<Return>", lambda event = None:self.windowclosed())
else:
self.okbutton.focus() #You dont need to give focus to a widget in the TopLevel, you can give the focus to the TopLevel, depending how you want it
#self.window.focus() works too
def windowclosed(self): #function to call when TopLevel is removed
self.window.destroy()
self.window = None
These are all overly complicated solutions imo.
I just use win32gui as such:
toplevel_hwid = win32gui.FindWindow(None, '<Top Level Window Title>')
if toplevel_hwid:
print(f'Top level window already open with HWID: {toplevel_hwid}')
win32gui.SetForegroundWindow(toplevel_hwid)
return
else:
<create new top level>
Simple, easy, and gives you the flexibility to close, move, focus, etc.
I'm using Python's TkInter module for a GUI. Below is a simple checkbox code.
def getCheckVal():
print cbVar.get()
windowTime=Tk.Tk()
cbVar = Tk.IntVar()
btnC = Tk.Checkbutton(windowTime, text="Save", variable = cbVar, command=getCheckVal)
btnC.grid()
windowTime.mainloop()
This code works fine. Each time I tick the checkbox, I get 1, else 0.
However, when I run the same code in a function that is called from another TkInter command (when a button is pressed), it stops working. I always get 0 as the value.
class GUIMainClass:
def __init__(self):
'''Create the main window'''
self.window = Tk.Tk()
def askUser(self):
def getCheckVal():
print cbVar.get()
windowTime=Tk.Tk()
cbVar = Tk.IntVar()
btnC = Tk.Checkbutton(windowTime, text="Save", variable = cbVar,
command=getCheckVal)
btnC.grid()
windowTime.mainloop()
def cmdWindow(self):
frameShow=Tk.Frame(self.window)
frameShow.grid()
btnSwitch = Tk.Button(frameShow, text='Show Plots', command=self.askUser)
btnSwitch.grid()
self.window.mainloop()
GUIObj=GUIMainClass()
GUIObj.cmdWindow()
This is very unusual. What could be going wrong?
EDIT: I've used 2 mainloops because I want a separate window (windowTime) to open up when I click "Show Plots" button. This new window should have the checkbox in it.
Your windowTime, cbVar, etc. variables are defined in the function's local scope. When askUser() completes execution, those values are thrown away. Prepend self. to them to save them as instance variables.
There should only be one mainloop() in your program, to run the main Tkinter root object. Try putting it as the very last line in the program. I recommend doing some reading on Effbot for how to set up a Tkinter application.
I'm not sure what all you're trying to do, but one problem is that the TK.IntVar called cbVar that you create in your askUser() method will be deleted when the function returns, so you need to attach it to something that will still exist after that happens. While you could make it a global variable, a better choice would be to make it an attribute of something more persistent and has a longer "lifespan".
Another likely issue is that generally there should only be one call to mainloop() in a single Tkinter application. It appears what you want to do is display what is commonly known as a Dialog Window, which Tkinter also supports. There's some standard ones built-in, plus some more generic classes to simplify creating custom ones. Here's some documentation I found which describes them in some detail. You may also find it helpful to look at their source code.
In Python 2 it's in the /Lib/lib-tk/tkSimpleDialog.py file and
in Python 3 the code's in a file named /Lib/tkinter/simpledialog.py.
Below is code that takes the latter approach and derives a custom dialog class named GUIButtonDialog from the generic one included the Tkinter library which is simply named Dialog.
try:
import Tkinter as Tk # Python 2
from tkSimpleDialog import Dialog
except ModuleNotFoundError:
import tkinter as Tk # Python 3
from tkinter.simpledialog import Dialog
class GUIButtonDialog(Dialog):
"""Custom one Button dialog box."""
def __init__(self, btnText, parent=None, title=None):
self.btnText = btnText
Dialog.__init__(self, parent, title)
def getCheckVal(self):
print(self.cbVar.get())
def body(self, master):
"""Create dialog body."""
self.cbVar = Tk.IntVar()
self.btnC = Tk.Checkbutton(master, text=self.btnText, variable=self.cbVar,
command=self.getCheckVal)
self.btnC.grid()
return self.btnC # Return the widget to get inital focus.
def buttonbox(self):
# Overridden to suppress default "OK" and "Cancel" buttons.
pass
class GUIMainClass:
def __init__(self):
"""Create the main window."""
self.window = Tk.Tk()
def askUser(self):
"""Display custom dialog window (until user closes it)."""
GUIButtonDialog("Save", parent=self.window)
def cmdWindow(self):
frameShow = Tk.Frame(self.window)
frameShow.grid()
btnSwitch = Tk.Button(frameShow, text='Show Plots', command=self.askUser)
btnSwitch.grid()
self.window.mainloop()
GUIObj = GUIMainClass()
GUIObj.cmdWindow()