As stated in the title, how do I prevent the use of global variables in Tkinter? I have started to learn Tkinter and am running into a problem in almost all of my projects. The problem I run into is that most Widgets need to interact with each other creating a lot of dependencies. Below is the code from one of my projects that has this problem. I included all the code from this project because the problem arises throughout the code.
As an example consider the apply_filter function. In order to receive the input from the user, the Apply Filter button's command function needs access to the entry fields. One way this can be solved is by passing all the entry fields to the command function as arguments. However, I don't like this option because this means that I either have to place all the code into on giant block making the code more difficult to read or I have to pass it through multiple functions which leads to the same result. The option I chose here was to make the inputs global so that I can access them from anywhere within my code. At least the code is easier to read then. Of course this is probably not a good solution. When the GUI scales up this will create a giant mess of dependencies that could easily lead to bugs.
Long story short, I want to know: Is there a better way to deal with these dependencies so that I don't need to create tons of global variables or create unreadable code? Thanks in advance for any effort taken.
import tkinter as tk
from tkinter import filedialog as fd
import pandas as pd
import numpy as np
import sys
start_filter_row = 0
start_filter_column = 0
data_location = ""
df = None
root = None
filterr = {"customercode":{"column name": "Customer (code)", "value": None, "row_number": start_filter_row},
"partnr":{"column name": "Part nr", "value": None, "row_number": start_filter_row + 1},
"partclass":{"column name": "Part class", "value": None, "row_number": start_filter_row + 2},
"partfamily":{"column name": "Part familiy", "value": None, "row_number": start_filter_row + 3},
"startdate":{"column name": "Transaction date", "value": None, "row_number": start_filter_row + 4},
"enddate":{"column name": "Transaction date", "value": None, "row_number": start_filter_row + 5}
}
inputs = dict()
'''
Load Data
'''
def load_dataset():
global df
root.update()
database_location = fd.askopenfilename()
df = load_data(database_location)
def load_data(database_location):
try:
df = pd.read_pickle(database_location)
return df
except Exception as e:
print("Data location not set!", e)
root.destroy()
sys.exit()
'''
create Widgets
'''
def create_filter_inputs():
for key, value in filterr.items():
label = tk.Label(root, text = key)
label.grid(row = value["row_number"], column = start_filter_column, sticky = "w")
entry = tk.Entry(root)
entry.focus_set()
entry.grid(row = value["row_number"], column = start_filter_column + 1, sticky = "w")
inputs[key] = entry
def create_filter_buttons():
apply_button = tk.Button(root, text = "Apply Filter", command = apply_filter)
apply_button.grid(row = start_filter_row + len(filterr), column = 0, sticky = "news")
clear_button = tk.Button(root, text = "Clear Filter", command = clear_filter)
clear_button.grid(row = start_filter_row + len(filterr), column = 1, sticky = "news")
'''
Button functions
'''
def clear_filter():
for key, entry in inputs.items():
entry.delete(0, 'end')
apply_filter()
def apply_filter():
for key, entry in inputs.items():
filterr[key]["value"] = entry.get().split(";")
def main():
global data_location, root
'''
Setting up Tkinter Frame and loading dataset
'''
root = tk.Tk()
root.title("Data Tool V0.1")
loading_label = tk.Label(root, text = "Loading...", width = 30, height = 5)
loading_label.pack()
load_dataset()
loading_label.destroy()
n_rows, n_cols = (4, 3)
root.columnconfigure(tuple(range(n_cols)), weight=1)
root.rowconfigure(tuple(range(n_rows)), weight=1)
'''
Setting up GUI
'''
create_filter_inputs()
create_filter_buttons()
root.mainloop()
if __name__ == "__main__":
main()
You can avoid using globals by converting your GUI into a class format and making them class variables instead.
class GUI:
def __init__():
.....
self.myvariable = ...
def function():
newvar = self.myvariable - 5
Its also worth noting that variables defined outside of functions can be seen from within functions, but not vice versa:
>>> def f(a):
... print(a)
...
>>> b=2
>>> f(b)
2
Related
I'm trying to create a small interface with Tkinter with 4 radio buttons and each time I click on one of the buttons it will print the value of the button, I've managed to do that!
However, I would like the printed value to be saved in an Excel file for example each time I click on one of the buttons in my tkinter window.
Below is the code I used and the result I got:
from tkinter import *
import tkinter as tk
master = Tk()
master.geometry("175x175")
# Tkinter string variable
# able to store any string value
v = IntVar(master, "0") #0 pour qu'aucun des boutons soit sélectioné par défaut
# Dictionary to create multiple buttons
values = {"classe 1" : "1",
"classe 2" : "2",
"classe 3" : "3",
"classe 4" : "4"}
def affiche ():
val = v.get()
print(val)
data = [
['class1', val],
]
labelValue = tk.Label(master,
text="""Choose your class affectation:""",
justify = tk.LEFT,
padx = 20).pack()
for (text, value) in values.items():
Radiobutton(master, text = text, variable = v, command = affiche,
value = value).pack(side =TOP, ipady = 5)
# Infinite loop can be terminated by
# keyboard or mouse interrupt
# or by any predefined function (destroy())
mainloop()
On the right of the capture below are the values I would like to save in an excel file each time I click.
I got them by clicking respectively on class1, class2, class3 and class4
enter image description here
I am a beginner, any help is appreciated!
Thank you very much.
I just completed the previous program
def affiche ():
val = v.get()
print(val)
workbook = xlsxwriter.Workbook('Mon fichier.xlsx')
worksheet = workbook.add_worksheet("My sheet")
data = [
['class1', val],
['class2', val],
['class3', val],
['class4', val],
]
caption = 'My data.'
# Set the columns widths.
worksheet.set_column('B:G', 12)
# Write the caption.
worksheet.write('B1', caption)
# Add a table to the worksheet.
worksheet.add_table('B4:L4', {'header_row': 0})
# Table data can also be written separately, as an array or individual cells.
worksheet.write_row('B4', data[0])
worksheet.write_row('B5', data[1])
worksheet.write_row('B6', data[2])
worksheet.write_row('B7', data[3])
workbook.close()
I am making a program where a user will choose a dataset from a dropdown box, and will subsequently then load the data in for analysis later on in the program. My current problem is that the data is not being loaded in, and I am not sure why
from tkinter import *
datasets = ["dataset 1", "dataset 2", "dataset 3", "dataset 4"]
master = Tk()
variable = StringVar(master)
variable.set(datasets[0])
data_options = OptionMenu(master, variable, *datasets)
data_options.pack()
from sklearn import datasets
import pandas as pd
def dat_import():
if variable.get() == "dataset 1":
data = pd.DataFrame(datasets.load_iris().data)
return data
if variable.get() == "dataset 2":
data = pd.DataFrame(datasets.load_diabetes().data)
return data
if variable.get() == "dataset 3":
data = pd.DataFrame(datasets.load_digits().data)
return data
if variable.get() == "dataset 4":
data = pd.DataFrame(datasets.load_boston().data)
return data
button = Button(master, text = "Import Dataset", command = dat_import)
button.pack()
mainloop()
Simply, all I need is a user to select choice from dropdown box, press import dataset and that dataset is saved as a dataframe for access later in the program.
Any help would be greatly appreciated, because I cannot find anywhere that will explain how to do this!
You should not return anything from a button callback, as it is not accessible. Instead globalize it so you can use it in a later function or anywhere else:
def dat_import():
global data
value = variable.get()
if value == datasets[0]:
data = pd.DataFrame(datasets.load_iris().data)
elif value == datasets[1]:
data = pd.DataFrame(datasets.load_diabetes().data)
elif value == datasets[2]:
data = pd.DataFrame(datasets.load_digits().data)
else:
data = pd.DataFrame(datasets.load_boston().data)
A better way to reduce the code might be to use dictionary dispatching here, so something like:
datasets = ["dataset 1", "dataset 2", "dataset 3", "dataset 4"]
dis = {datasets[0] : pd.DataFrame(datasets.load_iris().data,
datasets[1] : pd.DataFrame(datasets.load_diabetes().data,
datasets[2] : pd.DataFrame(datasets.load_digits().data,
datasets[3] : pd.DataFrame(datasets.load_boston().data,
}
def dat_import():
global data
data = dis[variable.get()]
I'm creating a counter to count how many empty cells there are when a user uploads a CSV file. I am also using treeview to display the contents of the CSV. The print("There are", emptyCells.sum(), "empty cells") works and prints the number to the console but I want to display this in a label so the user can view this in the GUI. It is not displaying anything but a "row" is being added to the application after a file has been uploaded where the label should be as everything moves down but no contents are being inserted into the label.
emptyCells = (df[df.columns] == " ").sum()
# print("There are", emptyCells.sum(), "empty cells")
tree.pack(side=BOTTOM, pady=50)
messagebox.showinfo("Success", "File Uploaded Successfully")
stringVariable = StringVar()
printVariable = ("There are", emptyCells.sum(), "empty cells")
#print(printVariable)
stringVariable.set(printVariable)
lbl = Label(windowFrame, textvariable=stringVariable, font=25)
lbl.pack()
According to your question you want to update your tkinter label by a button click. You would do this with something like this:
from tkinter import *
from tkinter import messagebox
root = Tk(className="button_click_label")
root.geometry("200x200")
messagebox.showinfo("Success","Test")
emptyCells = (df[df.columns] == " ").sum()
l1 = Label(root, text="Emptycells?")
def clickevent():
txt = "there are", emptyCells
l1.config(text=txt)
b1 = Button(root, text="clickhere", command=clickevent).pack()
l1.pack()
root.mainloop()
It is not tested with the pandas library but should work for you!
The problem with the tkinter label is not happening when I try to reproduce the problem, the label shows. The cause must be somewhere else in the code.
I've not got pandas installed so I've summed a list instead. This shows a GUI with two labels when I run it.
import tkinter as tk
emptyCells = [ 1, 1, 1, 1, 1, 1, 1 ] # keep it simple.
windowFrame = tk.Tk()
old = tk.StringVar()
stringVariable = tk.StringVar()
old_print = ("There are", sum(emptyCells), "empty cells") # Returns a tuple
printVariable = "There are {} empty cells".format( sum(emptyCells) ) # Returns a string.
old.set( old_print )
stringVariable.set(printVariable)
lbl_old = tk.Label( windowFrame, textvariable = old )
lbl_old.pack()
lbl = tk.Label(windowFrame, textvariable=stringVariable, font=25)
lbl.pack()
windowFrame.mainloop()
Does this work when you run it? Does it help identify where the problem is in the code which doesn't show the labels?
Don't you have the sum you need already in the emptyCells variable? Why do you need to use the .sum() function again in the print statement?
printVariable = f"There are {emptyCells} empty cells"
1 I chosed values from OptionMenu
2 clicked button
But callback function parse printed default values of OptionMenu.
What I did wrong?
My code
from tkinter import *
from functools import partial
def parse(shop, city):
print(f"Parse {city} {shop}") # prints "all shops" but i expected "magnit" because i chosed it
master = Tk()
variable_shop = StringVar(master)
variable_shop.set("all shops") # default value
w = OptionMenu(master, variable_shop, "all shops", "5ka", "magnit", "three").pack()
variable_city = StringVar(master)
variable_city.set("all cities") # default value
w2 = OptionMenu(master, variable_city, "all cities", "Moscow", "Saint Petersburg").pack()
callback = partial(parse, variable_shop.get(), variable_city.get())
b1 = Button(text="Run with values from OptionMenu", command=callback).pack()
mainloop()
Your callback line runs when the program boots, so it saves the values at boot. You need to move those get() calls someplace that gets run later, when the user clicks the button. Putting them in the parse function makes sense, and also removes the need for partial.
from tkinter import *
def parse():
city = variable_city.get()
shop = variable_shop.get()
print(f"Parse {city} {shop}")
master = Tk()
variable_shop = StringVar(master)
variable_shop.set("all shops") # default value
OptionMenu(master, variable_shop, "all shops", "5ka", "magnit", "three").pack()
variable_city = StringVar(master)
variable_city.set("all cities") # default value
OptionMenu(master, variable_city, "all cities", "Moscow", "Saint Petersburg").pack()
Button(text="Run with values from OptionMenu", command=parse).pack()
mainloop()
Consider this line of code:
callback = partial(parse, variable_shop.get(), variable_city.get())
It is functionally identical to this:
shop = variable_shop.get()
city = variable_city.get()
callback = partial(parse, shop, city)
In other words, you're calling the get methods about a millisecond after creating it, rather than waiting until the user has clicked the button.
You don't need to use partial at all. Just have the callback call a regular function, and have the function retrieve the values. This will make the code easier to write, easier to understand, and easier to debug.
def parse():
shop = variable_shop.get()
city = variable_city.get()
print(f"Parse {city} {shop}")
...
b1 = Button(text="Run with values from OptionMenu", command=parse)
b1.pack()
How can I append the priority of this application to the file as seen in the code below?
#Option boxes for assigning a priority level to the request
priority = StringVar()
Radiobutton(self.f4, text = "High", variable=priority, value="1").grid(row = 4, column = 1, sticky = W)
Radiobutton(self.f4, text = "Low", variable=priority, value="2").grid(row = 6, column = 1, sticky = W)
#Button for "Confirm application"
self.app_con = Button(self.f4, text=" Confirm and finish ", command=self.apphistory)
self.app_con.grid(row=4, column=2
def apphistory(self):
fo = open("Application_History.txt", "a")
fo.writelines(["Priotiry: "+str(priority.get()),"\n"])
fo.close
Have you tried changing
variable=priority
to
variable=self.priority
This way, the Radiobutton knows where to search for the variable (in your program / class) instead of looking for it in the Radiobutton-Class itself.
And as mentioned in the comments, you need to create the variable
self.priority
Your scope is obviously wrong.
Try something like this:
self.app_con = Button(self.f4, text=" Confirm and finish ", command=lambda:self.apphistory(priority))
...
def apphistory(self, priority):
fo = open("Application_History.txt", "a")
fo.writelines(["Priotiry: "+str(priority.get()),"\n"])
fo.close()