OptionMenu command argument changing when function called - python

I'm new to tkinter and I'm trying to create an OptionMenu that updates a frame when an option is chosen from the menu. The issue I'm running into is that the frame I'm passing as an argument to the select function is changing to the menu selection. Can someone help me understand why this is happening?
Here is an example code. When I print self.display I get the expected value of .!frame, but after I pass it as an argument to select and print I get the value of the option chosen from the drop down menu.
import tkinter as tk
class menu(tk.Frame):
def __init__(self, parent, display, *args, **kwargs):
tk.Frame.__init__(self, parent, *args, **kwargs)
self.parent = parent
self.display = display
menu_frame = tk.Frame(self.parent)
self.default_val = tk.StringVar()
self.default_val.set("Default")
print(self.display)
menu = tk.OptionMenu(menu_frame, self.default_val, "A", "B", "C", command=lambda arg=self.display: self.select(arg))
menu.pack()
menu_frame.pack()
def select(self, frame):
print(frame)
selection = self.default_val.get()
self.default_val.set("Default")
app = tk.Tk()
app.geometry("400x200")
display_frame = tk.Frame(app)
display_frame.pack()
menu(app, display_frame)
app.mainloop()

Related

Tkinter: Updating Images in labels that aren't directly accessible

In my GUI, i wanted to display an image that changes depending on some value. The image would change between self.img1 and self.img2. I created separate classes for the container and the pages. The container is defined as such:
class Gui(Tk):
def __init__(self, *args, **kwargs):
Tk.__init__(self, *args, **kwargs)
container = Frame(self)
container.pack(side="top", fill = "both", expand = TRUE)
container.grid_rowconfigure(0, weight = 1)
self.MyReading = StringVar()
self.redpic = Image.open("red.png")
self.redpic = self.redpic.resize((100,100), Image.ANTIALIAS)
self.greenpic = Image.open("green.png")
self.greenpic = self.greenpic.resize((100,100), Image.ANTIALIAS)
self.img1 = ImageTk.PhotoImage(self.redpic)
self.img2 = ImageTk.PhotoImage(self.greenpic)
self.frames={}
for F in (StartPage, PageOne):
frame = F(container, self)
self.frames[F] = frame
frame.grid(row = 0, column = 0, sticky = "nsew")
self.show_frame(StartPage)
def show_frame(self, cont):
frame = self.frames[cont]
frame.tkraise()
And the page displaying the image:
class StartPage(Frame):
def __init__(self, parent, controller):
Frame.__init__(self,parent)
label = Label(self, text="StartPage")
label.grid()
label1 = Label(self, textvariable = controller.MyReading)
label1.grid();
self.label4 = Label(self, image = controller.img1)
self.label4.grid();
self.label4.image = controller.img1
button1 = Button (self, text = "Show PageOne", command = lambda: controller.show_frame(PageOne))
button1.grid()
It is currently displaying img1. Now, to instantiate the GUI:
root = Gui()
update_reading()
root.mainloop()
update_reading() updates my other labels defined with StringVar(). I was wondering how would I go about updating label4 (which shows the image) if I can only instantiate/get access to Gui()? I only know that I could change the label4 through configure(). Is there a textvariable equivalent for images?
EDIT: I forgot to put the logic that I wanted to implement. It is basically:
If foo == TRUE:
--change the image to img1--
else:
--change the image to img2--
for some foo that exists outside of Gui.
EDIT2: Following through a previous comment's logic, I made some small changes to the code In the Gui:
class Gui(Tk):
def __init__(self, *args, **kwargs):
self.ColorVar = DoubleVar()
And within StartPage(), the changes are:
class StartPage(Frame):
def __init__(self, parent, controller):
controller.ColorVar.trace("w",self.IdkChief(controller))
def IdkChief(self, controller):
global val1
if float(val1) < 2.50 :
self.label4.configure(image = controller.img2)
self.label4.image = controller.img2
else:
self.label4.configure(image = controller.img1)
self.label4.image = controller.img1
Then the changes on ColorVar is defined in update_reading()as such:
def update_reading():
global val1
root.ColorVar.set(val1)
root.after(100,update_reading)
Where val1 is a changing float value. I decided to change it from a boolean logic to a float one to increase flexibility. It would then throw me a generic error
Exception in Tkinter callback Traceback (most recent call last):
File
"C:\Users\AppData\Local\Programs\Python\Python37\lib\tkinter__init__.py",
line 1705, in call
return self.func(*args) TypeError: 'NoneType' object is not callable
This error would repeat until the GUI is closed.
You can use tkinter variable trace function to set up a callback function to be executed whenever the variable is updated. Inside the callback function, you can then update the label based on the value of the variable.
Below is sample code blocks (based on your posted code design) to achieve your goal:
class Gui:
def __init__(self, *args, **kwargs):
...
self.ColorVar = DoubleVar()
...
class StartPage(Frame):
def __init__(self, parent, controller):
...
# register a callback to be executed whenever variable is modified
controller.ColorVar.trace('w', lambda *args: self.IdkChief(controller))
def IdkChief(self, controller):
img = controller.img1 if controller.ColorVar.get() < 2.5 else controller.img2
self.label4.config(image=img)

