Saving an instance of tkinter GUI - python

I am working on a GUI with an "Add entry box" button that adds an entry box when clicked. I also want to add a "Save for next session" button that, when clicked, will load the current entries when the user opens the GUI next time. The problem is that sometimes it will be 5 fields to enter, other times 305. How can I save the number of input fields and their values? I don't want to use the pickle library.
import customtkinter
import threading
import tkinter
class MyGui:
def __init__(self, app):
self.app = app
self.entry_list = []
self.entry_1 = customtkinter.CTkEntry(master=self.app, placeholder_text="entry")
self.entry_1.pack(pady=10, padx=10)
self.add_entry_button = customtkinter.CTkButton(master=self.app, text="Add entry", command=self.add_entry)
self.add_entry_button.pack(pady=10, padx=10)
self.save_button = customtkinter.CTkButton(master=self.app, text="Save for next session", command=self.save_instance)
self.save_button.pack(pady=10, padx=10)
def add_entry(self):
self.entry_list.append(self.entry_1)
self.entry_1 = customtkinter.CTkEntry(master=self.app, placeholder_text="entry")
self.entry_1.pack(pady=10, padx=10)
def save_instance(self):
pass
app = customtkinter.CTk()
running = MyGui(app)
app.mainloop()
As you can see I have this self.entry_list with all entry field objects, but I do not know how to use it to load the GUI instance later.

You will need to use some sort of storage mechanism. You don't want to use pickle, which is one mechanism. You can also use a flat file or a database. In this case, since you're using entry widgets, a flat file with the content of each entry is sufficient. A sqlite database might arguably be better, but in this specific case it might be overkill.
Whatever you choose, it's important to know that you can't save tkinter objects to disk. The implementation of tkinter makes that essentially impossible. So, you must save the data from which you can restore the tkinter object rather than saving the object itself.
In your case you're saving entries to the list self.entry_list, so writing the file could just be a matter of iterating over the entries to get the values and write them to a file (assuming you want to preserve the values)
The following example hard-codes the filename for brevity, but in production code you shouldn't hard-code it.
def save_instance(self):
data = [entry.get() for entry in self.entry_list]
with open("entries.txt", "w") as f:
text = "\n".join(data)
f.write(text)
Restoring the entries is just a matter of reversing the process. First, though, we need to modify add_entry to take the default value. Also, your implementation oddly uses self.entry_1 which isn't necessary so I've removed it.
The following example assumes the data which was saved to the file is the default value of the entry widget. If instead, it's the name of the placeholder text you can easily make that modification.
def add_entry(self, text=""):
entry = customtkinter.CTkEntry(master=self.app, placeholder_text="entry")
self.entry_list.append(entry)
entry.pack(pady=10, padx=10)
entry.insert("end", text)
Next, create a method for reading the data and creating the entries. It's important to note that readlines includes the newline on each line, which we strip out.
def restore_entries(self):
with open("entries.txt", "r") as f:
data = f.readlines()
for line in data:
self.add_entry(line.strip())
This definitely isn't the only way to solve the problem. If you don't want to save the default values (or placeholder text?) you could just store the number of entries you want to keep. Or, like I mentioned earlier, you could use a database. With a database you could store the number of rows, and then only save the text for the entries that have text. Or, instead of a flat file you could save the data in a structured format such as json.

Related

Adding and updating new widget in tkinter in runtime

