Python: Sorting based on class attribute - python-3.x

I am a rookie making a to-do list-GUI in Tkinter. Every task in the to-do list has a class attribute associated with it.
So a task is an object of Task: and it has a value attribute self.value.
I want to sort all the tasks displayed in the GUI based on the attribute self.value. So for instance the task with the highest value is displayed on top of the list and the task with the lowest value is displayed at the bottom of the list.
Note that it is not the value that must be displayed in the gui but the attribute self.name but it has to be displayed in an order based on the self.value.
I only understand conceptually what to do (i think). Somethng like, i need to make a list based on the self.value, then sort the "items" in the list from highest to lowest value, and then display the task.namein the gui based on the ordering of self.value.
Maybe I am retarded?
What would be the norm to do here?
EDIT
Code:
from tkinter import Tk, Frame, Button, Entry, Label, Toplevel
import tkinter.messagebox # Import the messagebox module
task_list = []
#value_list = []
class Task:
def __init__(self, n, h):
self.name = n
self.hours = h
def show_tasks():
task = task_list[-1]
print('\n')
print('_______________________')
print('\n')
print('Task:')
print(task.name)
print('\n')
print('Hours')
print(task.hours)
def open_add_task():
taskwin = Toplevel(root)
taskwin.focus_force()
#NAME
titlelabel = Label(taskwin, text='Title task concisely:', font=('Roboto',11,'bold')).grid(column=1, row=0)
name_entry = Entry(taskwin, width=40, justify='center')
name_entry.grid(column=1, row=1)
#HOURS
hourlabel = Label(taskwin, text='Whole hours \n required', font=('Roboto',10)).grid(column=1, row=2)
hour_entry = Entry(taskwin, width=4, justify='center')
hour_entry.grid(column=1, row=3)
def add_task():
if name_entry.get() != '':
task_list.append(Task(name_entry.get(), hour_entry.get()))
show_tasks()
listbox_tasks.insert(tkinter.END, name_entry.get())
name_entry.delete(0, tkinter.END)
taskwin.destroy()
else:
tkinter.messagebox.showwarning(title='Whoops', message='You must enter a task')
Add_button = Button(taskwin, text='Add', font=('Roboto',10), command=add_task).grid(column=1, row=4)
def sort_tasks():
pass
root = Tk()
task_frame = Frame()
# Create UI
your_tasks_label = Label(root, text='THESE ARE YOUR TASKS:', font=('Roboto',10, 'bold'), justify='center')
your_tasks_label.pack()
listbox_tasks = tkinter.Listbox(root, height=10, width=50, font=('Roboto',10), justify='center') # tkinter.Listbox(where it should go, height=x, width=xx)
listbox_tasks.pack()
#BUTTONS
New_Task_Button = Button(root, text='New Task', width=42, command=open_add_task)
New_Task_Button.pack()
root.mainloop()

There are multiple ways to do what you are trying to achieve.
The simplest way of all is to use the list.sort method and set reverse=True, which will sort in descending order. Assuming all your list values will be Task instances, In your case, you could do
todo_list.sort(key=lambda a: a.value, reverse=True)
Assuming your value attributes are integers. I made a small example.
names_values={"name1": 5, "name2": 10, "name3": 2}
todo_list = []
class Task:
def __init__(self, name: str, value: int):
self.name = name
self.value = value
for key, value in names_values.items():
task = Task(key, value)
todo_list.append(task)
print("Before sorting: ", todo_list)
print("\nBefore sorting values: ", [(x.name, x.value) for x in todo_list])
todo_list.sort(key=lambda a: a.value, reverse=True)
print("\nAfter sorting: ", todo_list)
print("\nAfter sorting values: ", [(x.name, x.value) for x in todo_list])
Now you have a sorted list. You can now, iterate through list and use object.name to insert it to your list view.
updating since you added MRE:
Since you also need to show the sorted list in the GUI. you need to reload your list view. One way would to clear your list view and insert it again is as shown below.
...
def add_task():
if name_entry.get() != '':
task_list.append(Task(name_entry.get(), int(hour_entry.get())))
show_tasks()
reload()
taskwin.destroy()
else:
tkinter.messagebox.showwarning(title='Whoops', message='You must enter a task')
Add_button = Button(taskwin, text='Add', font=('Roboto',10), command=add_task).grid(column=1, row=4)
def reload():
task_list.sort(key=lambda a: a.hours, reverse=True)
listbox_tasks.delete(0, tkinter.END)
for x in task_list:
listbox_tasks.insert(tkinter.END, x.name)
...

Related

Python/tkinter - defining checkbox looping over a string of labels

