projeto

Projeto Guiado: Aplicativo de Lista de Tarefas (To-Do List)

Aprenda sobre projeto guiado: aplicativo de lista de tarefas (to-do list)

75 min
Aula 2 de 5

Projeto Guiado: Aplicativo de Lista de Tarefas (To-Do List) 📝

Olá, futuro desenvolvedor de interfaces! Nesta aula prática, vamos colocar a mão na massa e construir um aplicativo completo de lista de tarefas (To-Do List) usando o Tkinter. Este projeto é uma excelente maneira de consolidar os conceitos aprendidos sobre design de UI, manipulação de widgets, tratamento de eventos e, o mais importante, como estruturar uma aplicação Tkinter de forma organizada e eficiente.

Ao final desta aula, você terá um aplicativo funcional que permite adicionar, remover e marcar tarefas como concluídas, além de persistir os dados em um arquivo, para que suas tarefas não sejam perdidas ao fechar o programa! 🚀


1. Introdução ao Projeto: To-Do List com Tkinter ✨

Um aplicativo de lista de tarefas é um clássico para iniciantes e um projeto fundamental para entender como diferentes componentes de uma interface gráfica interagem para criar uma experiência de usuário útil. Com o Tkinter, podemos construir rapidamente uma interface intuitiva para gerenciar nossas atividades diárias.

Nossos objetivos com este projeto são:

  • Aplicar conceitos de Tkinter: Widgets (Entry, Button, Listbox, Scrollbar, Frame), gerenciadores de layout (Grid).
  • Entender a arquitetura de uma aplicação Tkinter: Usando uma abordagem orientada a objetos (OOP) para melhor organização do código.
  • Implementar manipulação de eventos: Responder a cliques de botão e seleções na lista.
  • Gerenciar dados: Adicionar, remover e atualizar itens em uma lista.
  • Persistência de dados: Salvar e carregar tarefas de um arquivo para que o estado do aplicativo seja mantido entre as sessões.

Vamos construir nosso aplicativo passo a passo, garantindo que cada componente seja explicado e integrado de forma clara.


2. Estrutura do Projeto e Configuração Inicial 🏗️

Para manter nosso código organizado e escalável, utilizaremos uma abordagem orientada a objetos (OOP). Nosso aplicativo será encapsulado em uma classe TodoApp, que conterá a lógica da interface do usuário e as funcionalidades do aplicativo.

Vamos começar com a estrutura básica da nossa janela principal.

# todo_app.py
import tkinter as tk
from tkinter import ttk, messagebox
import json
import os
 
