I am trying to clean up my code. I currently have a long list of labels that are 'printed' if a certain condition is met. For example:
if 0000:
output_0 = Label(frame_20, text="Condition 0 costs $15",
bg='white', padx=10, pady=10).grid(column=0,row=0)
if 0001:
output_1 = Label(frame_20, text="Condition 1 costs $20",
bg='white', padx=10, pady=10).grid(column=0,row=0)
Ideally what I want is to separate the cost variable and the text entirely from the Label so that my code looks something like this:
if 0000:
cost_0 = 15
outputText = ("Condition 0 costs %d", cost_0)
if 0001:
cost_1 = 20
outputText = ("Condition 1 costs %d", cost_1)
output_x = Label(frame_20, vartext=outputText,
bg='white', padx=10, pady=10).grid(comumn=0,row=0)
I assume you are asking for some feature that you don't know yet - and also guessing you have way more conditions than your example.
For string formmating, use f-string. That is easiest, yet robust way to do so. For more details, check python online docs.
In a nutshell, it's used like this:
a = 10
f" <any_text> {a} <even_more_text> "
About dynamic widget adding, forget about naming every label widget you created. Just iterate thru condition pairs and generate widgets, then store those in list.
import tkinter
from typing import Iterable, Tuple, List
# List your conditions - expressions, bool, lambda, whatever in pair with associated value
conditions = (10 == 6, 15), (True, 20), (5, 25)
class App(tkinter.Frame):
def __init__(self, root_):
super().__init__(root_)
self.root = root_
self.pack()
# anything to store the reference - list, class, anything
self.labels: List[tkinter.Label] = []
def generate_labels(self, conditions_: Iterable[Tuple[bool, int]]):
for idx, (condition, price_tag) in enumerate(conditions_):
if condition:
self.labels.append(tkinter.Label(self, text=f"Condition {idx} costs ${price_tag}"))
for label in self.labels:
label.pack()
root = tkinter.Tk()
app_reference = App(root)
app_reference.generate_labels(conditions)
root.mainloop()
And there's no such parameter vartext in tkinter.Label. I think you wanted to type textvariable.
If what you wanted is to have a state and checking if it matches the condition, then this will do:
import tkinter
class App(tkinter.Frame):
def __init__(self, root_):
super().__init__(root_)
self.root = root_
self.pack()
self.solution_dict = {"0000": 15, "0001": 20, "0002": 25}
self.label: tkinter.Label = None
def get_solution(self, condition: str):
if condition in self.solution_dict.keys():
self.label = tkinter.Label(self, text=f"Condition {condition} costs ${self.solution_dict[condition]}")
self.label.pack()
root = tkinter.Tk()
app_reference = App(root)
app_reference.get_solution("0001")
root.mainloop()
If there's more than one condition matching, try if dictionary key is inside the condition string.
import tkinter
from typing import List
class App(tkinter.Frame):
def __init__(self, root_):
super().__init__(root_)
self.root = root_
self.pack()
self.solution_dict = {"0000": 15, "0001": 20, "0002": 25}
self.labels: List[tkinter.Label] = []
def get_solution(self, condition: str):
for key in self.solution_dict.keys():
if key in condition:
widget = tkinter.Label(self, text=f"Condition {key} costs ${self.solution_dict[key]}")
widget.pack()
self.labels.append(widget)
root = tkinter.Tk()
app_reference = App(root)
app_reference.get_solution("0001 & 0002")
root.mainloop()
Related
I am remaking a GUI calculator app in Tkinter, in order to learn about classes, methods, attributes, and also to shorten my original code. In order to shorten the code, I made a frame class that generates frames, entries, labels and dropdown menus, so I don't have to create them individually. Everything went well until I got to the dropdown menu part. When the user selects a different option from the Filters - dropdown menu like V, or B or L etc. the value in frame 1 -> entry[1] doesn't update. The method that updates the value in that entry is called add(self) and it's a part of calculator class.
Here is the simple version
import numpy as np
import tkinter as tk
window = tk.Tk()
window.geometry("920x500")
window.resizable(0,0)
window.title('Exposure Time Calculator')
class Calculator:
def __init__(self, window):
self.create_test_frame1()
self.create_test_frame2()
self.add(None)
def create_test_frame1(self):
labelvalues=['val 1','val 2']
entryvalues=['203','1333']
self.frame_1 = frame('Test Frame 1',labelvalues,entryvalues,6, 2, 0, 0, "no",0,0,0,0,0,30,40)
def create_test_frame2(self):
labelvalues = ['val 3','val 4']
entryvalues = ['10','24.5']
option_menu_values = ['B','V','R','I','Luminance','Hydrogen 3nm']
self.frame_2 = frame('Frame 2', labelvalues, entryvalues, 14, 2, 0, 2,
"option_menu1_yes", option_menu_values,'Filters',
0,0,0,
5,20)
def add(self, e):
qe = self.frame_1.entry[1]
bandOption = self.frame_2.clicked.get()
if bandOption == "B":
qe.delete(0,tk.END)
qe.insert(0,22)
elif bandOption == "V":
qe.delete(0,tk.END)
qe.insert(0,33)
class frame:
# Creates a frame class for automatic frame generation
# with entries, labels and/or option menus
# 1. name : frame name
# 2. label_default_values: name of labels
# 3. entry_default_values: default values in entries
# 4. entry_width: the entries dimensions
# 5. I: number of labels and entries
# 6. grid_row: frame grid row placement
# 7. grid_column: frame grid column placement
# 8. option_menu: true or false if user wants a option list or not
# 9. option_list_values: list for option menu
# 10. option_label: name for option menu label
# 11. ipax, ipady: padding
# 12. comand: comand for option list
def __init__(self, name, label_default_values, entry_default_values, entry_width, I, grid_row, grid_column,
option_menu1, option_list_values, option_label,
option_menu2, option2_list_values,option_label2,
ipad_x, ipad_y
):
self.name = name
self.label_default_values = label_default_values
self.entry_default_values = entry_default_values
self.I = I
self.grid_row = grid_row
self.grid_column = grid_column
self.dropMenu_options = option_list_values
self.label = option_label
self.entry_width = entry_width
self.dropMenu_options2 = option2_list_values
self.option_label2 = option_label2
self.ipad_x = ipad_x
self.ipad_y = ipad_y
frame = tk.LabelFrame(window, text = name, highlightbackground='grey', highlightthickness=1)
frame.grid(row=self.grid_row, column=self.grid_column, padx=5, pady=5, ipadx=ipad_x, ipady=ipad_y)
if option_menu1 == "option_menu1_yes":
self.clicked = tk.StringVar()
self.clicked.set(self.dropMenu_options[0])
self.drop = tk.OptionMenu(frame, self.clicked, *self.dropMenu_options, command = self.add)
self.drop.grid(row=5, column=1, sticky="ew")
label = tk.Label(frame, text = option_label, highlightbackground='grey', highlightthickness=1)
label.grid(row = 5, column = 0, sticky = "w")
if option_menu2 == "option_menu2_yes":
self.clicked2 = tk.StringVar()
self.clicked2.set(self.dropMenu_options2[0])
self.drop2 = tk.OptionMenu(frame, self.clicked2, *self.dropMenu_options2)
self.drop2.grid(row=6, column=1, sticky="ew")
label = tk.Label(frame, text = option_label2, highlightbackground='grey', highlightthickness=1)
label.grid(row = 6, column = 0, sticky = "w")
self.entry ={}
for i in range(0, self.I):
label = tk.Label(frame, text = self.label_default_values[i], justify = "left")
label.grid(row=i, column=0, sticky = "w")
self.entry[i] = tk.Entry(frame, textvariable = float(self.entry_default_values[i]), width=self.entry_width)
self.entry[i].grid(row=i, column=1, sticky = "e")
self.entry[i].delete(0, tk.END)
self.entry[i].insert(0, self.entry_default_values[i])
c=Calculator(window)
window.mainloop()
The method add is in the Calculator class, so instead of self.add you need to call add on the calculator. Since the frame doesn't know what the calculator is, you need to pass it in when constructing the frame.
Something like the following, where the calculator instance is passed as the first option:
self.frame_1 = frame(self, 'Test Frame 1', ...)
Next, you need to define your class to accept and save the reference to the calculator and then use it in the command of the OptionMenu:
class frame:
def __init__(self, calculator, name, ...):
self.calculator = calculator
...
self.drop = tk.OptionMenu(..., command = self.calculator.add)
Also, you define add like this:
def add(self, e):
I assume that means you think the second parameter is an event object. It is not. It is the value that was picked from the optionmenu.
Arguably, a better way to define this would be to actually use this new value if provided, and fall back to calling get if a value isn't provided. Also, you can reduce the wall of if statements into a single dictionary lookup to make the code shorter and more robust.
def add(self, new_value=None):
qe = self.frame_1.entry[1]
bandOption = self.frame_2.clicked.get() if new_value is None else new_value
band = {"B": 22, "V": 33}
qe.delete(0, "end")
qe.insert(0, band[bandOption])
This solution is 2/3 the size of your original, and more flexible and easier to maintain.
There are 2 problems:
The first one is that you mentioned and to fix it:
rename def add(self) to def add(self, e) and rename add() to add(None). Then change lambda event: self.add to self.add
The second one is:
AttributeError: 'frame' object has no attribute 'frame_camera'
but is not question related
It works if I define add(event) outside classes.
def add(event):
qe = c.frame_1.entry[1]
bandOption = c.frame_2.clicked.get()
if bandOption == "B":
qe.delete(0,tk.END)
qe.insert(0,22)
elif bandOption == "V":
qe.delete(0,tk.END)
qe.insert(0,33)
And this in the frame class:
self.drop = tk.OptionMenu(frame, self.clicked, *self.dropMenu_options, command = lambda event:add(event))
I know there isn't much documentation on this but I would like to restrict entry in a tkinter entry box. Following some code I found on here I currently am doing this:
def float_only(self,S,d):
if d == '1': #insert
if not S in ['.','0','1','2','3','4','5','6','7','8','9']:
return False
return True
and later:
mod_box = tk.Toplevel(self.root)
self.priceVar = tk.StringVar(mod_box)
self.priceVar.set('0.00')
vcmd = (mod_box.register(self.float_only),'%S','%d')
priceEntry = tk.Entry(mod_box,textvariable=self.priceVar,validate='key',validatecommand=vcmd)
This works to only allow the decimal and numbers, but I'd really like if I could have it so each number press puts the inputted number in the last decimal place and moved the rest up while still restricting entry to only numbers like cash registers do. For instance if I inputted 2 then 4 then 0, entry box would show:
0.00 -> 0.02 -> 0.24 -> 2.40
then I wouldn't need to allow the decimal (so they couldn't enter multiple times) and it would just be a smoother experience. Of course I barely knew what I was doing when I got to the point I am now, so help would be appreciate greatly.
Here's my solution.
remove . from pricevar.get() and turn it into a list
get the char of the key that was pressed and check if it is numeric
if it is numeric, append it to the list
insert the decimal back into the list 2 places from the end
join the list without the 0 index and assign it to pricevar.set
import tkinter as tk
class App(tk.Tk):
def __init__(self):
tk.Tk.__init__(self)
self.pricevar = tk.StringVar()
self.pricevar.set('0.00')
dflt = dict(width=5, font='Consolas 16 bold', insertontime=0)
self.priceEntry = tk.Entry(self, textvariable=self.pricevar, **dflt)
self.priceEntry.grid()
self.priceEntry.bind('<Key>', self.price)
def price(self, event):
o = list(self.pricevar.get().replace('.', ''))
c = event.char
if c.isnumeric():
o.append(c)
o.insert(-2, '.')
self.pricevar.set(''.join(o[1:]))
return 'break'
if __name__ == '__main__':
app = App()
app.mainloop()
If you want to turn it into it's own widget it would look something like the below. In this example I removed the StringVar and set the Entry text directly. I also included a reset button and a label. Price also has a lock on it so that once it is 'full' no more text can be entered unless it is reset. If you change the value in the constructor (ex value='00.00') it will all still work and the label will be automatically sized to fit the value
import tkinter as tk
class PriceEntry(tk.Frame):
#focus in/out colors
FOC = '#FFFFFF'
FIC = '#DDDDFF'
def __init__(self, master, row=0, column=0, text='undefined', value='0.00'):
tk.Frame.__init__(self, master)
self.grid(row=row, column=column)
#price label
tk.Label(self, text=text, font='Calibri 12 bold').grid(row=0, column=0, padx=(6, 2), pady=2)
#price entry
self.price = tk.Entry(self, background=PriceEntry.FOC, width=len(value), font='Consolas 16 bold', insertontime=0)
self.price.grid(row=0, column=1, padx=2, pady=2)
#insert start value
self.value = value
self.price.insert('end', value)
#capture all keypresses
self.price.bind('<Key>', self.keyHandler)
#since we turned the cursor off, use a different way to indicate focus
self.price.bind('<FocusOut>', self.indicate)
self.price.bind('<FocusIn>', self.indicate)
#price reset button
tk.Button(self, text=chr(10226), relief='flat', bd=0, font='none 12 bold', command=self.reset).grid(row=0, column=2)
def reset(self):
self.price.delete(0, 'end') #delete old text
self.price.insert('end', self.value) #insert init value
def keyHandler(self, event):
o = list(self.price.get().replace('.', '')) #remove decimal and return text as a list
if (c := event.char).isnumeric() and not int(o[0]): #if character is numeric and price isn't "full"
o.append(c) #append character to list
o.insert(-2, '.') #replace decimal
self.price.delete(0, 'end') #delete old text
self.price.insert('end', ''.join(o[1:])) #insert new text
return 'break' #stop further propagation
def indicate(self, event):
if str(event) == '<FocusOut event>':
self.price['background'] = PriceEntry.FOC
elif str(event) == '<FocusIn event>':
self.price['background'] = PriceEntry.FIC
class App(tk.Tk):
WIDTH, HEIGHT, TITLE = 800, 600, 'Price Busters'
def __init__(self):
tk.Tk.__init__(self)
#init widget at row0 column0
self.minimum = PriceEntry(self, 0, 0, 'min')
#init widget at row0 column1 with a higher price potential
self.maximum = PriceEntry(self, 0, 1, 'max', '000.00')
if __name__ == '__main__':
app = App()
app.title(App.TITLE)
app.geometry(f'{App.WIDTH}x{App.HEIGHT}')
app.resizable(width=False, height=False)
app.mainloop()
I’m writing a code for an ecosystem simulation in which the user can adjust initial values as e.g. population size, fertility, and so on. Instead of writing a method for each variable I would like to use a general method that can be called for each variable. I also want to exclude non-integers as input and to restrict the acceptable value to a certain range.
However, the last requirement fails in my code as the range limits for each variable affect each other. Basically because I cannot add arguments to the entry.bind method. How to solve this?
Below, you'll find a simplified version of my code in which the problem happens.
import tkinter as tk
class Window():
def __init__(self):
tk.Label(master, text ='Fox number').grid(row=0,column=0)
tk.Label(master, text ='Hare number').grid(row=1,column=0)
self.fox_entry=tk.Entry(master, width=5)
self.fox_entry.grid(row=0, column=1)
self.hare_entry=tk.Entry(master, width=5)
self.hare_entry.grid(row=1, column=1)
class Ecosystem():
def __init__(self):
self.foxnumber = 10
self.harenumber = 100
def initiate(self):
def input_user(entry,value, minval, maxval):
self.value = value
self.inputvalue = value
self.minval = minval
self.maxval = maxval
def get_value(event):
try:
self.inputvalue = int(entry.get())
print(self.inputvalue)
except ValueError:
entry.delete(0,'end')
entry.insert(0,self.value)
self.inputvalue = self.value
if self.inputvalue not in range(self.minval, self.maxval+1):
entry.delete(0,'end')
entry.insert(0,self.value)
self.inputvalue = self.value
print(self.inputvalue)
entry.bind('<Return>', get_value)
value = self.inputvalue
return value
my_win.fox_entry.insert(0,self.foxnumber)
self.foxnumber = input_user(my_win.fox_entry,self.foxnumber, 0, 50)
my_win.hare_entry.insert(0,self.harenumber)
self.harenumber = input_user(my_win.hare_entry,self.harenumber, 0, 200)
# more variables will added later on
master = tk.Tk()
my_win = Window()
my_ecosystem = Ecosystem()
my_ecosystem.initiate()
master.mainloop()
I'm trying to avoid to multiply functions in code by using
def Return_Label(self,number)
with a parameter.
Any Idea how to use string in order to define variable name usable to .set value to StringVar()?
Example code below:
import tkinter as tk
from tkinter import *
class WINDOW():
def __init__(self):
self.Settings_Window()
def Settings_Window(self):
self.settings_window = tk.Tk()
self.settings_window.minsize(200,200)
self.entry = Entry(self.settings_window)
self.entry.pack()
self.entry2 = Entry(self.settings_window)
self.entry2.pack()
self.label1input = StringVar()
self.label = Label(self.settings_window,textvariable=self.label1input, bg='yellow')
self.label.pack(expand='yes',fill='x')
self.label2input = StringVar()
self.label2 = Label(self.settings_window, textvariable=self.label2input, bg='yellow')
self.label2.pack(expand='yes', fill='x')
self.button = Button(self.settings_window,text='SETUP1',command=self.Next)
self.button.pack()
self.button2 = Button(self.settings_window,text='SETUP2',command=self.Next2)
self.button2.pack()
self.settings_window.mainloop()
def Next(self):
self.number=1
self.Return_Label(self.number)
def Next2(self):
self.number=2
self.Return_Label(self.number)
def Return_Label(self,number):
self.entry_field_value = self.entry.get()
print(self.entry_field_value)
#self.label1input.set(self.entry_field_value)
setattr(self,'label'+str(number)+'input.set',self.entry_field_value)
window=WINDOW()
I prefer a list approach to managing multiple entry fields and updating values.
By using list you can use the index value to manage the labels as well :D.
See the below example of how you can use list to deal with all the values and updates.
import tkinter as tk
from tkinter import *
class Window(tk.Tk):
def __init__(self):
super().__init__()
self.minsize(200, 200)
self.entry_list = []
self.label_list = []
entry_count = 2
for i in range(entry_count):
self.entry_list.append(Entry(self))
self.entry_list[i].pack()
for i in range(entry_count):
self.label_list.append(Label(self,bg='yellow'))
self.label_list[i].pack(expand='yes', fill='x')
Button(self, text='SETUP', command=self.Return_Label).pack()
def Return_Label(self):
for ndex, lbl in enumerate(self.label_list):
lbl.config(text=self.entry_list[ndex].get())
if __name__ == '__main__':
Window().mainloop()
Create lists of objects rather than individual attributes for each object. For example,
import tkinter as tk
from tkinter import *
class Window:
def __init__(self):
self.settings_window()
def Settings_Window(self):
self.settings_window = tk.Tk()
self.settings_window.minsize(200,200)
self.entries = [
Entry(self.settings_window),
Entry(self.settings_window)
]
for e in self.entries:
e.pack()
self.labelinputs = [
StringVar(),
StringVar()
]
self.labels = [
Label(self.settings_window, textvariable=label, bg='yellow')
for label in self.labelinputs
]
for l in self.labels:
l.pack(expand='yes', fill='x')
self.buttons = [
Button(self.settings_window,text='SETUP1',command=lambda: self.return_label(0))
Button(self.settings_window,text='SETUP2',command=lambda: self.return_label(1))
]
for b in self.buttons:
b.pack()
self.settings_window.mainloop()
def return_label(self,number):
entry_field_value = self.entry.get()
self.labelsinput[number].set(entry_field_value)
window=WINDOW()
Dynamicly computing variable names should be avoided at all costs. They are difficult to do correctly, and it makes your code hard to understand, hard to maintain, and hard to debug.
Instead, store the widgets in a dictionary or list. For example:
def __init___(self):
...
self.vars = {}
...
self.vars[1] = StringVar()
self.vars[2] = StringVar()
...
def Return_Label(self,number):
self.entry_field_value = self.entry.get()
var = self.vars[number]
var.set(self.entry_field_value)
Though, you really don't need to use StringVar at all -- they usually just add extra overhead without providing any extra value. You can save the labels instead of the variables, and call configure on the labels
self.labels[1] = Label(...)
...
self.labels[number].configure(text=self.entry_field_value)
I've cloned a class called ListBoxChoice found on the web found (adding some needed features) below:
from Tkinter import *
class ListBoxChoice(object):
def __init__(self, master=None, title=None, message=None,\
list=[]):
self.master = master
self.value = None
self.list = list[:]
self.modalPane = Toplevel(self.master)
self.modalPane.transient(self.master)
self.modalPane.grab_set()
self.modalPane.bind("<Return>", self._choose)
self.modalPane.bind("<Escape>", self._cancel)
if title:
self.modalPane.title(title)
if message:
Label(self.modalPane, text=message).pack(padx=5, pady=5)
listFrame = Frame(self.modalPane)
listFrame.pack(side=TOP, padx=5, pady=5)
scrollBar = Scrollbar(listFrame)
scrollBar.pack(side=RIGHT, fill=Y)
# get the largest value of the 'list' to set the width
widthOfList = 0
for k in list:
if len(str(k)) > widthOfList:
widthOfList = len(str(k))
# now pad some space to back of the widthOfList
widthOfList = widthOfList + 2
self.listBox = Listbox(listFrame, selectmode=SINGLE,\
width=widthOfList)
self.listBox.pack(side=LEFT, fill=Y)
scrollBar.config(command=self.listBox.yview)
self.listBox.config(yscrollcommand=scrollBar.set)
self.list.sort()
for item in self.list:
self.listBox.insert(END, item)
buttonFrame = Frame(self.modalPane)
buttonFrame.pack(side=BOTTOM)
chooseButton = Button(buttonFrame, text="Choose",\
command=self._choose)
chooseButton.pack()
cancelButton = Button(buttonFrame, text="Cancel",\
command=self._cancel)
cancelButton.pack(side=RIGHT)
def _choose(self, event=None):
try:
firstIndex = self.listBox.curselection()[0]
self.value = self.list[int(firstIndex)]
except IndexError:
self.value = None
self.modalPane.destroy()
def _cancel(self, event=None):
self.modalPane.destroy()
def returnValue(self):
self.master.wait_window(self.modalPane)
return self.value
if __name__ == '__main__':
import random
root = Tk()
returnValue = True
list = [random.randint(1,100) for x in range(50)]
while returnValue:
returnValue = ListBoxChoice(root, "Number Picking",\
"Pick one of these crazy random numbers",\
list).returnValue()
print returnValue
Now this example says to do something like this:
results = ListBoxChoice(root, list=listOfItems).returnValue().
What I'm trying to do is provide a list of values from which the user selects a single value. The window should close before I use the results from the selected value. Here is that code:
from tkinter import Tk, Label
form ListBoxChoice import ListBoxChoice
...
eventList = ["20190120","20190127","20190203"]
root = Tk()
root.withdraw() # This causes the ListBoxChoice object not to appear
selectValue = ListBoxChoice(root, title="Event",\
message="Pick Event", list=eventList).returnValue()
root.wait_window() # Modal Pane/window closes but not the root
print("selectValue:", selectValue)
A root window is placed behind the modalPane (Toplevel). I have to close that window before the calling process continues. So there is a block in place.
I've tried to put a sleep(1.01) command above but had no impact.
How do I get the ListBoxChoice to close once the selection has been made
before my print statement of the selectValue? For it is at that point I want to use the results to plot data.
If I don't use root.wait_winow(), it is only when the plot is closed (end of the process) that the ListBoxChoice box close as well.
Suggestions?
Slightly updated
Here's a version of the ListBoxChoice class which I think works the way you desire. I've updated my previous answer slightly so the class is now defined in a separate module named listboxchoice.py. This didn't change anything I could see when I tested—it other words it still seems to work—but I wanted to more closely simulate the way you said you're using it the comments.
It still uses wait_window() because doing so is required to give tkinter's mandatory event-processing-loop the opportunity to run (since mainloop() isn't called anywhere). There's some good background material in the article Dialog Windows about programming tkiner dialogs you might find useful. The added root.withdraw() call eliminates the issue of not being able to close it because it's not there. This is fine since there's no need to have the empty window being displayed anyway.
test_lbc.py
import random
try:
import Tkinter as tk # Python 2
except ModuleNotFoundError:
import tkinter as tk # Python 3
from listboxchoice import ListBoxChoice
root = tk.Tk()
root.withdraw() # Hide root window.
values = [random.randint(1, 100) for _ in range(50)]
choice = None
while choice is None:
choice = ListBoxChoice(root, "Number Picking",
"Pick one of these crazy random numbers",
values).returnValue()
print('choice: {}'.format(choice))
listboxchoice.py
""" ListBoxChoice widget to display a list of values and allow user to
choose one of them.
"""
try:
import Tkinter as tk # Python 2
except ModuleNotFoundError:
import tkinter as tk # Python 3
class ListBoxChoice(object):
def __init__(self, master=None, title=None, message=None, values=None):
self.master = master
self.value = None
if values is None: # Avoid use of mutable default argument value.
raise RuntimeError('No values argument provided.')
self.values = values[:] # Create copy.
self.modalPane = tk.Toplevel(self.master, takefocus=True)
self.modalPane.bind("<Return>", self._choose)
self.modalPane.bind("<Escape>", self._cancel)
if title:
self.modalPane.title(title)
if message:
tk.Label(self.modalPane, text=message).pack(padx=5, pady=5)
listFrame = tk.Frame(self.modalPane)
listFrame.pack(side=tk.TOP, padx=5, pady=5)
scrollBar = tk.Scrollbar(listFrame)
scrollBar.pack(side=tk.RIGHT, fill=tk.Y)
# Get length the largest value in 'values'.
widthOfList = max(len(str(value)) for value in values)
widthOfList += 2 # Add some padding.
self.listBox = tk.Listbox(listFrame, selectmode=tk.SINGLE, width=widthOfList)
self.listBox.pack(side=tk.LEFT, fill=tk.Y)
scrollBar.config(command=self.listBox.yview)
self.listBox.config(yscrollcommand=scrollBar.set)
self.values.sort()
for item in self.values:
self.listBox.insert(tk.END, item)
buttonFrame = tk.Frame(self.modalPane)
buttonFrame.pack(side=tk.BOTTOM)
chooseButton = tk.Button(buttonFrame, text="Choose", command=self._choose)
chooseButton.pack()
cancelButton = tk.Button(buttonFrame, text="Cancel", command=self._cancel)
cancelButton.pack(side=tk.RIGHT)
def _choose(self, event=None):
try:
firstIndex = self.listBox.curselection()[0]
self.value = self.values[int(firstIndex)]
except IndexError:
self.value = None
self.modalPane.destroy()
def _cancel(self, event=None):
self.modalPane.destroy()
def returnValue(self):
self.master.wait_window(self.modalPane)
return self.value