Weird behavior with Tkinter and lambda in a loop? [duplicate] - python

This question already has answers here:
tkinter creating buttons in for loop passing command arguments
(3 answers)
Closed last month.
I am writing an interface which consists of a label with an image, an entrybox as well as a stringvar, and a button.
I am trying to create these in a loop as there are 6 of them, to try to write more efficient code.
here's what I have:
x = 0
y = 2
for mapfile in ["pillars","cosmic","double","underpass","utopia","octagon"]:
self.widgets[mapfile+"-image"] = ImageTk.PhotoImage(Image.open("./images/png/"+mapfile+".png"))
self.widgets[mapfile+"-label"] = ttk.Label(self, image=self.widgets[mapfile+"-image"])
self. Widgets[mapfile+"-string"]= tk.StringVar()
self.widgets[mapfile+"-entry"] = ttk.Entry(self, textvariable=self.widgets[mapfile+"-string"])
self.widgets[mapfile+"-button"]= ttk.Button(self, text="Browse", padding=3, command=lambda: self.browseFiles(self.widgets[mapfile+"-string"]))
self.widgets[mapfile+"-label"].grid(column=x, row=y, columnspan=2)
self.widgets[mapfile+"-entry"].grid(column=x, row=y+1, sticky="we", padx=3, pady=0)
self.widgets[mapfile+"-button"].grid(column=x+1, row=y+1, sticky="we")
x += 2
if x > 4:
x = 0
y += 2
You can see here, I create all the elements needed for each "map" and grid them.
note in the stringvar, and the button, the command I am calling a lambda function to pass a reference to the stringvar to the browseFiles method.
Now, in the browseFiles method, here it is:
def browseFiles(self, mapfile):
print(str(mapfile))
filename = fd.askopenfilename( initialdir=mapfile.get(), \
title="Select Map", \
filetypes=(("Map Files", "*.upk *.udk"),
("ZIP Files", "*.zip")))
if filename != "":
mapfile.set(filename)
return filename
(the comments are there bc its not working)
within that code there, the only stringvar that is getting passed is the final one created in the loop (in this case, no matter which button I click, it prints "PYVAR_5". With the code uncommented, it only pulls (and updates) the octagon entry.
...why?
Here's the full script so you can try it out:
#!/usr/bin/env python
import os
import tkinter as tk
from tkinter import ttk
from tkinter import filedialog as fd
from tkinter import messagebox as mb
import shutil as sh
import zipfile as zip
from PIL import ImageTk, Image
# import win32api as win
class Merl(tk.Frame):
def __init__(self, root):
tk.Frame.__init__(self)
self.root = root
self.root.title("Map Editor for Rocket League")
self.grid(column=0,row=0,sticky=("nsew"))
self.widgets = {}
# for element in ["image","label","button","entry","stringVar","command"]:
x = 0
y = 2
for mapfile in ["pillars","cosmic","double","underpass","utopia","octagon"]:
self.widgets[mapfile+"-image"] = ImageTk.PhotoImage(Image.open("./images/png/"+mapfile+".png"))
self.widgets[mapfile+"-label"] = ttk.Label(self, image=self.widgets[mapfile+"-image"])
self.widgets[mapfile+"-string"]= tk.StringVar()
self.widgets[mapfile+"-entry"] = ttk.Entry(self, textvariable=self.widgets[mapfile+"-string"])
self.widgets[mapfile+"-button"]= ttk.Button(self, text="Browse", padding=3, command=lambda: self.browseFiles(self.widgets[mapfile+"-string"]))
self.widgets[mapfile+"-label"].grid(column=x, row=y, columnspan=2)
self.widgets[mapfile+"-entry"].grid(column=x, row=y+1, sticky="we", padx=3, pady=0)
self.widgets[mapfile+"-button"].grid(column=x+1, row=y+1, sticky="we")
x += 2
if x > 4:
x = 0
y += 2
self.widgets["cosmic-string"].set("/home/betty/test.upk")
print(self.widgets)
print(self.widgets["cosmic-string"].get())
for button in ["pillars","cosmic","double","underpass","utopia","octagon"]:
print(self.widgets[button+'-button'].cget("command"))
def browseFiles(self, mapfile):
print(str(mapfile))
filename = fd.askopenfilename( initialdir=mapfile.get(), \
title="Select Map", \
filetypes=(("Map Files", "*.upk *.udk"),
("ZIP Files", "*.zip")))
if filename != "":
mapfile.set(filename)
return filename
root = tk.Tk()
merl = Merl(root)
root.mainloop()

Thanks to #jasonharper - posted this link: tkinter creating buttons in for loop passing command arguments
apparently lambda functions "make a note" of the variable, then resolve it at runtime instead of storing it during the loop, so you have to define the variable in the lambda declaration. my fix was:
....command=lambda mapfile=mapfile:....

Related

How to make a button with the name of a file and also call that file?

So I am trying to make a sort of journal program where you can create an entry that is then saved as a text file. I currently have it set up so you can recall an entry by typing in the file name and clicking load, but I want to make a list of buttons on the right side that has all of the file names and then will load the respective file when clicked, any suggestions?
Here is my code:
from cProfile import label
from cgitb import text
from email.quoprimime import quote
import tkinter as tk
from tkinter import *
from tkinter import ttk
import tkinter
from traceback import print_tb
import os
from pip import main
def open_main():
#instantiate main screen
mainscreen = tk.Tk()
mainscreen.state("zoomed")
mainscreen.title("Welcome")
#file name text box
filename = tk.Entry(mainscreen)
filename.place(relx=.5, rely=.1, anchor=CENTER)
#save entry function
def save_entry():
savefile = open("%s .txt" % filename.get(), "w+")
savefile.write(T.get("1.0","end"))
savefile.close
refresh()
#load entry function
def loadentry():
loadentry = open("%s .txt" % filename.get(), "r")
quote = loadentry.readlines()
T.delete("1.0", END)
T.insert(END, quote)
#create frame to place main text box
mainframe = tkinter.Frame(mainscreen)
mainframe.place(relwidth=.65, relheight=.75, relx=.05, rely=.5, anchor=W)
#label the file name text box
tk.Label(mainscreen, text="Make an Entry:").place(relx=.5, rely=.035, anchor=CENTER)
tk.Label(mainscreen, text="Date: MMDDYYYY").place(relx=.5, rely=.07, anchor=CENTER)
#create main text box within the main frame
S = Scrollbar(mainframe)
T = Text(mainframe)
S.pack(side=RIGHT, fill=Y)
T.pack(side=LEFT, expand=True, fill=BOTH)
S.config(command=T.yview)
T.config(yscrollcommand=S.set)
#create second frame next to main frame to hold buttons
sideframe = tkinter.Frame(mainscreen)
sideframe.place(relwidth=.2, relheight=.75, relx=.7, rely=.5, anchor=W)
side_bar = Scrollbar(sideframe)
side_box = Text(sideframe)
side_bar.pack(side=RIGHT, fill=Y)
side_box.pack(side=LEFT, expand=True, fill=BOTH)
#create and load buttons
def loadbutton(item):
bfilename = item
bfileentry = open(bfilename, "r")
bquote = bfileentry.readlines()
T.delete("1.0",END)
T.insert(END,bquote)
#add buttons to box initially
entry_initate = [f for f in os.listdir(os.getcwd()) if f.endswith('.txt')]
for item in entry_initate:
mybutton = Button(side_box, text=item, command = lambda m = item: loadbutton(item))
mybutton.pack(fill=BOTH)
#refresh buttons when a new entry is saved
def refresh():
entry_raw = [f for f in os.listdir(os.getcwd()) if f.endswith('.txt')]
for item in entry_raw:
mybutton = Button(side_box, text=item, command = lambda m = item: loadbutton(item))
mybutton.pack(fill=BOTH)
list = side_box.slaves()
for l in list:
l.destroy()
for item in entry_raw:
mybutton = Button(side_box, text=item, command = lambda m = item: loadbutton(item))
mybutton.pack(fill=BOTH)
#Save and load entry buttons
Button(mainscreen, text="Save Entry", command=save_entry).place(relx=.5, rely=.9, anchor=CENTER)
Button(mainscreen, text="Load Entry", command=loadentry).place(relx=.5, rely=.95, anchor=CENTER)
mainscreen.mainloop()
I currently just have the side box just commented out, it was originally just a text box that had the file names listed in it.
Sorry if its a little messy, im still pretty new to python.
You should use variable loadfileb instead of button_press on .readlines() inside loadbutton(). Also use read() instead of readlines() and you need to insert the read content into text box.
def loadbutton(button_press):
with open(button_press, "r") as loadfileb:
quote = loadfileb.read()
T.delete('1.0', 'end')
T.insert('end', quote)
for item in entry_raw:
tk.Button(sideframe, text=item, command=lambda m=item: loadbutton(m)).pack()
Note that there are different way to initiate widgets, like tk.Entry(...), tkinter.Frame(...) and Button(...) in your code. So I think you have imported tkinter like below:
import tkinter
from tkinter import *
import tkinter as tk
Recommend to use import tkinter as tk only.

Getting variable out of Tkinter

I would like to ask if anyone knows how to get out a variable from an Entry in Tkinter to be used in future calculation.
Let us assume that I want to create a prompt where the user needs to place two numbers in the two different Entry widgets.
These numbers are to be used in another script for calculation. How can I retrieve the values from the prompt created in Tkinter?
In my opinion, I would need to create a function with the code bellow and make it return the value from the Tkinter prompt. However, I cannot return the numbers because I'm destroying the root window. How can I get pass this, preferably without global variables.
Best Regards
from tkinter import *
from tkinter import ttk
#Start of window
root=Tk()
#title of the window
root.title('Title of the window')
def get_values():
values=[(),(value2.get())]
return values
# Creates a main frame on the window with the master being the root window
mainframe=ttk.Frame(root, width=500, height=300,borderwidth=5, relief="sunken")
mainframe.grid(sticky=(N, S, E, W))
###############################################################################
#
#
# Label of the first value
label1=ttk.Label(master=mainframe, text='First Value')
label1.grid(column=0,row=0)
# Label of the second value
label2=ttk.Label(master=mainframe, text='Second Value')
label2.grid(column=0,row=1)
###############################################################################
#
#
# Entry of the first value
strvar1 = StringVar()
value1 = ttk.Entry(mainframe, textvariable=strvar1)
value1.grid(column=1,row=0)
# Entry of the second value
strvar2 = StringVar()
value2 = ttk.Entry(mainframe, textvariable=strvar2)
value2.grid(column=1,row=1)
# Creates a simplle button widget on the mainframe
button1 = ttk.Button(mainframe, text='Collect', command=get_values)
button1.grid(column=2,row=1)
# Creates a simplle button widget on the mainframe
button2 = ttk.Button(mainframe, text='Exit', command=root.destroy)
button2.grid(column=2,row=2)
root.mainloop()
You use a class because the class instance and it's variables remain after tkinter exits.https://www.tutorialspoint.com/python/python_classes_objects.htm And you may want to reexamine some of your documentation requirements, i.e. when the statement is
"root.title('Title of the window')", adding the explanation "#title of the window" is just a waste of your time..
""" A simplified example
"""
import sys
if 3 == sys.version_info[0]: ## 3.X is default if dual system
import tkinter as tk ## Python 3.x
else:
import Tkinter as tk ## Python 2.x
class GetEntry():
def __init__(self, master):
self.master=master
self.entry_contents=None
self.e = tk.Entry(master)
self.e.grid(row=0, column=0)
self.e.focus_set()
tk.Button(master, text="get", width=10, bg="yellow",
command=self.callback).grid(row=10, column=0)
def callback(self):
""" get the contents of the Entry and exit
"""
self.entry_contents=self.e.get()
self.master.quit()
master = tk.Tk()
GE=GetEntry(master)
master.mainloop()
print("\n***** after tkinter exits, entered =", GE.entry_contents)
So, I have taken Curly Joe's example and made a function with the his sketch
The final result, for anyone wanting to use this as a template for a input dialog box:
def input_dlg():
import tkinter as tk
from tkinter import ttk
class GetEntry():
def __init__(self, master):
self.master=master
self.master.title('Input Dialog Box')
self.entry_contents=None
## Set point entries
# First point
self.point1 = ttk.Entry(master)
self.point1.grid(row=0, column=1)
self.point1.focus_set()
# Second point
self.point2 = ttk.Entry(master)
self.point2.grid(row=1, column=1)
self.point2.focus_set()
# labels
ttk.Label(text='First Point').grid(row=0, column=0)
ttk.Label(text='Second Point').grid(row=1, column=0)
ttk.Button(master, text="Done", width=10,command=self.callback).grid(row=5, column=2)
def callback(self):
""" get the contents of the Entries and exit the prompt"""
self.entry_contents=[self.point1.get(),self.point2.get()]
self.master.destroy()
master = tk.Tk()
GetPoints=GetEntry(master)
master.mainloop()
Points=GetPoints.entry_contents
return list(Points)
In python, functions are objects, as in get_values is an object.
Objects can have attributes.
Using these two, and the knowledge that we can't really return from a button command, we can instead attach an attribute to an already global object and simply use that as the return value.
Example with button
try: # In order to be able to import tkinter for
import tkinter as tk # either in python 2 or in python 3
except ImportError:
import Tkinter as tk
def on_button_press(entry):
on_button_press.value = entry.get()
entry.quit()
def main():
root = tk.Tk()
entry = tk.Entry(root)
tk.Button(root, text="Get Value!", command=lambda e = entry : on_button_press(e)).pack()
entry.pack()
tk.mainloop()
return on_button_press.value
if __name__ == '__main__':
val = main()
print(val)
Minimalistic example
Similarly modules are also objects, if you want to avoid occupying global namespace extremely, you can attach a new attribute to the module you're using
See:
try: # In order to be able to import tkinter for
import tkinter as tk # either in python 2 or in python 3
except ImportError:
import Tkinter as tk
if __name__ == '__main__':
tk.my_value = lambda: [setattr(tk, 'my_value', entry.get()), root.destroy()]
root = tk.Tk()
entry = tk.Entry(root)
root.protocol('WM_DELETE_WINDOW', tk.my_value)
entry.pack()
tk.mainloop()
print(tk.my_value)

Tkinter: all radio buttons are selected

Why in this code when clicking a button when a new window opens, all the radio buttons are selected?
class CodeButton:
def __init__(self, root):
self.btn = Button(root, text="Code",width=20, height=1,bg="white", fg="black")
self.btn.bind("<Button-1>", make_code_window)
self.btn.pack()
def make_code_window(event):
new_root = Toplevel()
new_root.minsize(width=300, height=300)
var = IntVar()
var.set(0)
for i in range(8):
Radiobutton(new_root, text=str(i), variable=var, value=i).pack()
def main():
root = Tk()
root.minsize(width=400, height=250)
CodeButton(root)
root.mainloop()
It's got something to do with storing the IntVar in a local variable in the function that will be discarded as soon as the make_code_window() function returns. You can fix the problem by making the IntVar an attribute of the new_root window widget, so it will exist at least as long as the widget using it does.
The code in your example isn't very realistic in the sense that typically one would want to use the current value of the IntVar for something somewhere else in the Python code, but that wouldn't be possible since it's only stored temporarily in local variable which exists only during the execution of the function that created it.
try:
from tkinter import *
except ImportError: # Python 2
from Tkinter import *
class CodeButton:
def __init__(self, root):
self.btn = Button(root, text="Code",width=20, height=1,bg="white", fg="black")
self.btn.bind("<Button-1>", make_code_window)
self.btn.pack()
def make_code_window(event):
new_root = Toplevel()
new_root.minsize(width=300, height=300)
var = new_root.var = IntVar() # changed
var.set(0)
for i in range(8):
Radiobutton(new_root, text=str(i), variable=var, value=i).pack()
def main():
root = Tk()
root.minsize(width=400, height=250)
CodeButton(root)
root.mainloop()
main()
(Following-up on the discussion we were having in the comments section of my other answer.)
Yes, passing the IntVar as an argument to the event handler function is a little tricky—in fact it's sometimes called The extra arguments trick. ;-)
Here's an example of applying it to your code:
try:
from tkinter import *
except ImportError: # Python 2
from Tkinter import *
class CodeButton:
def __init__(self, root):
self.btn = Button(root, text="Code",width=20, height=1,bg="white", fg="black")
self.btn.bind("<Button-1>",
# Extra Arguments Trick
lambda event, var=root.var: make_code_window(event, var))
self.btn.pack()
def make_code_window(event, var): # note added "var" argument
new_root = Toplevel()
new_root.minsize(width=300, height=300)
var.set(-99) # deselect by using value not associated with any RadioButtons
for i in range(8):
Radiobutton(new_root, text=str(i), variable=var, value=i).pack()
def main():
root = Tk()
root.minsize(width=400, height=250)
root.var = IntVar() # create it here to give access to it in the rest of your code
CodeButton(root)
root.mainloop()
main()

How can I create a non-unique browse button in python?

I am using; Python 3.4, Windows 8, tkinter. I am trying to create a generic browse button that will get a file name and assign it to a variable.
I have created the following code to do this.
from tkinter import *
from tkinter import filedialog
from tkinter import ttk
class Application(Frame):
# A GUI Application.
# Initialize the Frame
def __init__(self, master):
Frame.__init__(self, master)
nbook = ttk.Notebook(root)
nbook.pack(fill='both', expand='yes')
f1 = ttk.Frame(nbook)
nbook.add(f1, text='QC1')
self.qc1_tab(f1)
# create QC1 tab contents
def qc1_tab(self, tab_loc):
# Set up file name entry.
Label(tab_loc, text="Select file:").grid(pady=v_pad, row=0, column=0, sticky=W)
self.flnm = ttk.Entry(tab_loc, width=60)
self.flnm.focus_set()
self.flnm.grid(pady=v_pad, row=0, column=1, columnspan=2, sticky=W)
ttk.Button(tab_loc, text="Browse...", width=10, command=self.browse).grid(row=0, column=3)
def browse(self):
temp = filedialog.askopenfilename()
self.flnm.delete(0, END)
self.flnm.insert(0, temp)
root = Tk()
app = Application(root)
root.mainloop()
The only problem with this is that the browse button is tied to self.flnm and cannot be used for anything else. I plan to use the browse button several times to acquire the file name of several different files and would rather not have multiple browse commands.
I need to call it from a button and somehow assign it to a variable afterwards.
I was thinking of something like
ttk.Button(..., command=lambda: self.flnm = self.browse)
...
def browse(self):
filename = filedialog.askopenfilename()
return filename
but that failed terribly.
How can I make a general purpose browse button?
You can write:
def browse(self, target):
temp = filedialog.askopenfilename()
target.delete(0, END)
target.insert(0, temp)
ttk.Button(..., command=lambda: self.browse(self.flnm))

TKinter Change Label with "Next" Button

The program is meant to review a set of sentences one at a time. I want to show one and then when the "next" button is clicked, it shows the next input. Right now it blasts through them. How do I get it to stop? I have a feeling I'm missing something small.
So here's the code:
from Tkinter import *
import ttk
root = Tk()
def iterate(number):
return number + 1
inputs = open("inputs.txt").readlines
lines = inputs()
numlines = len(lines)
x=0
for tq in lines:
sentence = lines[x].strip('\n')
sen = StringVar()
sen.set(sentence)
x = iterate(x)
ttk.Label(textvariable = sen).grid(column=1, row=1, columnspan=99)
ttk.Button(text = "next", command = x).grid(column=99, row=5, pady=5)
root.update()
root.mainloop()
To change what is displayed in a label you can call the configure method, giving it any of the same arguments you give when you create it. So, you would create a single label, then call this method to modify what is displayed.
The basic logic looks like this:
def do_next():
s = get_next_string_to_display()
the_label.configure(text=s)
the_label = ttk.Label(...)
the_button = ttk.Button(..., command=do_next)
This is the code I ultimately used to solve the issue:
from Tkinter import *
import ttk
root = Tk()
root.title("This space intentionally left blank")
root.minsize(800,200)
mainframe = ttk.Frame(root)
mainframe.grid(column = 0, row = 0)
def nextInputs(*args):
sen.set(inputs())
inputs = open("inputs.txt").readline
sen = StringVar()
ttk.Label(mainframe, textvariable=sen).grid(column=1, row=1, columnspan=99)
Button = ttk.Button(mainframe, text = "next", command = nextInputs).grid(column=99, row=5, pady=5)
root.bind('<Return>', nextInputs)
root.mainloop()

Categories