Guia de estudo

Aula 26 · Sex, 10/07

Projeto final — desenvolvimento

Hoje é studio: vocês codam e eu tiro dúvidas. E um bônus opcional — transformar o app num executável.

Mão na massa no projeto + como gerar um .exe do seu app (Windows).

Studio: dev + acompanhamento Checkpoint UML × código Bônus: gerar o .exe

Como funciona hoje

Studio: vocês desenvolvem, eu tiro dúvidas

A aula é mão na massa. Venham com o modelo de ontem rascunhado.

  • 💻 Vocês desenvolvem o projeto; eu tiro dúvidas — POO, arquivos, SQLite, Tkinter.
  • 🔍 Revisão de código ao vivo e checkpoint: o diagrama UML bate com o código?
  • 🎯 Foco no núcleo mínimo rodando (1 hierarquia + 1 tabela CRUD + 1 janela) antes de sofisticar.

Bônus (opcional)

Seu app virando um programa de verdade (.exe)

Depois que o projeto roda, dá pra empacotar num executável — abre com dois cliques, sem Python instalado.

  • 📦 Um único .exe que roda em qualquer Windows — você manda pro colega e ele só abre.
  • 🛠️ A ferramenta é o PyInstaller. Tkinter e SQLite já vão junto (biblioteca padrão).
  • É EXTRA, não requisito. Premia quem quiser ir além — o projeto vale por si.

Bônus · antes de empacotar

Antes de empacotar: prepare o caminho do banco

Quando o app virar .exe, o Path(__file__) passa a apontar pra uma pasta temporária e o banco "some". Ajuste o caminho ANTES de empacotar:

onde fica o banco
import sys
from pathlib import Path

if getattr(sys, "frozen", False):     # True só no executável
    BASE = Path(sys.executable).parent  # pasta do .exe
else:
    BASE = Path(__file__).parent        # pasta do .py (aula 24)
BANCO = BASE / "loja.db"
✅ Com isso o .db vai ficar ao lado do .exe e persistir de verdade. Sem essa correção, ao empacotar ele cairia num temp que o Windows apaga. (Testado — funciona.)

Bônus · como gerar

PyInstaller em 2 comandos

Na pasta do projeto, com o app já rodando:

PowerShell (Windows)
pip install pyinstaller
python -m PyInstaller --onefile --windowed app.py
# o executável sai em:  dist\app.exe
--onefile = um único .exe · --windowed = sem a janela preta de terminal atrás.
💡 É python -m PyInstaller (P e I maiúsculos) — o comando curto pyinstaller às vezes dá "não é reconhecido" (é PATH); pelo Python sempre funciona.

Bônus · fechando

Antes de entregar o executável

  • Gere com python -m PyInstaller --onefile --windowed (à prova de PATH).
  • Aplique a correção do caminho do banco (sys.frozen → sys.executable).
  • NÃO empacote o .db — ele nasce ao lado do .exe na 1ª execução.
  • Antivírus reclamou? É falso positivo comum do --onefile — libere o arquivo.
  • Entregue o .exe junto do código no GitHub. É bônus: o projeto vale por si.

Exemplo · Etapa 1

Documento de motivação (exemplo)

Um exemplo real da Etapa 1: o problema, a motivação, as funcionalidades e onde entra cada conceito de POO. Role o PDF pra ver as duas páginas.

Exemplo · Etapa 2

Diagrama de classes (exemplo)

O mesmo projeto virando diagrama UML: as classes, os atributos, os métodos e as três relações (herança, composição, associação).

Exemplo · Etapa 3

Código do projeto (controle_assinaturas.py)

O mesmo projeto virando código: POO + SQLite + Tkinter — exatamente o que o diagrama descreve.

controle_assinaturas.py
"""Controle de Assinaturas — POO + SQLite + Tkinter.

Exemplo do projeto final: este código bate com o documento de motivação
(Etapa 1) e o diagrama de classes (Etapa 2). Usa só o que foi visto nas aulas.
"""
import sqlite3
import tkinter as tk
from tkinter import messagebox
from abc import ABC, abstractmethod
from pathlib import Path

BANCO = Path(__file__).parent / "assinaturas.db"   # ao lado do script (aula 24)


