Back to Home

Tkinter ToDo: editing, colors, and task sorting

The article describes the expansion of the Tkinter ToDo application with new features: task editing via modal windows, color indication of priorities, sorting of unfinished tasks, and storing creation dates. Full code with backward compatibility of JSON data and Listbox styling techniques is provided.

Tkinter ToDo: colors, sorting, and editing modal windows
Advertisement 728x90

Extending a Tkinter ToDo App: Editing, Colors, Sorting, and Dates

Extending the functionality requires updating the task structure. Each task now includes fields: id, text, completed, created_at (ISO string), and priority (high/medium/low). For backward compatibility, migration of old data with default values is implemented.

Loading and saving functions ensure seamless integration:

import json
import os
from datetime import datetime

TASKS_FILE = "tasks.json"

def load_tasks():
    """Loads tasks from a JSON file with backward compatibility"""
    if not os.path.exists(TASKS_FILE):
        return []
    with open(TASKS_FILE, "r", encoding="utf-8") as f:
        try:
            tasks = json.load(f)
            # Migrate old tasks (add created_at and priority if missing)
            for task in tasks:
                if "created_at" not in task:
                    task["created_at"] = datetime.now().isoformat()
                if "priority" not in task:
                    task["priority"] = "medium"
            return tasks
        except json.JSONDecodeError:
            return []

def save_tasks(tasks):
    """Saves tasks to a JSON file"""
    with open(TASKS_FILE, "w", encoding="utf-8") as f:
        json.dump(tasks, f, ensure_ascii=False, indent=2)

def get_next_id(tasks):
    """Returns the next available ID for a new task"""
    if not tasks:
        return 1
    return max(task["id"] for task in tasks) + 1

datetime.now().isoformat() generates a sortable string in the format 2025-03-22T15:30:45.123456.

Google AdInline article slot

Sorting and Visual Indicators

Sorting is implemented without altering the original order of tasks in self.tasks. The get_sorted_tasks method returns a copy:

  • First, incomplete tasks (completed == False)
  • Then, completed tasks
  • Within groups — by id
class TodoApp:
    
    def get_sorted_tasks(self):
        """Returns tasks sorted for display: active first, then completed"""
        active_tasks = [task for task in self.tasks if not task["completed"]]
        completed_tasks = [task for task in self.tasks if task["completed"]]
        active_tasks.sort(key=lambda x: x["id"])
        completed_tasks.sort(key=lambda x: x["id"])
        return active_tasks + completed_tasks

    def refresh_task_list(self):
        """Updates the task list display with sorting and color indicators"""
        self.task_listbox.delete(0, tk.END)
        sorted_tasks = self.get_sorted_tasks()

        for task in sorted_tasks:
            status = "✓" if task["completed"] else "○"
            try:
                created = datetime.fromisoformat(task["created_at"])
                date_str = created.strftime("%m/%d/%y %H:%M")
            except:
                date_str = "date unknown"

            display_text = f"{status} [{date_str}] {task['id']}. {task['text']}"

            self.task_listbox.insert(tk.END, display_text)

            bg_color = "white"
            fg_color = "black"

            if task["completed"]:
                fg_color = "green"
                bg_color = "light gray"
            else:
                if task["priority"] == "high":
                    fg_color = "red"
                    bg_color = "#ffe6e6"
                elif task["priority"] == "medium":
                    fg_color = "orange"
                    bg_color = "#fff0e6"

            index = tk.END
            self.task_listbox.itemconfig(tk.END, bg=bg_color, fg=fg_color)

Key techniques:

  • itemconfig(index, bg=..., fg=...) for styling Listbox rows
  • strftime("%m/%d/%y %H:%M") for localized date format
  • Lazy sorting only during refresh_task_list

Modal Edit Window

Editing opens a Toplevel window with grab_set() for modality. The logic finds the task by id from the sorted list:

Google AdInline article slot
class TodoApp:
   def edit_task(self):
        selection = self.task_listbox.curselection()
        if not selection:
            messagebox.showwarning("Warning", "Select a task to edit")
            return

        selected_index = selection[0]
        sorted_tasks = self.get_sorted_tasks()
        selected_task = sorted_tasks[selected_index]
        task_id = selected_task["id"]

        task_to_edit = None
        task_index = None
        for i, task in enumerate(self.tasks):
            if task["id"] == task_id:
                task_to_edit = task
                task_index = i
                break

        if not task_to_edit:
            return

        edit_window = tk.Toplevel(self.root)
        edit_window.title(f"Editing Task #{task_id}")
        edit_window.geometry("450x250")
        edit_window.resizable(False, False)
        edit_window.grab_set()

        tk.Label(edit_window, text="Task Text:").pack(pady=(10, 0), anchor=tk.W, padx=10)
        text_entry = tk.Text(edit_window, height=4, width=50, font=("Arial", 10))
        text_entry.pack(pady=5, padx=10, fill=tk.BOTH, expand=True)
        text_entry.insert("1.0", task_to_edit["text"])

        tk.Label(edit_window, text="Priority:").pack(pady=(5, 0), anchor=tk.W, padx=10)
        priority_var = tk.StringVar(value=task_to_edit["priority"])
        priority_frame = tk.Frame(edit_window)
        priority_frame.pack(pady=5, padx=10, anchor=tk.W)

        tk.Radiobutton(priority_frame, text="High", variable=priority_var, value="high", fg="red").pack(side=tk.LEFT, padx=5)
        tk.Radiobutton(priority_frame, text="Medium", variable=priority_var, value="medium", fg="orange").pack(side=tk.LEFT, padx=5)
        tk.Radiobutton(priority_frame, text="Low", variable=priority_var, value="low").pack(side=tk.LEFT, padx=5)

        button_frame = tk.Frame(edit_window)
        button_frame.pack(pady=15)

        def save_edit():
            new_text = text_entry.get("1.0", "end-1c").strip()
            if not new_text:
                messagebox.showwarning("Warning", "Task text cannot be empty", parent=edit_window)
                return

            task_to_edit["text"] = new_text
            task_to_edit["priority"] = priority_var.get()
            save_tasks(self.tasks)
            self.refresh_task_list()
            edit_window.destroy()
            messagebox.showinfo("Success", f"Task #{task_id} updated")

        tk.Button(button_frame, text="Save", command=save_edit, bg="light green", width=10).pack(side=tk.LEFT, padx=5)
        tk.Button(button_frame, text="Cancel", command=edit_window.destroy, bg="light gray", width=10).pack(side=tk.LEFT, padx=5)

Integration: Add self.edit_button = tk.Button(bottom_frame, text="✏ Edit", command=self.edit_task) to create_widgets.

Add Task Dialog with Priority

The add_task method uses a similar Toplevel structure with focus on Entry and radio buttons:

  • text_entry.focus() for UX
  • Validation with strip() and emptiness check
  • Auto-generation of id, created_at, completed=False

The full method code ensures consistency with editing.

Google AdInline article slot

Key Points

  • Backward Compatibility: Automatic migration of created_at and priority on load
  • Performance: Sorting a copy of the list, leaving persistent data untouched
  • Listbox Styling: itemconfig(tk.END, bg=..., fg=...) for dynamic colors
  • Modal Windows: Toplevel + grab_set() + resizable(False, False)
  • Date Formatting: strftime for readable display of ISO strings

— Editorial Team

Advertisement 728x90

Read Next