tkinter ttk OptionMenu losing checkmark highlight when updating option list

When I update a ttk.OptionMenu widget using this example: https://stackoverflow.com/a/7403530 , I lose the check mark that showed up before when I selected an item if I was using the initial list of items.
How do I get back the checkmark for the selected item?
Init Code: self.om = ttk.OptionMenu(self, self.om_variable,'a', *['a','b','c'])
Before:
After Update:
Code here:
import tkinter as tk
class SampleApp(tk.Tk):
def __init__(self, *args, **kwargs):
tk.Tk.__init__(self, *args, **kwargs)
self.om_variable = tk.StringVar(self)
b1 = tk.Button(self, text="Colors", width=8, command=self.use_colors)
b2 = tk.Button(self, text="Sizes", width=8, command=self.use_sizes)
self.om = tk.OptionMenu(self, self.om_variable, ())
self.om.configure(width=20)
self.use_colors()
b1.pack(side="left")
b2.pack(side="left")
self.om.pack(side="left", fill="x", expand=True)
def _reset_option_menu(self, options, index=None):
'''reset the values in the option menu
if index is given, set the value of the menu to
the option at the given index
'''
menu = self.om["menu"]
menu.delete(0, "end")
for string in options:
menu.add_command(label=string,
command=lambda value=string:
self.om_variable.set(value))
if index is not None:
self.om_variable.set(options[index])
def use_colors(self):
'''Switch the option menu to display colors'''
self._reset_option_menu(["red","orange","green","blue"], 0)
def use_sizes(self):
'''Switch the option menu to display sizes'''
self._reset_option_menu(["x-small", "small", "medium", "large"], 0)
if __name__ == "__main__":
app = SampleApp()
app.mainloop()
The ttk version, which you used previously, adds the checkmark functionality by default and a check will appear on the selected item. However, when you manually add items, you'll need to use the method add_radiobutton instead of add_command. This is what enables the check mark (on both tk and ttk versions).
import tkinter.tkk as tkk
def __init__(self, *args, **kwargs):
...
self.om = ttk.OptionMenu(self, self.om_variable)
...
def _reset_option_menu(self, options, index=None):
...
menu.add_radiobutton(
label=string,
command=tk._setit(self.om_variable, string)
)
...

Why does adding a custom menu break keyboard event binding in Python3/tkinter?

