Python - argument passed through lambda function not retaining its state - python

I have an module that produces the following form (using Python Tkinter):
As you can see the form could have three states:
Uneditable - entry boxes are disabled - Cancel and Edit button active
Editable with no changes - entry boxes are active, Cancel button and
the Edit button becomes a disabled Save button and
Editable with changes - entry boxes are active, Cancel button becomes a revert button and the Save button becomes active
My code used to achieve this is as follows:
def display_selected_user(event, user_frame, editable):
def fill_entry_boxes():
# Clear entry boxes
f_name.entry.delete(0, 'end')
l_name.entry.delete(0, 'end')
email.entry.delete(0, 'end')
# Populate with original data
f_name.entry.insert(0, user_dict[user_id]['f_name'])
l_name.entry.insert(0, user_dict[user_id]['l_name'])
email.entry.insert(0, user_dict[user_id]['email'])
cancel_btn.config(text="Cancel")
def cancel_revert_process():
if not changes_made():
user_detail_frame.grid_forget()
close_btn.grid(row=2, column=2, padx=10, pady=10, sticky='E')
else:
MsgBox = messagebox.askquestion('Revert Changes',
f'Changes have been made - are you sure you want to revert?',
icon='question')
if MsgBox == 'yes':
fill_entry_boxes()
edit_save_btn.config(state='disabled', bg='gray80')
def save_or_edit(event, _editable):
if _editable:
save_process(_editable)
else:
make_editable_process(_editable)
def save_process(_editable):
# Notify user of process commencement
user_frame_messenger = MessageBox(user_frame, "")
user_frame_messenger.grid(row=3, column=0, columnspan=3, sticky='nsew')
user_frame_messenger.update_content(user_frame, "Saving user changes to database - one moment")
# Save Process
changes = record_changes()
client = MongoClient('mongodb+srv://xxxxxx.zzyri.mongodb.net/test?')
db = client['football_data']
collection = db['users']
for key in changes[1]:
collection.update_one({"_id": ObjectId(changes[0])},
{"$set": {key: changes[1][key]}}
)
# Notify user of process completion
user_frame_messenger.destroy()
messagebox.showinfo("User detail updated",
"The changes to the user details have been saved")
user_frame.grid_forget()
cancel_revert_process()
_editable = False
def make_editable_process(_editable):
f_name.entry.config(state='normal')
l_name.entry.config(state='normal')
email.entry.config(state='normal')
_editable = True
edit_save_btn.config(text="Save", state='disabled', bg='gray80')
def key_pressed(event):
if changes_made():
cancel_btn.config(text="Revert")
edit_save_btn.config(state='normal', bg="#43E082")
else:
cancel_btn.config(text="Cancel")
edit_save_btn.config(state='disabled', bg='gray80')
# Record data of selected user
user = user_lst.tree.focus()
user_data = user_lst.tree.item(user)
user_id = user_data['values'][0]
# Create objects
f_name = LabelEntryCombo(user_frame, "First name:")
l_name = LabelEntryCombo(user_frame, "Last name:")
email = LabelEntryCombo(user_frame, "Email:")
cancel_btn = ColourSchemedButton(user_frame, "PaleGreen", "Cancel")
edit_save_btn = ColourSchemedButton(user_frame, "GreenBlue", "Edit", state='normal')
# Fill entry boxes
fill_entry_boxes()
# Bind objects
f_name.entry.bind('<KeyRelease>', lambda e: key_pressed(e))
l_name.entry.bind('<KeyRelease>', lambda e: key_pressed(e))
email.entry.bind('<KeyRelease>', lambda e: key_pressed(e))
cancel_btn.config(command=cancel_revert_process)
edit_save_btn.config(command=lambda e=event, d=editable: save_or_edit(e, d))
# Place and removal of objects
close_btn.grid_forget()
user_detail_frame.grid(row=2, columnspan=3, padx=10, pady=10, sticky='NW')
f_name.frame.grid(row=0, columnspan=3, pady=10, padx=10)
l_name.frame.grid(row=1, columnspan=3, pady=10, padx=10)
email.frame.grid(row=2, columnspan=3, pady=10, padx=10)
cancel_btn.grid(row=3, column=1, pady=10, padx=10)
edit_save_btn.grid(row=3, column=2, pady=10, padx=10)
# Configure grid
user_detail_frame.grid_columnconfigure(0, weight=1)
user_detail_frame.grid_columnconfigure(1, weight=0)
user_detail_frame.grid_columnconfigure(2, weight=0)
# Set state of objects
f_name.entry.config(state='disabled')
l_name.entry.config(state='disabled')
email.entry.config(state='disabled')
As you may pick up - I bind save_edit_btn with the following config using a lambda function which passes the editable variable used as a state handler edit_save_btn.config(command=lambda e=event, d=editable: save_or_edit(e, d))
That editable variable is initalisated outside this function as False and passed to the function display_selected_user() (A callback function from the main programme if you will) at the very top. So when the save_edit_btn is pressed the function makes the entries active and changes the other widgets accordingly (as per function make_editable_process()) and changes the editable variable to True (even in the main program where it was initilised)
Problem is that after that the process goes back to the lambda line and resets the editable variable back to False - I don't understand why - and as a result the save_process() can never be initalised.
Any ideas on why this is the case will be appreciated - please let me know if you require any more code or detail.