# ── Modelo: a hierarquia de assinaturas (aulas 04, 10, 09) ─────────────
class Assinatura(ABC):
    def __init__(self, nome, valor, id=None):
        self.id = id
        self.nome = nome
        self.valor = valor            # passa pelo setter (valida)

    @property
    def valor(self):
        return self._valor

    @valor.setter
    def valor(self, novo):
        if novo < 0:
            raise ValueError("valor não pode ser negativo")
        self._valor = novo

    @abstractmethod
    def custo_mensal(self):
        pass

    def to_params(self):              # objeto -> tupla p/ o banco (aula 24)
        return (self.nome, self.valor, self.tipo)

    @classmethod
    def from_row(cls, linha):         # linha do banco -> objeto (aula 24)
        id, nome, valor, tipo = linha
        classe = TIPOS[tipo]          # o tipo escolhe a classe: Simple Factory (aula 20)
        return classe(nome, valor, id)

    def __str__(self):                # representação amigável, valor em R$ com vírgula (aula 24)
        return f"{self.nome} — R$ {self.custo_mensal():.2f}/mês".replace(".", ",")


class AssinaturaMensal(Assinatura):
    tipo = "mensal"

    def custo_mensal(self):
        return self.valor


class AssinaturaAnual(Assinatura):
    tipo = "anual"

    def custo_mensal(self):
        return self.valor / 12


# dicionário {texto: Classe} — a Simple Factory da aula 20
TIPOS = {"mensal": AssinaturaMensal, "anual": AssinaturaAnual}


# ── Persistência: Repositório SQLite (aula 24) ─────────────────────────
class RepositorioAssinatura:
    def __init__(self, caminho=BANCO):
        self.caminho = caminho
        self.criar_tabela()

    def criar_tabela(self):
        with sqlite3.connect(self.caminho) as con:
            con.execute(
                "CREATE TABLE IF NOT EXISTS assinaturas ("
                "id INTEGER PRIMARY KEY AUTOINCREMENT, "
                "nome TEXT NOT NULL, "
                "valor REAL NOT NULL, "
                "tipo TEXT NOT NULL)"
            )

    def salvar(self, assinatura):
        with sqlite3.connect(self.caminho) as con:
            cursor = con.execute(
                "INSERT INTO assinaturas (nome, valor, tipo) VALUES (?, ?, ?)",
                assinatura.to_params(),
            )
            assinatura.id = cursor.lastrowid
            return assinatura

    def listar(self):
        with sqlite3.connect(self.caminho) as con:
            cursor = con.execute("SELECT id, nome, valor, tipo FROM assinaturas ORDER BY id")
            return [Assinatura.from_row(linha) for linha in cursor.fetchall()]

    def atualizar(self, assinatura):
        with sqlite3.connect(self.caminho) as con:
            con.execute(
                "UPDATE assinaturas SET nome = ?, valor = ?, tipo = ? WHERE id = ?",
                (assinatura.nome, assinatura.valor, assinatura.tipo, assinatura.id),
            )

    def remover(self, id):
        with sqlite3.connect(self.caminho) as con:
            con.execute("DELETE FROM assinaturas WHERE id = ?", (id,))