class TodoApp:
    def __init__(self, root):
        self.root = root
        self.root.title("Lista de Tarefas 📝")
        self.root.geometry("400x550")
        self.root.resizable(False, False) # Impede o redimensionamento da janela
 
        # Configurações de estilo para uma aparência mais moderna
        self.style = ttk.Style()
        self.style.theme_use("clam") # Ou "alt", "default", "vista", "xpnative"
        self.style.configure("TFrame", background="#f0f0f0")
        self.style.configure("TButton", font=("Arial", 10), padding=5)
        self.style.configure("TLabel", font=("Arial", 12), background="#f0f0f0")
        self.style.configure("TEntry", font=("Arial", 12), padding=5)
        self.style.configure("TListbox", font=("Arial", 12), padding=5)
 
        self.tasks = [] # Lista interna para armazenar as tarefas
 
        self._create_widgets()
        self._load_tasks() # Carrega as tarefas ao iniciar o app
 
        # Salva as tarefas ao fechar a janela
        self.root.protocol("WM_DELETE_WINDOW", self._on_closing)
 
    def _create_widgets(self):
        # Frame principal para organizar os elementos
        main_frame = ttk.Frame(self.root, padding="10")
        main_frame.pack(fill=tk.BOTH, expand=True)
 
        # Título
        title_label = ttk.Label(main_frame, text="Minhas Tarefas", font=("Arial", 16, "bold"))
        title_label.grid(row=0, column=0, columnspan=2, pady=10)
 
        # Campo de entrada para novas tarefas
        self.task_entry = ttk.Entry(main_frame, width=30)
        self.task_entry.grid(row=1, column=0, padx=5, pady=5, sticky="ew")
        self.task_entry.bind("<Return>", lambda event: self._add_task()) # Adiciona tarefa ao pressionar Enter
 
        # Botão para adicionar tarefa
        add_button = ttk.Button(main_frame, text="Adicionar Tarefa", command=self._add_task)
        add_button.grid(row=1, column=1, padx=5, pady=5, sticky="ew")
 
        # Listbox para exibir as tarefas
        self.task_listbox = tk.Listbox(main_frame, height=15, selectmode=tk.SINGLE,
                                       bg="#ffffff", fg="#333333", selectbackground="#a6d5fa",
                                       selectforeground="#000000", borderwidth=1, relief="solid")
        self.task_listbox.grid(row=2, column=0, columnspan=2, padx=5, pady=10, sticky="nsew")
 
        # Scrollbar para a Listbox
        scrollbar = ttk.Scrollbar(main_frame, orient=tk.VERTICAL, command=self.task_listbox.yview)
        scrollbar.grid(row=2, column=2, sticky="ns")
        self.task_listbox.config(yscrollcommand=scrollbar.set)
 
        # Frame para os botões de ação
        button_frame = ttk.Frame(main_frame)
        button_frame.grid(row=3, column=0, columnspan=2, pady=10, sticky="ew")
        button_frame.columnconfigure(0, weight=1)
        button_frame.columnconfigure(1, weight=1)
 
        # Botões de ação
        mark_completed_button = ttk.Button(button_frame, text="Marcar Concluída", command=self._mark_task_completed)
        mark_completed_button.grid(row=0, column=0, padx=5, sticky="ew")
 
        remove_button = ttk.Button(button_frame, text="Remover Tarefa", command=self._remove_task)
        remove_button.grid(row=0, column=1, padx=5, sticky="ew")
 
        # Configurações de redimensionamento da grid
        main_frame.grid_rowconfigure(2, weight=1) # Faz a listbox expandir verticalmente
        main_frame.grid_columnconfigure(0, weight=1) # Faz o campo de entrada expandir horizontalmente
 
    def _add_task(self):
        """Adiciona uma nova tarefa à lista."""
        task_text = self.task_entry.get().strip()
        if task_text:
            self.tasks.append({"text": task_text, "completed": False})
            self._update_task_listbox()
            self.task_entry.delete(0, tk.END) # Limpa o campo de entrada
        else:
            messagebox.showwarning("Atenção", "O campo de tarefa não pode estar vazio!")
 
    def _remove_task(self):
        """Remove a tarefa selecionada da lista."""
        try:
            selected_index = self.task_listbox.curselection()[0]
            del self.tasks[selected_index]
            self._update_task_listbox()
        except IndexError:
            messagebox.showwarning("Atenção", "Nenhuma tarefa selecionada para remover.")
 
    def _mark_task_completed(self):
        """Marca a tarefa selecionada como concluída."""
        try:
            selected_index = self.task_listbox.curselection()[0]
            self.tasks[selected_index]["completed"] = not self.tasks[selected_index]["completed"] # Alterna o status
            self._update_task_listbox()
        except IndexError:
            messagebox.showwarning("Atenção", "Nenhuma tarefa selecionada para marcar.")
 
    def _update_task_listbox(self):
        """Atualiza a Listbox com as tarefas da lista interna `self.tasks`."""
        self.task_listbox.delete(0, tk.END) # Limpa a Listbox
        for i, task in enumerate(self.tasks):
            display_text = f"[CONCLUÍDA] {task['text']}" if task['completed'] else task['text']
            self.task_listbox.insert(tk.END, display_text)
            if task['completed']:
                # Aplica um estilo visual para tarefas concluídas
                self.task_listbox.itemconfig(i, fg="gray", selectforeground="gray")
            else:
                self.task_listbox.itemconfig(i, fg="black", selectforeground="black")
 
 
    def _load_tasks(self):
        """Carrega as tarefas de um arquivo JSON."""
        if os.path.exists("tasks.json"):
            with open("tasks.json", "r") as f:
                self.tasks = json.load(f)
            self._update_task_listbox()
 
    def _save_tasks(self):
        """Salva as tarefas em um arquivo JSON."""
        with open("tasks.json", "w") as f:
            json.dump(self.tasks, f, indent=4)
 
    def _on_closing(self):
        """Chamado quando a janela é fechada, para salvar as tarefas."""
        self._save_tasks()
        self.root.destroy() # Destrói a janela e encerra o aplicativo
 
if __name__ == "__main__":
    root = tk.Tk()
    app = TodoApp(root)
    root.mainloop()
 

Explicação Detalhada do Código 🧑‍🏫

Vamos quebrar o código em partes para entender cada componente:

1. Imports Essenciais