Any change you do to a local variable, like _editable in make_editable_process, is only visible inside that function, so see that change outside, you need to either return it, save into some external to the function given mutable object (like those various button thing), or declare it as either global or nonlocal
For your case I think the nonlocal would suffice.
example
what you're experimenting is this
>>> def fun():
a=5
def f():
a=42
print(a)
f()
print(a)
>>> fun()
5
5
>>>
a in f is a local variable to f, any change to it is only visible inside, to change it outside do any of previously mentioned methods, for example with nonlocal:
>>> def fun2():
a=5
def f():
nonlocal a
a=42
print(a)
f()
print(a)
>>> fun2()
5
42
>>>

For values like True/False, numbers, strings Python sends its value to function (or other variable) - not reference to variable.
For example:
a = False
_editable = a
_editable = True
print(a)
and a is still False, not True.
In tkinter you would use tkinter.BooleanVar() for this.
import tkinter as tk
a = tk.BooleanVar(value=False)
# a.set(False)
_editable = a
_editable.set(True)
print(a.get())
and nowe a changes value when you change _editable
Remeber to use .get() to get value, and .set() instead of = to set value.
EDIT:
In other situations you would have to use return and assign result back to variable
def function(_editable):
# ... code ...
_editable = True
return _editable
a = False
a = function(a) # assign returned value to `a`
print(a)
but when function is assigned to Button (or in bind(), or after()) then it can't get returned value and assign back to variable - and then usually we use global to assing value directly to external variable
def function(_editable):
global a
# ... code ...
_editable = True
a = _editable
a = False
function(a) # without assignig returned value
print(a)

Related

Tkinter widget where User chooses parameter values, but how do I store these values?

My objective is to create three variables called 'Number of pipes','Anisotropy ratio', 'Filter depth (in cm)' which values are chosen by the user (through a User interface). I achieved to create the scrollbar of Tkinter and to enter values but these are not stored as variables. Could someone give me a hand solving that? I am new in Python and specially I am struggling with Tkinter. The code is developped in Python 3.7:
root = tk.Tk()
root.geometry('1000x1000')
#Description show in window
info=tk.Label(root,anchor='e')
info.pack()
#Parameters
parameters = ('Number of pipes','Anisotropy ratio', 'Filter depth (in cm)')
def ask_parameter(entry):
user_pipes = str (entry['Number of pipes'].get())
user_aniso = str (entry['Anisotropy ratio'].get()) #effective screen length = b
user_depth = str (entry['Filter depth (in cm)'].get())
print(user_pipes,user_aniso,user_depth)
#if parameters
# return True
# else:
# tkinter.messagebox.showwarning ('Only numbers', 'Try again')
# return True
#
def form(parameters):
entry = {}
for parameter in parameters:
print(parameter)
row = tk.Frame(root)
lab = tk.Label(row, width=15, text=parameter+": ", anchor='w')
ent = tk.Entry(row)
row.pack(side=tk.TOP,
fill=tk.X,
padx=2,
pady=2)
lab.pack(side=tk.LEFT)
ent.pack(side=tk.RIGHT, expand=tk.YES,fill=tk.X)
entry[parameter] = ent
return entry
if __name__ == '__main__':
ents = form(parameters)
save_button = tk.Button(root)
save_button.configure(text='Save', command=lambda: ask_parameter(ents))
save_button.pack()
root.mainloop()
Any problem is shown with the code but the parameters are not stored as variables with the entered values.
Thank you very much for your time.
Your code is storing the values inputted through the GUI to variables, your problem is probably that your variables aren't global, so you won't be able to use them outside of the function they are created in. you can either get around this with a return, or make them global by putting global varname above the definition of each variable as well as at the start of every function that will use it.

