Mão na massa no projeto + como gerar um .exe do seu app (Windows).
Como funciona hoje
A aula é mão na massa. Venham com o modelo de ontem rascunhado.
Bônus (opcional)
Depois que o projeto roda, dá pra empacotar num executável — abre com dois cliques, sem Python instalado.
.exe que roda em qualquer Windows — você manda pro colega e ele só abre. Bônus · antes de empacotar
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:
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" .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
Na pasta do projeto, com o app já rodando:
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
Exemplo · Etapa 1
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
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
O mesmo projeto virando código: POO + SQLite + Tkinter — exatamente o que o diagrama descreve.
"""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() Além da ementa
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.
Materiais 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)
.exe. Não é exigido — só se fizer o bônus do executável. avaliação · 2ª VA prática
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:
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.
.py por email até 13/07Um 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.
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.
💾Não terminou o projeto? Pode mandar mesmo assim — parcial, só com alguns trechos, ou sabendo que ainda vai mexer. A entrega que vale pra nota é a do GitHub; o arquivo do email não é usado pra avaliar, e mudar o projeto depois da prova também não tem problema.
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.
Segunda a gente fecha: ajustes finais, últimos bugs, e preparar a entrega (repo no GitHub até ~15/07) e a apresentação (16/07).