I am trying to build a GUI creator using Python and Tkinter, but ran into a problem.
My problem is How to add\update widgets in runtime?
for example:
I have created the main window.
In that main window, I have created a frame name w_frame which contains a bunch of widget.
Based on my input in the Text or Entry widget beside the w_frame, I want to update a particular widget.
Lets say w_frame contains a Entry widget, radio button, button and label all available with the basic or main attributes need to display it.
Now I want to change the background color of label.
In short I want to write the code label_name.property_name=value or for example a_label.bg=red in the text widget and as soon as I press apply button, the widget should change.
I have searched on web, but not able to find the required solution. Also tried using How can i update a certain widget in tkinter, but that does not work depending on my input.
from tkinter import *
root=Tk()
w_frame=Frame()
w_frame.pack()
def update_Frame():
a=u_text_wid.get("1.0",END)
b.config(a)
root.update()
def add_wid_in_frame():
global a,b
a=Button(w_frame,text='heelo')
a.pack()
b=Label(w_frame,text='heelo')
b.pack()
u_text_wid=Text()
u_text_wid.pack()
button1=Button(text="add",command=add_wid_in_frame)
button1.pack()
button1=Button(text="update",command=update_Frame)
button1.pack()
root.mainloop()
this results me in an error
unknown option "-bg="red"
Note:
I want to update the widget based on the property value provided by the user, so it wont be hard-code into the script.
You are getting the error because every thing you retrieve from Text widget is a string and you cannot directly pass an string to .config method, you need a keyword and then you can assign value which can be string.
According to your question and the comments on the question, what i have figured out is:
You want to run lable.config(bg='red') from the Text widget.
You want to change the property of specific widget.
Here's what you can do:
To run Tkinter code form Text widget, you can use:
getattr method
eval method
Just to change property of widget:
def update_Frame():
global bcd
a = u_text_wid.get("1.0", "end-1c")
b=a.split(",")
c=[tuple(i.split("=")) if "=" in i else i for i in b]
d=dict(i for i in c)
for key,value in d.items():
bcd[key]=value
We can use string to change property only in this format widget_name[key]=value.
Some Useful Links:
Eval()
Getattr()
For your case, you can use ast.literal_eval() to convert a JSON string to dictionary and use the dictionary in .config():
from ast import literal_eval
...
def update_Frame():
a = u_text_wid.get("1.0", "end-1c") # don't include ending newline
cnf = literal_eval(a) # convert JSON string to dictionary
b.config(cnf)
Example input of the JSON string:
{"fg":"yellow", "bg":"red"}
Note that you can also use json module to convert the JSON string as well.

How can I prompt user for multiple inputs, then print summary to a file?

So far I'm able to print at the end if the user selects 'n' to not order another hard drive, but need to write to a file. I've tried running the code as 'python hdorders.py >> orders.txt', but it won't prompt for the questions; only shows a blank line and if I break out using Ctrl-C, it writes blank entries and while loops in the file. I hope this makes sense.
ui = raw_input("Would you like to order more hard drives?(y/n) ")
if ui == 'n':
print '\n','\n',"**** Order Summary ****",'\n',row,'\n',"Number of HD's:",b,'\n',"Disk Slot Position(s):",c,'\n',"Disk Size(s):",d,"GB",'\n',"Dimensions:",e,'\n','\n',
endFlag = True
I'd also like it so that if they select 'y', it will save to a file and start over for another disk order (saving the previous info to the file first). Then once they are done (for example going through the program twice) and select 'n', it will have the final details appended to the same file as the first order.
I've found that when extensive user input is desired, a GUI may be the best option. I only try to do command line input if my script uses a small amount of user inputs that I can argparse out. Personally, I would make a tkinter combobox for each of these inputs and have a button at the bottom of the GUI that processes all the inputs and writes them to a file. Here is a skeleton of how I make a GUI
import tkinter as tk
class OOP:
def __init__(self):
self.win = tk.Tk()
self.win.title("My Title")
self.user_input = tk.StringVar()
self.create_widgets()
def lookup_csv_file(self):
file = self.user_input.get()
print(file)
def create_widgets(self):
tk.Button(self.win, text="Lookup CSV file", width=42, command=self.lookup_csv_file).pack(expand=1, fill='both')
tk.Entry(self.win, textvariable=self.user_input).pack(expand=1, fill='both')
app = OOP()
app.win.mainloop()
This code shows several important things to note:
1) GUI's should be made with Object Oriented Programming (OOP) for most cases
2) The variables you want to keep should be initialized in the __init__ section as tk.StringVar(), tk.IntVar(), etc.. and then attached to GUI sections (as seen in create_widgets(self): section the entry's text variable is attached to our varible
3) To access the variable you use its .get() method as seen in lookup_csv_file section. As well the variable has a .set() method if you would like to put a value there. For instance you can do self.user_input = tk.StringVar() followed with self.user_input.set('Default CSV file') and the GUI will initialize with that shown.
4) When assigning commands to buttons, do not include the (). If instead of command=self.lookup_csv_file you put command=self.lookup_csv_file() the command will run during initialization.
These are some of the finer points that were hard for me to learn, but with this you should be able to quickly learn by looking at the documentation available!
I've tried running the code as 'python hdorders.py >> orders.txt', but it won't prompt for the questions
You don't see the prompts since you redirect the standard output, whereto also the prompts go, to the file orders.txt. Better open the file within your program, without redirection at the shell:
if ui == 'n':
orders = open('orders.txt', 'a') # 'a' for appending
print >>orders, '\n','\n',"**** Order Summary ****",'\n',row,'\n',"Number of HDs:",b,…
orders.close()
- run as python hdorders.py.