In the following code, when I press the button to add some secondary windows, and then try to close a window by using "Command-w" it does not always close the active window. But if I disable the menu creation by commenting the line self.gerar_menu(), windows are opened and closed as expected (I mean, by clicking the red 'x' button or by pressing Command-W in OS X). Any idea about what is wrong here?
Here is my current test code:
#!/usr/bin/env python3.6
# encoding: utf-8
import tkinter as tk
import tkinter.font
from tkinter import ttk
class baseApp(ttk.Frame):
"""
Parent classe for main app window (will include some aditional methods and properties).
"""
def __init__(self, master, *args, **kwargs):
super().__init__(master, *args, **kwargs)
self.master = master
self.mainframe = ttk.Frame(master)
self.mainframe.pack()
class App(baseApp):
""" Base class for the main application window """
def __init__(self, master, *args, **kwargs):
super().__init__(master, *args, **kwargs)
self.master = master
#self.gerar_menu() # This line breaks "Command-w" functionality
self.lbl_text = ttk.Label(self.mainframe, text="This is the Main Window")
self.lbl_text.pack()
self.btn = ttk.Button(self.mainframe, text="Open Second window",
command=lambda: self.create_detail_window(self, number=0))
self.btn.pack()
self.newDetailsWindow = {}
self.windows_count=0
def gerar_menu(self):
""" generate the application menu """
self.menu = tk.Menu(root)
root.config(menu=self.menu)
self.fileMenu = tk.Menu(self.menu)
self.menu.add_cascade(label="File", menu=self.fileMenu)
self.fileMenu.add_command(label="New Document", command=None, accelerator="Command+n")
def create_detail_window(self, *event, number=None):
self.windows_count += 1
self.newDetailsWindow[self.windows_count]=tk.Toplevel()
self.newDetailsWindow[self.windows_count].geometry('900x600+80+130')
self.newDetailsWindow[self.windows_count].title(f'Detail: {self.windows_count}')
self.newDetailsWindow[self.windows_count].wm_protocol("WM_DELETE_WINDOW", self.newDetailsWindow[self.windows_count].destroy)
self.newDetailsWindow[self.windows_count].bind("Command-w", lambda event: self.newDetailsWindow[-1].destroy())
self.detail_window = detailWindow(self.newDetailsWindow[self.windows_count], self.windows_count)
self.newDetailsWindow[self.windows_count].focus()
print(self.newDetailsWindow)
class detailWindow(ttk.Frame):
""" Base class for secondary windows """
def __init__(self, master, rep_num, *args,**kwargs):
super().__init__(master,*args,**kwargs)
self.num_rep = rep_num
self.master.minsize(900, 600)
self.master.maxsize(900, 600)
print(f"Showing details about nr. {self.num_rep}")
self.mainframe = ttk.Frame(master)
self.mainframe.pack()
self.lbl_text = ttk.Label(self.mainframe,
text=f"Showing details about nr. {self.num_rep}")
self.lbl_text.pack()
if __name__ == "__main__":
root = tk.Tk()
janela_principal = App(root)
root.title('Main Window')
root.bind_all("<Mod2-q>", exit)
root.mainloop()
If you want to create a binding for closing a window, you need the function to act upon the actual window that received the event. Your code is always deleting the last window that was opened no matter which window received the event.
The first step is to bind to a function rather than using lambda. While lambda has its uses, binding to a named function is much easier to debug and maintain.
Once the function is called, the event object can tell you which window got the event via the widget attribute of the event object. Given this window, you can get the toplevel window that this window is in (or itself, if it's a toplevel window) via the winfo_toplevel command.
For example:
window = tk.Toplevel(...)
...
window.bind("<Command-w>", self.destroy_window)
...
def destroy_window(self, event):
window = event.widget.winfo_toplevel()
window.destroy()

Tkinter Optionmenu-options list to be update(Upon selection, should remove that selection from options)

Option Menu has 3 choices "a","b","c".Suppose user selects "b" for the first optionMenu. When he click on the add button, the 2nd optionMenu should only display two options "a","c" because he has already selected option "b"
My code is displaying the three options irrespective of the option/choice selected. Is there any way out for this
import tkinter
from tkinter import *
class Window(Frame):
def __init__(self,master):
Frame.__init__(self,master)
self.master=master
self.func()
def func(self):
self.count=0
self.op_row=0
button=Button(self.master,text="Add",command= self.func_op)
button.grid(column=0,row=0)
label=Label(self,text="Welcome")
label.grid(column=0,row=0)
def func_op(self):
self.count=self.count+1
self.op_row=self.op_row+1
self.var=StringVar()
options=["a","b","c"]
op=OptionMenu(self.master,self.var,*options)
op.grid(column=0,row=self.op_row)
if __name__ == "__main__":
root = Tk()
aplication = Window(root)
root.mainloop()
The optionmenu isn't designed to have elements removed when they are selected. It's specifically designed to display the item that was selected.
If you want to let the user pick something from a menu and then remove the item from the menu, you should just use a menubutton and a menu. That is exactly what an OptionMenu is, except that it has additional behavior that you explicitly don't want to use.
Here's a simple example:
import Tkinter as tk
class Example(tk.Frame):
def __init__(self, parent):
tk.Frame.__init__(self, parent)
self.menubutton = tk.Menubutton(self, text="Pick an option", indicatoron=True,
borderwidth=1, relief="raised")
self.menu = tk.Menu(self.menubutton, tearoff=False)
self.menubutton.configure(menu=self.menu)
for choice in ("a", "b", "c"):
self.menu.add_command(label=choice,
command=lambda option=choice: self.set_option(option))
self.text = tk.Text(self)
self.menubutton.pack(side="top")
self.text.pack(side="top", fill="both", expand=True)
def set_option(self, option):
self.text.insert("end", "you have chosen %s\n" % option)
self.menu.delete(option)
if __name__ == "__main__":
root = tk.Tk()
Example(root).pack(fill="both", expand=True)
root.mainloop()
Your users might find it very strange and confusing to see items disappear from a menu. It your main goal is to simply prevent the user from picking the same option twice you can simply disable the item once it is chose with something like this in place of the delete statement:
self.menu.entryconfigure(option, state="disabled")
This should do the trick. Now the Name menu has 3 element a,b,c and if the button is pressed the chosen one will be "added" (printed to the console) and will disappear from the list.
import sys
if sys.version_info[0] >= 3:
import tkinter as tk
else:
import Tkinter as tk
class App(tk.Frame):
def __init__(self, master):
tk.Frame.__init__(self, master)
self.dict = ['a','b','c']
self.variable_a = tk.StringVar()
self.optionmenu_a = tk.OptionMenu(self, self.variable_a, *self.dict)
tk.Button(self, text="Add", command=self.func).pack()
self.optionmenu_a.pack()
self.pack()
def func(self):
menu = self.optionmenu_a["menu"]
print self.variable_a.get() + " added"
menu.delete(self.dict.index(self.variable_a.get()))
if __name__ == "__main__":
root = tk.Tk()
app = App(root)
app.mainloop()
You cant delete the last element as far as i know. But if you delete the last element you can just delete the whole options menu.
EDIT: Edited according to OPs comment, and edited the code

tkinter app adding a right click context menu?

I have a python-tkinter gui app that I've been trying to find some way to add in some functionality. I was hoping there would be a way to right-click on an item in the app's listbox area and bring up a context menu. Is tkinter able to accomplish this? Would I be better off looking into gtk or some other gui-toolkit?
You would create a Menu instance and write a function that calls
its post() or tk_popup() method.
The tkinter documentation doesn't currently have any information about tk_popup().
Read the Tk documentation for a description, or the source:
library/menu.tcl in the Tcl/Tk source:
::tk_popup --
This procedure pops up a menu and sets things up for traversing
the menu and its submenus.
Arguments:
menu - Name of the menu to be popped up.
x, y - Root coordinates at which to pop up the menu.
entry - Index of a menu entry to center over (x,y).
If omitted or specified as {}, then menu's
upper-left corner goes at (x,y).
tkinter/__init__.py in the Python source:
def tk_popup(self, x, y, entry=""):
"""Post the menu at position X,Y with entry ENTRY."""
self.tk.call('tk_popup', self._w, x, y, entry)
You associate your context menu invoking function with right-click via:
the_widget_clicked_on.bind("<Button-3>", your_function).
However, the number associated with right-click is not the same on every platform.
library/tk.tcl in the Tcl/Tk source:
On Darwin/Aqua, buttons from left to right are 1,3,2.
On Darwin/X11 with recent XQuartz as the X server, they are 1,2,3;
other X servers may differ.
Here is an example I wrote that adds a context menu to a Listbox:
import tkinter # Tkinter -> tkinter in Python 3
class FancyListbox(tkinter.Listbox):
def __init__(self, parent, *args, **kwargs):
tkinter.Listbox.__init__(self, parent, *args, **kwargs)
self.popup_menu = tkinter.Menu(self, tearoff=0)
self.popup_menu.add_command(label="Delete",
command=self.delete_selected)
self.popup_menu.add_command(label="Select All",
command=self.select_all)
self.bind("<Button-3>", self.popup) # Button-2 on Aqua
def popup(self, event):
try:
self.popup_menu.tk_popup(event.x_root, event.y_root, 0)
finally:
self.popup_menu.grab_release()
def delete_selected(self):
for i in self.curselection()[::-1]:
self.delete(i)
def select_all(self):
self.selection_set(0, 'end')
root = tkinter.Tk()
flb = FancyListbox(root, selectmode='multiple')
for n in range(10):
flb.insert('end', n)
flb.pack()
root.mainloop()
The use of grab_release() was observed in an example on effbot.
Its effect might not be the same on all systems.
I made some changes to the conext menu code above in order to adjust my demand and I think it would be useful to share:
Version 1:
import tkinter as tk
from tkinter import ttk
class Main(tk.Frame):
def __init__(self, master):
tk.Frame.__init__(self, master)
master.geometry('500x350')
self.master = master
self.tree = ttk.Treeview(self.master, height=15)
self.tree.pack(fill='x')
self.btn = tk.Button(master, text='click', command=self.clickbtn)
self.btn.pack()
self.aMenu = tk.Menu(master, tearoff=0)
self.aMenu.add_command(label='Delete', command=self.delete)
self.aMenu.add_command(label='Say Hello', command=self.hello)
self.num = 0
# attach popup to treeview widget
self.tree.bind("<Button-3>", self.popup)
def clickbtn(self):
text = 'Hello ' + str(self.num)
self.tree.insert('', 'end', text=text)
self.num += 1
def delete(self):
print(self.tree.focus())
if self.iid:
self.tree.delete(self.iid)
def hello(self):
print ('hello!')
def popup(self, event):
self.iid = self.tree.identify_row(event.y)
if self.iid:
# mouse pointer over item
self.tree.selection_set(self.iid)
self.aMenu.post(event.x_root, event.y_root)
else:
pass
root = tk.Tk()
app=Main(root)
root.mainloop()
Version 2:
import tkinter as tk
from tkinter import ttk
class Main(tk.Frame):
def __init__(self, master):
master.geometry('500x350')
self.master = master
tk.Frame.__init__(self, master)
self.tree = ttk.Treeview(self.master, height=15)
self.tree.pack(fill='x')
self.btn = tk.Button(master, text='click', command=self.clickbtn)
self.btn.pack()
self.rclick = RightClick(self.master)
self.num = 0
# attach popup to treeview widget
self.tree.bind('<Button-3>', self.rclick.popup)
def clickbtn(self):
text = 'Hello ' + str(self.num)
self.tree.insert('', 'end', text=text)
self.num += 1
class RightClick:
def __init__(self, master):
# create a popup menu
self.aMenu = tk.Menu(master, tearoff=0)
self.aMenu.add_command(label='Delete', command=self.delete)
self.aMenu.add_command(label='Say Hello', command=self.hello)
self.tree_item = ''
def delete(self):
if self.tree_item:
app.tree.delete(self.tree_item)
def hello(self):
print ('hello!')
def popup(self, event):
self.aMenu.post(event.x_root, event.y_root)
self.tree_item = app.tree.focus()
root = tk.Tk()
app=Main(root)
root.mainloop()
from tkinter import *
root=Tk()
root.geometry("500x400+200+100")
class Menu_Entry(Entry):
def __init__(self,perant,*args,**kwargs):
Entry.__init__(self,perant,*args,**kwargs)
self.popup_menu=Menu(self,tearoff=0,background='#1c1b1a',fg='white',
activebackground='#534c5c',
activeforeground='Yellow')
self.popup_menu.add_command(label="Cut ",command=self.Cut,
accelerator='Ctrl+V')
self.popup_menu.add_command(label="Copy ",command=self.Copy,compound=LEFT,
accelerator='Ctrl+C')
self.popup_menu.add_command(label="Paste ",command=self.Paste,accelerator='Ctrl+V')
self.popup_menu.add_separator()
self.popup_menu.add_command(label="Select all",command=self.select_all,accelerator="Ctrl+A")
self.popup_menu.add_command(label="Delete",command=self.delete_only,accelerator=" Delete")
self.popup_menu.add_command(label="Delete all",command=self.delete_selected,accelerator="Ctrl+D")
self.bind('<Button-3>',self.popup)
self.bind("<Control-d>",self.delete_selected_with_e1)
self.bind('<App>',self.popup)
self.context_menu = Menu(self, tearoff=0)
self.context_menu.add_command(label="Cut")
self.context_menu.add_command(label="Copy")
self.context_menu.add_command(label="Paste")
def popup(self, event):
try:
self.popup_menu.tk_popup(event.x_root, event.y_root, 0)
finally:
self.popup_menu.grab_release()
def Copy(self):
self.event_generate('<<Copy>>')
def Paste(self):
self.event_generate('<<Paste>>')
def Cut(self):
self.event_generate('<<Cut>>')
def delete_selected_with_e1(self,event):
self.select_range(0, END)
self.focus()
self.event_generate("<Delete>")
def delete_selected(self):
self.select_range(0, END)
self.focus()
self.event_generate("<Delete>")
def delete_only(self):
self.event_generate("<BackSpace>")
def select_all(self):
self.select_range(0, END)
self.focus()
ent=Menu_Entry(root)
ent.pack()
root.mainloop()
Important Caveat:
(Assuming the event argument that contains the coordinates is called "event"): Nothing will happen or be visible when you call tk_popup(...) unless you use "event.x_root" and "event.y_root" as arguments. If you do the obvious of using "event.x" and "event.y", it won't work, even though the names of the coordinates are "x" and "y" and there is no mention of "x_root" and "y_root" anywhere within it.
As for the grab_release(..), it's not necessary, anywhere. "tearoff=0" also isn't necessary, setting it to 1 (which is default), simply adds a dotted line entry to the context menu. If you click on it, it detaches the context menu and makes it its own top-level window with window decorators. tearoff=0 will hide this entry. Moreover, it doesn't matter if you set the menu's master to any specific widget or root, or anything at all.

Categories

Resources