# ── Interface: a janela Tkinter (aulas 22 e 23) ────────────────────────
class App(tk.Tk):
    def __init__(self):
        super().__init__()
        self.title("Controle de Assinaturas")
        self.repo = RepositorioAssinatura()

        # form num Frame com grid (label ao lado do entry) — pack só na janela (aula 22)
        form = tk.Frame(self)
        form.pack(padx=16, pady=10)
        tk.Label(form, text="Nome").grid(row=0, column=0, sticky="e", padx=6, pady=6)
        self.entrada_nome = tk.Entry(form)
        self.entrada_nome.grid(row=0, column=1, padx=6, pady=6)
        tk.Label(form, text="Valor").grid(row=1, column=0, sticky="e", padx=6, pady=6)
        self.entrada_valor = tk.Entry(form)
        self.entrada_valor.grid(row=1, column=1, padx=6, pady=6)

        # sem dropdown (não foi dado): um botão por tipo
        tk.Button(self, text="Adicionar mensal", command=lambda: self.adicionar("mensal")).pack(padx=16, pady=4, fill="x")
        tk.Button(self, text="Adicionar anual", command=lambda: self.adicionar("anual")).pack(padx=16, pady=4, fill="x")

        self.lista = tk.Listbox(self, width=40, height=8)
        self.lista.pack(padx=16, pady=10, fill="both", expand=True)

        tk.Button(self, text="Editar selecionada", command=self.editar_selecionada).pack(padx=16, pady=4, fill="x")
        tk.Button(self, text="Remover selecionada", command=self.remover_selecionada).pack(padx=16, pady=4, fill="x")

        self.rotulo_total = tk.Label(self, text="Total mensal: R$ 0,00")
        self.rotulo_total.pack(pady=8)

        # rótulo de status: avisos em vermelho, confirmações em verde (aula 23)
        self.rotulo_status = tk.Label(self, text="")
        self.rotulo_status.pack(pady=6)

        self.atualizar_lista()

    def adicionar(self, tipo):
        nome = self.entrada_nome.get()
        valor_texto = self.entrada_valor.get()
        if not nome or not valor_texto:
            self.rotulo_status.config(text="Preencha o nome e o valor.", fg="red")
            return
        try:
            # aceita vírgula (padrão BR): troca por ponto antes do float (aula 24)
            # o float valida o número; o setter valida o negativo (aula 04)
            assinatura = TIPOS[tipo](nome, float(valor_texto.replace(",", ".")))
        except ValueError:
            self.rotulo_status.config(text="Valor inválido: use um número positivo.", fg="red")
            return
        self.repo.salvar(assinatura)
        self.rotulo_status.config(text=f"'{nome}' adicionada.", fg="green")
        self.entrada_nome.delete(0, tk.END)
        self.entrada_valor.delete(0, tk.END)
        self.atualizar_lista()

    def editar_selecionada(self):
        selecao = self.lista.curselection()          # o que está marcado (aula 23)
        if not selecao:
            self.rotulo_status.config(text="Selecione uma assinatura para editar.", fg="red")
            return
        assinatura = self.repo.listar()[selecao[0]]  # o índice do Listbox é o da lista

        # abre uma janela secundária de edição (Toplevel, aula 23)
        janela = tk.Toplevel(self)
        janela.title("Editar assinatura")

        cabecalho = f"Editando: {assinatura.nome} - Assinatura {assinatura.tipo}"
        tk.Label(janela, text=cabecalho, font=("Arial", 11, "bold")).pack(padx=16, pady=12)

        # mesmo padrão: form num Frame com grid (label ao lado do entry)
        form = tk.Frame(janela)
        form.pack(padx=16, pady=8)
        tk.Label(form, text="Nome").grid(row=0, column=0, sticky="e", padx=6, pady=6)
        entrada_nome = tk.Entry(form)
        entrada_nome.insert(0, assinatura.nome)      # campos já vêm preenchidos
        entrada_nome.grid(row=0, column=1, padx=6, pady=6)
        tk.Label(form, text="Valor").grid(row=1, column=0, sticky="e", padx=6, pady=6)
        entrada_valor = tk.Entry(form)
        entrada_valor.insert(0, f"{assinatura.valor:.2f}".replace(".", ","))   # abre com 2 casas e vírgula
        entrada_valor.grid(row=1, column=1, padx=6, pady=6)

        aviso = tk.Label(janela, text="")
        aviso.pack(pady=4)

        def salvar():
            nome = entrada_nome.get()
            valor_texto = entrada_valor.get()
            if not nome or not valor_texto:
                aviso.config(text="Preencha o nome e o valor.", fg="red")
                return
            try:
                assinatura.nome = nome
                # aceita vírgula (padrão BR) antes do float (aula 24)
                assinatura.valor = float(valor_texto.replace(",", "."))   # setter valida o negativo
            except ValueError:
                aviso.config(text="Valor inválido: use um número positivo.", fg="red")
                return
            self.repo.atualizar(assinatura)
            janela.destroy()                            # fecha a janela de edição
            self.rotulo_status.config(text=f"'{nome}' atualizada.", fg="green")
            self.atualizar_lista()

        tk.Button(janela, text="Salvar", command=salvar).pack(padx=16, pady=12, fill="x")

    def remover_selecionada(self):
        selecao = self.lista.curselection()
        if not selecao:
            self.rotulo_status.config(text="Selecione uma assinatura para remover.", fg="red")
            return
        assinatura = self.repo.listar()[selecao[0]]
        if not messagebox.askyesno("Remover", f"Remover '{assinatura.nome}'?"):
            return
        self.repo.remover(assinatura.id)
        self.rotulo_status.config(text=f"'{assinatura.nome}' removida.", fg="green")
        self.atualizar_lista()

    def total_mensal(self):
        total = 0
        for assinatura in self.repo.listar():
            total += assinatura.custo_mensal()       # polimorfismo (aula 09)
        return total

    def atualizar_lista(self):
        self.lista.delete(0, tk.END)                 # refresh do Listbox (aula 23)
        for assinatura in self.repo.listar():
            self.lista.insert(tk.END, str(assinatura))
        # total em R$ com vírgula (aula 24)
        self.rotulo_total.config(text=f"Total mensal: R$ {self.total_mensal():.2f}".replace(".", ","))