Widget validation in tkinter

I want the user to be able to enter an integer value in the Spinbox widget. If the value entered is not an integer or is an integer outside the Spinbox limits, as soon as the Spinbox loses focus, the value in the Spinbox's content must revert to a default value.
In the example code, I use the Entry widget just for the Spinbox can lose focus.
If the user comes back to Spinbox to enter a new value, his entry is not validated.
I confirm Malcolm's remark in Interactively validating Entry widget content in tkinter that the validatecommand=command feature gets cleared as soon as this command updates the widget's value.
Is there a way to get the value entered in the Spinbox repeatedly validated and not just once?
from tkinter import *
class GUI:
def __init__(self):
# root window of the whole program
self.root = Tk()
self.root.title('Validate Spinbox')
# registering validate and invalid commands
validate_cmd = (self.root.register(self.validate), '%P')
invalid_cmd = (self.root.register(self.invalid))
# creating a Label
items_lbl = Label(self.root, text="# of items (5-10):")
items_lbl.grid(row=0, column=0)
# creating a Spinbox widget
self.items_var = StringVar()
self.items_var.set(7)
items_count = Spinbox(self.root, textvariable=self.items_var,
from_=5, to=10, width=4, validate='focusout',
validatecommand=validate_cmd,
invalidcommand=invalid_cmd)
items_count.grid(row=0, column=1)
# creating an Entry widget
self.entry_var = StringVar()
self.entry_var.set("Input some text here")
text_entry = Entry(self.root, textvariable=self.entry_var)
text_entry.grid(row=1, column=0)
def validate(self, entry):
try:
value = int(entry)
valid = value in range(5, 11)
except ValueError:
valid = False
if not valid:
self.root.bell()
return valid
def invalid(self):
self.items_var.set(7)
if __name__ == '__main__':
main_window = GUI()
mainloop()
I found a great explanation here (in the last paragraph of the chapter Validation):
http://stupidpythonideas.blogspot.fr/2013/12/tkinter-validation.html
If your validatecommand (or invalidcommand) modifies the Entry directly or indirectly (e.g., by calling set on its StringVar), the validation will get disabled as soon as your function returns. (This is how Tk prevents an infinite loop of validate triggering another validate.) You have to turn it back on (by calling config). But you can't do that from inside the function, because it gets disabled after your function returns.
But you need to apply some changes to be able to use this trick.
You need to make the Spinbox an instance attribute, with self :
self.items_count = Spinbox(self.root, textvariable=self.items_var,
from_=5, to=10, width=4, validate='focusout',
validatecommand=validate_cmd,
invalidcommand=invalid_cmd)
self.items_count.grid(row=0, column=1)
And then you can call self.items_count.after_idle(...) inside the validate method :
def validate(self, entry):
try:
value = int(entry)
valid = value in range(5, 11)
except ValueError:
valid = False
if not valid:
self.root.bell()
self.items_count.after_idle(lambda: self.items_count.config(validate='focusout'))
return valid

Setting a tkinter variables using a generic set_value() method

