How to make permanent Entry Changes to a label or button - python-3.x

Ive made a sample program on how generally it looks like.. my goal is to have the Data Entry write permanently to the button so dat if I run the program again its update the current price.
from tkinter import*
import tkinter as tk
import tkinter.simpledialog
def changeP1(event):
btnT4=tk.Button(root,text='Updating...',width=10,bg='green')
btnT4.grid(in_=root,row=1,column=2)
btnT4.bind('<1>',changeP1)
askC1=tk.simpledialog.askfloat('Updating...','What is the current price?')
btnT4=tk.Button(root,text=('RM {:,.2f}'.format(askC1)),width=10)
btnT4.grid(in_=root,row=1,column=2)
btnT4.bind('<1>',changeP1)
def changeP2(event):
btnT4=tk.Button(root,text='Updating...',width=10,bg='green')
btnT4.grid(in_=root,row=2,column=2)
btnT4.bind('<1>',changeP2)
askC2=tk.simpledialog.askfloat('Updating...','What is the current price?')
btnT4=tk.Button(root,text=('RM {:,.2f}'.format(askC2)),width=10)
btnT4.grid(in_=root,row=2,column=2)
btnT4.bind('<1>',changeP2)
def changeP3(event):
btnT4=tk.Button(root,text='Updating...',width=10,bg='green')
btnT4.grid(in_=root,row=3,column=2)
btnT4.bind('<1>',changeP3)
askC3=tk.simpledialog.askfloat('Updating...','What is the current price?')
btnT4=tk.Button(root,text=('RM {:,.2f}'.format(askC3)),width=10)
btnT4.grid(in_=root,row=3,column=2)
btnT4.bind('<1>',changeP3)
root=Tk()
Title=['Item','Unit','Price']
Item=['Kopi O','Teh O','Teh Tarik']
Unit= '1 cup'
Price=[1,0.9,1.2]
cl=[0,1,2]
rw=[1,2,3]
for i in range(3):
btnT1=tk.Button(root,text=Title[i],width=10,bg='yellow')
btnT1.grid(in_=root,row=0,column=cl[i])
for x in range(3):
btnT2=tk.Button(root,text=Item[x],width=10)
btnT2.grid(in_=root,row=rw[x],column=0)
for y in range(3):
btnT3=tk.Button(root,text=Unit,width=10)
btnT3.grid(in_=root,row=rw[y],column=1)
for z in range(3):
btnT4=tk.Button(root,text=('RM {:,.2f}'.format(Price[z])),width=10)
btnT4.grid(in_=root,row=rw[z],column=2)
if z in range(0,1):
btnT4.bind('<1>',changeP1)
if z in range(1,2):
btnT4.bind('<1>',changeP2)
if z in range(2,3):
btnT4.bind('<1>',changeP3)
root.mainloop()
and if theres anyway to make this simpler..

You have 2 options (well that I know of) since you're dynamically creating your buttons. Both options only require one function.
If you wish to use bind then you can get the selected widget using event.widget
def onChange(event):
ans = tk.simpledialog.askfloat('Updating...','What is the current price?')
if ans: # checks if None is returned when clicking cancel
event.widget.config(text='RM {:,.2f}'.format(ans))
And so in your loop you'd only have the one bind btnT4.bind('<1>', onChange).
Alternatively use the command attribute for the button to assign the function to be called when the button is pressed. Using command for the button is generally more pythonic than binding.
This requires you to also create a list to store the buttons, to allow the function to know which widget to change.
btn_list = [] # create an empty list for the buttons
# Your for loop will look like this the command parameter instead
# and append the button to the list
for z in range(3):
btnT4=tk.Button(root,text=('RM {:,.2f}'.format(Price[z])),width=10,\
command=lambda i=z: onChange(i))
btnT4.grid(in_=root,row=rw[z],column=2)
btn_list.append(btnT4)
lambda will pass the value of z into the onChange function to create a unique call for that button. The value of 'z' is relative to the index position of the button in the list.
The onChange function when called will ask for the new input, and if valid will update the button object stored in the list using the index.
# Your change function will look like this
def onChange(i):
ans = tk.simpledialog.askfloat('Updating...','What is the current price?')
if ans:
btn_list[i].config(text='RM {:,.2f}'.format(ans))