App().mainloop()
✅ Documento → diagrama → código: os três batem. É o mesmo projeto (Controle de Assinaturas) nas 3 etapas.

Além da ementa

Dá pra ir muito além — e tudo continua sendo POO

A versão PRO deste mesmo app usa vários recursos que a gente não viu — e que nem estão na ementa do curso: tema escuro, tabela (Treeview), dropdown, janela modal… tudo só com Tkinter da biblioteca padrão. Nossa missão aqui é abrir a porta: mostrar que dá pra ir bem mais longe. A partir daqui, é vocês evoluírem. E no fim, tudo é POO — a interface muda, mas o modelo (classes, herança, polimorfismo) continua o mesmo.

Tela do Controle de Assinaturas PRO
controle_assinaturas_pro.py — só Tkinter (tk + ttk), sem biblioteca externa. Mesmo modelo POO por baixo.

Materiais de referência

Baixe o exemplo — use de referência

O projeto-exemplo completo — documento, diagrama e código — num único .zip. Baixe, abra e use de referência pra montar o de vocês:
📦 Baixar o .zip (5 arquivos)

  • 📄 Os 2 PDFs — o documento de motivação e o diagrama de classes (Etapas 1 e 2). Modelo de como entregar.
  • 🐍 controle_assinaturas.py — o código simples, só com o que vimos em aula. É este que serve de guia pro de vocês.
  • 📦 controle_assinaturas_install.py — igual ao simples, com uma diferença mínima no caminho do banco pra rodar como .exe. Não é exigido — só se fizer o bônus do executável.
  • controle_assinaturas_pro.py — só pra conhecimento. Se quiser algo assim: garanta primeiro que o simples RODA, e só depois sofistique — senão perde tempo. O que vale é a lógica.

avaliação · 2ª VA prática

O seu projeto final vira consulta na prova

A prova prática da 2ª VA é com consulta ao próprio projeto. Todo o código que você escrever — a hierarquia com ABC, o @property, o from_row/to_params, o CRUD no SQLite, a janela Tkinter — vira material de apoio na hora. Mas tem uma regra pra isso valer:

1

Desenvolva o seu projeto final

Quanto mais completo e organizado, mais trechos prontos você tem pra reaproveitar. O projeto que você constrói nestas aulas é exatamente o que vira a sua consulta.

2

Mande o .py por email até 13/07

Um arquivo por pessoa, para talkto@theleoad.com, com o assunto PROJETO 2VA - Arquivo para a prova - Seu Nome. É o único jeito de garantir o seu código na prova.

3

Receba o seu código junto com a prova

No dia, eu entrego o seu arquivo junto com a prova — cada um recebe o seu. Aí é só consultar à vontade enquanto resolve as questões.

Atenção: Prazo: 23h59 do dia 13/07 (véspera da prova) — quem não enviar até lá não terá o arquivo na prova.

Vale só o projeto que você está desenvolvendo, sem dicas, respostas ou 'colas' plantadas no código. Todos os arquivos serão conferidos: se eu notar qualquer coisa fora do padrão, o seu .py inteiro não será disponibilizado — não removo o trecho e libero o resto, o arquivo simplesmente não vai pra prova.

próxima aula

Aula 27 · 13/07

Projeto final — finalização

Segunda a gente fecha: ajustes finais, últimos bugs, e preparar a entrega (repo no GitHub até ~15/07) e a apresentação (16/07).

Ajustes finais Preparar entrega + apresentação Repo até ~15/07