I have this GUI application that I have been obsessing about for over a week now and I think it is slightly harder than what I can manage with the limited programming/python knowledge I have gained so far - however, I just can't stop thinking about it and it is driving me insane that I can't figure it out.
I have created a GUI with Tkinter which is a Todo-list application. However, every task in the application must have some information associated with it.
So let's say we create a task called "Homework".
I want to associate some attributes with Homework.
So, among others, some attributes would be "Impact" which represents the impact of completing the task on a scale from 0-10 (e.g. 10: I will fail my class if I don't complete my Homework task.), and deadline - which is self-explanatory. Those are just 2 of the attributes I want (there will be more though).
So as far as I have come to understand, this would be a great time to utilize classes.
If I understand it correctly, I would have to create a class Task:
and then set the attributes for every instance of Task
to something.
I created the GUI before I became familiar with classes and some of the attribute information I want to associate with a given task can already be specified in the GUI (without any functionality) after creating a task, but I don't know how to use that information and connect it to the given task so that it has the functionality that I want.
I want to be able to enter a task, specify some attributes associated with that task, and then I want to be able to 'summon' that task and its' associated attributes for some simple math and/or sorting. I only want the user to see that task itself - the calculations/sorting will just happen behind the scenes.
My question: Should I do this with classes or is there a more beginner-level approach to this which would be easier to implement into my already existing code? I really, really want to figure out how to go about this so any qualified pointers or in the right direction or examples, explanations, etc. will be truly appreciated.
This is my code:
from tkcalendar import *
import tkinter.messagebox # Import the messagebox module
import pickle # Module to save to .dat
import tkinter as tk
TASKS_FILE = "tasks.dat" ##Where the task strings are saved
task_list = [] ##Don't know what I am doing yet
impact_number = [] ##Don't know if this could be useful
root = tk.Tk() # ?????
root.title('SmaToDo') # Name of the program/window
# ROOT WINDOW
# Gets the requested values of the height and widht.
windowWidth = root.winfo_reqwidth()
windowHeight = root.winfo_reqheight()
# Gets both half the screen width/height and window width/height
positionRight = int(root.winfo_screenwidth()/2 - windowWidth/2)
positionDown = int(root.winfo_screenheight()/2 - windowHeight/2)
# Positions the window in the center of the page.
root.geometry("+{}+{}".format(positionRight, positionDown))
def new_task():
def add_task():
global task
global impact_window
task = entry_task.get() # we get the task from entry_task and we get the input from the entry_task type-field with .get()
if task != '': # If textbox inputfield is NOT empty do this:
listbox_tasks.insert(tkinter.END, task)
entry_task.delete(0, tkinter.END) #
task_window.destroy()
else:
tkinter.messagebox.showwarning(title='Whoops', message='You must enter a task')
task_window.destroy()
task_window = tk.Toplevel(root)
task_window.title('Add a new task')
task_label = tk.Label(task_window, text = 'Title your task concisely:', justify='center')
task_label.pack()
# Entry for tasks in new window
entry_task = tk.Entry(task_window, width=50, justify='center')
entry_task.pack()
# Add task button in new window
button_add_task = tk.Button(task_window, text='Add task', width=42, command=lambda: [add_task(), impact()])
button_add_task.pack()
def impact():
global impact_next_button
global impact_window
global impact_label
global impact_drop
global options
global select
impact_window = Toplevel(root)
impact_window.title('Impact')
options = StringVar()
options.trace_add
task_label = tk.Label(impact_window, text=str(task), font='bold')
task_label.pack()
impact_label = tk.Label(impact_window, text = 'Specify below the impact of completing this task \n (10 being the highest possible impact)', justify='center')
impact_label.pack()
impact_drop = OptionMenu(impact_window, options, '0','1','2','3','4','5','6','7','8','9','10')
impact_drop.pack()
impact_next_button = tk.Button(impact_window, text='Next', command=connectivity)
impact_next_button.pack(side=tkinter.RIGHT)
def select():
global impact_next_button
global impact_window
global impact_label
global impact_drop
global options
impact_number.append(str(task) + options.trace_add('write', lambda *args: print(str(task)+' '+ 'impact' + ' ' + options.get())))
options.set(options.get())
def connectivity():
impact_window.destroy()
clicked = StringVar()
clicked.set('Select')
global connectivity_next_button
global connectivity_window
global connectivity_label
global connectivity_drop
connectivity_window = Toplevel(root)
connectivity_window.title('Connectivity')
task_label = tk.Label(connectivity_window, text=str(task), font='bold')
task_label.pack()
connectivity_label = tk.Label(connectivity_window, text = 'Specify below the connectivity this task has to other tasks\n (e.g. tasks you can not complete unless you have completed this task)', justify='center')
connectivity_label.pack()
# var1 = IntVar()
Checkbutton(connectivity_window, text="Each task from list must be 'checkable' in this window").pack() # variable=var1).pack()
connectivity_next_button = tk.Button(connectivity_window, text='Next', command=lambda: [Proximity(), select()])
connectivity_next_button.pack(side=tkinter.RIGHT)
def Proximity():
connectivity_window.destroy()
global proximity_button
global proximity_window
global proximity_label
global proximity_drop
global cal
global proximity_output_date
proximity_window = Toplevel(root)
proximity_window.title('Proxmity')
task_label = tk.Label(proximity_window, text=str(task), font='bold')
task_label.pack()
proximity_label = tk.Label(proximity_window, text = 'Specify a deadline for when this task must be completed', justify='center')
proximity_label.pack()
cal = Calendar(proximity_window, selectmode='day', year=2021, month=4, day=27)
cal.pack()
def get_date():
proximity_output_date.config(text=cal.get_date())
proximity_date_button = tk.Button(proximity_window, text='Pick date', command=get_date)
proximity_date_button.pack()
proximity_output_date = tk.Label(proximity_window, text='')
proximity_output_date.pack()
proximity_button = tk.Button(proximity_window, text='Next', command=manageability)
proximity_button.pack(side=tkinter.RIGHT)
def manageability():
print('Deadline:'+' '+cal.get_date())
proximity_window.destroy()
clicked = StringVar()
clicked.set('Select')
global manageability_next_button
global manageability_window
global manageability_label
global manageability_drop
manageability_window = Toplevel(root)
manageability_window.title('Manageability')
task_label = tk.Label(manageability_window, text=str(task), font='bold')
task_label.pack()
manageability_label = tk.Label(manageability_window, text = 'Specify how difficult this task is to complete \n (0 being extremely difficult and 10 being extremely easy)', justify='center')
manageability_label.pack()
manageability_drop = OptionMenu(manageability_window, clicked, '0','1','2','3','4','5','6','7','8','9','10')
manageability_drop.pack()
manageability_next_button = tk.Button(manageability_window, text='Next', command=urgency)
manageability_next_button.pack(side=tkinter.RIGHT)
def urgency():
pass
def delete_task():
try:
task_index = listbox_tasks.curselection()[0]
listbox_tasks.delete(task_index)
except:
tkinter.messagebox.showwarning(title='Oops', message='You must select a task to delete')
def save_tasks():
tasks = listbox_tasks.get(0, listbox_tasks.size())
pickle.dump(tasks, open('tasks.dat', 'wb'))
# Create UI
your_tasks_label = tk.Label(root, text='THESE ARE YOUR TASKS:', font=('Roboto',10, 'bold'), justify='center')
your_tasks_label.pack()
frame_tasks = tkinter.Frame(root)
frame_tasks.pack()
scrollbar_tasks = tkinter.Scrollbar(frame_tasks)
scrollbar_tasks.pack(side=tkinter.RIGHT, fill=tkinter.Y)
listbox_tasks = tkinter.Listbox(frame_tasks, height=10, width=50, font=('Roboto',10), justify='center') # tkinter.Listbox(where it should go, height=x, width=xx)
listbox_tasks.pack()
listbox_tasks.config(yscrollcommand=scrollbar_tasks.set)
scrollbar_tasks.config(command=listbox_tasks.yview)
try:
tasks = pickle.load(open('tasks.dat', 'rb'))
listbox_tasks.delete(0, tkinter.END)
for task in tasks:
listbox_tasks.insert(tkinter.END, task)
except:
tkinter.messagebox.showwarning(title='Phew', message='You have no tasks')
# Add task button
button_new_task = tkinter.Button(root, text='New task', width=42, command=new_task)
button_new_task.pack()
button_delete_task = tkinter.Button(root, text='Delete task', width=42, command=delete_task)
button_delete_task.pack()
button_save_tasks = tkinter.Button(root, text='Save tasks', width=42, command=save_tasks)
button_save_tasks.pack()
root.mainloop() # Runs the program - must be at the very buttom of the code
Some people on here have already helped me out a tremendous amount which I truly appreciate.
So I made some sample code:
from tkinter import Tk, Frame, Button, Entry, Label, Canvas, Scrollbar, OptionMenu, Toplevel, StringVar
import pickle
class Task:
def __init__(self, name, type_, importance):
self.name = name
self.type = type_
self.importance = importance
try:
with open('saved_tasks.dat', 'rb') as file:
task_list = pickle.load(file)
except FileNotFoundError and EOFError:
with open('saved_tasks.dat', 'w') as file:
pass
print('Could not locate the file or the file was empty. A new file was created.')
task_list = []
task_types = ['Misc', 'Math', 'Science', 'Economics', 'Biology', 'Homework']
def show_tasks():
for widget in task_frame.winfo_children():
widget.destroy()
for task in task_list:
Label(task_frame, text=f'{task.name} | Type: {task.type} | Importance: {task.importance}').pack(fill='x')
def open_add_task():
win = Toplevel(root)
win.focus_force()
Label(win, text='Write task name').grid(column=0, row=0)
Label(win, text='Choose type of task').grid(column=1, row=0)
Label(win, text='Choose importance of task').grid(column=2, row=0)
entry = Entry(win)
entry.grid(column=0, row=1, sticky='ew')
type_var = StringVar(value=task_types[0])
OptionMenu(win, type_var, *task_types).grid(column=1, row=1, sticky='nsew')
imp_var = StringVar(value=1)
OptionMenu(win, imp_var, *range(1, 10+1)).grid(column=2, row=1, sticky='nsew')
def add_task():
task_list.append(Task(entry.get(), type_var.get(), imp_var.get()))
show_tasks()
Button(win, text='Add Task', command=add_task).grid(column=0, row=2, columnspan=3, sticky='nsew')
def sort_tasks():
type_ = sort_type.get()
importance = sort_imp.get()
order = asc_desc_var.get()
for widget in task_frame.winfo_children():
widget.destroy()
for task in task_list if order == 'Ascending' else task_list[::-1]:
if type_ == 'All' and importance == 'Any':
Label(task_frame, text=f'{task.name} | Type: {task.type} | Importance: {task.importance}').pack(fill='x')
elif type_ == 'All':
if importance == task.importance:
Label(task_frame, text=f'{task.name} | Type: {task.type} | Importance: {task.importance}').pack(fill='x')
elif type_ != 'All':
if type_ == task.type and importance == 'Any':
Label(task_frame, text=f'{task.name} | Type: {task.type} | Importance: {task.importance}').pack(fill='x')
elif type_ == task.type and importance == task.importance:
Label(task_frame, text=f'{task.name} | Type: {task.type} | Importance: {task.importance}').pack(fill='x')
root = Tk()
btn_frame = Frame(root)
btn_frame.pack(fill='x')
sort_type = StringVar(value='All')
OptionMenu(btn_frame, sort_type, 'All', *task_types).grid(column=0, row=0, sticky='nsew')
sort_imp = StringVar(value='Any')
OptionMenu(btn_frame, sort_imp,'Any', *range(1, 10+1)).grid(column=1, row=0, sticky='nsew')
asc_desc_var = StringVar(value='Ascending')
OptionMenu(btn_frame, asc_desc_var, 'Ascending', 'Descending').grid(column=2, row=0, sticky='nsew')
Button(btn_frame, text='Sort', command=sort_tasks).grid(column=0, row=1, columnspan=3, sticky='nsew')
Button(btn_frame, text='Add New', command=open_add_task).grid(column=3, row=0, rowspan=2, sticky='nsew')
task_frame_main = Frame(root)
task_frame_main.pack()
task_frame = Frame()
canvas = Canvas(task_frame_main)
canvas.pack(side='left', expand=True, fill='both')
canvas.create_window((0, 0), window=task_frame, tag='task_frame', anchor='nw')
scrollbar = Scrollbar(task_frame_main, orient='vertical', command=canvas.yview)
scrollbar.pack(side='right', fill='y')
canvas.config(yscrollcommand=scrollbar.set)
canvas.bind('<Configure>', lambda e: canvas.config(scrollregion=canvas.bbox('task_frame')))
show_tasks()
root.mainloop()
try:
with open('saved_tasks.dat', 'wb') as file:
pickle.dump(task_list, file)
print('File saved.')
except Exception as e:
print(f'Exception was raised:\n{e}')
The main part here is the Task
class, as You can see it is pretty small and that was the whole point, it doesn't have to be anything large. And the task_list
can be easily serialized using pickle
and easily loadable and it will keep those classes with their given attributes.
If You have any other questions, ask.
Explaining everything (or most at least):
First import everything You need and I like to do it like this when working with tkinter.
Then I defined task_list
which will be used to store added tasks, and this can and probably should be replace by a file so You would read a file and then append the saved tasks to this list or just use json or pickle to import the file as a list which makes it easier. Anyways, then I just created a list to store all the types of tasks, it can be modified and again this is not as necessary but the list can be loaded from a file for example if there is an option to add another type (which requires quite some coding) it would be great to save them to a file so the info is saved.
Now about class Task
:
this class doesn't have a lot of attributes, in fact just three to store basic stuff like name
of the task, type
of the task and how important it is.
There is not much to this but to mention it is possible to use for example pickle
module and apply it to the task_list
(which will store instances of Task
) and it will serialize this object while keeping its attributes so when loading from pickle
it will still be possible to access the object the same way.
Now moving on to the show_tasks()
function.
So this is a general function for just showing everything that is appended to the task_list
a.k.a. all tasks. This function puts Label
s on the frame specified below but first it deletes everything off that frame because otherwise it will append all the items each time which means that it will grow exponentaly (You can test if You want by removing the first loop and adding a few tasks).
Next is open_add_task()
function which is just for adding tasks.
It starts by creating another window Toplevel
and puts focus on it. (sometimes I like to bind
such windows so that when user clicks out of the window and this window loses focus close it so that there are not multiple such windows laying around but that is up to You).
Then I created a few labels that help users indicate what is asked from them.
Then I placed entry so that user can type in the task name and then so to avoid user typing error I gave them a choice to choose which category and importance they want so that they don't mistype something.
Then I defined a function (add_task()
) for collecting data from user inputs, creating the class instance and appending it to the task_list
. And it finishes with creating a Button that calls this function.
Onto sort_tasks
.
This function is linked with widgets on root so there is the reference. so first get what the user has inputed and store in variables for easier reuse. Again user input is made easier by just giving choices. Again all widgets are cleared and then comes the sorting/logical part which probably can be improved and I didn't add the option to sort by importance number (so how high is it) so it sorts by time added (again there is no exact time but just order in the list is the time added). The logic could be improved but it mostly does what it is supposed to do (it was late and I couldn't properly think) but yeah there is not much to explain just step through it yourself.
Next bit is the main window which is not inside any functions as to not use global
. first I initiated Tk()
which is the basic and at the end there is .mainloop()
used - the basic stuff. Then there is a lot of frames used to help organize the widgets and then all the widgets are added in this case I'm talking about the widgets that contribute to sort_tasks()
function. So added all the choice menus and the button to execute sort_tasks
. And a button to call open_add_task()
.
Next bit is the sort of main frame where all the tasks are shown. There is canvas which is important, because a Frame
cannot be scrolled itself, whereas canvas can be scrolled so essentially what happens, is that a frame gets added to the canvas and the scrollregion
is set to the Frame
size. Then added the scrollbar (will add link to this).
And that is pretty much it.
EDIT:
Added functionality for saving and reading file so that tasks can be saved and loaded. For some reason the file has to be loaded after Task
because apparently otherwise it throws an error which I don't completely understand but I dealt with it by moving class above the whole thing. I also found an issue where when new tasks are added and they exceed the visible frame limit, scrollbar does not work, but then again opening the file again it works fine, so that could be dealt with. Also as You can see I added save function after mainloop()
so that whenever user closes the window it saves tasks to file. (added to sources)
Sources: