python-3.xlisttkinterpickletkinter.checkbutton

2 issues: Python Pickle .dat append to list and tkinter checkbutton issue


I've had a lot of issues with this application because I am simply not good enough yet, but I am almost done with it and just want to finish it so I can move on to some slightly lower level projects.

It is a tkinter to-do application.

You can add a Task to a listbox

For every Task, there are some associated attributes, among others: ````self.value = vandself.connectivity = c. The hierarchy of the tasks displayed in the listbox is determined by the value of val_var``` (e.g. the higher the value the higher on the list it will be displayed).

The Task and the associated attributes are determined by the user's input when one creates another task.

The Task is appended to a list task_list and after the user has added more than 1 task to the list, the next time one adds a task one will have the option to check existing tasks that it is connected with somehow.

The list is sorted so the task with the highest value (val_var) is displayed at the top of the Listbox and the task with the lowest value is displayed at the bottom of the Listbox. You can "Save tasks" and then launch the application at a later time where you can then "Load tasks".

Issue 1:

After loading tasks from a saved .dat file, it displays in the Listbox in the order it was saved in. However, if you now want to add another task at least two undesirable things happen:

  1. The tasks now loaded into the Listbox are now not displayed as checkbuttons upon adding a new task.
  2. When you add another task (again this is after loading the .dat file) the Listbox will delete what was just loaded and the Listbox will only display the newly added task.

I am somehow interested in being able to load the Tasks instances from the .dat file and then append them to the task_list so they are a part of the current session/instance of the application, but I don't know how one might do that.

Issue 2:

On a given session where tasks have been added to the Listbox, they can be deleted from the listbox using the "Delete task" button. The selected task in the Listbox is deleted, but it is not the same task that is deleted from the task_list.

To test what I mean by this one can add a couple of tasks to the Listbox and then delete one after doing so. Notice upon trying to create yet another new task that the one just deleted from the Listbox will still be shown as a checkbutton - however, another task that wasn't just deleted has now vanished as a checkbutton.

Any help with these issues will be sincerely appreciated.

Here's the code:

from tkinter import Tk, Frame, Button, Entry, Label, OptionMenu, Toplevel, StringVar, Checkbutton, DoubleVar
import tkinter.messagebox 
import pickle

root = Tk()

task_list = []

class Task:
    def __init__(self, n, i, h, v, c): 
        self.name = n
        self.impact = i
        self.hours = h
        self.value = v
        self.connectivity = c

def open_add_task():
    taskwin = Toplevel(root)

    #NAME
    titlelabel = Label(taskwin, text='Title task concisely:').grid(column=1, row=0)
    name_entry = Entry(taskwin, width=40, justify='center')
    name_entry.grid(column=1, row=1)

    #IMPACT
    impactlabel = Label(taskwin, text='Impact').grid(column=1, row=2)
    imp_var = StringVar(value=0)
    OptionMenu(taskwin, imp_var, *range(0, 10+1)).grid(column=1, row=3, sticky='ns')

    #HOURS(required)
    hourlabel = Label(taskwin, text='Whole hours \n required').grid(column=1, row=16)
    hour_entry = Entry(taskwin, width=4, justify='center')
    hour_entry.grid(column=1, row=17)

    #CONNECTIVITY
    C_lab = Label(taskwin,text="Connectivity to other tasks").grid(column=1, row=18)
    placement=19
    vars = [] # list to hold the DoubleVar used by Checkbutton
    for task in task_list:
        # add a DoubleVar to the list
        vars.append(DoubleVar())
        # use the task.value as the "onvalue" option
        Checkbutton(taskwin, text=task.name, variable=vars[-1], onvalue=task.value, offvalue=0).grid(column=1, row=placement, sticky="w")
        placement+=1

    def add_task():
        if name_entry.get() != '': # If textbox inputfield is NOT empty do this

            #CONNECTIVITY
            connectivity = sum(v.get() for v in vars)/10 +1 #if no connectivity the rest below is multiplied by 1
            #VALUE
            val_var = ((((int(imp_var.get())/5) + 1) * (connectivity)+(float(hour_entry.get())/10))) #-(float(hour_entry.get())/20) #-hours fra højere rangerende opgaver skal minusses urgency_var # c = 1+task1(int(imp_var.get())+(int(man_var))+task2(repeat)+task3(repeat)


            task_list.append(Task(name_entry.get(), imp_var.get(), hour_entry.get(), val_var, connectivity))
            reload()
            taskwin.destroy()
        else:
            tkinter.messagebox.showwarning(title='Whoops', message='You must enter a task')

    Add_button = Button(taskwin, text='Add', command=add_task).grid(column=1, row=placement, sticky="ew")
    placement+=1

def reload():
    task_list.sort(key=lambda a: a.value, reverse=True)
    listbox_tasks.delete(0, tkinter.END)

    for x in task_list:
        listbox_tasks.insert(tkinter.END, x.name)

def delete_task():
    try:
        task_index = listbox_tasks.curselection()[0]
        listbox_tasks.delete(task_index)
        tasks = listbox_tasks.get(0, listbox_tasks.size())
        pickle.dump(tasks, open('Todo.dat', 'wb'))
        del task_list[0]
    except:
        tkinter.messagebox.showwarning(title='Error', message='You must select a task to delete')

def save_tasks():
    tasks = listbox_tasks.get(0, listbox_tasks.size())
    pickle.dump(tasks, open('Todo.dat', 'wb'))

def load_tasks():
    try:
        tasks = pickle.load(open('Todo.dat', 'rb'))
        listbox_tasks.delete(0, tkinter.END)
        for task in tasks:
            listbox_tasks.insert(tkinter.END, task)
    except:
        tkinter.messagebox.showwarning(title='Error', message='You have no tasks')

# Create UI
your_tasks_label = Label(root, text='Your tasks:', font=('roboto',11, 'bold'), justify='center')
your_tasks_label.pack(pady=5)


scrollbar_tasks = tkinter.Scrollbar(root)
scrollbar_tasks.pack(side=tkinter.RIGHT, fill=tkinter.Y)

listbox_tasks = tkinter.Listbox(root, height=10, width=45, font=('', 11, 'bold'), fg=('grey'), justify='center') # tkinter.Listbox(where it should go, height=x, width=xx)
listbox_tasks.pack(padx=5, pady=5)

listbox_tasks.config(yscrollcommand=scrollbar_tasks.set)
scrollbar_tasks.config(command=listbox_tasks.yview)

#BUTTONS
New_Task_Button = Button(root, text='New Task', width=42, command=open_add_task)
New_Task_Button.pack()

button_delete_task = Button(root, text='Delete task', width=42, command=delete_task)
button_delete_task.pack()


button_save_tasks = Button(root, text='Save tasks', width=42, command=save_tasks)
button_save_tasks.pack()

button_load_tasks = Button(root, text='Load tasks', width=42, command=load_tasks)
button_load_tasks.pack(pady=5)

root.mainloop()

Solution

  • Your problem is fairly simple. You need to save the objects of the Task class instead of saving the strings present inside the Listbox.

    That said you should never give bare except clause like the one you did, always specify the exception you want to catch. You will find it hard to find the exact problem if you don't.

    For example In this block of your code:

    try:
         tasks = pickle.load(open('Todo.dat', 'rb'))
         listbox_tasks.delete(0, tkinter.END)
         for task in tasks:
             listbox_tasks.insert(tkinter.END, task)
    except:
         tkinter.messagebox.showwarning(title='Error', message='You have no tasks')
    

    The exception here occurs when the file is not found. But now what if there is an empty file and if everything goes well and no exceptions are raised, the message will not be displayed even if there is no task in the file. A more appropriate thing would be to check if there are any contents in the file and then show the message.

    I also often see you rewriting things. For example here:

    def delete_task():
        ...
        tasks = listbox_tasks.get(0, listbox_tasks.size())
        pickle.dump(tasks, open('Todo.dat', 'wb'))
        ...
    

    Do you really need to write the same thing that you have written under the save function?

    Here is your corrected code:

    ...
    task_list = []
    
    class Task:
        def __init__(self, n, i, h, v, c): 
            self.name = n
            self.impact = i
            self.hours = h
            self.value = v
            self.connectivity = c
    
    ...
    
    def reload():
        task_list.sort(key=lambda a: a.value, reverse=True)
        listbox_tasks.delete(0, tkinter.END)
    
        for x in task_list:
            listbox_tasks.insert(tkinter.END, x.name)
    
    
    def delete_task():
        try:
            task_index = listbox_tasks.curselection()[0]
            listbox_tasks.delete(task_index)
            task_list.pop(task_index)
            save_tasks()
            
        except IndexError:
            tkinter.messagebox.showwarning(title='Error', message='You must select a task to delete')
    
    
    def save_tasks():
    
        with open('Todo.dat', 'wb') as pk:
            pickle.dump(task_list, pk)
    
    
    def load_tasks():
        global task_list
        try:
            with open('Todo.dat', 'rb') as pk:
                task_list = list(pickle.load(pk))
    
            reload()
                
        except Exception as e:  # FileNotFound Error
            print(e)
            tkinter.messagebox.showwarning(title='Error', message='You have no tasks')
    
    # Create UI
    your_tasks_label = Label(root, text='Your tasks:', font=('roboto',11, 'bold'), justify='center')
    your_tasks_label.pack(pady=5)
    
    ...