I want to define a row of tk checkbox widget looping on a list of labels. Everything works fine except that I can't seem to be able to set the IntVar value on the if statement, although it works for the state. What am I doing wrong?
def CheckboxLine(self, frame, fformat=None):
self.__set_col(0) # set widget column to zero
var_c = []
widget_c = []
if fformat is None:
fformat = ['text', 'excel', 'xml']
# widget definition
for item in fformat:
var_c.append(tk.IntVar(master=frame))
widget_c.append(tk.Checkbutton(master=frame, text=item, variable=var_c[-1]))
if item == 'text':
var_c[-1].set(1)
widget_c[-1]['state']='disabled'
else:
var_c[-1].set(0)
widget_c[-1]['state']='normal'
widget_c[-1].grid(row=self.__row, column=self.__col, columnspan=1, padx=self.__padx, pady=self.__pady, sticky="nsew")
self.__next_col()
self.__next_row()
Define var_c and widget_c outside of your function. The reason behind this is unclear to me, unfortunately.
import tkinter as tk
root = tk.Tk()
var_c = []
widget_c = []
def CheckboxLine(frame, fformat=None):
if fformat is None:
fformat = ['text', 'excel', 'xml']
# widget definition
for idx, item in enumerate(fformat):
var_c.append(tk.IntVar(value=0))
widget_c.append(tk.Checkbutton(master=frame, text=item, variable=var_c[-1]))
if item == "text":
var_c[-1].set(1)
widget_c[-1]["state"] = "disabled"
widget_c[-1].grid(row=idx, column=0)
CheckboxLine(root)
root.mainloop()

How to get the values of all the OptionMenu widgets within a frame inside a canvas in Tkinter

I'm writing a minimalist image tagging app that will list out all the image files in a specific location alongside a dropdown menu to select the options for tagging the image. Once the images are tagged, I need to save the changes to a JSON file and I've got a button for that. How can we read all the options selected so that it can be written into a file?
Following is the code so far:
from tkinter import N, RIGHT, Button, OptionMenu, Scrollbar, StringVar, Tk, Canvas, Frame, Label
class App:
def __init__(self):
self.root = Tk()
self.tags = ['Apples', 'Oranges', 'Berries']
self.GetRows()
self.SaveButton()
self.root.mainloop()
def GetRows(self):
self.canvas = Canvas(self.root)
self.scroll_y = Scrollbar(self.root, orient="vertical", command=self.canvas.yview)
self.frame = Frame(self.canvas)
lst = [f"A01{str(i)}.JPG" for i in range(100)]
for idx, r in enumerate(lst):
filename = Label(self.frame, text=r)
filename.grid(row=idx+2, column=0, sticky=N)
label = StringVar()
drop = OptionMenu(self.frame, label, *self.tags)
drop.grid(row=idx+2, column=1)
# put the frame in the canvas
self.canvas.create_window(0, 0, anchor='nw', window=self.frame)
# make sure everything is displayed before configuring the scrollregion
self.canvas.update_idletasks()
self.canvas.configure(scrollregion=self.canvas.bbox('all'),
yscrollcommand=self.scroll_y.set)
self.canvas.pack(fill='both', expand=True, side='left')
self.scroll_y.pack(fill='y', side='right')
def SaveState(self):
pass
def SaveButton(self):
self.save_button = Button(self.root, text="Save Changes", padx=50, pady=10, command=self.SaveState)
self.save_button.pack(side=RIGHT)
if __name__ == '__main__':
App()
The SaveState method is what will be used to write the selections so far into a file.
Thanks in advance!
In order to make OptionMenu results available try modifying your code so that
all StringVars are accessible outside of GetRows.
def GetRows(self):
...
# Define label as a list
self.label = []
for idx, r in enumerate(lst):
filename = Label(self.frame, text=r)
filename.grid(row=idx+2, column=0, sticky=N)
label = StringVar()
drop = OptionMenu(self.frame, label, *self.tags)
drop.grid(row=idx+2, column=1)
# Save StringVar reference
self.label.append(label)
...
def SaveState(self):
self.data = dict()
# Retrieve results into dictionary
for i, a in enumerate(self.label):
self.data[f"A_{i}"] = a.get()
print(self.data)
Then use json.dump(self.data, a_file) to save it

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:
The tasks now loaded into the Listbox are now not displayed as checkbuttons upon adding a new task.
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()
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)
...

Change an attribute from an instance with a tkinter Listbox