import tkinter as tk
from tkinter import ttk, messagebox
import json
import os
  • tkinter as tk: O módulo principal do Tkinter.
  • tkinter.ttk: Oferece widgets "temáticos" que se parecem mais modernos e nativos do sistema operacional. É sempre recomendado usar ttk quando possível.
  • tkinter.messagebox: Usado para exibir caixas de diálogo de aviso ou informação.
  • json: Módulo padrão do Python para trabalhar com dados JSON, que usaremos para salvar e carregar nossas tarefas.
  • os: Módulo para interagir com o sistema operacional, útil para verificar a existência do arquivo tasks.json.

2. Classe TodoApp

class TodoApp:
    def __init__(self, root):
        self.root = root
        self.root.title("Lista de Tarefas 📝")
        self.root.geometry("400x550")
        self.root.resizable(False, False) # Impede o redimensionamento da janela
 
        # ... (restante do __init__)
  • O construtor __init__ recebe a janela principal (root) como argumento.
  • self.root.title(): Define o título da janela.
  • self.root.geometry(): Define o tamanho inicial da janela (largura x altura).
  • self.root.resizable(False, False): Desabilita o redimensionamento da janela pelo usuário, o que pode ser útil para manter o layout fixo em aplicativos mais simples.

3. Estilização com ttk.Style

        self.style = ttk.Style()
        self.style.theme_use("clam") # Ou "alt", "default", "vista", "xpnative"
        self.style.configure("TFrame", background="#f0f0f0")
        # ... (outras configurações de estilo)
  • ttk.Style(): Permite personalizar a aparência dos widgets ttk.
  • self.style.theme_use("clam"): Define o tema visual. Experimente outros como "alt", "default", "vista" (Windows) ou "xpnative" (Windows) para ver as diferenças.
  • self.style.configure(): Permite configurar propriedades de estilo para tipos específicos de widgets (ex: "TFrame", "TButton"). Isso ajuda a dar uma aparência mais consistente e moderna ao aplicativo.

4. Lista Interna de Tarefas (self.tasks)

        self.tasks = [] # Lista interna para armazenar as tarefas
  • Esta lista Python armazenará nossos objetos de tarefa. Cada tarefa será um dicionário com {"text": "Minha tarefa", "completed": False}. Usar uma estrutura de dados Python para gerenciar o estado do aplicativo é uma boa prática, pois a Listbox é apenas uma representação visual.

5. Criação dos Widgets (_create_widgets)

    def _create_widgets(self):
        main_frame = ttk.Frame(self.root, padding="10")
        main_frame.pack(fill=tk.BOTH, expand=True)
 
        # ... (criação dos widgets)
 
        # Configurações de redimensionamento da grid
        main_frame.grid_rowconfigure(2, weight=1)
        main_frame.grid_columnconfigure(0, weight=1)
  • ttk.Frame: Um contêiner para agrupar outros widgets. Usamos um main_frame para centralizar o conteúdo e aplicar um padding.
  • main_frame.pack(fill=tk.BOTH, expand=True): Faz com que o frame principal ocupe todo o espaço disponível na janela.
  • ttk.Label: Para o título do aplicativo.
  • ttk.Entry: Onde o usuário digita a nova tarefa.
    • self.task_entry.bind("<Return>", lambda event: self._add_task()): Associa a tecla Enter no campo de entrada à função _add_task, tornando a adição de tarefas mais rápida.
  • ttk.Button: Para "Adicionar Tarefa", "Marcar Concluída" e "Remover Tarefa". O parâmetro command é crucial, pois ele vincula o clique do botão a uma função Python.
  • tk.Listbox: Onde as tarefas são exibidas.
    • selectmode=tk.SINGLE: Permite selecionar apenas uma tarefa por vez.
    • bg, fg, selectbackground, selectforeground: Personaliza as cores da Listbox e da seleção.
  • ttk.Scrollbar: Adiciona uma barra de rolagem à Listbox para lidar com muitas tarefas. A barra é configurada para rolar a Listbox (command=self.task_listbox.yview) e a Listbox é configurada para usar a barra (yscrollcommand=scrollbar.set).
  • Gerenciador de Layout grid:
    • widget.grid(row=X, column=Y, ...): Posiciona os widgets em uma grade (linhas e colunas).
    • columnspan: Faz um widget ocupar múltiplas colunas.
    • padx, pady: Adiciona espaçamento horizontal e vertical.
    • sticky="nsew": Faz o widget "grudar" nas bordas da célula da grid, preenchendo o espaço disponível.
    • main_frame.grid_rowconfigure(2, weight=1) e main_frame.grid_columnconfigure(0, weight=1): Estas linhas são importantes para o redimensionamento. El dizem que a linha 2 (onde está a Listbox) e a coluna 0 (onde está o Entry) devem expandir-se quando a janela é redimensionada, garantindo que a Listbox cresça verticalmente e o Entry horizontalmente.

