Related
I am trying to create a UNIT CONVERTER which is a GUI application in Python using Tkinter. I have created one main OptionMenu and two other OptionMenus. These other two OptionMenus are dependent on the main OptionMenu i.e.upon selecting a value from the main OptionMenu, the list of values in the other two OptionMenus changes. I have created two buttons "Convert" and "Reset". In the Reset Button, I am trying to reset the selections on all three OptionMenus.
Source Code
import tkinter as tk
from tkinter import messagebox
from math import *
root = tk.Tk()
root.title("Unit Converter")
root.geometry("600x400")
# A function for updating the dropdown lists upon selecting the operation.
def updateSubLists(self):
y.set('')
z.set('')
subUnitListFrom['menu'].delete(0,'end')
subUnitListTo['menu'].delete(0,'end')
for item in list(listOfUnits.get(x.get())):
subUnitListFrom['menu'].add_command(label=item,command=tk._setit(y,item))
subUnitListTo['menu'].add_command(label=item,command=tk._setit(z,item))
y.set(list(listOfUnits.get(x.get()))[0])
z.set(list(listOfUnits.get(x.get()))[0])
# A callback function to validate if the data entered is only digit or not.
def validateUserInput(input):
"""This method validates the data entered by the User to check if the entered data is a number or not."""
if input.isdigit() == True:
return True
elif input == "":
return True
else:
messagebox.showinfo("Information","Only number is allowed")
return False
# A function for resetting the entries selected.
def resetEntries():
""" This method helps in resetting the entries given as a input or selected by the User"""
x.set("")
unitList["menu"].delete(0,'end')
for item in list(listOfUnits.keys()):
unitList['menu'].add_command(label=item,command=tk._setit(x,item))
x.set(list(listOfUnits.keys())[0])
#updateSubLists('')
y.set('')
z.set('')
subUnitListFrom['menu'].delete(0,'end')
subUnitListTo['menu'].delete(0,'end')
for item in list(listOfUnits.get(x.get())):
subUnitListFrom['menu'].add_command(label=item,command=tk._setit(y,item))
subUnitListTo['menu'].add_command(label=item,command=tk._setit(z,item))
y.set(list(listOfUnits.get(x.get()))[0])
z.set(list(listOfUnits.get(x.get()))[0])
# Lists and Sub-lists creation
#listOfUnits = ['Area','Energy','Frequency','Length','Mass','Pressure','Speed','Temperature',
#'Time','Volume']
listOfUnits = {"Area":['Square Kilometer','Squatre Meter','Square Mile','Square Yard','Square Foot','Square Inch','Hectare','Acre'],
"Energy":['Joule','Kilo Joule','Gram Calorie','Kilo Calorie'],
"Frequency":['Hertz','Kilohertz','Megahertz','Kilohertz'],
"Length":['Kilometer','Meter','Centimeter','Millimeter','Micrometer','Nanometer','Mile','Yard','Foot','Inch'],
"Mass":['Tonne','Kilogram','Microgram','Milligram','Gram','Pound','Ounce'],
"Pressure":['Bar','Pascal','Pound per square inch','Standard atmosphere','Torr'],
"Speed":['Miles per hour','Meter per second','Foot per second','Kilometer per hour','Knot'],
"Temperature":['Celcius','Farhenheit','Kelvin'],
"Time":['Nanosecond','Microsecond','Millisecond','Second','Minute','Hour','Day','Week','Month','Calender Year','Decade','Century'],
"Volume":['Litre','Millilitre','Imperial Gallon','Imperial Pint']
}
# label text for header title
headerLbl = tk.Label(root,text="UNIT CONVERTER",fg="black",bg="light grey",font = ("Times New Roman", 30,"bold","italic","underline"))
headerLbl.grid(row = 0,column = 1,columnspan = 3)
# Label text for conversion tye selection
lbl1 = tk.Label(root,text="Type of Conversion:")
lbl1.grid(row = 1,column = 0,padx=20,pady=20)
# OptionMenu creation for the list of Units to select
global x
x = tk.StringVar()
x.set(list(listOfUnits.keys())[0])
unitList = tk.OptionMenu(root,x,*listOfUnits.keys(),command=updateSubLists)
unitList.grid(row = 1,column = 1,padx=20,pady=40)
# Label text for conversion type selection
lbl2 = tk.Label(root,text="From")
lbl2.grid(row = 2,column = 0)
# OptionMenu creation for the list of Sub Units to select.
global y
y = tk.StringVar()
y.set(list(listOfUnits.get(x.get()))[0])
subUnitListFrom = tk.OptionMenu(root,y,*listOfUnits.get(x.get()))
subUnitListFrom.grid(row=2,column=1,padx=20,pady=40)
# Entry widget for From label
fromEntry = tk.Entry(root,width=20)
valid_info = root.register(validateUserInput) # register the function for the validation
fromEntry.config(validate="key",validatecommand=(valid_info,'%P')) # Adding the properties for validation elements
fromEntry.grid(row=2,column=2)
# Label text for conversion type selection
lbl3 = tk.Label(root,text="To")
lbl3.grid(row = 3,column = 0)
# OptionMenu creation for the list of Sub Units to select.
global z
z = tk.StringVar()
z.set(list(listOfUnits.get(x.get()))[0])
subUnitListTo = tk.OptionMenu(root,z,*listOfUnits.get(x.get()))
subUnitListTo.grid(row=3,column=1)
# Entry widget for From label
ToEntry = tk.Entry(root,width=20,state="readonly")
ToEntry.grid(row=3,column=2)
# Logic for the convert button
convert_button = tk.Button(root,text="CONVERT",fg="black",bg ="yellow",font=("Times New Roman",12,"bold"))
convert_button.grid(row=4,column=1,padx=20,pady=40)
# Logic for the reset button
reset_button = tk.Button(root,text="RESET",fg="black",bg="yellow",font=("Times New Roman",12,"bold"),command=resetEntries)
reset_button.grid(row=4,column=2,padx=20,pady=40)
root.mainloop()
Problem Statement:
When clicked on Reset, logic works successfully but when I again select a new value in the main OptionMenu, the corresponding list of values are not reflecting in the other two OptionMenus. I am not able to understand "after I click the Reset Button , why my other two dropdowns are not reflecting the corresponding values when I change the value of the main OptionMenu".
You forget to pass updateSubLists as the third argument of tk._setit(...) inside resetEntries():
def resetEntries():
""" This method helps in resetting the entries given as a input or selected by the User"""
x.set("")
unitList["menu"].delete(0,'end')
for item in list(listOfUnits.keys()):
unitList['menu'].add_command(label=item,command=tk._setit(x,item,updateSubLists))
...
from tkinter import *
import pandas as pd
df = pd.DataFrame({'item': list('abcde'), 'default_vals': [2,6,4,5,1]})
def input_data(df):
box = Tk()
height = str(int(25*(df.shape[0]+2)))
box.geometry("320x" + height)
box.title("my box")
#initialise
params, checkButtons, intVars = [], [], []
default_vals = list(df.default_vals)
itemList = list(df.item)
for i,label in enumerate(itemList):
Label(box, text = label).grid(row = i, sticky = W)
params.append(Entry(box))
params[-1].grid(row = i, column = 1)
params[-1].insert(i, default_vals[i])
intVars.append(IntVar())
checkButtons.append(Checkbutton(variable = intVars[-1]))
checkButtons[-1].grid(row = i, column = 3)
def sumbit(event=None):
global fields, checked
fields = [params[i].get() for i in range(len(params))]
checked = [intVars[i].get() for i in range(len(intVars))]
box.destroy()
#add submit button
box.bind('<Return>', sumbit)
Button(box, text = "submit",
command = sumbit).grid(row = df.shape[0]+3, sticky = W)
box.focus_force()
mainloop()
return fields, checked
I am new to tkinter and not sure what I a trying to do is possible.
At present, my script (simplified here to a function rather than a class) builds a box with all the default values entered in the fields:
Instead, I want to start with empty fields which, once the corresponding checkButton is clicked will get the default value (should still be able to manually change it through the field as happens now), and also, once any value is entered in a given field, the corresponding checkButton is selected.
Are these possible?
It is possible, but let me preface my solution with a few cautions on your current code:
It's rarely advisable to do a star import (from tkinter import *) as you don't have any control over what gets imported into your namespace. It's more advisable to explicitly import what you need as a reference:
import tkinter as tk
tk.Label() # same as if you wrote Label()
tk.IntVar() # same as if you called IntVar()
The behaviour you wanted, while possible, might not be necessarily user friendly. What happens when a user has already entered something, and unchecks the checkbox? Or what happens if the checkbox was selected and then the user deleted the information? These might be things you want to think about.
Having said that, the solution is to use add a trace callback function over your variable(s). You'll also need to add a StringVar() for the Entry boxes as you wanted a two way connection:
# add strVars as a list of StringVar() for your Entry box
params, checkButtons, intVars, strVars = [], [], [], []
During your iteration of enumerate(itemList), add these:
# Create new StringVar()
strVars.append(StringVar())
# add a trace callback for tracking changes over the StringVar()
strVars[-1].trace_add('write', lambda var, var_idx, oper, idx=i: trace_strVar(idx))
# update your Entry to set textvariable to the new strVar
params.append(Entry(box, textvariable=strVars[-1]))
# similarly, add a trace for your IntVar
intVars[-1].trace_add('write', lambda var, var_idx, oper, idx=i: trace_intVar(idx))
You'll need to define the two trace callback functions before you iterate through the widget creations:
def trace_intVar(idx):
# if Checkbox is checked and Entry is empty...
if intVars[idx].get() and not params[idx].get():
# prefill Entry with default value
params[idx].insert(0, df.default_vals[idx])
def trace_strVar(idx):
# if Entry has something...
if strVars[idx].get():
# and Checkbox is not checked...
if not intVars[idx].get():
# Set the checkbox to checked.
intVars[idx].set(True)
# but if Entry is empty...
else:
# Set the Checkbox to uncheck.
intVars[idx].set(False)
Remember I mentioned the behaviour - I took a little liberty to clear the Checkbox if Entry is empty. If you however don't wish to do that, you'll need to modify the handling a little.
Note on the way the trace_add is written. The callback function is always passed with three default arguments, namely the Variable Name, The Variable Index (if any) and Operation (see this great answer from Bryan Oakley). Since we don't need any in this case (we can't reverse reference the variable name to the linked index between the variable lists), we'll have to manually wrap the callback with another lambda and ignore the three arguments:
lambda var, # reserve first pos for variable name
var_idx, # reserve second pos for variable index
oper, # reserve third pos for operation
idx=i: # pass in i by reference for indexing point
trace_intVar(idx) # only pass in the idx
You cannot just pass lambda...: trace_intVar(i) as i will be passed by value instead of reference in that case. Trust me, I've made this error before. Therefore we pass another argument idx with its default set to i, which will now be passed by reference.
If trace_add doesn't work, use trace('w', ...) instead.
For prosperity, here's the complete implemented solution to your question:
from tkinter import *
import pandas as pd
df = pd.DataFrame({'item': list('abcde'), 'default_vals': [2,6,4,5,1]})
def input_data(df):
box = Tk()
height = str(int(25*(df.shape[0]+2)))
box.geometry("320x" + height)
box.title("my box")
#initialise
params, checkButtons, intVars, strVars = [], [], [], []
default_vals = list(df.default_vals)
itemList = list(df.item)
def trace_intVar(idx):
if intVars[idx].get() and not params[idx].get():
params[idx].insert(0, df.default_vals[idx])
def trace_strVar(idx):
if strVars[idx].get():
if not intVars[idx].get():
intVars[idx].set(True)
else:
intVars[idx].set(False)
for i,label in enumerate(itemList):
Label(box, text = label).grid(row = i, sticky = W)
strVars.append(StringVar())
strVars[-1].trace_add('write', lambda var, var_idx, oper, idx=i: trace_strVar(idx))
params.append(Entry(box, textvariable=strVars[-1]))
params[-1].grid(row = i, column = 1)
#params[-1].insert(i, default_vals[i]) # <-- You don't need this any more
intVars.append(IntVar())
intVars[-1].trace_add('write', lambda var, var_idx, oper, idx=i: trace_intVar(idx))
checkButtons.append(Checkbutton(variable = intVars[-1]))
checkButtons[-1].grid(row = i, column = 3)
def sumbit(event=None):
global fields, checked
fields = [params[i].get() for i in range(len(params))]
checked = [intVars[i].get() for i in range(len(intVars))]
box.destroy()
#add submit button
box.bind('<Return>', sumbit)
Button(box, text = "submit",
command = sumbit).grid(row = df.shape[0]+3, sticky = W)
box.focus_force()
mainloop()
return fields, checked
I am trying to create option menus in a loop, and the number of option menus is dependent on a variable. So I'm trying to use exec in my code.
I used the following to pass the value of 'i' to connect to which variable is changing the value.
But once I call the trace, the option I select in the Option menu, does not get updated in the Option menu box. If I do not call the trace funtion, it is getting updated in the display.
trackProcessMenu is the callback function.
Please let me know, where I am making the mistake.
Adding my code:
for i in range(0,numOfLibFiles):
exec('self.processOptionMenuVar_%d = StringVar()'%i)
process_menu = ("ff","ss","tt","fff","sss","ttt")
exec('self.processOptionMenu_%d = OptionMenu(self, self.processOptionMenuVar_%d, *process_menu )'%(i,i))
exec('self.processOptionMenu_%d.config(indicatoron=0,compound=RIGHT,image= self.downArrowImage, anchor = CENTER , direction = RIGHT)'%i)
exec('self.processOptionMenuVar_%d.set("--")'%i)
exec('self.processOptionMenu_%d.grid(row = i, column =1, sticky = N ,padx=30, pady =7 )'%i)
def trackProcessMenu(self,*args):
i = args[0]
exec('process = self.processOptionMenuVar_%d.get()'%i)
You should not use exec this way. A good rule of thumb is that you should never use exec until you can answer the question "why should I never use exec?" :-) exec has it's uses, but this isn't one of them.
Instead of trying to automagically generate variable names, keep your widgets in a list or dictionary.
For example:
option_vars = []
option_menus = []
for i in range(0,numOfLibFiles):
process_menu = ("ff","ss","tt","fff","sss","ttt")
var = StringVar()
om = OptionMenu(self, var, *process_menu)
om.config(indicatoron=0,compound=RIGHT,image= self.downArrowImage, anchor = CENTER , direction = RIGHT)
var.set("--")
om.grid(row = i, column =1, sticky = N ,padx=30, pady =7)
option_vars.append(var)
option_menus.append(om)
With the above, you can now reference the variables and menus with a simple index:
print("option 1 value is:", option_vars[1].get())
I've been searching around and i am not able to find a proper explanation of the syntax of OptionMenu within Tkinter.
how would i get the current chosen option with in the OptionMenu?
def homeTeamOption(self, frame, sortedList):
def func():
print(homeTeamName)
return
homeTeam = tk.StringVar(frame)
returnValueAwayTeam = []
options = sortedList
homeTeamName = tk.StringVar()
drop = OptionMenu(frame, homeTeamName, *options, command=func())
drop.place(x=200, y= 100, anchor="nw")
To get the value of the OptionMenu you need to get the value of the associated variable. In your case it would be:
homeTeamName.get()
If you want to do this via the command, you must set the option to a reference to the function:
drop = OptionMenu(...command=func)
i'm trying to build multiple option menus sharing the same "base item list". A multiple selection of one item in different menus should not be possible, so all menus have to be updated when an item is selected in one of the available menus.
from tkinter import *
# for example 5 fields
number_of_fields = 5
starting_list = ["item1","item2","item3","item4","item5"]
entry_list = []
option_list = []
option_var = []
def quit():
raise SystemExit()
# if an item is selected in one of the
# menus run this function
def reset_menu(sel_item):
# for each field
for field in range(number_of_fields):
new_list = []
selection = option_var[field].get()
# look for selected items in all menus
# and build new list which contains all
# items from the starting_list minus the
# items which are already selected
# keep the one selected (for a menu itself)
for option in starting_list:
marker = 0
for j in range(number_of_fields):
if(str(option_var[j].get()) == str(option)):
marker = 1
if(marker == 0):
new_list.append(str(option))
else:
pass
if(str(selection) == str(option)):
new_list.append(str(option))
# print new generated item list
# just to be sure it works so far
print("field",field,"new list=",new_list)
# NOW HERE SOMETHING IS WRONG I GUESS
# empty menu
option_list[field]["menu"].delete(0, "end")
# add new menu items
for item in new_list:
option_list[field]['menu'].add_command(label=item, command=lambda value=item:option_var[field].set(value))
root = Tk()
root.title("OptionMenu")
# menu variable for each field
for i in range(number_of_fields):
option_var.append(StringVar(root))
# initial value for each field
for i in range(number_of_fields):
option_var[i].set("")
# create menu for each field
for i in range(number_of_fields):
option_list.append(OptionMenu(root, option_var[i], *starting_list, command=reset_menu))
# create entry for each field
for i in range(number_of_fields):
entry_list.append(Entry(root))
# build gui
for i in range(number_of_fields):
entry_list[i].grid(row=int(i),column=0,sticky=N+S+W+E)
option_list[i].grid(row=int(i), column=1,sticky=N+S+W+E)
button = Button(root, text="OK", command=quit)
button.grid(row=number_of_fields,column=1,sticky=N+S+W+E)
mainloop()
Now everthing seems to be fine until i try to update the menus. The new menu item lists are generated correctly (see print statement) and the menus have the right items, but after selected one menu, the only menu that changes its selected state is the last one. Any ideas?
Regards Spot
I found your question because I too was trying to complete the same task. After doing a bit of poking around in dir(tkinter), I have found a solution, which you have inspired me to create an account to post.
I have left your original comments in the code for sections that I left unchanged.
First, your code for generating your options is unnecessarily cluttered. Instead of manually populating the list from empty, it seems cleaner to remove items from the full list.
You are currently using tkinter.OptionMenu(). If you instead use tkinter.ttk.OptionMenu(), it has a method called set_menu(*values) that takes any number of values as its arguments and sets the choices of that menu to be those arguments.
If you make the switch, there one thing to note - ttk's OptionMenu does not allow its default value to chosen in the dropdown, so it's recommended to make that value blank, as I have done in the declaration for starting_list.
In order to persist the blank option, I added an additional blank option, in order for it to be selectable. This way, if you mistakenly choose the wrong selection, you can revert your choice.
from tkinter import *
from tkinter.ttk import *
# for example 5 fields
number_of_fields = 5
starting_list = ["","item1","item2","item3","item4","item5"]
entry_list = []
option_list = []
option_var = []
def quit():
raise SystemExit()
# if an item is selected in one of the
# menus run this function
def reset_menu(sel_item):
# for each field
for field in range(number_of_fields):
new_list = [x for x in starting_list]
selection = option_var[field].get()
# look for selected items in all menus
# and build new list which contains all
# items from the starting_list minus the
# items which are already selected
# keep the one selected (for a menu itself)
for option in starting_list[1:6]:
#add selectable blank if option is selected
if (str(selection) == str(option)):
new_list.insert(0,"")
for j in range(number_of_fields):
if(str(selection) != str(option) and str(option_var[j].get()) == str(option)):
new_list.remove(option)
# print new generated item list
# just to be sure it works so far
print("field",field,"new list=",new_list)
#set new options
option_list[field].set_menu(*new_list)
root = Tk()
root.title("OptionMenu")
# menu variable for each field
for i in range(number_of_fields):
option_var.append(StringVar(root))
# initial value for each field
for i in range(number_of_fields):
option_var[i].set("")
# create menu for each field
for i in range(number_of_fields):
option_list.append(OptionMenu(root, option_var[i], *starting_list, command=reset_menu))
# create entry for each field
for i in range(number_of_fields):
entry_list.append(Entry(root))
# build gui
for i in range(number_of_fields):
entry_list[i].grid(row=int(i),column=0,sticky=N+S+W+E)
option_list[i].grid(row=int(i), column=1,sticky=N+S+W+E)
button = Button(root, text="OK", command=quit)
button.grid(row=number_of_fields,column=1,sticky=N+S+W+E)
mainloop()
Something you may want to look into is making your option generation a bit more efficient. Right now, for n options, you're looping through your menus n^2 times. I would suggest looking at passing the value that was just selected in the callback instead of searching each menu to see what was previously selected.
As an additional minor note, your "OK" button causes a crash. I'm not sure if that was intentional behavior, a quirk in my system, or something else.
I hope this helps!
its been a while and ive found a possible solution for my problem...here is the code:
from tkinter import *
from tkinter import _setit
# for example 5 fields
number_of_fields = 5
starting_list = ["choose","item1","item2","item3","item4","item5"]
entry_list = []
option_list = []
option_var = []
def quit():
raise SystemExit()
# print entry_field text and selected option_menu item
def output():
print("---------------------------------------")
for nr,item in enumerate(entry_list):
if(item.get() != ""):
print(item.get() + " --> " + option_var[nr].get())
print("---------------------------------------")
# if an item is selected in one of the
# menus run this function
def reset_menu(*some_args):
for field in range(number_of_fields):
new_list = []
selection = option_var[field].get()
for option in starting_list[1:]:
marker = 0
for j in range(number_of_fields):
if(str(option_var[j].get()) == "choose"):
continue
if(str(option_var[j].get()) == str(option)):
marker = 1
if(marker == 0):
new_list.append(str(option))
else:
pass
if(str(selection) == str(option)):
new_list.append(str(option))
option_list[field]["menu"].delete(0, "end")
option_list[field]["menu"].insert(0, "command", label="choose", command=_setit(option_var[field], "choose"))
# add new menu items
for i in range(len(new_list)):
option_list[field]["menu"].insert(i+1, "command", label=new_list[i], command=_setit(option_var[field], new_list[i]))
root = Tk()
root.title("OptionMenu")
# menu variable for each field
for i in range(number_of_fields):
option_var.append(StringVar(root))
# initial value for each field
for i in range(number_of_fields):
# set "choose" as default value
option_var[i].set("choose")
# trace each variable and call "reset_menu" function
# if variable change
option_var[i].trace("w", reset_menu)
# create menu for each field
for i in range(number_of_fields):
option_list.append(OptionMenu(root, option_var[i], *starting_list))
# create entry for each field
for i in range(number_of_fields):
entry_list.append(Entry(root))
entry_list[i].insert(0, "entry"+str(i))
# build gui
for i in range(number_of_fields):
entry_list[i].grid(row=int(i), column=0, sticky=N+S+W+E)
option_list[i].grid(row=int(i), column=1, sticky=N+S+W+E)
button1 = Button(root, text="OK", command=quit)
button2 = Button(root, text="PRINT", command=output)
button1.grid(row=number_of_fields, column=0, sticky=N+S+W+E)
button2.grid(row=number_of_fields, column=1, sticky=N+S+W+E)
mainloop()
This solution also runs under python 2.7, just change "from tkinter ..." to "from Tkinter ...".
Please take a look at the smarter solution sephirothrr has posted (see post above)!
Regards
Spot