Tkinter- Open and Save the UI with all the widgets. Trouble while incrementing the rows

Here, is the piece of code that I have written. I have to make Open and Save Button functional. So, my both functions are working fine. I am able to Save and Load UI but the basic problem is that after loading, when I click on add rows, the rows doesn't added in below the already existed row. It has been a week working on it. I am in trouble and doesnt know the wayout
from tkinter import *
import dill
from collections import OrderedDict
class Program:
def __init__(self):
self.row=0
self.entries=[]
self.current_widget=0
self.Ordered_dictionary_for_entry_widget=OrderedDict()
self.values_for_entry_dictionary=[]
self.AddButton = Button(text="Add Row", command=self.add_button_command)
self.AddButton.grid(column=0,row=0)
self.save_button=Button(text="save",command=self.save_button_command)
self.save_button.grid(column=0,row=1)
self.load_button=Button(text="Open",command=self.Open_button_command)
self.load_button.grid(column=0,row=2)
self.total_entries_length=len(self.entries)
def add_button_command(self):
self.entry=Entry()
self.entry.grid(column=1,row=self.row)
self.entries.append(self.entry)
self.row=self.row+1
def save_button_command(self):
self.total_entries_length=len(self.entries)
print(self.total_entries_length)
for widget in self.entries:
self.Ordered_dictionary_for_entry_widget["Name"+str(self.current_widget+1)]=widget.get()
self.current_widget=self.current_widget+1
with open("example_fully_functional.txt","wb") as f:
dill.dump(self.Ordered_dictionary_for_entry_widget,f)
def Open_button_command(self):
print("Total entries length",self.total_entries_length)
with open("example_fully_functional.txt","rb") as f:
self.Ordered_dictionary_for_entry_widget=dill.load(f)
for key,values in self.Ordered_dictionary_for_entry_widget.items():
self.values_for_entry_dictionary.append((values))
print(self.values_for_entry_dictionary)
for i in (self.values_for_entry_dictionary):
self.entry=Entry()
self.entry.grid(column=1,row=i)
self.entries.append(self.entry)
print("Entry loaded",self.entries_loaded)
#Insert the entries back into the UI
[self.entries.insert(0,self.values_for_entry_dictionary) for
self.entries,self.values_for_entry_dictionary in
zip(self.entries,self.values_for_entry_dictionary)]
program = Program()
mainloop()
Ok to answer the direct question: self.row is not incremented in Open_button_command so it is inaccurate when add_button_command tries to add a new Entry,
for i in (self.values_for_entry_dictionary):
self.entry=Entry()
self.entry.grid(column=1,row=i)
self.entries.append(self.entry)
## THIS ONE HERE ##
self.row+=1
#####
I want to suggest a better solution then keeping track of the next column in a variable but before I can we need to fix up a few things, first in Open_button_command:
for i in (self.values_for_entry_dictionary):
self.entry=Entry()
self.entry.grid(column=1,row=i)
...
You are iterating over the values that need to be inserted into the entries not the indices, to get the indices to use in .grid you can use range(len(X)) instead:
for i in range(len(self.values_for_entry_dictionary)):
or better yet use enumerate to make the Entries and fill them at the same time:
for i,value in enumerate(self.values_for_entry_dictionary):
self.entry=Entry()
self.entry.grid(column=1,row=i)
self.entry.insert(0,value)
...
this way you don't need this:
[self.entries.insert(0,self.values_for_entry_dictionary) for
self.entries,self.values_for_entry_dictionary in
zip(self.entries,self.values_for_entry_dictionary)]
which overrides self.entries and self. self.values_for_entry_dictionary during the loop so a lot of information gets messed up during that, use enumerate instead.
Once that is cleaned up and self.entries will consistently be a list of all the Entry widgets in window the self.row should always be equal to len(self.entries) so it would be much preferable to use a property to calculate it every time:
class Program:
#property
def row(self):
return len(self.entries)
...
Then comment out any statement trying to set self.row=X since you don't have or need a setter for it. Every time you use self.row it will calculate it with the property and add_button_command will always add a new entry to the bottom.
However in Open_button_command you are still creating new widgets even if there is already an Entry in the window, it would make more sense to check if there is already one that can be reused:
def Open_button_command(self):
...
for i,value in enumerate(self.values_for_entry_dictionary):
if i<len(self.entries):
self.entry = self.entries[i]
self.entry.delete(0,"end") #remove any previous content
else:
self.entry=Entry()
self.entry.grid(column=1,row=i)
self.entries.append(self.entry)
# either way:
self.entry.insert(0,value)
Although this still breaks if you hit the open button twice since you do not reset self.values_for_entry_dictionary when opening a file, so the values in the file are added to any already open Entries so it would be a good idea to also reset it when opening a file:
def Open_button_command(self):
self.values_for_entry_dictionary=[]
...
If you wanted help with a working but buggy code you might consider submitting it for review and I'd be happy to provide other tips but I think this is sufficient to at least get the example code working as expected.