6. Funcionalidades do Aplicativo

  • _add_task():

    • Obtém o texto do self.task_entry.
    • Verifica se o texto não está vazio (strip() remove espaços em branco).
    • Adiciona um dicionário {"text": task_text, "completed": False} à lista self.tasks.
    • Chama _update_task_listbox() para refletir a mudança na UI.
    • Limpa o task_entry.
    • Usa messagebox.showwarning para avisar se o campo estiver vazio.
  • _remove_task():

    • Usa self.task_listbox.curselection() para obter o índice da tarefa selecionada. Retorna uma tupla de índices. [0] pega o primeiro (e único, pois selectmode=tk.SINGLE).
    • del self.tasks[selected_index]: Remove a tarefa da lista interna.
    • Chama _update_task_listbox().
    • Usa um bloco try-except IndexError para lidar com o caso de nenhuma tarefa estar selecionada.
  • _mark_task_completed():

    • Similar a _remove_task para obter o índice.
    • self.tasks[selected_index]["completed"] = not self.tasks[selected_index]["completed"]: Alterna o status completed da tarefa (de True para False ou vice-versa).
    • Chama _update_task_listbox().
  • _update_task_listbox(): Crucial para a atualização da UI.

    • self.task_listbox.delete(0, tk.END): Limpa completamente a Listbox.
    • Itera sobre self.tasks e insere cada tarefa novamente.
    • Formata o texto para exibir [CONCLUÍDA] se a tarefa estiver concluída.
    • self.task_listbox.itemconfig(i, fg="gray", selectforeground="gray"): Altera a cor do texto para cinza para tarefas concluídas, melhorando a visualização.

7. Persistência de Dados (JSON) 💾

  • _load_tasks():

    • Verifica se o arquivo tasks.json existe usando os.path.exists().
    • Se existir, abre o arquivo em modo de leitura ("r").
    • json.load(f): Carrega o conteúdo JSON do arquivo para self.tasks.
    • Chama _update_task_listbox() para exibir as tarefas carregadas.
  • _save_tasks():

    • Abre o arquivo tasks.json em modo de escrita ("w").
    • json.dump(self.tasks, f, indent=4): Salva a lista self.tasks no arquivo tasks.json em formato JSON. indent=4 formata o JSON para ser legível.
  • _on_closing():

    • Esta função é vinculada ao evento de fechamento da janela (self.root.protocol("WM_DELETE_WINDOW", self._on_closing)).
    • Quando o usuário clica no "X" para fechar a janela, _on_closing é chamado.
    • Ele primeiro chama self._save_tasks() para garantir que todas as tarefas atuais sejam salvas.
    • Em seguida, self.root.destroy() fecha a janela e encerra o aplicativo.

8. Execução do Aplicativo (if __name__ == "__main__":)

if __name__ == "__main__":
    root = tk.Tk()
    app = TodoApp(root)
    root.mainloop()
  • Este bloco garante que o código dentro dele só será executado quando o script for rodado diretamente (não quando importado como módulo).
  • root = tk.Tk(): Cria a janela principal do Tkinter.
  • app = TodoApp(root): Cria uma instância da nossa classe TodoApp, passando a janela principal.
  • root.mainloop(): Inicia o loop de eventos do Tkinter. Este é o coração de qualquer aplicação Tkinter; ele escuta eventos (cliques, digitação, etc.) e os despacha para as funções apropriadas. O aplicativo permanece aberto até que a janela seja fechada.

3. Código de Exemplo Oficial (Melhores Práticas) 🧑‍💻

O código acima segue as melhores práticas para desenvolvimento de aplicações Tkinter, incluindo:

  • Orientação a Objetos: Encapsulando a lógica da aplicação em uma classe.
  • Uso de ttk: Para uma interface mais moderna e nativa.
  • Gerenciamento de Layout: Uso eficiente do grid para posicionamento flexível e responsivo.
  • Separação de Preocupações: Embora seja um aplicativo simples, a lógica de UI e a lógica de dados (lista self.tasks) são gerenciadas separadamente, com a _update_task_listbox agindo como um "renderizador".
  • Persistência de Dados: Integração com o módulo json para salvar e carregar o estado do aplicativo.
  • Tratamento de Eventos: Vinculação de comandos a botões e eventos de teclado (<Return>).
  • Tratamento de Fechamento da Janela: Garantindo que os dados sejam salvos antes de o aplicativo ser encerrado.