I'd like to have a generic method that will change the value of a tkinter variable. I want to be able to do this from a menu option that brings up a new dialog box which is from a generic method that can be used for different variables. the code I have so far is below:
import sys
from tkinter import *
from tkinter import ttk
from tkinter import filedialog
import sequencer as seq
class View(ttk.Frame):
"""Main Gui class"""
def __init__(self, master = None):
ttk.Frame.__init__(self, master, borderwidth=5, width=450, height=500)
self.master = master
self.grid(column=0, row=0, sticky=(N, S, E, W))
self.columnconfigure(0, weight=1)
###############################
### User editable variables ###
self.precision = IntVar(value=4, name='precision')
self.sensitivity = IntVar(value = 50, name='sensitivity')
### User editable variables ###
###############################
self.create_menus()
def create_menus(self):
"""Produces the menu layout for the main window"""
self.master.option_add('*tearOff', FALSE)
self.menubar = Menu(self.master)
self.master['menu'] = self.menubar
# Menu Variables
menu_file = Menu(self.menubar)
menu_edit = Menu(self.menubar)
# Add the menus to the menubar and assign their variables
self.menubar.add_cascade(menu=menu_file, label="File")
self.menubar.add_cascade(menu=menu_edit, label = "Edit")
### ADD COMMANDS TO THE MENUES ###
### File ###
menu_file.add_command(label="Quit", command=self.master.destroy)
### Edit ###
menu_edit.add_command(label="Backbone", command=lambda : self.edit_backbone())
menu_edit.add_command(label="Precision", command=lambda : self.precision.set(self.set_value_int("Precision")))
menu_edit.add_command(label="Sensitivity", command=lambda : self.sensitivity.set(self.set_value_int("Sensitivity")))
menu_edit.add_command(label="print precision", command=lambda : print(self.precision.get()))
menu_edit.add_command(label="print sensitivity", command=lambda : print(self.sensitivity.get()))
def set_value_int(self, name):
"""Standards dialog that return a user define value of a specific type"""
t = Toplevel(self)
t.title("Set " + name)
label = ttk.Label(t, text="Set "+name)
label.grid(row=0)
entry = ttk.Entry(t)
entry.grid(row=1)
cancel = ttk.Button(t, text="Cancel", command=lambda : t.destroy())
cancel.grid(column=0, row=2)
okey = ttk.Button(t, text="Okey", command=lambda : okey(entry.get()))
okey.grid(column=1, row=2)
def okey(value):
"""return value according to type"""
try:
t.destroy()
return int(value)
except:
self.error_box("value must be and integer")
def error_box(self, error_message="Unknown error"):
"""(Frame, String) -> None
Opens an window with an Okey button and a custom error message"""
t=Toplevel(self)
t.title("Error")
label = ttk.Label(t, text=error_message)
label.grid(row=0)
okey = ttk.Button(t, text="Okey", command=lambda : t.destroy())
okey.grid(row=1)
if __name__ == "__main__":
root = Tk()
root.title("Sequencer")
view = View(root)
root.mainloop()
print("End")
The Edit-> print xxxxxx commands are purely for testing purposes to check if the values have changed. If these are executed before trying to change the values of precision or sensitivity then they work as expected.
If you try to change either of the tkinter variables in the way I have tried to do they become None types and I can't see why. I can only assume that you are not allowed to change them in the way that I have but I can't think of another way to do it without having a separated method for each variable which I'd like to avoid.
Baicly I'd like the user to be able to customise the variables precision and sensitivity and use the same method in the code to change the values.
Extra but not necessarily vital:- If there is a way to define which type the variable should be in the methods arguments as well that would be even better as I will have other variables for the user to change later and they will be of different types.
Any help would be much appreciated. I have tried to be as clear as I can but let me know if anything is not.
Thanks
self.set_value_int always returns None, so it's always going to set your variable to none.
Instead of trying to write a complex lambda that is hard to debug, put all of your logic inside the function. Have the function set the value. All you need to do is tell it what variable to set:
menu_edit.add_command(label="Precision",
command=lambda name="Precision", var=self.precision: self.set_value_int(name, var))
...
def set_value_int(self, name, var):
...
def okey():
s = entry.get()
try:
var.set(int(s))
...

CheckBox Using method that is inside another method