Related

Python Tkinter: Creating checkbuttons from a list

I am building a gui in tkinter with a list task_list = [].
Tasks are appended to/deleted from the list in the gui.
I want a window with checkboxes for every item in the list.
So if there's 10 items in the list, there should also be 10 checkboxes.
If there's 5 items in the list, there should be 5 corresponding checkboxes.
Can this be done?
I can't find anything on it
Thanks!
Here.
from tkinter import *
task_list=["Call","Work","Help"]
root=Tk()
Label(root,text="My Tasks").place(x=5,y=0)
placement=20
for tasks in task_list:
Checkbutton(root,text=str(tasks)).place(x=5,y=placement)
placement+=20
root.mainloop()
Using grid.
from tkinter import *
task_list=["Call","Work","Help"]
root=Tk()
Label(root,text="My Tasks").grid(row=0,column=0)
placement=3
for tasks in task_list:
Checkbutton(root,text=str(tasks)).grid(row=placement,column=0,sticky="w")
placement+=3
root.mainloop()
Here is my code for this issue:
from tkinter import Tk, Checkbutton, IntVar, Frame, Label
from functools import partial
task_list = ['Task 1', 'Task 2', 'Task 3', 'Work', 'Study']
def choose(index, task):
print(f'Selected task: {task}' if var_list[index].get() == 1 else f'Unselected task: {task}')
root = Tk()
Label(root, text='Tasks').grid(column=0, row=0)
frame = Frame(root)
frame.grid(column=0, row=1)
var_list = []
for index, task in enumerate(task_list):
var_list.append(IntVar(value=0))
Checkbutton(frame, variable=var_list[index],
text=task, command=partial(choose, index, task)).pack()
root.mainloop()
First I would like to mention that it is possible to mix layout manager methods like in this example. The main window uses grid as layout management method and I have gridded a frame to the window, but notice that Checkbuttons are getting packed, that is because frame is a different container so it is possible to use a different layout manager, which in this case makes it easier because pack just puts those checkbuttons one after another.
The other stuff:
There is the task list which would contain the tasks.
Then I have defined a function choose() this function prints out something. It depends on a variable. The comparison happens like this: print out this if value is this else print out this. It is just an if/else statement in one line and all it checks is if the IntVar in that list in that specific index is value 1 so "on". And there are two argument this function takes in: index and task. The index is meant to get the according IntVar from the var_list and the task is meant to display what tasks was chosen or unchosen.
Then the simple root = Tk() and root.mainloop() at the end.
Then is the label that just explains it.
Then the frame and here You can see that both label and frame were gridded using .grid()
Then the var_list = [] just creates an empty list.
Then comes the loop:
It loops over each item in the task_list and extracts the index of that item in the list and the value itself. This is done by using enumerate() function.
Each iteration appends a IntVar(value=0) to the var_list and since this appending happens at the same time as the items are read from the task_list the index of that IntVar in the list is the same as the current item in the task_list so that same index can be used for access.
Then a Checkbutton is created, its master is the frame (so that .pack() can be used) and the text=task so it corresponds to task name, the variable is set as a specific item in the var_list by index and this all has to be done so that a reference to that IntVar is kept. Then comes command=partial(choose, index, task) which may seem confusing but all partial does is basically this function will now execute always with the variables just given so those variables will always be the same for this function for this Checkbutton. And the first argument of partial is the function to be executed and next are arguments this function takes in. Then the Checkbutton gets packed.
If You have any questions ask.
Useful sources:
About partial() (though there are other sources too)
About Checkbutton (other sources about this too)
One line if/else statements

Run function when an item is selected from a dropdown