4. Exercícios e Desafios 🚀

Agora que você tem um aplicativo funcional, é hora de expandir suas habilidades e adicionar novos recursos!

Aqui estão alguns desafios para você:

Tarefas Essenciais (para fixar o aprendizado):

  • Validação de Entrada Aprimorada: Adicione uma verificação para garantir que o usuário não adicione tarefas duplicadas.
  • Botão "Limpar Concluídas": Crie um novo botão que remova todas as tarefas marcadas como concluídas da lista.
  • Confirmação de Exclusão: Antes de remover uma tarefa, use messagebox.askyesno() para perguntar ao usuário se ele tem certeza.
  • Contador de Tarefas: Adicione um ttk.Label na parte inferior da janela que exiba o número total de tarefas e o número de tarefas concluídas. Atualize este contador sempre que a lista for modificada.

Desafios Extras (para ir além):

  • Prioridade de Tarefas:
    • Adicione um ttk.Combobox ou tk.OptionMenu para permitir que o usuário selecione uma prioridade (Ex: "Alta", "Média", "Baixa") ao adicionar uma tarefa.
    • Modifique a exibição na Listbox para incluir a prioridade (Ex: [ALTA] Minha Tarefa).
    • Implemente a funcionalidade de ordenar as tarefas por prioridade.
  • Edição de Tarefas:
    • Adicione um botão "Editar Tarefa". Quando clicado, a tarefa selecionada deve ser carregada de volta para o task_entry, permitindo que o usuário a modifique e salve as alterações (talvez com um botão "Salvar Edição" que substitua o "Adicionar Tarefa" temporariamente).
  • Filtro de Tarefas:
    • Adicione botões ou um ttk.Combobox para filtrar as tarefas exibidas: "Todas", "Ativas", "Concluídas".
  • Datas de Vencimento:
    • Integre um seletor de data (pode ser um ttk.Entry com validação de formato ou um widget mais avançado se você pesquisar por "Tkinter date picker").
    • Permita que o usuário adicione uma data de vencimento à tarefa.
    • Exiba a data na Listbox e, opcionalmente, destaque tarefas que estão atrasadas.
  • Melhorias de UI/UX:
    • Adicione ícones aos botões (pesquise sobre tk.PhotoImage e como usá-lo com ttk.Button).
    • Implemente atalhos de teclado para as ações principais (ex: Ctrl+A para adicionar, Delete para remover).

Não se preocupe se não conseguir fazer todos os desafios de primeira. O importante é tentar, pesquisar e aprender com o processo. Cada desafio superado é um passo a mais em sua jornada de programação! 💡


5. Resumo e Próximos Passos 📚

Parabéns! Você construiu um aplicativo de lista de tarefas completo com Tkinter. Nesta aula, você aprendeu a:

  • Estruturar um aplicativo Tkinter usando Programação Orientada a Objetos.
  • Utilizar diversos widgets ttk para criar uma interface moderna.
  • Gerenciar o layout da UI com o gerenciador grid.
  • Implementar a lógica para adicionar, remover e modificar itens em uma lista.
  • Persistir dados em um arquivo JSON, garantindo que o estado do aplicativo seja salvo.
  • Lidar com eventos de usuário, como cliques de botão e fechamento de janela.

Este projeto é um alicerce sólido para a criação de aplicações GUI mais complexas.

Próximos Passos na sua Jornada Tkinter:

  1. Explore mais Widgets: O Tkinter tem muitos outros widgets (ex: tk.Text, tk.Canvas, ttk.Notebook). Experimente-os!
  2. Gerenciadores de Layout: Aprofunde-se nos outros gerenciadores de layout (pack e place) e entenda quando usar cada um.
  3. Bancos de Dados: Para aplicações que precisam gerenciar muitos dados, integrar um banco de dados (como SQLite) é o próximo passo lógico. Você pode substituir o json por operações de banco de dados.
  4. Modularização: Para projetos maiores, você pode dividir seu código em múltiplos arquivos (ex: ui.py, data_manager.py, main.py).
  5. Testes: Aprenda a escrever testes unitários para garantir que a lógica do seu aplicativo funcione como esperado.

Continue praticando e construindo. A melhor forma de aprender é fazendo! Feliz codificação! 🎉

© 2025 Escola All Dev. Todos os direitos reservados.

Projeto Guiado: Aplicativo de Lista de Tarefas (To-Do List) - Curso de Python com Tkinter para Criação de Interfaces | escola.all.dev.br