Python Tkinter - Multiple entry box with same validation

I have created around 5 Entry boxes and binded them as well. Take this as model:
def makeEntry(self, master, data):
self.entry = Entry(master, width=8, textvariable=data)
self.entry.bind("<Leave>", lambda event, value=data: self.validate(event, value))
Now, I did also a validate method that check if the input was a string (and if so, the highlight background of the entry would change to red). The problem which is still taking me a lot of time is that I would need that the method should be able to check every entries, and if at least one of them has got a red background, then a final button should be disabled (button.configure(state=DISABLED)).
With just one entry it would be much easier, I would simply check if the background was red (status = str(self.myOneEntry.cget("highlightbackground"))), but what about with more entries?
If you want to check all of your entries, keep them in a list. Then, write a function that iterates over the list and sets the button state to disabled if any widget has a red background. You can then call this whenever something changes, such as within your validation function for each widget.
Example:
class Example(...):
def __init__(...):
self.entries = []
for data in ("one","two","three"):
entry = makeEntry(...)
self.entries.append(entry)
def _update_button(self):
for entry in self.entries:
if entry.cget("background") == "red":
self.button.configure(state="disabled")
return
self.button.configure(state="normal")
Note: you'll need to make sure that makeEntry(...) returns a reference to the created widget.
Also, you don't have to use makeEntry. You can create your widgets however you want. The point is to save references to the widgets in a data structure that you can iterate over. It doesn't matter how the widgets are created.

Accessing Temp Directory On Tkinter

im new in this page and i love the answers nice job, with all the help of users, im new on python and wanna make a print of dir in a entry or label doesnt matter for example:
def directory(): os.listdir('/')
files=StringVar()
files.set(directory)
entry=Entry(root, textvariable=files).grid()
obviously in tkinter, the last code make a "print" a list of directories, but make me a list horizontal with this ',' for each folder different, i want it vertical list on this "entry" or "label", suppose later need a scroll bar but, there no is a problem, and make the same for temporal folder on windows look like that...
def directory(): os.listdir('%temp%')
files=StringVar()
files.set(directory)
entry=Entry(root, textvariable=files).grid()
but this %temp% doesnt works directly on python how can i make a listdir of folder?
Since displaying the contents of a directory is going to generally require multiple lines of text, one for each item in it, you have to use atk.Labelortk.Textwidget since a tk.Entrycan only handle a single line of text.
In addition you'll need to convert thelistthat os.listdir() returns into a multi-line string before setting itstextoption. Regardless, in order to be use yourdirectory() function needs to return a value to be useful.
The following code does these basic things and shows how to expand the value of the%temp%environment variable usingos.path.expandvars(). Alternatively, you can get the same value by using thetempfile.gettempdir()function Sukrit recommended.
import os
from Tkinter import *
def directory(folder):
return '\n'.join(os.listdir(folder)) # turn list into multiline string
class App:
def __init__(self, master):
frame = Frame(master)
frame.pack()
files = directory(os.path.expandvars('%temp%'))
self.label = Label(root, text=files)
self.label.pack(side=LEFT)
root = Tk()
app = App(root)
root.mainloop()

Categories

Resources