I have a dropdown in tkinter, that i have populated with some items.
OPTIONS = [0,1,2,3,4,5,6,7]
clicked = tk.StringVar()
clicked.set(OPTIONS[0]) # default value
drop = tk.OptionMenu(frame2, clicked, *OPTIONS)
drop.place(relx = 0.65, rely=0.25, relwidth=0.08, relheight=0.6)
However, when a user selects a value, i want other things to happen as well.
Like returning the value to a global variable, or making the state of a button normal, so it's visible again.
How can i run a function, when an item is selected, or when a different item is selected?
EDIT:
Following the suggestions of TheLizzard, i changed my code to this:
# this function is triggered, when a value is selected from the dropdown
def dropdown_selection():
global dropdown_value
dropdown_value = clicked.get()
print("You changed the selection. The new selection is %s." % dropdown_value)
button_single['state'] = 'normal'
OPTIONS = list(range(8))
clicked = tk.StringVar(master=frame2)
clicked.set(OPTIONS[0])
clicked.trace("w", dropdown_selection)
drop = tk.OptionMenu(frame2, clicked, *OPTIONS)
drop.place(relx = 0.65, rely=0.25, relwidth=0.08, relheight=0.6)
However, i get this error:
TypeError: dropdown_selection() takes 0 positional arguments but 3 were given
Try this:
import tkinter as tk
def changed(*args):
print("You changed the selection. The new selection is %s." % clicked.get())
root = tk.Tk()
OPTIONS = list(range(8))
clicked = tk.StringVar(master=root) # Always pass the `master` keyword argument
clicked.set(OPTIONS[0]) # default value
clicked.trace("w", changed)
drop = tk.OptionMenu(root, clicked, *OPTIONS)
drop.pack()
root.mainloop()
In tkinter you can add callbacks to variables like StringVar using <tkinter variable>.trace(mode, callback) for more info read this.
Also always pass in the master keyword argument to all tkinter widgets/variables. You did this with the OptionMenu (it's the first argument usually). But you didn't do it for the StringVar. If you always pass the master keyword argument, you can save yourself a headache.
Edit:
When tkinter calls the callback when the variable is changed it passes some arguments (I don't think they are useful) so make sure that the callback accepts them. Instead of having def callback() use def callback(*args).

populating one combobox based on another combo box using tkinter python

from tkinter import *
from tkinter.ttk import Combobox
v1=[]
root = Tk()
root.geometry('500x500')
frame1=Frame(root,bg='#80c1ff',bd=5)
frame1.place(relx=0.5,rely=0.1,relwidth=0.75,relheight=0.1,anchor='n')
lower_frame=Frame(root,bg='#80c1ff',bd=10)
lower_frame.place(relx=0.5,rely=0.25,relwidth=0.75,relheight=0.6,anchor='n')
v=[]
def maincombo():
Types=["MA","MM","MI","SYS","IN"]
combo1=Combobox(frame1,values=Types)
combo1.place(relx=0.05,rely=0.25)
combo2=Combobox(frame1,values=v)
combo2.bind('<<ComboboxSelected>>', combofill)
combo2.place(relx=0.45,rely=0.25)
def combofill():
if combo1.get()=="MA":
v=[1,2,3,45]
combo2=Combobox(frame1,values=v)
combo2.place(relx=0.45,rely=0.25)
if combo1.get()=="MM":
v=[5,6,7,8,9]
combo2=Combobox(frame1,values=v)
combo2.place(relx=0.45,rely=0.25)
maincombo()
root.mainloop()
I want to populate the one combobox based on selection of other combobox I,e types.But failed to do so with simple functions.
Looking at you code, most of what you need is already there. The changes I have made are as follows:
Bound to combo1 rather than combo2 (as combo1 is the one you want to monitor)
Set combo1 and combo2 as global variables (so they can be used in the combofill method)
Set the combofill method to accept the event arg (it would raise a TypeError otherwise)
Use the .config method on combo2 rather than creating a new one each time
Set combo2 to be empty when neither "MA" or "MM" are selected
Here is my implementation of that:
from tkinter import *
from tkinter.ttk import Combobox
v1=[]
root = Tk()
root.geometry('500x500')
frame1=Frame(root,bg='#80c1ff',bd=5)
frame1.place(relx=0.5,rely=0.1,relwidth=0.75,relheight=0.1,anchor='n')
lower_frame=Frame(root,bg='#80c1ff',bd=10)
lower_frame.place(relx=0.5,rely=0.25,relwidth=0.75,relheight=0.6,anchor='n')
v=[]
def maincombo():
global combo1, combo2
Types=["MA","MM","MI","SYS","IN"]
combo1=Combobox(frame1,values=Types)
combo1.place(relx=0.05,rely=0.25)
combo1.bind('<<ComboboxSelected>>', combofill)
combo2=Combobox(frame1,values=v)
combo2.place(relx=0.45,rely=0.25)
def combofill(event):
if combo1.get()=="MA":
v=[1,2,3,45]
elif combo1.get()=="MM":
v=[5,6,7,8,9]
else:
v=[]
combo2.config(values=v)
maincombo()
root.mainloop()
A couple other ideas for potential future consideration:
I would recommend using the grid manager rather than the place manager as it will stop widgets overlapping, etc. (on my system, combo2 slightly covers combo1)
Use a dictionary rather than if ... v=... elif ... v= ... and then use the get method so you can give the default argument. For example:
v={"MA": [1,2,3,45],
"MM": [5,6,7,8,9]}. \
get(combo1.get(), [])
EDIT:
Responding to the question in the comments, the following is my implementation of how to make a "toggle combobox" using comma-separated values as requested.
As the combobox has already overwritten the value of the text area when our <<ComboboxSelected>> binding is called, I had to add a text variable trace so we could keep track of the previous value of the text area (and therefore append the new value, etc.). I am pretty sure that explanation is completely inadequate so: if in doubt, look at the code!
from tkinter import *
from tkinter.ttk import Combobox
root = Tk()
def log_last():
global last, cur
last = cur
cur = tx.get()
def append_tx(event):
if last:
v = last.split(",")
else:
v = []
v = list(filter(None, v))
if cur in v:
v.remove(cur)
else:
v.append(cur)
tx.set(",".join(v))
combo.selection_clear()
combo.icursor("end")
last, cur = "", ""
tx = StringVar()
combo = Combobox(root, textvariable=tx, values=list(range(10)))
combo.pack()
combo.bind("<<ComboboxSelected>>", append_tx)
tx.trace("w", lambda a, b, c: log_last())
root.mainloop()

Python 3.7 + tkInter : How can I make sure a button is assigned an individual function from a file?

I am having some issues wrapping my head around something I encountered in python recently.
So, basically, I want to allow for a user to load several json files, all listed in a python list. These files contain parameters used to create buttons with, namely, the color the button should have, the text that should be displayed in it and the command that it needs to execute once clicked.
def createTags(self):
for items in self.LoadedInstallProfiles:
with open(items, "r") as jsonfiles:
self.loadeddata = json.load(jsonfiles)
self.tag = Button(self.tagmenu, text=self.loadeddata.get("profilename"), background=
self.loadeddata.get("profilecolor"), command=print(self.loadeddata.get("profilename")))
self.tag.pack(side="top",fill="x")
The problem is: the buttons show up with their individual color and text, but all seem to print out the same profilename when clicked, which is that in the last json file in the list.
I common way is to store the created button widgets in a list. I have modified your method. See below.
def createTags(self):
# First create the widget and append to list variable
self.tags = [] #List to store button widgets
for items in self.LoadedInstallProfiles:
with open(items, "r") as jsonfiles:
loadeddata = json.load(jsonfiles)
text = loadeddata.get("profilename")
bg = loadeddata.get("profilecolor")
tag = Button( self.tagmenu, text=text, background=bg, command=print(text) )
self.tag.append( tag )
# Then display the widgets
for tag in self.tags:
tag.pack(side="top",fill="x")
I imagine the problem with command=print(self.loadeddata.get("profilename")) is similar to the problem with lambda statements (that said I am surprised your buttons work at all They should print once at init and then never work after that because you are calling print at button creation instead of saving a reference to print).
Due to the nature of how lambda works here in a loop like this you end up only printing the last value in the loop for all commands. Instead you need to use a lambda statement and also define the value in the lambda for each loop to accurately record the correct data for the print statement.\
I created 3 test files for this:
test.json:
{"profilename":"test", "profilecolor": "green"}
test2.json:
{"profilename":"test2", "profilecolor": "blue"}
test3.json:
{"profilename":"test3", "profilecolor": "orange"}
Example code:
import tkinter as tk
import json
class Window(tk.Tk):
def __init__(self):
super().__init__()
self.btn_list = []
for file in ['test.json', 'test2.json', 'test3.json']:
with open(file, 'r') as f:
self.btn_list.append(json.load(f))
self.create_tags()
def create_tags(self):
for item in self.btn_list:
tk.Button(self, text=item.get("profilename"), background=item.get("profilecolor"),
command=lambda x=item.get("profilename"): print(x)).pack(side="top", fill="x")
if __name__ == '__main__':
Window().mainloop()
Results:

Tkinter - How to trace expanding list of variables

What I am trying to do track when any values in a list of StringVar change, even when the list is expanding. Any additions to the list before the trace statement will result in the callback. But any additions afterward, such as when pressing a button, will not cause any callback.
import tkinter as tk
root = tk.Tk()
frame = tk.Frame(root)
frame.grid(row=0)
L = []
def add_entry(event):
L.append(tk.StringVar())
tk.Entry(frame,textvariable=L[len(L)-1]).grid(row=len(L),padx=(10,10),pady=(5,5))
add = tk.Button(frame,text='add Entry',command='buttonpressed')
add.grid(row=0)
add.bind('<Button-1>',add_entry)
for i in range(2):
L.append(tk.StringVar())
tk.Entry(frame,textvariable=L[len(L)-1]).grid(row=len(L),padx=(10,10),pady=(5,5))
for i in L:
i.trace('w',lambda *arg:print('Modified'))
root.mainloop()
Modifying the first two Entry's prints out Modified, but any Entry's after the trace is run, such as the ones produced when a button is pressed, will not.
How do I make it so that trace method will run the callback for the entire list of variables even if the list is expanded?
Simple suggestion, change your add_entry function to something like this:
def add_entry(event):
L.append(tk.StringVar())
tk.Entry(frame,textvariable=L[len(L)-1]).grid(row=len(L),padx=(10,10),pady=(5,5))
L[len(L)-1].trace('w',lambda *arg:print('Modified'))
Extra suggestions:
This add = tk.Button(frame,text='add Entry',command='buttonpressed') is assigning a string to command option, means it will try to execute that string when button is clicked(which will do nothing). Instead, you can assign your function add_entry to command option and it will call that function when button is clicked and you can avoid binding Mouse Button1 click to your Button(Note: No need to use argument event in function when using like this). Read more here
Python supports negative indexing of List, so you can call L[-1] to retrieve the last element in the list instead of calling L[len(L)-1]).
Once you change your add_entry function as suggested, you can reduce your code to
import tkinter as tk
root = tk.Tk()
frame = tk.Frame(root)
frame.grid(row=0)
L = []
def add_entry():
global L
L.append(tk.StringVar())
tk.Entry(frame,textvariable=L[-1]).grid(row=len(L),padx=(10,10),pady=(5,5))
L[-1].trace('w',lambda *arg:print('Modified'))
add = tk.Button(frame,text='add Entry',command=add_entry)
add.grid(row=0)
for i in range(2):
add_entry()
root.mainloop()

Resources