Hi i want to use the method checkbutton_value1 in another method AGM.
def AGM():
def A1():
print "A1"
def A2():
print "A2"
def checkbutton_value1():
x=var1.get()
I tried using checkbutton_value1 for checkbutton command but it won't work.
master = Tk() # Open up GUI connection
master.title('Program Application')
var1=IntVar()
checkbox_1 = Checkbutton(master, text='Interpolate Graph', variable=var1,command=checkbutton_value1)
checkbox_1.pack()
master.mainloop() # Continue loop till user close tab
Error message
NameError: name 'checkbutton_value1' is not defined
This is probably happening because you've defined checkbutton_value1 inside AGM's namespace.
What you need to do is this:
def checkbutton_value1():
x = var1.get()
master = Tk() # Open up GUI connection
master.title('Program Application')
var1 = IntVar()
checkbox_1 = Checkbutton(master, text='Interpolate Graph',
variable=var1, command=checkbutton_value1)
checkbox_1.pack()
master.mainloop() # Continue loop till user close tab
Now, this will work. However, in suck cases, its just better to use a lambda:
checkbox_1 = Checkbutton(master, text='Interpolate Graph',
variable=var1, command=lambda: var1.get())
Could you post a larger snippet? That is probably failing because checkbutton_value1 is being defined in a scope where the line referencing it doesn't have access.
For example, this doesn't produce that error:
class test:
def foo():
pass
print(test.foo())

Simple gui that display a repeated set of images when a button is pushed

I made a very simple gui that has a button and shows an image(.gif). My goal is to output another .gif whenever you press the button. There are 2 .gif files in my file directory and the point is to keep switching between these two whenever you press the button.
#Using python2.7.2
import Tkinter
root = Tkinter.Tk()
try:
n
except:
n = 0
def showphoto(par):
if par%2 == 0:
try:
label2.destroy()
except:
pass
photo = Tkinter.PhotoImage(file="masc.gif")
label2 = Tkinter.Label(image=photo)
label2.image = photo
label2.pack()
else:
try:
label2.destroy()
except:
pass
photo = Tkinter.PhotoImage(file="123.gif")
label2 = Tkinter.Label(image=photo)
label2.image = photo
label2.pack()
myContainer1 = Tkinter.Frame(root, width = 100, height = 100)
myContainer1.pack()
def callback(event):
global n
showphoto(n)
n = n + 1
button1 = Tkinter.Button(myContainer1)
button1["text"]= "Next pic"
button1["background"] = "green"
button1.bind("<Button-1>", callback(n))
button1.pack()
root.mainloop()
The current code just outputs the first image (masc.gif) but when I press the button it doesn't switch to the other image(123.gif). What am I doing wrong?
This can achieved much easier with classes as the class holds all the data necessary without the use of global variables.
import Tkinter as tk
from collections import OrderedDict
class app(tk.Frame):
def __init__(self,master=None, **kwargs):
self.gifdict=OrderedDict()
for gif in ('masc.gif','123.gif'):
self.gifdict[gif]=tk.PhotoImage(file=gif)
tk.Frame.__init__(self,master,**kwargs)
self.label=tk.Label(self)
self.label.pack()
self.button=tk.Button(self,text="switch",command=self.switch)
self.button.pack()
self.switch()
def switch(self):
#Get first image in dict and add it to the end
img,photo=self.gifdict.popitem(last=False)
self.gifdict[img]=photo
#display the image we popped off the start of the dict.
self.label.config(image=photo)
if __name__ == "__main__":
A=tk.Tk()
B=app(master=A,width=100,height=100)
B.pack()
A.mainloop()
Of course, this could be done more generally ... (the list of images to cycle through could be passed in for example), and this will switch through all the images in self.gifs ...
This approach also removes the necessity to destroy and recreate a label each time, instead we just reuse the label we already have.
EDIT
Now I use an OrderedDict to store the files. (keys=filename,values=PhotoImages). Then we pop the first element out of the dictionary to plot. Of course, if you're using python2.6 or earlier, you can just keep a list in addition to the dictionary and use the list to get the keys.
button1 = Tkinter.Button(myContainer1)
button1["text"]= "Next pic"
button1["background"] = "green"
button1.bind("<Button-1>", callback(n))
First, you bind the <Button-1> event to None (that's what callback(n) evaluates to). You should bind it to callback (no parentheses a.k.a the call operator).
Second, I suggest you change callback to not accept any arguments, remove the bind call and create your button as:
button1 = Tkinter.Button(myContainer1, command=callback)

Categories

Resources