I am new here, and I am a beginner in tkinter. I want to change the attribute of a specific instance from a class. To select the instance, I want to use a Listbox. Here is simple version of the code for the creation of the Listbox. Is it possible to change the flag attribute of the selected instance (if apple is cliked on, change apple.flag to 1) ?
Thanks!
import tkinter as tk
class fruits:
all_fruits = []
def __init__(self,name):
self.flag = 0
self.name = name
fruits.all_fruits.append(self.name)
root = tk.Tk()
#Main window
frame = tk.Frame(root)
frame.pack()
#Fruits selection
selectorlist = tk.Listbox(frame)
selectorlist.pack()
#Fruits creation
apple = fruits('apple')
pear = fruits('pear')
#List creation
for item in fruits.all_fruits:
selectorlist.insert(tk.END, item)
#Effect on click
selectorlist.bind("<Button-1>",lambda *a: print("Flag is 1"))
root.mainloop()
With a few changes it can be done, I tried to add as many comments as I could to help explain why.
import tkinter as tk
class fruits:
def __init__(self, name):
self.flag = 0
self.name = name
def clicked(e):
# Function that gets the selected value, then changes the flag property and prints the flag.
selected = e.widget.get(e.widget.curselection()) # Gets the index of the current selection and retrieves it's value.
all_fruits[selected].flag = 1
print(selected, "changed to", all_fruits[selected].flag)
test() # This is a function call to print the entire dictionary so changes can be seen.
def test():
# Function to print the all_fruits dictionary.
for k,v in all_fruits.items():
print(k, v.flag)
print()
root = tk.Tk()
#Main window
frame = tk.Frame(root)
frame.pack()
#Fruits selection
selectorlist = tk.Listbox(frame)
selectorlist.pack()
#Fruits creation
names = ['apple', 'pear', 'grape', 'orange', 'pineapple'] # List of fruit names.
all_fruits = {} # Define a dictionary for all fruits.
for name in names:
# Add each name as a dictionary key with the class as its value.
all_fruits[name] = fruits(name)
#List creation
for item in all_fruits:
# Insert each dictionary key into the listbox
selectorlist.insert(tk.END, item)
#Effect on click
selectorlist.bind("<<ListboxSelect>>", clicked) # Changed this to call a function
root.mainloop()
To implement Add/Remove functions it would almost be better to restructure the class so that there is a separate class managing the all of the fruits.
I added some functions for add/remove that could be helpful later:
import tkinter as tk
class fruit:
"""A Class for each fruit."""
def __init__(self, name):
self.flag = 0
self.name = name
def change(self, value):
self.flag = value
class fruits:
"""A Class for all the fruits."""
def __init__(self):
self.all_fruits = {}
def add(self, listbox, name):
self.all_fruits[name] = fruit(name)
listbox.insert(tk.END, name)
print("Added", name)
def remove(self, listbox, name):
indx = listbox.get(0, tk.END).index(name)
listbox.delete(indx)
del self.all_fruits[name]
print("Deleted", name)
def change(self, name, value):
self.all_fruits[name].change(value)
def clicked(e):
# Function that gets the selected value, then changes the flag property and prints the flag.
selected = e.widget.get(e.widget.curselection()) # Gets the index of the current selection and retrieves it's value.
Fruits.change(selected, 1)
print(selected, "changed to 1")
test() # This is a function call to print the entire dictionary so changes can be seen.
def test():
# Function to print the all_fruits dictionary.
for k,v in Fruits.all_fruits.items():
print(k, v.flag)
print()
root = tk.Tk()
#Main window
frame = tk.Frame(root)
frame.pack()
#Fruits selection
selectorlist = tk.Listbox(frame)
selectorlist.pack()
#Fruits creation
names = ['apple', 'pear', 'grape', 'orange', 'pineapple', 'carrot'] # List of fruit names.
Fruits = fruits() # Creates an instance of fruits
for name in names:
Fruits.add(selectorlist, name) # Add each name with the class's add function.
Fruits.remove(selectorlist, "carrot")
#Effect on click
selectorlist.bind("<<ListboxSelect>>", clicked) # Changed this to call a function
root.mainloop()

which checkbutton has been checked

I have created a checkbox. I want the program prints 'python' when I tick it in created checkbox.
but it doesn't work...I don't get error but it doesn't print 'python'
please help.
this is my code
#!/usr/bin/python3
from tkinter import *
class Checkbar(Frame):
def __init__(self, parent=None, picks=[], side=LEFT, anchor=W):
Frame.__init__(self, parent)
self.vars = []
for pick in picks:
var = IntVar()
chk = Checkbutton(self, text=pick, variable=var)
chk.pack(side=side, anchor=anchor, expand=YES)
self.vars.append(var)
def state(self):
return map((lambda var: var.get()), self.vars)
def __getitem__(self,key):
return self.vars[key]
if __name__ == '__main__':
root = Tk()
lng = Checkbar(root, ['Python', 'Ruby', 'Perl', 'C++'])
tgl = Checkbar(root, ['English','German'])
lng.pack(side=TOP, fill=X)
tgl.pack(side=LEFT)
lng.config(relief=GROOVE, bd=2)
def allstates():
## print(lng[0])
if lng[0] == 1:
print ('python')
Button(root, text='Quit', command=root.quit).pack(side=RIGHT)
Button(root, text='Peek', command=allstates).pack(side=RIGHT)
root.mainloop()
lng[0] is an IntVar, not an int, so it's never equal to one.
You need to use the get method to compare the value of the variable to 1:
def allstates():
if lng[0].get() == 1:
print ('python')
Another solution is to change the __getitem__ method of the Checkbar so that it returns the value of the variable instead of the variable itself as I proposed in answer to Python Form: Using TKinter --> run script based on checked checkboxes in GUI
def __getitem__(self, key):
return self.vars[key].get()
In this case, you don't have to change the allstates function.

Resources