Guia de estudo

Aula 23 · Ter, 07/07

Interface gráfica com Tkinter — parte 2

O cadastro de ontem só listava num Label redesenhado na mão. Hoje ele vira um CRUD visual completo: seleciona, edita, remove — e a app inteira vira uma classe.

A aula 22 montou a primeira janela e o cadastro que salva em arquivo. Mas a coleção aparecia num Label reescrito na unha — não dá pra selecionar, remover nem editar. Hoje a gente resolve: o Listbox lista de verdade, o bind() reage à seleção, o messagebox confirma, o Toplevel edita numa segunda janela, e a app INTEIRA herda de tk.Tk. Nada de Python novo — só colheita: listas (18), arquivos (17), herança (07) e o Observer (21) ganhando uma tela.

Listbox & bind messagebox & Toplevel CRUD visual

roteiro da aula

O que vamos aprender hoje

01

Listbox & bind

O widget que LISTA coleções na tela e deixa selecionar. E o bind(): reagir à seleção, generalizando o command= pra qualquer evento.

02

Diálogos & variáveis

messagebox pra erros e confirmações (askyesno vira um bool), e o StringVar: um rótulo que se atualiza sozinho quando o valor muda.

03

Toplevel & App(tk.Tk)

Uma segunda janela pra editar sem perder a principal, e o salto de arquitetura: a app INTEIRA herda de tk.Tk (aula 07).

04

CRUD visual + Menu

Tudo junto num gestor de clientes: criar, ler, atualizar e excluir na tela, com barra de Menu e persistência em disco (aula 17).

ponte com a aula 22

De onde paramos — o cadastro da aula 22

  • 🧾 Ontem terminamos com o cadastro_lista.py: um formulário que cria objetos Cliente, guarda numa lista (aula 18) E grava num arquivo (aula 17). Fecha e reabre — os dados continuam lá.
  • 🖼️ A coleção aparecia num Label dentro de um Frame, redesenhado na mão por atualizar_painel() com "\n".join(...). Funciona pra MOSTRAR — mas é só texto.
  • 📣 E a frase-âncora da 22 continua valendo: widget é objeto (aula 02); janela é composição (aula 05); callback é Observer (aula 21). Hoje ela ganha widgets novos.
  • 🐍 De novo: nada de Python novo. Só colheita — listas (18), arquivos (17), herança e super() (07), o Observer (21). Muda ONDE aparece: numa interface que finalmente deixa MEXER nos dados.

o problema de hoje

O furo do painel-Label → o CRUD visual

  • 🚫 O painel-Label só sabe uma coisa: mostrar. Não dá pra clicar num cliente pra selecioná-lo, não rola quando a lista cresce, e remover ou editar um item é impossível — é texto pintado na tela.
  • 🔤 As quatro operações de qualquer sistema com dados têm nome: CRUDCreate (criar), Read (ler/listar), Update (editar) e Delete (excluir). Ontem só fizemos Create e Read; faltam Update e Delete.
  • 🧰 Pra fechar o CRUD na tela, precisamos de widgets novos: Listbox (lista selecionável), bind() (reagir à seleção), messagebox (confirmar), Toplevel (editar noutra janela) e StringVar (rótulos que se atualizam).
  • 🏗️ E um salto de organização: com tanta coisa, funções soltas e variáveis globais viram bagunça. A app INTEIRA vira uma classe que herda de tk.Tk (aula 07) — o Contador da aula 22 já ensaiou o caminho.

listbox · o problema

Listbox: a coleção que aparece na tela

  • 🩹 O painel de ontem era um Label com o texto da lista colado por "\n".join(...). O usuário LÊ, mas não INTERAGE: não seleciona uma linha, não rola, não escolhe um item pra apagar.
  • 📋 A solução é um widget feito pra isso: o Listbox — uma lista de linhas selecionáveis e roláveis. Cada linha é um texto; clicar numa linha a MARCA. É o par visual da list da aula 18.
  • 🧩 A API mínima: tk.Listbox(pai, width=, height=) cria; .insert(tk.END, texto) adiciona no fim; .delete(0, tk.END) limpa tudo (ou .delete(i) apaga uma); .curselection() devolve os índices marcados; .get(i) lê o texto da linha i.
  • 🔑 O detalhe que faz TODO o resto funcionar: se você povoou o Listbox na MESMA ordem da sua lista Python, o índice do Listbox é o índice da lista. curselection()[0] vira o índice de clientes[i]. Guarde isso.

listbox · primeiro código

Listbox na prática: povoar, selecionar, ler

Uma lista de sabores (aula 18) vira um Listbox na tela. Um botão lê a linha marcada com curselection() + get() — tudo com o command= que você já conhece da aula 22.

📋
tk.Listbox(...) e .insert(tk.END, sabor) num for transformam a list da aula 18 em linhas na tela. tk.END quer dizer "no fim".
🖱️
lista.curselection() devolve uma TUPLA de índices (vazia se nada marcado); .get(indice) lê o texto daquela linha. O if selecao: evita erro quando nada foi selecionado.
listbox_basico.py
import tkinter as tk

sabores = ["Calabresa", "Mussarela", "Portuguesa", "Frango"]   # aula 18: lista de strings

def mostrar():                                # aula 22: callback de um command=
    selecao = lista.curselection()            # aula 23: tupla com os indices marcados
    if selecao:                               # aula 01: so age se ha selecao
        indice = selecao[0]                   # o primeiro indice selecionado
        print(f"Escolheu: {lista.get(indice)}")   # aula 23: .get(i) devolve o texto da linha i

janela = tk.Tk()                              # aula 22: a janela raiz
janela.title("Sabores")
janela.geometry("240x220")

lista = tk.Listbox(janela, width=30, height=6)   # aula 23 (NOVO): widget de lista selecionavel
for sabor in sabores:                         # aula 18: percorre a lista
    lista.insert(tk.END, sabor)               # aula 23: .insert(tk.END, ...) poe no fim
lista.pack(padx=10, pady=10)                  # aula 22: pack posiciona

botao = tk.Button(janela, text="Mostrar selecionado", command=mostrar)  # aula 21: command= e o observer
botao.pack()

janela.mainloop()                             # aula 22: loop de eventos
# selecionar "Mussarela" e clicar: Escolheu: Mussarela
curselection() devolve uma tupla porque um Listbox PODE permitir várias seleções. No modo padrão (uma só), a tupla tem no máximo um índice — por isso pegamos selecao[0]. Se nada estiver marcado, a tupla é vazia e o if selecao: pula o bloco.

listbox · primeiro código

A janela do listbox_basico.py

Os quatro sabores listados no Listbox e o botão embaixo. Clique numa linha pra marcá-la (fica destacada) e depois em Mostrar selecionado — o texto sai no terminal.

Screenshot de uma janela Tkinter com o título 'Sabores': uma lista com quatro linhas (Calabresa, Mussarela, Portuguesa, Frango), com uma delas selecionada e destacada, e abaixo um botão 'Mostrar selecionado'.
Compare com o painel-Label de ontem: aqui a linha clicada fica DESTACADA e a lista rola sozinha se tiver mais itens do que cabe. É essa capacidade de SELECIONAR que abre a porta pro remover e o editar do resto da aula.

bind · o problema

Reagir à seleção: entra o bind()

  • 🤔 O command= só existe no Button: ele reage a UM evento (o clique) de UM widget. Mas e reagir à SELEÇÃO de um Listbox? Ou a um duplo-clique? O command= não cobre isso.
  • 🔗 A ferramenta geral é o bind(): widget.bind("<<ListboxSelect>>", handler) liga um EVENTO NOMEADO a um callback em QUALQUER widget. É o command= generalizado — o Observer da aula 21 pra além do botão.
  • 📨 Diferença que pega todo mundo: o handler do bind recebe um argumento, o eventodef ao_selecionar(evento):. O command= não passa nada; o bind passa o objeto-evento (que traz detalhes de quem disparou).
  • 🏷️ Os eventos têm nomes entre <>: "<<ListboxSelect>>" (mudou a seleção), "<Double-Button-1>" (duplo-clique), "<Return>" (tecla Enter). Você inscreve um observer pra cada um.

bind · em código

bind: a lista reage sozinha à seleção

Sem botão nenhum: assim que o usuário clica numa cor da lista, o rótulo abaixo se atualiza. O bind("<<ListboxSelect>>", ...) inscreve o handler — que recebe o evento como argumento.

🔗
lista.bind("<<ListboxSelect>>", ao_selecionar) — igual ao command=, entrega a FUNÇÃO sem parênteses (aula 21). O evento <<ListboxSelect>> dispara toda vez que a seleção muda.
📨
def ao_selecionar(evento): — o handler PRECISA aceitar o parâmetro evento, mesmo que não o use. É a assinatura que o bind exige (o command=, não).
selecao.py
import tkinter as tk

cores = ["Vermelho", "Verde", "Azul", "Amarelo"]   # aula 18: lista

def ao_selecionar(evento):                    # aula 23 (NOVO): o handler recebe o evento
    selecao = lista.curselection()            # aula 23: indices marcados
    if selecao:                               # aula 01: so age se ha selecao
        rotulo.config(text=f"Voce marcou: {lista.get(selecao[0])}")   # aula 22: .config() na tela

janela = tk.Tk()                              # aula 22: janela raiz
janela.title("Selecao reativa")
janela.geometry("240x220")

lista = tk.Listbox(janela, height=5)          # aula 23: o Listbox
for cor in cores:                             # aula 18: percorre a lista
    lista.insert(tk.END, cor)                 # aula 23: povoa o Listbox
lista.pack(padx=10, pady=10)
lista.bind("<<ListboxSelect>>", ao_selecionar)   # aula 23 (NOVO): bind liga evento -> callback

rotulo = tk.Label(janela, text="Marque uma cor")   # aula 22: Label
rotulo.pack()

janela.mainloop()                             # aula 22: loop de eventos
# ao clicar em "Azul" na lista, o rotulo vira: Voce marcou: Azul
O <> (com dois pares de <<>>) é um evento 'virtual' do Tkinter — de alto nível, ligado ao widget. Já e (um par de <>) são eventos 'físicos' de mouse e teclado. Você não precisa decorar a lista: o padrão é sempre o mesmo — bind(nome_do_evento, handler).

bind · comparando

command= × bind()

As duas formas de inscrever um observer num widget. Não competem — se completam: command= pro clique do botão, bind() pra todo o resto.

command= — o atalho do Button

command
botao = tk.Button(
    janela, text="OK",
    command=ao_clicar,   # SEM parenteses
)

# - so no Button (e alguns widgets)
# - reage a UM evento: o clique
# - o callback NAO recebe argumento:
#     def ao_clicar():

bind() — o geral, pra qualquer evento

bind
lista.bind(
    "<<ListboxSelect>>",
    ao_selecionar,       # SEM parenteses
)

# - em QUALQUER widget
# - qualquer evento nomeado:
#     selecao, duplo-clique, tecla...
# - o handler RECEBE o evento:
#     def ao_selecionar(evento):
Os dois inscrevem um observer (aula 21) e recebem a função SEM parênteses — a armadilha dos parênteses da aula 22 vale pros dois. A diferença prática: command= é o atalho pro clique do botão; bind() é o canivete que liga qualquer evento a qualquer widget, e passa o evento pro handler.

ao vivo 1 · vamos construir juntos

Exercício ao Vivo 1: a lista que remove

Vamos evoluir o cadastro da aula 22: trocar o painel-Label por um Listbox de verdade, reagir à seleção com bind() e — o que faltava — REMOVER um cliente selecionado, da lista E do arquivo. Em camadas: cada uma entra, roda, e só então vem a próxima.

1

Camada 1 — a classe com salvar_todos

A Cliente mantém carregar_todos() da aula 17, mas ganha um @classmethod novo: salvar_todos(clientes), que reescreve o arquivo INTEIRO em modo "w". Remover obriga a regravar tudo — "a" só anexa, não apaga.

2

Camada 2 — o Listbox e o preencher_lista()

A lista clientes nasce de carregar_todos(). Um Listbox mostra a coleção; preencher_lista() faz delete(0, tk.END) + um insert por cliente. Um bind("<<ListboxSelect>>") mostra quem está selecionado.

3

Camada 3 — remover() com curselection + del

O observer do botão: pega curselection(), valida (nada marcado → status vermelho e return), acha o cliente por clientes[indice], faz del clientes[indice], chama salvar_todos() e redesenha.

4

Testar os dois caminhos

Clicar Remover sem selecionar nada (status vermelho, nada muda) e selecionar + Remover (some da lista, do total E do arquivo). Feche e reabra pra confirmar que o cliente não voltou — o salvar_todos() gravou a lista já sem ele.

resolução 1 · camada 1

Camada 1: Cliente + carregar_todos + salvar_todos

A classe é quase a da aula 22 — mas com uma diferença que muda tudo: além de carregar_todos(), um salvar_todos() que reescreve o arquivo INTEIRO em "w". Remover um cliente exige regravar o resto sem ele.

📂
carregar_todos() é o mesmo @classmethod da aula 17: lê o arquivo, desfaz o split(";") e monta a lista de objetos (aula 18). O try/except FileNotFoundError segura a 1ª execução.
💾
salvar_todos(clientes) abre em "w" e regrava TODA a lista — uma linha por cliente. Diferente do salvar() da aula 22 (que era "a", só anexava): remover/editar precisam do "w", que reescreve do zero.
clientes_lista.py
import tkinter as tk
from pathlib import Path

PASTA = Path(__file__).parent      # aula 17: a pasta onde este .py esta
ARQUIVO = PASTA / "clientes.txt"   # aula 17: o mesmo "pasta / arquivo"

# Camada 1: a classe com persistencia (aula 17)
class Cliente:
    def __init__(self, nome, email):
        self.nome = nome
        self.email = email

    def __str__(self):                            # aula 03: representacao do objeto
        return f"{self.nome} <{self.email}>"

    @classmethod
    def carregar_todos(cls, caminho=ARQUIVO):     # aula 17: arquivo -> lista de objetos
        clientes = []
        try:
            with open(caminho, "r", encoding="utf-8") as f:   # aula 17: "r" = leitura
                for linha in f:                       # aula 17: uma linha por cliente
                    nome, email = linha.strip().split(";")   # aula 18: desfaz o split
                    clientes.append(cls(nome, email))        # aula 18: monta a lista
        except FileNotFoundError:                 # aula 17: 1a execucao, sem arquivo
            pass
        return clientes

    @classmethod
    def salvar_todos(cls, clientes, caminho=ARQUIVO):   # aula 23 (NOVO): reescreve o arquivo INTEIRO
        with open(caminho, "w", encoding="utf-8") as f:     # aula 17: "w" = reescreve do zero
            for c in clientes:                    # aula 18: percorre a lista
                f.write(f"{c.nome};{c.email}\n")  # aula 17: uma linha por cliente
Por que salvar_todos com "w" e não o salvar() com "a" da aula 22? Porque "a" só SABE adicionar no fim. Pra remover um cliente do meio, o jeito é reescrever o arquivo inteiro com a lista já sem ele — e "w" trunca e regrava do zero. É a mesma distinção de modos da aula 17, agora do outro lado.

resolução 1 · camada 2

Camada 2: o Listbox e o preencher_lista()

Adeus painel-Label: agora a coleção mora num Listbox. preencher_lista() é a fonte única da verdade na tela — limpa tudo e reinsere a lista. E o bind() mostra o cliente selecionado no status.

📋
preencher_lista() faz delete(0, tk.END) (limpa o Listbox) e um insert(tk.END, str(c)) por cliente — o str(c) usa o __str__ (aula 03). Chamar essa função é redesenhar a lista do zero a partir de clientes.
🔗
lista.bind("<<ListboxSelect>>", ao_selecionar) reage à seleção. Dentro do handler, clientes[selecao[0]] — o índice do Listbox É o índice da lista, porque os dois estão na mesma ordem.
clientes_lista.py
# Camada 2: a lista de objetos, o Listbox e o preencher_lista()
clientes = Cliente.carregar_todos()           # aula 17: a lista ja NASCE cheia do arquivo

def preencher_lista():                         # redesenha o Listbox a partir da lista
    lista.delete(0, tk.END)                    # aula 23: .delete(0, tk.END) limpa o Listbox
    for c in clientes:                         # aula 18: percorre os objetos
        lista.insert(tk.END, str(c))           # aula 23: str(c) usa o __str__ (aula 03)
    rotulo_total.config(text=f"Total: {len(clientes)} cliente(s)")   # aula 18: len()

def ao_selecionar(evento):                     # aula 23: handler do bind (recebe evento)
    selecao = lista.curselection()             # aula 23: indices marcados
    if selecao:                                # aula 01: so age se ha selecao
        cliente = clientes[selecao[0]]         # aula 23: indice do Listbox == indice da lista
        rotulo_status.config(text=f"Selecionado: {cliente.nome}", fg="black")   # aula 22

janela = tk.Tk()                               # aula 22: janela raiz
janela.title("Clientes")
janela.geometry("320x320")

rotulo_titulo = tk.Label(janela, text="Clientes cadastrados")
rotulo_titulo.pack(pady=5)                     # aula 22: pack empilha

lista = tk.Listbox(janela, width=40, height=8)   # aula 23: o Listbox
lista.pack(padx=10, pady=5)
lista.bind("<<ListboxSelect>>", ao_selecionar)   # aula 23: bind liga a selecao ao handler

rotulo_total = tk.Label(janela, text="Total: 0 cliente(s)")
rotulo_total.pack()

rotulo_status = tk.Label(janela, text="")      # aula 22: painel de avisos, comeca vazio
rotulo_status.pack(pady=4)
preencher_lista() sempre limpa (delete(0, tk.END)) ANTES de reinserir. Se você só desse insert, cada chamada DUPLICARIA as linhas. Limpar-e-reinserir é o padrão de redesenho de um Listbox — o equivalente ao atualizar_painel() da aula 22, agora com um widget de lista de verdade.

resolução 1 · camada 3

Camada 3: remover() com curselection + del + salvar_todos

O que faltava na aula 22: apagar um cliente. O observer do botão pega a seleção, remove da lista com del e regrava o arquivo — nessa ordem. Valida antes: sem seleção, avisa e desiste.

🗑️
del clientes[indice] remove o item da lista pelo índice — é o MESMO del que apagava uma chave do dicionário na aula 18, agora numa list. Depois, salvar_todos(clientes) regrava o arquivo já sem ele.
🛡️
A validação vem primeiro (aula 14): if not selecao: → status vermelho e return. Só com um cliente selecionado o del acontece — o padrão "valide na entrada, aja depois" da aula 22.
clientes_lista.py
# Camada 3: remover() -- o observer do botao
def remover():                                 # aula 21: observer do botao Remover
    selecao = lista.curselection()             # aula 23: indices marcados
    if not selecao:                            # aula 01: nada selecionado?
        rotulo_status.config(text="Selecione um cliente primeiro.", fg="red")
        return                                 # aula 14: sai ANTES de mexer na lista
    indice = selecao[0]                        # aula 23: indice do Listbox == indice da lista
    cliente = clientes[indice]                 # aula 18: acessa o objeto pelo indice
    del clientes[indice]                       # aula 23 (NOVO): del lista[i] remove pelo indice
    Cliente.salvar_todos(clientes)             # aula 23: reescreve o arquivo em "w"
    preencher_lista()                          # aula 23: redesenha o Listbox
    rotulo_status.config(text=f"{cliente.nome} removido.", fg="green")   # aula 22: .config()

botao_remover = tk.Button(janela, text="Remover selecionado", command=remover)  # aula 21: command=
botao_remover.pack(pady=8)

preencher_lista()                              # aula 23: mostra ja o que veio do arquivo
janela.mainloop()                              # aula 22: loop de eventos
A ordem importa: pegue o cliente (clientes[indice]) ANTES do del, senão o índice já não aponta mais pra ele. E salvar_todos() vem DEPOIS do del — o arquivo tem que refletir a lista já sem o removido. Lista e disco andam juntos: mudou a lista, regrava.

resolução 1 · o código completo

Tudo junto: clientes_lista.py

As três camadas no arquivo final. Classe com salvar_todos, Listbox, bind na seleção e o remover(). É o cadastro da aula 22 que finalmente deixa APAGAR — e o apagar sobrevive ao fechar.

🗺️
Leia como um mapa: a classe com persistência (carregar_todos/salvar_todos, aula 17), a lista clientes (aula 18), o Listbox + bind (aula 23) e o observer remover (command=, aula 21).
🔁
O teste que prova: remova um cliente, FECHE a janela e rode de novo — ele não volta, porque salvar_todos() regravou o clientes.txt sem ele. Lista e disco em sincronia.
clientes_lista.py
import tkinter as tk
from pathlib import Path

PASTA = Path(__file__).parent      # aula 17: a pasta onde este .py esta
ARQUIVO = PASTA / "clientes.txt"   # aula 17: "pasta / arquivo"

class Cliente:                                # o modelo, com persistencia (aula 17)
    def __init__(self, nome, email):
        self.nome = nome
        self.email = email

    def __str__(self):                        # aula 03: representacao do objeto
        return f"{self.nome} <{self.email}>"

    @classmethod
    def carregar_todos(cls, caminho=ARQUIVO):   # aula 17: arquivo -> lista
        clientes = []
        try:
            with open(caminho, "r", encoding="utf-8") as f:
                for linha in f:
                    nome, email = linha.strip().split(";")   # aula 18: desfaz o split
                    clientes.append(cls(nome, email))        # aula 18: monta a lista
        except FileNotFoundError:             # aula 17: sem arquivo ainda
            pass
        return clientes

    @classmethod
    def salvar_todos(cls, clientes, caminho=ARQUIVO):   # aula 23: reescreve TUDO em "w"
        with open(caminho, "w", encoding="utf-8") as f:
            for c in clientes:
                f.write(f"{c.nome};{c.email}\n")

clientes = Cliente.carregar_todos()           # aula 17: a lista nasce cheia

def preencher_lista():                         # redesenha o Listbox a partir da lista
    lista.delete(0, tk.END)                    # aula 23: limpa o Listbox
    for c in clientes:                         # aula 18: percorre
        lista.insert(tk.END, str(c))           # aula 23: str(c) usa o __str__ (aula 03)
    rotulo_total.config(text=f"Total: {len(clientes)} cliente(s)")   # aula 18: len()

def ao_selecionar(evento):                     # aula 23: handler do bind
    selecao = lista.curselection()             # aula 23: indices marcados
    if selecao:
        cliente = clientes[selecao[0]]         # aula 23: indice do Listbox == indice da lista
        rotulo_status.config(text=f"Selecionado: {cliente.nome}", fg="black")   # aula 22

def remover():                                 # aula 21: observer do botao Remover
    selecao = lista.curselection()
    if not selecao:                            # aula 01: nada selecionado?
        rotulo_status.config(text="Selecione um cliente primeiro.", fg="red")
        return                                 # aula 14: sai antes de mexer na lista
    indice = selecao[0]
    cliente = clientes[indice]                 # aula 18: objeto pelo indice
    del clientes[indice]                       # aula 23: del lista[i]
    Cliente.salvar_todos(clientes)             # aula 23: reescreve o arquivo ("w")
    preencher_lista()                          # aula 23: redesenha
    rotulo_status.config(text=f"{cliente.nome} removido.", fg="green")

janela = tk.Tk()                               # aula 22: a tela
janela.title("Clientes")
janela.geometry("320x320")

rotulo_titulo = tk.Label(janela, text="Clientes cadastrados")
rotulo_titulo.pack(pady=5)

lista = tk.Listbox(janela, width=40, height=8)   # aula 23: o Listbox
lista.pack(padx=10, pady=5)
lista.bind("<<ListboxSelect>>", ao_selecionar)   # aula 23: bind

rotulo_total = tk.Label(janela, text="Total: 0 cliente(s)")
rotulo_total.pack()

rotulo_status = tk.Label(janela, text="")      # painel de avisos
rotulo_status.pack(pady=4)

botao_remover = tk.Button(janela, text="Remover selecionado", command=remover)  # aula 21
botao_remover.pack(pady=8)

preencher_lista()                              # aula 23: mostra o que veio do arquivo
janela.mainloop()                              # aula 22: loop de eventos
# cadastre pela EV2 (ou edite o clientes.txt), selecione uma linha e clique Remover:
# o cliente some da lista E do arquivo (salvar_todos reescreveu em "w")
Ele compartilha o clientes.txt com o gestor da EV2 — cadastre lá, remova aqui. Digite junto, não copie: os dedos aprendem o que o olho só reconhece.

resolução 1 · armadilha

Armadilha: índice do Listbox × índice da lista

O curselection() devolve um índice do LISTBOX. Ele só serve pra achar o objeto na sua lista Python se as duas estiverem na MESMA ordem — e é fácil quebrar isso sem perceber.

Mesma ordem — o índice casa

sincronizados
# preencher_lista() insere na ordem
# EXATA da lista clientes:
for c in clientes:
    lista.insert(tk.END, str(c))

# entao a linha 2 do Listbox e o
# clientes[2]. curselection()[0]
# vira o indice certo da lista:
indice = lista.curselection()[0]
cliente = clientes[indice]   # casa!

Ordens diferentes — o índice mente

dessincronizados
# se voce ORDENA so o Listbox
# (ou so a lista) sem redesenhar
# o outro, as ordens divergem.

# o usuario clica na linha 0 do
# Listbox, mas clientes[0] agora
# e OUTRO cliente -> voce remove
# o errado, em silencio.
#
# regra: mexeu na lista? chame
# preencher_lista() logo depois.
A defesa é simples: a lista Python é a fonte da verdade, e o Listbox é só o espelho dela. Toda vez que clientes muda (add, remove, sort), chame preencher_lista() pra redesenhar — assim o índice do Listbox nunca descola do índice da lista.

resolução 1 · o resultado

A janela do EV1

O Listbox listando os clientes, o total embaixo, o status e o botão Remover selecionado. Clique numa linha e depois em Remover — o cliente some da lista, do total e do arquivo.

Screenshot de uma janela Tkinter com o título 'Clientes': o texto 'Clientes cadastrados', uma lista (Listbox) com dois ou três clientes no formato 'nome <email>' e uma linha selecionada, abaixo o total de clientes, uma mensagem de status e o botão 'Remover selecionado'.
Se o clique não seleciona nada visível, confira que o bind está no Listbox (não na janela). Se remover apaga o cliente ERRADO, a lista e o Listbox estão em ordens diferentes — chame preencher_lista() depois de mexer na lista. Se some da tela mas volta ao reabrir, faltou o salvar_todos().

messagebox · o problema

messagebox: erros e confirmações

  • 🔇 Até agora, um erro virava texto vermelho num Label discreto — fácil de não ver. E uma ação destrutiva como EXCLUIR? Precisa PARAR o usuário e perguntar "tem certeza?" antes de apagar. Um Label não faz isso.
  • 💬 O Tkinter já traz caixas de diálogo PRONTAS no módulo messagebox: from tkinter import messagebox. São janelinhas modais — aparecem na frente, e o programa ESPERA a resposta antes de continuar.
  • 📢 As três de hoje: messagebox.showinfo(titulo, msg) (aviso), messagebox.showerror(titulo, msg) (erro), e a estrela: messagebox.askyesno(titulo, msg), que mostra Sim/Não e DEVOLVE um bool.
  • 🔀 O askyesno vira condição direto: if messagebox.askyesno(...):True no botão afirmativo (Sim/Yes), False no negativo (Não/No). O texto do botão depende do idioma do sistema (a imagem à frente mostra isso); o retorno, não — é sempre o bool da aula 01, agora vindo de um clique do usuário.

messagebox · em código

messagebox na prática: perguntar e avisar

Um botão dispara um askyesno: se o usuário confirma, um showinfo; se não, um showerror. Repare como o retorno booleano do askyesno entra direto no if.

messagebox.askyesno(...) devolve True/False — por isso cabe direto no if (aula 01). É a ponte entre um clique do usuário e uma decisão no seu código.
💬
showinfo e showerror só AVISAM (uma caixa com OK). Não devolvem nada útil — servem pra dar um retorno claro e impossível de ignorar, diferente do Label discreto.
dialogos.py
import tkinter as tk
from tkinter import messagebox               # aula 23 (NOVO): os dialogos prontos

def confirmar():                             # aula 22: callback do command=
    if messagebox.askyesno("Confirmar", "Deseja mesmo continuar?"):  # aula 23: askyesno -> bool
        messagebox.showinfo("Feito", "Continuando!")       # aula 23: showinfo, caixa de aviso
    else:                                    # aula 01: o bool alimenta o if/else
        messagebox.showerror("Cancelado", "Operacao cancelada.")   # aula 23: showerror, caixa de erro

janela = tk.Tk()                             # aula 22: janela raiz
janela.title("Dialogos")
janela.geometry("240x140")

botao = tk.Button(janela, text="Continuar", command=confirmar)   # aula 21: observer
botao.pack(pady=40)

janela.mainloop()                            # aula 22: loop de eventos
# responder "Sim": abre a caixa "Feito: Continuando!"
# responder "Nao": abre a caixa de erro "Cancelado"
Modal quer dizer que a caixa BLOQUEIA a janela de trás até você responder — o mainloop pausa naquela pergunta. É exatamente o que se quer antes de excluir algo: nada acontece enquanto o usuário não decide. Além dessas três, existem askokcancel, askquestion e a família filedialog (abrir/salvar arquivo) — citadas pra você ir além.

messagebox · o resultado

Um askyesno na tela

A caixa modal do askyesno: título, mensagem e dois botões, Sim e Não. Enquanto ela está aberta, a janela de trás fica bloqueada — o mainloop espera a resposta.

Screenshot de uma caixa de diálogo modal do Tkinter, com o título 'Confirmar', a mensagem 'Deseja mesmo continuar?' e dois botões em inglês, 'No' e 'Yes', sobre a janela principal ao fundo.
É essa caixa que vai proteger o Excluir do gestor: o del só acontece se o usuário clicar no botão afirmativo. E repare: aqui os botões saíram em INGLÊS, 'Yes/No'. Não é bug — os rótulos dos botões do messagebox vêm SEMPRE do idioma do sistema operacional, não do seu código; o askyesno não deixa você definir esse texto. Este computador está em inglês, então saiu 'Yes/No'; num sistema em português sairia 'Sim/Não'. O que NÃO muda é o retorno: o botão afirmativo devolve True, o negativo False.

stringvar · variável observável

StringVar: o rótulo que se atualiza sozinho

Ontem, atualizar um rótulo era rotulo.config(text=...) na mão, toda vez. O StringVar inverte: você liga o Label à variável uma vez, e mudar a variável com .set() redesenha o rótulo SOZINHO.

🔗
tk.StringVar() é uma variável OBSERVÁVEL; Label(..., textvariable=var) INSCREVE o rótulo nela. Chamou var.set(...)? Todo Label ligado se redesenha. É o Observer da aula 21 um nível acima do command=.
🎯
Útil quando VÁRIOS lugares mostram o mesmo valor: um .set() só atualiza todos de uma vez — sem caçar cada .config(). No gestor, o total de clientes vira um StringVar.
total_var.py
import tkinter as tk

itens = []                                    # aula 18: lista (o carrinho)

def adicionar():                              # aula 22: observer do botao
    itens.append(campo.get())                 # aula 18: append + aula 22: .get()
    total_texto.set(f"Itens no carrinho: {len(itens)}")   # aula 23: .set() atualiza o StringVar
    campo.delete(0, tk.END)                   # aula 22: limpa o Entry

janela = tk.Tk()                              # aula 22: janela raiz
janela.title("Carrinho")
janela.geometry("260x160")

total_texto = tk.StringVar()                  # aula 23 (NOVO): variavel de controle observavel
total_texto.set("Itens no carrinho: 0")       # aula 23: .set() define o valor

campo = tk.Entry(janela, width=20)            # aula 22: Entry
campo.pack(pady=8)
tk.Button(janela, text="Adicionar", command=adicionar).pack()   # aula 21: observer

# aula 23: textvariable liga o Label ao StringVar -- mudou a var, o Label se redesenha sozinho
tk.Label(janela, textvariable=total_texto).pack(pady=10)

janela.mainloop()                             # aula 22: loop de eventos
# cada clique com um item digitado incrementa: Itens no carrinho: 1, 2, 3...
Dá pra ir além: var.trace_add("write", callback) dispara uma função sua toda vez que a variável muda — o Observer explícito, com você escrevendo o observer. Existem também IntVar, DoubleVar e BooleanVar pros outros tipos. Hoje o StringVar já resolve o total da tela.

toplevel · o problema

Toplevel: uma segunda janela

  • ✏️ Pra EDITAR um cliente, o usuário precisa de campos com os dados atuais pra alterar. Fazer isso na janela principal atrapalha o cadastro e a lista. A saída: uma segunda janela, só pra edição — sem perder a principal.
  • 🪟 tk.Toplevel(pai) cria uma janela EXTRA, filha da principal. Tem .title() e .geometry() como a tk.Tk(), recebe widgets dentro (composição, aula 05) e some com .destroy() — mas NÃO leva outro mainloop(): o da app principal já cuida de todas.
  • ✍️ Pra edição, os campos precisam ABRIR preenchidos com o valor atual: campo.insert(0, cliente.nome) escreve no Entry a partir da posição 0. É o INVERSO do delete(0, tk.END) da aula 22 — um esvazia, o outro preenche.
  • 🧩 Nada de novo em POO: a Toplevel é mais um objeto que COMPÕE widgets (aula 05), e o botão Salvar dela é mais um Observer (aula 21). Só que agora a árvore de composição tem duas janelas.

toplevel · em código

Toplevel na prática: abrir e pré-preencher

Um botão na janela principal abre uma Toplevel com um campo já preenchido e um botão que a fecha. Repare: a segunda janela tem os mesmos métodos da raiz, mas NÃO tem mainloop() próprio.

🪟
tk.Toplevel(janela) nasce filha da principal e recebe widgets como qualquer container (composição, aula 05). segunda.destroy() fecha SÓ ela — a principal segue viva.
✍️
campo.insert(0, "...") escreve no Entry a partir do índice 0 — é como a edição vai ABRIR com os dados atuais. O par exato do delete(0, tk.END) que limpava o campo na aula 22.
toplevel_basico.py
import tkinter as tk

def abrir_janela():                           # aula 22: observer do botao
    segunda = tk.Toplevel(janela)             # aula 23 (NOVO): segunda janela, filha da principal
    segunda.title("Detalhes")
    segunda.geometry("220x120")
    campo = tk.Entry(segunda, width=20)       # aula 05: widgets DENTRO da Toplevel (composicao)
    campo.pack(pady=10)
    campo.insert(0, "texto pre-preenchido")   # aula 23 (NOVO): insert(0, ...) escreve no Entry
    tk.Button(segunda, text="Fechar", command=segunda.destroy).pack()   # aula 23: .destroy() fecha

janela = tk.Tk()                              # aula 22: a janela principal
janela.title("Principal")
janela.geometry("240x140")

tk.Button(janela, text="Abrir detalhes", command=abrir_janela).pack(pady=40)   # aula 21: observer

janela.mainloop()                             # aula 22: loop de eventos
# cada clique abre uma nova janela "Detalhes" sobre a principal
Uma Toplevel não leva mainloop() — o loop de eventos da app é UM só, o da janela raiz, e ele já despacha os eventos de todas as janelas. Criar uma segunda tk.Tk() (em vez de Toplevel) é erro clássico: dá duas raízes brigando. Segunda janela = Toplevel, sempre.

toplevel · o resultado

A janela Toplevel

A segunda janela (Detalhes) aberta sobre a principal, com o campo pré-preenchido pelo insert(0, ...) e o botão Fechar. As duas coexistem: fechar a Toplevel não fecha a principal.

Screenshot de duas janelas Tkinter: ao fundo a janela 'Principal' com um botão 'Abrir detalhes', e à frente uma janela menor 'Detalhes' com um campo de texto preenchido e um botão 'Fechar'.
É esse desenho que o gestor usa pra editar: a lista fica na janela principal, e o Editar abre uma Toplevel com os campos do cliente já preenchidos por insert(0, ...). Salvar grava e faz destroy(); a principal, atrás, se atualiza.

app-classe · o salto

A app vira uma classe: App(tk.Tk)

  • 🍝 Com Listbox, formulário, remover, editar, menu... o código de funções soltas e variáveis globais vira um novelo. Cada callback depende de nomes globais que estão espalhados pelo arquivo — difícil de ler, fácil de quebrar.
  • 🧬 A solução profissional: a app INTEIRA vira uma classe que herda de tk.Tk (aula 07). class App(tk.Tk): — agora self É a janela, e super().__init__() liga ao tk.Tk por baixo.
  • 📦 Tudo que era global vira ATRIBUTO: self.clientes, self.lista, self.campo_nome. E todo callback vira MÉTODO: self.adicionar, self.excluir. O estado e o comportamento moram juntos, no objeto (aula 02).
  • 🌱 Você já ensaiou isso: o Contador da aula 22 era o embrião — widgets como atributos, callback como método. Agora a própria janela é o objeto: app = App() monta tudo, app.mainloop() roda (método herdado).

app-classe · o molde

O esqueleto: class App(tk.Tk)

O Contador da aula 22, agora com a app INTEIRA herdando de tk.Tk. self é a própria janela: self.title(...), self.geometry(...), widgets como atributos e o callback como método. É o molde de toda GUI daqui pra frente.

🧬
class App(tk.Tk): + super().__init__() é a herança da aula 07: App É uma tk.Tk, então self.title(), self.config() e self.mainloop() são métodos herdados — você não os escreve, ganha de graça.
🔧
O pai de cada widget agora é self (a janela): tk.Label(self, ...). Estado em self.contador (atributo, aula 02), callback em self.contar (método) — a bagunça global vira um objeto organizado.
app_esqueleto.py
import tkinter as tk

# aula 07: a app INTEIRA herda de tk.Tk -- o Contador da aula 22 apontou o caminho
class App(tk.Tk):
    def __init__(self):
        super().__init__()                    # aula 07: super() chama o __init__ do tk.Tk
        self.title("Minha app")               # self E a janela: title() e metodo herdado
        self.geometry("260x160")
        self.contador = 0                     # aula 02: o estado vira atributo
        self.rotulo = tk.Label(self, text="Cliques: 0")   # aula 22: widget vira atributo, pai = self
        self.rotulo.pack(pady=20)
        self.botao = tk.Button(self, text="Contar", command=self.contar)   # aula 21: metodo como observer
        self.botao.pack()

    def contar(self):                         # aula 02: o callback vira METODO (carrega o self)
        self.contador += 1                    # aula 02: estado no atributo
        self.rotulo.config(text=f"Cliques: {self.contador}")   # aula 22: .config() na tela

app = App()                                   # instanciar a classe monta a janela
app.mainloop()                                # aula 22: mainloop agora e metodo herdado
# tres cliques: o rotulo vira "Cliques: 3"
super().__init__() é OBRIGATÓRIO e vem PRIMEIRO no __init__: é ele que inicializa a tk.Tk por baixo (sem isso, self ainda não é uma janela e criar widgets quebra). É a mesma regra da aula 07 — a subclasse chama o construtor da superclasse antes de acrescentar o seu.

app-classe · comparando

Global solto × App(tk.Tk)

O mesmo contador, dois estilos. À esquerda, funções e variáveis globais (o jeito da aula 22 pra telas pequenas); à direita, a app como classe (o jeito que escala pro CRUD de hoje).

Funções soltas + globais

antes
cliques = 0                  # global

def contar():
    global cliques           # precisa do global
    cliques += 1
    rotulo.config(
        text=f"Cliques: {cliques}")

janela = tk.Tk()
rotulo = tk.Label(janela, text="0")
rotulo.pack()
tk.Button(janela, text="Contar",
          command=contar).pack()
janela.mainloop()

class App(tk.Tk)

depois
class App(tk.Tk):
    def __init__(self):
        super().__init__()
        self.cliques = 0     # atributo
        self.rotulo = tk.Label(self,
            text="Cliques: 0")
        self.rotulo.pack()
        tk.Button(self, text="Contar",
            command=self.contar).pack()

    def contar(self):        # metodo
        self.cliques += 1
        self.rotulo.config(
            text=f"Cliques: {self.cliques}")

App().mainloop()
À esquerda, o estado é uma global que exige global cliques pra alterar; à direita, é self.cliques — sempre acessível, sem palavra mágica. Pra uma janelinha, o estilo solto serve; pra um CRUD com dezenas de widgets e callbacks, a classe é o que mantém tudo organizado. É a arquitetura do gestor da EV2.

app-classe · menu

Menu bar: a barra Arquivo | Ajuda

Toda app de verdade tem uma barra de menu no topo. No Tkinter é o tk.Menu: uma barra com submenus em cascata, cada item registrando um command= — o mesmo Observer da aula 21, agora num menu.

📊
Três peças: tk.Menu(self) é a barra; tk.Menu(barra, tearoff=0) é cada submenu; barra.add_cascade(label=, menu=) pendura o submenu na barra; sub.add_command(label=, command=) põe cada item clicável.
🔌
self.config(menu=barra) INSTALA a barra na janela — sem essa linha, o menu existe mas não aparece. E cada command= é um observer (aula 21): command=self.destroy fecha, command=self.sobre abre a caixa.
menu_bar.py
import tkinter as tk
from tkinter import messagebox               # aula 23: para o "Sobre"

class App(tk.Tk):                            # aula 07: herda de tk.Tk
    def __init__(self):
        super().__init__()                   # aula 07: super()
        self.title("App com menu")
        self.geometry("300x160")
        self._montar_menu()                  # aula 02: metodo organiza a montagem
        rotulo = tk.Label(self, text="Use o menu acima ↑")   # aula 22: Label preenche o corpo da janela
        rotulo.pack(expand=True)                              # aula 22: expand=True centraliza no espaço livre

    def _montar_menu(self):
        barra = tk.Menu(self)                # aula 23 (NOVO): a barra de menu
        menu_arquivo = tk.Menu(barra, tearoff=0)   # aula 23: um submenu (tearoff=0 tira o tracinho)
        menu_arquivo.add_command(label="Sair", command=self.destroy)   # aula 21: item -> command=
        barra.add_cascade(label="Arquivo", menu=menu_arquivo)   # aula 23: pendura o submenu na barra
        menu_ajuda = tk.Menu(barra, tearoff=0)
        menu_ajuda.add_command(label="Sobre", command=self.sobre)
        barra.add_cascade(label="Ajuda", menu=menu_ajuda)
        self.config(menu=barra)              # aula 23: instala a barra na janela

    def sobre(self):                         # aula 21: observer do item Sobre
        messagebox.showinfo("Sobre", "App de exemplo -- aula 23.")   # aula 23: messagebox

app = App()
app.mainloop()
# a janela abre com a barra "Arquivo | Ajuda"
# no corpo, o rótulo "Use o menu acima ↑" preenche a janela
# "Arquivo > Sair" fecha; "Ajuda > Sobre" abre a caixa de aviso
tearoff=0 remove a linha pontilhada no topo do submenu (um recurso antigo do Tk que 'destaca' o menu numa janelinha) — sempre passe tearoff=0 pra ficar com cara moderna. Os itens do menu chamam os MESMOS métodos que os botões chamariam: um menu é só outra porta pro mesmo observer.

ao vivo 2 · vamos construir juntos

Exercício ao Vivo 2: o gestor de clientes

O grande final: um gestor de clientes com o CRUD visual COMPLETO — criar, listar, editar e excluir — escrito como class App(tk.Tk), com barra de Menu e persistência em disco. Reúne TUDO: Listbox, bind, messagebox, StringVar, Toplevel, herança. Em quatro camadas, uma letra do CRUD por vez.

1

Camada 1 — a app como classe (READ)

class App(tk.Tk) com super().__init__(): carrega os clientes do arquivo (aula 17), guarda em self.clientes, monta o Listbox e o atualizar_lista(). A janela já abre listando o que estava salvo.

2

Camada 2 — adicionar() (CREATE)

O formulário (Nome, E-mail) num Frame com grid, e o adicionar(): valida com messagebox.showerror, faz append, salvar_todos() e redesenha. O total vira um StringVar que se atualiza sozinho.

3

Camada 3 — excluir() (DELETE)

Um auxiliar _indice_selecionado() (reusado por editar e excluir), e o excluir(): pega a seleção, PERGUNTA com messagebox.askyesno, e só então del + salvar_todos(). Nada some sem confirmação.

4

Camada 4 — editar() + Menu (UPDATE)

O editar() abre um Toplevel com os campos pré-preenchidos (insert(0, ...)); salvar altera os atributos do objeto, salvar_todos() e destroy(). Mais a barra de Menu (Arquivo, Ajuda). CRUD fechado.

resolução 2 · camada 1

Camada 1: class App(tk.Tk) — janela, Listbox, carregar (READ)

O esqueleto do gestor: a Cliente com persistência (a mesma da EV1) e a App(tk.Tk) que carrega os clientes e os mostra num Listbox. Só o READ do CRUD — o formulário e os botões entram nas próximas camadas.

🧬
class App(tk.Tk) + super().__init__(): a app É a janela (aula 07). O __init__ vira o roteiro da montagem — carrega o estado (self.clientes), cria o StringVar e chama os métodos que montam a tela.
📂
self.clientes = Cliente.carregar_todos() é o READ: a lista nasce cheia do clientes.txt (aula 17). atualizar_lista() despeja essa lista no Listbox — o mesmo limpar-e-reinserir da EV1, agora como método.
gestor_clientes.py
import tkinter as tk
from tkinter import messagebox
from pathlib import Path

PASTA = Path(__file__).parent      # aula 17
ARQUIVO = PASTA / "clientes.txt"   # aula 17

# a MESMA Cliente da EV1: carregar_todos + salvar_todos (aula 17)
class Cliente:
    def __init__(self, nome, email):
        self.nome = nome
        self.email = email

    def __str__(self):                            # aula 03
        return f"{self.nome} <{self.email}>"

    @classmethod
    def carregar_todos(cls, caminho=ARQUIVO):     # aula 17: arquivo -> lista
        clientes = []
        try:
            with open(caminho, "r", encoding="utf-8") as f:
                for linha in f:
                    nome, email = linha.strip().split(";")   # aula 18
                    clientes.append(cls(nome, email))        # aula 18
        except FileNotFoundError:
            pass
        return clientes

    @classmethod
    def salvar_todos(cls, clientes, caminho=ARQUIVO):   # aula 23: reescreve em "w"
        with open(caminho, "w", encoding="utf-8") as f:
            for c in clientes:
                f.write(f"{c.nome};{c.email}\n")

# Camada 1: a app INTEIRA vira classe (aula 07: herda de tk.Tk)
class App(tk.Tk):
    def __init__(self):
        super().__init__()                        # aula 07: super() liga ao tk.Tk
        self.title("Gestor de clientes")          # self E a janela
        self.geometry("380x460")
        self.clientes = Cliente.carregar_todos()  # aula 17: READ -- a lista nasce cheia
        self.total_texto = tk.StringVar()         # aula 23: StringVar do total
        self._montar_widgets()                    # aula 02: metodos organizam a montagem
        self.atualizar_lista()                    # aula 23: desenha a lista inicial

    def _montar_widgets(self):                    # (o formulario e os botoes entram nas proximas camadas)
        self.lista = tk.Listbox(self, width=46, height=10)   # aula 23: o Listbox (atributo)
        self.lista.pack(padx=10, pady=5)
        tk.Label(self, textvariable=self.total_texto).pack()  # aula 23: Label ligado ao StringVar

    def atualizar_lista(self):                    # aula 23: redesenha o Listbox (READ)
        self.lista.delete(0, tk.END)              # aula 23: limpa tudo
        for c in self.clientes:                   # aula 18: percorre
            self.lista.insert(tk.END, str(c))     # aula 23: __str__ por linha (aula 03)
        self.total_texto.set(f"Total: {len(self.clientes)} cliente(s)")   # aula 18: len()

app = App()
app.mainloop()
# a janela abre ja listando o que estava no clientes.txt
Repare no desenho: __init__ não faz o trabalho todo — ele DELEGA pra métodos (_montar_widgets, atualizar_lista). Isso mantém o construtor legível e cada método com uma responsabilidade. É o mesmo princípio de organização das aulas 02 e 05, agora guiando a estrutura de uma app inteira.

resolução 2 · camada 2

Camada 2: adicionar() inline + total via StringVar (CREATE)

O CREATE: o formulário entra num Frame com grid, e o adicionar() valida com messagebox, cria o objeto, grava e redesenha. O total de clientes agora é um StringVar — o rótulo se atualiza sozinho.

adicionar() é o observer do botão (aula 21): valida ANTES (aula 14) com messagebox.showerror, append na lista (aula 18), salvar_todos() no disco (aula 17), redesenha e limpa os campos com delete(0, tk.END) (aula 22).
🧩
O formulário usa grid DENTRO de quadro_form (um Frame), e o Listbox usa pack no self. Containers diferentes, gerentes diferentes — a regra de ouro da aula 22, sem mistura.
gestor_clientes.py
# Camada 2: CREATE -- o formulario e o adicionar()
class App(tk.Tk):
    # ... __init__ e atualizar_lista da Camada 1 ...

    def _montar_widgets(self):                    # agora com o formulario em cima
        quadro_form = tk.Frame(self)              # aula 05: Frame agrupa (composicao)
        quadro_form.pack(pady=8)
        tk.Label(quadro_form, text="Nome:").grid(row=0, column=0, sticky="e", padx=4, pady=4)   # aula 22: grid DENTRO do Frame
        self.campo_nome = tk.Entry(quadro_form, width=24)
        self.campo_nome.grid(row=0, column=1, padx=4, pady=4)
        tk.Label(quadro_form, text="E-mail:").grid(row=1, column=0, sticky="e", padx=4, pady=4)
        self.campo_email = tk.Entry(quadro_form, width=24)
        self.campo_email.grid(row=1, column=1, padx=4, pady=4)
        tk.Button(quadro_form, text="Adicionar", command=self.adicionar).grid(row=2, column=0, columnspan=2, pady=6)   # aula 21: observer
        self.lista = tk.Listbox(self, width=46, height=10)   # aula 23: Listbox (pack no self)
        self.lista.pack(padx=10, pady=5)
        tk.Label(self, textvariable=self.total_texto).pack()

    def adicionar(self):                          # aula 21: observer do botao Adicionar (CREATE)
        nome = self.campo_nome.get()              # aula 22: .get()
        email = self.campo_email.get()
        if nome == "" or email == "":             # aula 14: valida ANTES de criar
            messagebox.showerror("Campos vazios", "Preencha nome e e-mail.")   # aula 23
            return
        self.clientes.append(Cliente(nome, email))   # aula 18: append na lista
        Cliente.salvar_todos(self.clientes)       # aula 23: reescreve o arquivo ("w")
        self.atualizar_lista()                    # aula 23: redesenha
        self.campo_nome.delete(0, tk.END)         # aula 22: limpa os campos
        self.campo_email.delete(0, tk.END)
O total virou self.total_texto (um StringVar) lá na Camada 1, e o Label foi ligado a ele com textvariable=. Por isso adicionar() não precisa mexer no rótulo do total: atualizar_lista() chama total_texto.set(...) e o Label se redesenha sozinho. Menos .config() espalhado.

resolução 2 · camada 3

Camada 3: excluir() com askyesno + salvar_todos (DELETE)

O DELETE, agora com rede de segurança: antes de apagar, o askyesno PERGUNTA. E um método auxiliar, _indice_selecionado(), centraliza a checagem de seleção que o excluir e o editar vão reusar.

🛑
if not messagebox.askyesno(...): return — a exclusão só passa se o usuário clicar Sim. O bool do diálogo (aula 01) barrando a ação destrutiva. Só então del self.clientes[indice] + salvar_todos().
♻️
_indice_selecionado() é um método auxiliar (aula 02): devolve o índice marcado OU mostra um showerror e retorna None. Excluir e editar chamam o MESMO método — sem repetir a checagem de seleção nos dois.
gestor_clientes.py
# Camada 3: DELETE -- excluir() com confirmacao
class App(tk.Tk):
    # ... Camadas 1 e 2 ...

    def _indice_selecionado(self):                # aula 02: metodo auxiliar reutilizavel
        selecao = self.lista.curselection()       # aula 23: indices marcados
        if not selecao:                           # aula 01: nada selecionado?
            messagebox.showerror("Sem selecao", "Selecione um cliente na lista.")   # aula 23
            return None
        return selecao[0]                         # aula 23: indice do Listbox == indice da lista

    def excluir(self):                            # aula 21: observer do botao Excluir (DELETE)
        indice = self._indice_selecionado()       # reaproveita o auxiliar
        if indice is None:                        # aula 01: sem selecao, desiste
            return
        cliente = self.clientes[indice]           # aula 18: pega o objeto pelo indice
        if not messagebox.askyesno("Excluir", f"Excluir {cliente.nome}?"):   # aula 23: askyesno -> bool
            return                                # respondeu "Nao": nao faz nada
        del self.clientes[indice]                 # aula 23: del lista[i] remove pelo indice
        Cliente.salvar_todos(self.clientes)       # aula 23: reescreve o arquivo ("w")
        self.atualizar_lista()                    # aula 23: redesenha
O underscore em _indice_selecionado é convenção: sinaliza 'método interno, de uso da própria classe' (a mesma ideia do _montar_widgets). Python não impede de chamar de fora, mas o _ avisa: isto é engrenagem interna, não a interface pública da App. Reaproveitar em vez de copiar é a lição de sempre.

resolução 2 · camada 4

Camada 4: editar() abre Toplevel + Menu bar (UPDATE)

O UPDATE fecha o CRUD: editar() abre um Toplevel com os campos já preenchidos, e o Salvar de lá altera o objeto e regrava. Mais a barra de Menu. Repare no salvar_edicao() — uma função DENTRO do método, que enxerga o cliente.

✏️
editar() abre um Toplevel (aula 23) e pré-preenche com insert(0, cliente.nome). O salvar_edicao() altera os ATRIBUTOS do objeto (cliente.nome = ..., aula 02), chama salvar_todos() e fecha com destroy().
📊
_montar_menu() monta a barra (aula 23): Arquivo → Sair (self.destroy), Ajuda → Sobre (self.sobre). Os itens do menu chamam os mesmos métodos que um botão chamaria — só outra porta pro observer.
gestor_clientes.py
# Camada 4: UPDATE -- editar() abre um Toplevel; e a barra de Menu
class App(tk.Tk):
    # ... Camadas 1 a 3 (e __init__ chama self._montar_menu()) ...

    def _montar_menu(self):                       # aula 23: a barra de menu
        barra = tk.Menu(self)
        menu_arquivo = tk.Menu(barra, tearoff=0)
        menu_arquivo.add_command(label="Sair", command=self.destroy)   # aula 21: item -> command=
        barra.add_cascade(label="Arquivo", menu=menu_arquivo)
        menu_ajuda = tk.Menu(barra, tearoff=0)
        menu_ajuda.add_command(label="Sobre", command=self.sobre)
        barra.add_cascade(label="Ajuda", menu=menu_ajuda)
        self.config(menu=barra)                   # aula 23: instala a barra

    def sobre(self):
        messagebox.showinfo("Sobre", "Gestor de clientes -- aula 23.")   # aula 23

    def editar(self):                             # aula 21: observer do botao Editar (UPDATE)
        indice = self._indice_selecionado()
        if indice is None:
            return
        cliente = self.clientes[indice]           # aula 18: o objeto selecionado
        janela_edicao = tk.Toplevel(self)         # aula 23: segunda janela
        janela_edicao.title("Editar cliente")
        janela_edicao.geometry("300x150")
        tk.Label(janela_edicao, text="Nome:").grid(row=0, column=0, sticky="e", padx=5, pady=5)   # grid DENTRO da Toplevel
        campo_nome = tk.Entry(janela_edicao, width=24)
        campo_nome.grid(row=0, column=1, padx=5, pady=5)
        campo_nome.insert(0, cliente.nome)        # aula 23: insert(0, ...) pre-preenche
        tk.Label(janela_edicao, text="E-mail:").grid(row=1, column=0, sticky="e", padx=5, pady=5)
        campo_email = tk.Entry(janela_edicao, width=24)
        campo_email.grid(row=1, column=1, padx=5, pady=5)
        campo_email.insert(0, cliente.email)

        def salvar_edicao():                      # aula 21: observer do Salvar da Toplevel
            novo_nome = campo_nome.get()          # aula 22: .get()
            novo_email = campo_email.get()
            if novo_nome == "" or novo_email == "":   # aula 14: valida
                messagebox.showerror("Campos vazios", "Preencha nome e e-mail.")
                return
            cliente.nome = novo_nome              # aula 02: edita o atributo do objeto
            cliente.email = novo_email
            Cliente.salvar_todos(self.clientes)   # aula 23: PRECISA reescrever! (a armadilha)
            self.atualizar_lista()                # aula 23: redesenha
            janela_edicao.destroy()               # aula 23: fecha a Toplevel

        tk.Button(janela_edicao, text="Salvar", command=salvar_edicao).grid(row=2, column=0, columnspan=2, pady=10)
salvar_edicao() é definida DENTRO de editar() de propósito: assim ela enxerga o cliente, campo_nome e janela_edicao daquela edição específica (é um closure — a função lembra o ambiente onde nasceu). É o mesmo mecanismo do lambda da aula 18/22, só que com nome e várias linhas.

resolução 2 · o código completo

Tudo junto: gestor_clientes.py

As quatro camadas no arquivo final: o CRUD visual completo como class App(tk.Tk), com Menu, Toplevel de edição, confirmação de exclusão e persistência. É o retrato de tudo que a aula 22 e a 23 construíram.

🗺️
Um mapa das duas aulas: herança de tk.Tk (07), Listbox + bind + messagebox + StringVar + Toplevel (23), lista de objetos (18), persistência em "w" (17) e cada callback um Observer (21). Nada que você não tenha visto.
O CRUD fechado: Create (adicionar), Read (atualizar_lista), Update (editar), Delete (excluir). Adicione, edite, exclua, feche e reabra — tudo continua, porque cada operação chama salvar_todos().
gestor_clientes.py
import tkinter as tk
from tkinter import messagebox
from pathlib import Path

PASTA = Path(__file__).parent      # aula 17
ARQUIVO = PASTA / "clientes.txt"   # aula 17

class Cliente:                                # o modelo, com persistencia (aula 17)
    def __init__(self, nome, email):
        self.nome = nome
        self.email = email

    def __str__(self):                        # aula 03
        return f"{self.nome} <{self.email}>"

    @classmethod
    def carregar_todos(cls, caminho=ARQUIVO):   # aula 17: arquivo -> lista
        clientes = []
        try:
            with open(caminho, "r", encoding="utf-8") as f:
                for linha in f:
                    nome, email = linha.strip().split(";")   # aula 18
                    clientes.append(cls(nome, email))        # aula 18
        except FileNotFoundError:
            pass
        return clientes

    @classmethod
    def salvar_todos(cls, clientes, caminho=ARQUIVO):   # aula 23: reescreve TUDO em "w"
        with open(caminho, "w", encoding="utf-8") as f:
            for c in clientes:
                f.write(f"{c.nome};{c.email}\n")

class App(tk.Tk):                            # aula 07: a app inteira herda de tk.Tk
    def __init__(self):
        super().__init__()                   # aula 07: super()
        self.title("Gestor de clientes")
        self.geometry("380x460")
        self.clientes = Cliente.carregar_todos()   # aula 17: READ
        self.total_texto = tk.StringVar()    # aula 23: StringVar do total
        self._montar_menu()                  # aula 23: a barra de menu
        self._montar_widgets()               # aula 02: monta os widgets
        self.atualizar_lista()               # aula 23: desenha a lista inicial

    def _montar_menu(self):                  # aula 23: barra de menu
        barra = tk.Menu(self)
        menu_arquivo = tk.Menu(barra, tearoff=0)
        menu_arquivo.add_command(label="Sair", command=self.destroy)   # aula 21
        barra.add_cascade(label="Arquivo", menu=menu_arquivo)
        menu_ajuda = tk.Menu(barra, tearoff=0)
        menu_ajuda.add_command(label="Sobre", command=self.sobre)
        barra.add_cascade(label="Ajuda", menu=menu_ajuda)
        self.config(menu=barra)              # aula 23: instala a barra

    def _montar_widgets(self):               # aula 22: os widgets como atributos
        quadro_form = tk.Frame(self)         # aula 05: Frame agrupa (composicao)
        quadro_form.pack(pady=8)
        tk.Label(quadro_form, text="Nome:").grid(row=0, column=0, sticky="e", padx=4, pady=4)   # grid DENTRO do Frame
        self.campo_nome = tk.Entry(quadro_form, width=24)
        self.campo_nome.grid(row=0, column=1, padx=4, pady=4)
        tk.Label(quadro_form, text="E-mail:").grid(row=1, column=0, sticky="e", padx=4, pady=4)
        self.campo_email = tk.Entry(quadro_form, width=24)
        self.campo_email.grid(row=1, column=1, padx=4, pady=4)
        tk.Button(quadro_form, text="Adicionar", command=self.adicionar).grid(row=2, column=0, columnspan=2, pady=6)   # aula 21
        self.lista = tk.Listbox(self, width=46, height=10)   # aula 23: Listbox (pack no self)
        self.lista.pack(padx=10, pady=5)
        tk.Label(self, textvariable=self.total_texto).pack()   # aula 23: Label ligado ao StringVar
        quadro_acoes = tk.Frame(self)        # aula 05: outro Frame, para as acoes
        quadro_acoes.pack(pady=8)
        tk.Button(quadro_acoes, text="Editar", command=self.editar).pack(side="left", padx=5)    # aula 21
        tk.Button(quadro_acoes, text="Excluir", command=self.excluir).pack(side="left", padx=5)  # aula 21

    def atualizar_lista(self):               # aula 23: redesenha o Listbox (READ)
        self.lista.delete(0, tk.END)         # aula 23: limpa tudo
        for c in self.clientes:              # aula 18: percorre
            self.lista.insert(tk.END, str(c))   # aula 23: __str__ por linha (aula 03)
        self.total_texto.set(f"Total: {len(self.clientes)} cliente(s)")   # aula 18: len()

    def _indice_selecionado(self):           # aula 02: auxiliar reutilizavel
        selecao = self.lista.curselection()  # aula 23: indices marcados
        if not selecao:                      # aula 01
            messagebox.showerror("Sem selecao", "Selecione um cliente na lista.")   # aula 23
            return None
        return selecao[0]                    # aula 23: indice do Listbox == indice da lista

    def adicionar(self):                     # aula 21: observer -- CREATE
        nome = self.campo_nome.get()         # aula 22: .get()
        email = self.campo_email.get()
        if nome == "" or email == "":        # aula 14: valida antes de criar
            messagebox.showerror("Campos vazios", "Preencha nome e e-mail.")
            return
        self.clientes.append(Cliente(nome, email))   # aula 18: append
        Cliente.salvar_todos(self.clientes)  # aula 23: reescreve o arquivo ("w")
        self.atualizar_lista()               # aula 23: redesenha
        self.campo_nome.delete(0, tk.END)    # aula 22: limpa os campos
        self.campo_email.delete(0, tk.END)

    def excluir(self):                       # aula 21: observer -- DELETE
        indice = self._indice_selecionado()
        if indice is None:
            return
        cliente = self.clientes[indice]      # aula 18: objeto pelo indice
        if not messagebox.askyesno("Excluir", f"Excluir {cliente.nome}?"):   # aula 23: askyesno -> bool
            return
        del self.clientes[indice]            # aula 23: del lista[i]
        Cliente.salvar_todos(self.clientes)  # aula 23: reescreve ("w")
        self.atualizar_lista()

    def editar(self):                        # aula 21: observer -- UPDATE
        indice = self._indice_selecionado()
        if indice is None:
            return
        cliente = self.clientes[indice]      # aula 18: objeto selecionado
        janela_edicao = tk.Toplevel(self)    # aula 23: segunda janela
        janela_edicao.title("Editar cliente")
        janela_edicao.geometry("300x150")
        tk.Label(janela_edicao, text="Nome:").grid(row=0, column=0, sticky="e", padx=5, pady=5)   # grid DENTRO da Toplevel
        campo_nome = tk.Entry(janela_edicao, width=24)
        campo_nome.grid(row=0, column=1, padx=5, pady=5)
        campo_nome.insert(0, cliente.nome)   # aula 23: insert(0, ...) pre-preenche
        tk.Label(janela_edicao, text="E-mail:").grid(row=1, column=0, sticky="e", padx=5, pady=5)
        campo_email = tk.Entry(janela_edicao, width=24)
        campo_email.grid(row=1, column=1, padx=5, pady=5)
        campo_email.insert(0, cliente.email)

        def salvar_edicao():                 # aula 21: observer do Salvar da Toplevel
            novo_nome = campo_nome.get()     # aula 22: .get()
            novo_email = campo_email.get()
            if novo_nome == "" or novo_email == "":   # aula 14: valida
                messagebox.showerror("Campos vazios", "Preencha nome e e-mail.")
                return
            cliente.nome = novo_nome         # aula 02: edita o atributo
            cliente.email = novo_email
            Cliente.salvar_todos(self.clientes)   # aula 23: PRECISA reescrever! (a armadilha)
            self.atualizar_lista()
            janela_edicao.destroy()          # aula 23: fecha a Toplevel

        tk.Button(janela_edicao, text="Salvar", command=salvar_edicao).grid(row=2, column=0, columnspan=2, pady=10)

    def sobre(self):
        messagebox.showinfo("Sobre", "Gestor de clientes -- aula 23.")   # aula 23

app = App()                                  # instanciar monta a janela inteira
app.mainloop()                               # aula 22: mainloop herdado de tk.Tk
# adicione, selecione e clique Editar/Excluir; feche e reabra: os dados continuam
# (carregar_todos leu o clientes.txt na abertura)
Compartilha o clientes.txt com a EV1 (clientes_lista.py). São ~90 linhas — mas cada trecho é uma peça que você já montou isolada nas camadas. Digite junto, não copie: os dedos aprendem o que o olho só reconhece.

resolução 2 · armadilha

Armadilha: editar o objeto e esquecer o salvar_todos

O bug mais sorrateiro do CRUD: você altera o atributo do objeto, a tela mostra o valor novo... mas o arquivo continua com o antigo. Some no fechar — e ninguém percebe até reabrir.

Só mexeu no objeto — o disco não sabe

esqueceu
cliente.nome = novo_nome
cliente.email = novo_email
self.atualizar_lista()
# a TELA mostra o nome novo...
# mas o clientes.txt continua com
# o nome ANTIGO. Fechou e reabriu?
# carregar_todos() le o arquivo
# -> a edicao SUMIU.

Alterou e REGRAVOU

certo
cliente.nome = novo_nome
cliente.email = novo_email
Cliente.salvar_todos(self.clientes)
self.atualizar_lista()
# regrava o arquivo com a lista
# ja editada -> a mudanca
# sobrevive ao fechar.
A regra que vale pro CRUD inteiro: toda alteração na lista (add, edit, del) tem que chamar salvar_todos(). A lista viva na memória e o arquivo no disco precisam andar em sincronia — mexeu numa, regrava o outro. É a versão-update da armadilha "a vs w" da aula 22.

resolução 2 · o resultado

A janela principal do gestor

O gestor completo: barra de Menu no topo, o formulário (Nome, E-mail) com Adicionar, o Listbox com os clientes, o total (via StringVar) e os botões Editar e Excluir.

Screenshot da janela 'Gestor de clientes': no topo uma barra de menu com 'Arquivo' e 'Ajuda'; abaixo um formulário com campos Nome e E-mail e um botão Adicionar; no meio uma lista (Listbox) com vários clientes e o total abaixo; e na base dois botões, 'Editar' e 'Excluir'.
Cada elemento é um pedaço da aula: o Menu e o Listbox da 23, o formulário em grid da 22, o total em StringVar da 23. Selecione um cliente e clique Excluir pra ver o askyesno; clique Editar pra abrir o Toplevel da próxima imagem.

resolução 2 · o resultado

O Toplevel de edição

Ao clicar em Editar com um cliente selecionado, abre a segunda janela — os campos já preenchidos com os dados atuais (via insert(0, ...)). Salvar altera o objeto, regrava e fecha; a lista principal se atualiza.

Screenshot de duas janelas: ao fundo o 'Gestor de clientes' com a lista, e à frente a janela menor 'Editar cliente' com os campos Nome e E-mail preenchidos com os dados de um cliente e um botão 'Salvar'.
Os campos abrem PREENCHIDOS — é o insert(0, ...) da Toplevel colocando o valor atual pra você só ajustar o que mudou. Se abrirem vazios, faltou o insert; se salvar não muda a lista de trás, faltou o atualizar_lista() depois do salvar_todos().

multi-tela · o problema

Um app, várias telas: o problema

  • 🧩 Até aqui, uma janela = uma tela. Mas um sistema de verdade tem VÁRIAS: clientes, pizzas, relatórios... Empilhar tudo numa janela só vira um novelo; abrir uma Toplevel pra cada não é navegação, é poluição de janelas. Falta um jeito de TROCAR de tela no mesmo lugar.
  • 🖼️ A saída (parte 1): dar a cada TELA a sua própria classe — class TelaClientes(tk.Frame). É o MESMO salto do App(tk.Tk) (s25), um nível ABAIXO: em vez da janela inteira, cada PEDAÇO da janela vira um objeto que monta os próprios widgets (composição, aula 05).
  • 🃏 A saída (parte 2): as telas moram na MESMA célula de um container (todas em grid(row=0, column=0)), empilhadas como cartas. .tkraise() traz uma à FRENTE — trocar de tela é só levantar a carta certa. O nome disso é um app multi-tela.
  • 📣 E a navegação? Um botão Clientes e um item de menu Clientes vão chamar o MESMO método (mostrar_clientes). Duas portas, um só observer (aula 21) — exatamente como o botão e o menu da EV2 já apontavam pro mesmo método.

multi-tela · primeiro código

duas_telas.py: empilhar e alternar com tkraise

Duas telas (Frames) na MESMA célula do container, empilhadas. Dois botões trocam qual está na frente com tkraise() — o esqueleto de todo app multi-tela, sem nada além de Frame, grid e herança.

🃏
As duas telas são Frames no MESMO grid(row=0, column=0) — empilhadas. tkraise() (aula 23, NOVO) traz uma à frente; a outra fica atrás. Trocar de tela = levantar a carta certa, sem esconder nem recriar nada.
🧬
A app é uma class App(tk.Tk) (aula 07, como no s25): self é a janela, e mostrar_a/mostrar_b são métodos-observer dos botões (aula 21) — cada um só chama tkraise() na sua tela.
duas_telas.py
import tkinter as tk

class App(tk.Tk):  # aula 07: a app herda de tk.Tk
    def __init__(self):
        super().__init__()  # aula 07: super() liga ao tk.Tk
        self.title("Duas telas")
        self.geometry("300x200")

        quadro_botoes = tk.Frame(self)  # aula 05: um Frame para os botoes
        quadro_botoes.pack(pady=8)
        tk.Button(quadro_botoes, text="Tela A", command=self.mostrar_a).pack(side="left", padx=5)  # aula 21: botao -> observer (command=)
        tk.Button(quadro_botoes, text="Tela B", command=self.mostrar_b).pack(side="left", padx=5)  # aula 21: o outro botao

        container = tk.Frame(self)  # aula 05: o container das telas
        container.pack(fill="both", expand=True)
        self.tela_a = tk.Frame(container, bg="#cfe8ff")  # aula 23 (NOVO): cada tela e um Frame
        self.tela_b = tk.Frame(container, bg="#ffe0cf")  # a segunda tela, irma
        tk.Label(self.tela_a, text="Sou a Tela A", bg="#cfe8ff").pack(pady=40)  # aula 22: Label dentro da tela (composicao, aula 05)
        tk.Label(self.tela_b, text="Sou a Tela B", bg="#ffe0cf").pack(pady=40)
        self.tela_a.grid(row=0, column=0, sticky="nsew")  # aula 23 (NOVO): as duas na MESMA celula do grid
        self.tela_b.grid(row=0, column=0, sticky="nsew")  # empilhada na mesma celula

        self.mostrar_a()  # abre mostrando a Tela A

    def mostrar_a(self):
        self.tela_a.tkraise()  # aula 23 (NOVO): tkraise() traz a Tela A a frente

    def mostrar_b(self):
        self.tela_b.tkraise()  # aula 23: tkraise() traz a Tela B a frente

app = App()
app.mainloop()  # aula 22: loop de eventos
# clicar em "Tela B" traz a tela laranja a frente; "Tela A", a azul
Por que as duas telas ficam no MESMO grid(row=0, column=0)? Porque assim ocupam a mesma célula — uma cobre a outra, como cartas empilhadas. Com pack elas ficariam uma EMBAIXO da outra, as duas visíveis, e o tkraise() não teria o que esconder. Mesma célula + tkraise() é o par que faz a troca de telas.

ao vivo 3 · vamos construir juntos

Exercício ao Vivo 3: o app de cadastros

O fecho da aula: juntar clientes E pizzas num app de VÁRIAS telas. Cada cadastro vira uma class TelaX(tk.Frame) (o gestor da EV2 virado Frame), e uma App(tk.Tk) alterna entre elas com tkraise(), botões e menu. Em camadas, uma por vez.

1

Camada 1 — os dois modelos: Cliente + Pizza

As duas classes com persistência (aula 17): Cliente (nome, e-mail) e Pizza (sabor, preço), cada uma com carregar_todos()/salvar_todos() no seu próprio arquivo. A Pizza é a IRMÃ da Cliente — o mesmo molde, outros campos.

2

Camada 2 — TelaClientes(tk.Frame)

O gestor da EV2 INTEIRO, mas herdando de tk.Frame em vez de tk.Tk: formulário, Listbox, total em StringVar, adicionar/editar/excluir com messagebox e Toplevel. Uma tela autônoma, dona do próprio CRUD.

3

Camada 3 — TelaPizzas(tk.Frame), a irmã

A mesma estrutura da TelaClientes, agora com campos Sabor e Preço. Não uma classe genérica com parâmetros — duas telas irmãs, cada uma explícita. Repetição saudável: você VÊ o padrão se repetir.

4

Camada 4 — App(tk.Tk): container, tkraise, botões e menu

A App(tk.Tk) instancia as duas telas na MESMA célula do grid, e alterna com tkraise(). Botões Clientes/Pizzas e um menu Cadastros chamam os MESMOS métodos — duas portas pro mesmo observer.

resolução 3 · camada 1

Camada 1 — os dois modelos: Cliente + Pizza

As duas classes de dados, cada uma com sua persistência (aula 17). A Cliente é a mesma das EVs anteriores; a Pizza é a IRMÃ dela — o mesmo molde, com sabor e preco e o seu próprio arquivo.

📂
Cada classe tem carregar_todos() e salvar_todos() (aula 17): lê e regrava a lista de objetos no seu arquivo (clientes.txt, pizzas.txt). O salvar_todos() reescreve tudo em "w", como nas EVs 1 e 2.
👯
A Pizza é a IRMÃ da Cliente: mesmo molde (aula 02), campos sabor/preco. O preco fica STRING — a gente só usa o que já ensinou; nada de float() ou :.2f ainda.
app_cadastros.py
import tkinter as tk
from tkinter import messagebox
from pathlib import Path

PASTA = Path(__file__).parent  # aula 17: a pasta onde este .py esta
ARQ_CLIENTES = PASTA / "clientes.txt"  # aula 17: o MESMO clientes.txt da EV1 e da EV2
ARQ_PIZZAS = PASTA / "pizzas.txt"  # aula 17: o arquivo das pizzas (mesma ideia)

class Cliente:  # aula 02: o modelo Cliente, com persistencia (aula 17)
    def __init__(self, nome, email):
        self.nome = nome
        self.email = email
    def __str__(self):  # aula 03: representacao do objeto
        return f"{self.nome} <{self.email}>"
    @classmethod
    def carregar_todos(cls, caminho=ARQ_CLIENTES):  # aula 17: arquivo -> lista de objetos
        itens = []
        try:
            with open(caminho, "r", encoding="utf-8") as f:
                for linha in f:
                    nome, email = linha.strip().split(";")  # aula 18: desfaz o split(";")
                    itens.append(cls(nome, email))  # aula 18: monta a lista
        except FileNotFoundError:
            pass
        return itens
    @classmethod
    def salvar_todos(cls, clientes, caminho=ARQ_CLIENTES):  # aula 23: reescreve o arquivo INTEIRO em "w"
        with open(caminho, "w", encoding="utf-8") as f:
            for c in clientes:
                f.write(f"{c.nome};{c.email}\n")

class Pizza:  # aula 02: a irma da Cliente -- MESMO molde, campos sabor/preco
    def __init__(self, sabor, preco):
        self.sabor = sabor
        self.preco = preco  # o preco fica STRING (sem float/:.2f, so idiomas ja vistos)
    def __str__(self):
        return f"{self.sabor} - R$ {self.preco}"  # aula 03: representacao do objeto
    @classmethod
    def carregar_todos(cls, caminho=ARQ_PIZZAS):  # aula 17: arquivo -> lista
        itens = []
        try:
            with open(caminho, "r", encoding="utf-8") as f:
                for linha in f:
                    sabor, preco = linha.strip().split(";")  # aula 18: desfaz o split
                    itens.append(cls(sabor, preco))
        except FileNotFoundError:
            pass
        return itens
    @classmethod
    def salvar_todos(cls, pizzas, caminho=ARQ_PIZZAS):  # aula 23: reescreve TUDO em "w"
        with open(caminho, "w", encoding="utf-8") as f:
            for p in pizzas:
                f.write(f"{p.sabor};{p.preco}\n")

resolução 3 · camada 2

Camada 2 — TelaClientes(tk.Frame)

O gestor da EV2 INTEIRO, com uma só mudança de fundo: em vez de class App(tk.Tk), agora é class TelaClientes(tk.Frame). Deixou de ser a janela e virou um PEDAÇO dela — um Frame autônomo, dono do próprio CRUD.

🔁
É o App(tk.Tk) da EV2 virado Frame: super().__init__(pai) (aula 07) recebe o container como pai, e todo o resto — Listbox, StringVar, adicionar/editar/excluir — é idêntico. Só o TIPO de que ela herda mudou.
🧩
A tela é AUTÔNOMA: carrega os próprios clientes (aula 17), monta os próprios widgets (composição, aula 05) e trata o próprio CRUD. A App não sabe nada dos detalhes — só posiciona a tela e a traz à frente.
app_cadastros.py
class TelaClientes(tk.Frame):  # aula 23 (NOVO): a tela vira um tk.Frame (aula 05) -- era o App(tk.Tk) da EV2
    def __init__(self, pai):
        super().__init__(pai)  # aula 07: super() liga ao tk.Frame
        self.clientes = Cliente.carregar_todos()  # aula 17: READ -- a lista nasce cheia do arquivo
        self.total_texto = tk.StringVar()  # aula 23: StringVar do total
        self._montar_widgets()  # aula 02: metodos organizam a montagem
        self.atualizar_lista()  # aula 23: desenha a lista inicial
    def _montar_widgets(self):
        tk.Label(self, text="Clientes").pack(pady=6)  # aula 22: Label + pack no proprio Frame (self)
        quadro_form = tk.Frame(self)  # aula 05: um Frame agrupa o formulario (composicao)
        quadro_form.pack(pady=4)
        tk.Label(quadro_form, text="Nome:").grid(row=0, column=0, sticky="e", padx=4, pady=4)  # aula 22: grid DENTRO do quadro_form
        self.campo_nome = tk.Entry(quadro_form, width=24)
        self.campo_nome.grid(row=0, column=1, padx=4, pady=4)
        tk.Label(quadro_form, text="E-mail:").grid(row=1, column=0, sticky="e", padx=4, pady=4)
        self.campo_email = tk.Entry(quadro_form, width=24)
        self.campo_email.grid(row=1, column=1, padx=4, pady=4)
        tk.Button(quadro_form, text="Adicionar", command=self.adicionar).grid(row=2, column=0, columnspan=2, pady=6)  # aula 21: command= -- o observer do botao
        self.lista = tk.Listbox(self, width=44, height=8)  # aula 23: o Listbox (atributo)
        self.lista.pack(padx=10, pady=5)
        tk.Label(self, textvariable=self.total_texto).pack()  # aula 23: Label ligado ao StringVar
        quadro_acoes = tk.Frame(self)  # aula 05: outro Frame, para as acoes
        quadro_acoes.pack(pady=6)
        tk.Button(quadro_acoes, text="Editar", command=self.editar).pack(side="left", padx=5)  # aula 21: observer do Editar
        tk.Button(quadro_acoes, text="Excluir", command=self.excluir).pack(side="left", padx=5)  # aula 21: observer do Excluir
    def atualizar_lista(self):  # aula 23: redesenha o Listbox (READ)
        self.lista.delete(0, tk.END)  # aula 23: limpa tudo
        for c in self.clientes:
            self.lista.insert(tk.END, str(c))  # aula 23: __str__ por linha (aula 03)
        self.total_texto.set(f"Total: {len(self.clientes)} cliente(s)")  # aula 18: len()
    def _indice_selecionado(self):  # aula 02: auxiliar reutilizavel (editar e excluir)
        selecao = self.lista.curselection()  # aula 23: indices marcados
        if not selecao:
            messagebox.showerror("Sem selecao", "Selecione um cliente na lista.")  # aula 23: avisa se nada selecionado
            return None
        return selecao[0]
    def adicionar(self):  # aula 21: observer -- CREATE
        nome = self.campo_nome.get()
        email = self.campo_email.get()
        if nome == "" or email == "":  # aula 14: valida ANTES de criar
            messagebox.showerror("Campos vazios", "Preencha nome e e-mail.")
            return
        self.clientes.append(Cliente(nome, email))  # aula 18: append na lista
        Cliente.salvar_todos(self.clientes)  # aula 23: reescreve o arquivo ("w")
        self.atualizar_lista()  # aula 23: redesenha
        self.campo_nome.delete(0, tk.END)  # aula 22: limpa os campos
        self.campo_email.delete(0, tk.END)
    def excluir(self):  # aula 21: observer -- DELETE
        indice = self._indice_selecionado()
        if indice is None:
            return
        cliente = self.clientes[indice]
        if not messagebox.askyesno("Excluir", f"Excluir {cliente.nome}?"):  # aula 23: askyesno -> bool (aula 01)
            return
        del self.clientes[indice]  # aula 23: del lista[i]
        Cliente.salvar_todos(self.clientes)  # aula 23: regrava ("w")
        self.atualizar_lista()
    def editar(self):  # aula 21: observer -- UPDATE
        indice = self._indice_selecionado()
        if indice is None:
            return
        cliente = self.clientes[indice]
        janela_edicao = tk.Toplevel(self)  # aula 23: segunda janela (Toplevel)
        janela_edicao.title("Editar cliente")
        janela_edicao.geometry("300x150")
        tk.Label(janela_edicao, text="Nome:").grid(row=0, column=0, sticky="e", padx=5, pady=5)
        campo_nome = tk.Entry(janela_edicao, width=24)
        campo_nome.grid(row=0, column=1, padx=5, pady=5)
        campo_nome.insert(0, cliente.nome)  # aula 23: insert(0, ...) pre-preenche
        tk.Label(janela_edicao, text="E-mail:").grid(row=1, column=0, sticky="e", padx=5, pady=5)
        campo_email = tk.Entry(janela_edicao, width=24)
        campo_email.grid(row=1, column=1, padx=5, pady=5)
        campo_email.insert(0, cliente.email)
        def salvar_edicao():  # aula 21: closure -- enxerga o cliente desta edicao
            novo_nome = campo_nome.get()
            novo_email = campo_email.get()
            if novo_nome == "" or novo_email == "":
                messagebox.showerror("Campos vazios", "Preencha nome e e-mail.")
                return
            cliente.nome = novo_nome
            cliente.email = novo_email
            Cliente.salvar_todos(self.clientes)  # aula 23: PRECISA reescrever!
            self.atualizar_lista()
            janela_edicao.destroy()  # aula 23: fecha a Toplevel
        tk.Button(janela_edicao, text="Salvar", command=salvar_edicao).grid(row=2, column=0, columnspan=2, pady=10)  # aula 21: observer do Salvar da Toplevel

resolução 3 · camada 3

Camada 3 — TelaPizzas(tk.Frame)

A tela IRMÃ: a mesma estrutura da TelaClientes, agora com Sabor e Preço. Não é uma classe genérica parametrizada — são duas telas explícitas, e ver o padrão se repetir é o ponto.

👯
Duas IRMÃS, não uma classe genérica: TelaPizzas repete o desenho da TelaClientes com campos sabor/preco. Repetição saudável — no nível de vocês, ver o molde duas vezes ensina mais do que abstrair cedo demais.
🍕
O preco continua STRING (o __str__ mostra "{sabor} - R$ {preco}", aula 03). Sem float()/:.2f: o app guarda o que você digita, exatamente como a aula ensinou a persistir texto (aula 17).
app_cadastros.py
class TelaPizzas(tk.Frame):  # aula 23: a TELA irma -- MESMA estrutura, campos sabor/preco
    def __init__(self, pai):
        super().__init__(pai)  # aula 07: super() liga ao tk.Frame
        self.pizzas = Pizza.carregar_todos()  # aula 17: READ das pizzas
        self.total_texto = tk.StringVar()
        self._montar_widgets()
        self.atualizar_lista()
    def _montar_widgets(self):
        tk.Label(self, text="Pizzas").pack(pady=6)
        quadro_form = tk.Frame(self)
        quadro_form.pack(pady=4)
        tk.Label(quadro_form, text="Sabor:").grid(row=0, column=0, sticky="e", padx=4, pady=4)
        self.campo_sabor = tk.Entry(quadro_form, width=24)
        self.campo_sabor.grid(row=0, column=1, padx=4, pady=4)
        tk.Label(quadro_form, text="Preco:").grid(row=1, column=0, sticky="e", padx=4, pady=4)
        self.campo_preco = tk.Entry(quadro_form, width=24)
        self.campo_preco.grid(row=1, column=1, padx=4, pady=4)
        tk.Button(quadro_form, text="Adicionar", command=self.adicionar).grid(row=2, column=0, columnspan=2, pady=6)
        self.lista = tk.Listbox(self, width=44, height=8)  # aula 23: o Listbox das pizzas
        self.lista.pack(padx=10, pady=5)
        tk.Label(self, textvariable=self.total_texto).pack()
        quadro_acoes = tk.Frame(self)
        quadro_acoes.pack(pady=6)
        tk.Button(quadro_acoes, text="Editar", command=self.editar).pack(side="left", padx=5)
        tk.Button(quadro_acoes, text="Excluir", command=self.excluir).pack(side="left", padx=5)
    def atualizar_lista(self):
        self.lista.delete(0, tk.END)
        for p in self.pizzas:
            self.lista.insert(tk.END, str(p))
        self.total_texto.set(f"Total: {len(self.pizzas)} pizza(s)")
    def _indice_selecionado(self):
        selecao = self.lista.curselection()
        if not selecao:
            messagebox.showerror("Sem selecao", "Selecione uma pizza na lista.")
            return None
        return selecao[0]
    def adicionar(self):  # aula 21: observer -- CREATE (pizza)
        sabor = self.campo_sabor.get()
        preco = self.campo_preco.get()
        if sabor == "" or preco == "":
            messagebox.showerror("Campos vazios", "Preencha sabor e preco.")
            return
        self.pizzas.append(Pizza(sabor, preco))  # aula 18: append
        Pizza.salvar_todos(self.pizzas)  # aula 23: reescreve ("w")
        self.atualizar_lista()
        self.campo_sabor.delete(0, tk.END)
        self.campo_preco.delete(0, tk.END)
    def excluir(self):  # aula 21: observer -- DELETE
        indice = self._indice_selecionado()
        if indice is None:
            return
        pizza = self.pizzas[indice]
        if not messagebox.askyesno("Excluir", f"Excluir {pizza.sabor}?"):  # aula 23: askyesno -> bool
            return
        del self.pizzas[indice]  # aula 23: del lista[i]
        Pizza.salvar_todos(self.pizzas)
        self.atualizar_lista()
    def editar(self):  # aula 21: observer -- UPDATE
        indice = self._indice_selecionado()
        if indice is None:
            return
        pizza = self.pizzas[indice]
        janela_edicao = tk.Toplevel(self)  # aula 23: Toplevel de edicao
        janela_edicao.title("Editar pizza")
        janela_edicao.geometry("300x150")
        tk.Label(janela_edicao, text="Sabor:").grid(row=0, column=0, sticky="e", padx=5, pady=5)
        campo_sabor = tk.Entry(janela_edicao, width=24)
        campo_sabor.grid(row=0, column=1, padx=5, pady=5)
        campo_sabor.insert(0, pizza.sabor)
        tk.Label(janela_edicao, text="Preco:").grid(row=1, column=0, sticky="e", padx=5, pady=5)
        campo_preco = tk.Entry(janela_edicao, width=24)
        campo_preco.grid(row=1, column=1, padx=5, pady=5)
        campo_preco.insert(0, pizza.preco)
        def salvar_edicao():
            novo_sabor = campo_sabor.get()
            novo_preco = campo_preco.get()
            if novo_sabor == "" or novo_preco == "":
                messagebox.showerror("Campos vazios", "Preencha sabor e preco.")
                return
            pizza.sabor = novo_sabor
            pizza.preco = novo_preco
            Pizza.salvar_todos(self.pizzas)  # aula 23: regrava as pizzas
            self.atualizar_lista()
            janela_edicao.destroy()
        tk.Button(janela_edicao, text="Salvar", command=salvar_edicao).grid(row=2, column=0, columnspan=2, pady=10)

resolução 3 · camada 4

Camada 4 — App(tk.Tk): container, tkraise, botões e menu

A peça que junta tudo: a App(tk.Tk) instancia as duas telas na MESMA célula do grid, e alterna entre elas com tkraise(). Botões e menu chamam os mesmos mostrar_clientes/mostrar_pizzas.

📣
O martelo da aula: o botão Clientes e o item de menu Clientes chamam o MESMO método mostrar_clientes (e idem pra Pizzas). Duas portas, um só observer (aula 21) — a navegação inteira é isso.
🃏
O container usa grid (as duas telas na MESMA célula, empilhadas); os botões usam pack. Containers diferentes, gerentes diferentes — a regra de ouro da aula 22. tkraise() só escolhe qual carta fica na frente.
app_cadastros.py
class App(tk.Tk):  # aula 07: o container -- a app INTEIRA herda de tk.Tk
    def __init__(self):
        super().__init__()  # aula 07: super() liga ao tk.Tk
        self.title("Cadastros")
        self.geometry("420x520")
        quadro_botoes = tk.Frame(self)  # aula 05: um Frame para os botoes de troca
        quadro_botoes.pack(pady=8)
        tk.Button(quadro_botoes, text="Clientes", command=self.mostrar_clientes).pack(side="left", padx=5)  # aula 21: botao Clientes -> mostrar_clientes
        tk.Button(quadro_botoes, text="Pizzas", command=self.mostrar_pizzas).pack(side="left", padx=5)  # aula 21: botao Pizzas -> mostrar_pizzas
        container = tk.Frame(self)  # aula 05: o container das telas empilhadas
        container.pack(fill="both", expand=True)  # aula 22: pack preenche a janela
        self.tela_clientes = TelaClientes(container)  # aula 23: instancia a TelaClientes (um objeto, aula 02)
        self.tela_pizzas = TelaPizzas(container)  # aula 23: instancia a TelaPizzas
        self.tela_clientes.grid(row=0, column=0, sticky="nsew")  # aula 23 (NOVO): telas na MESMA celula do grid (row=0, column=0)
        self.tela_pizzas.grid(row=0, column=0, sticky="nsew")  # aula 23: a irma, empilhada na MESMA celula
        self._montar_menu()  # aula 23: monta a barra de menu
        self.mostrar_clientes()  # aula 23: abre mostrando a tela de clientes
    def _montar_menu(self):
        barra = tk.Menu(self)  # aula 23: a barra de menu
        menu_cadastros = tk.Menu(barra, tearoff=0)  # aula 23: submenu (tearoff=0)
        menu_cadastros.add_command(label="Clientes", command=self.mostrar_clientes)  # aula 21: item de menu -> o MESMO metodo do botao
        menu_cadastros.add_command(label="Pizzas", command=self.mostrar_pizzas)  # aula 21: item Pizzas -> mesmo metodo do botao
        barra.add_cascade(label="Cadastros", menu=menu_cadastros)
        self.config(menu=barra)  # aula 23: instala a barra
    def mostrar_clientes(self):
        self.tela_clientes.tkraise()  # aula 23 (NOVO): tkraise() traz a tela A FRENTE
    def mostrar_pizzas(self):
        self.tela_pizzas.tkraise()  # aula 23: tkraise() na tela de pizzas

resolução 3 · o código completo

Tudo junto: app_cadastros.py

As quatro camadas no arquivo final: dois modelos, duas telas-Frame irmãs e a App(tk.Tk) que as empilha e alterna com tkraise(). Um app multi-tela completo, colhendo tudo das aulas 02, 05, 07, 17, 18, 21 e 22.

🗺️
Leia como um mapa: os modelos com persistência (aula 17), duas TelaX(tk.Frame) autônomas (o gestor da EV2 virado Frame), e a App(tk.Tk) (aula 07) que compõe as telas (aula 05) e navega com tkraise(). Cada callback é um observer (aula 21).
🔁
O teste que prova: cadastre um cliente E uma pizza, alterne com os botões/menu, FECHE e reabra — os dois continuam, porque cada tela chama salvar_todos() no seu arquivo. Duas coleções, dois arquivos, um app.
app_cadastros.py
import tkinter as tk
from tkinter import messagebox
from pathlib import Path

PASTA = Path(__file__).parent  # aula 17: a pasta onde este .py esta
ARQ_CLIENTES = PASTA / "clientes.txt"  # aula 17: o MESMO clientes.txt da EV1 e da EV2
ARQ_PIZZAS = PASTA / "pizzas.txt"  # aula 17: o arquivo das pizzas (mesma ideia)

class Cliente:  # aula 02: o modelo Cliente, com persistencia (aula 17)
    def __init__(self, nome, email):
        self.nome = nome
        self.email = email
    def __str__(self):  # aula 03: representacao do objeto
        return f"{self.nome} <{self.email}>"
    @classmethod
    def carregar_todos(cls, caminho=ARQ_CLIENTES):  # aula 17: arquivo -> lista de objetos
        itens = []
        try:
            with open(caminho, "r", encoding="utf-8") as f:
                for linha in f:
                    nome, email = linha.strip().split(";")  # aula 18: desfaz o split(";")
                    itens.append(cls(nome, email))  # aula 18: monta a lista
        except FileNotFoundError:
            pass
        return itens
    @classmethod
    def salvar_todos(cls, clientes, caminho=ARQ_CLIENTES):  # aula 23: reescreve o arquivo INTEIRO em "w"
        with open(caminho, "w", encoding="utf-8") as f:
            for c in clientes:
                f.write(f"{c.nome};{c.email}\n")

class Pizza:  # aula 02: a irma da Cliente -- MESMO molde, campos sabor/preco
    def __init__(self, sabor, preco):
        self.sabor = sabor
        self.preco = preco  # o preco fica STRING (sem float/:.2f, so idiomas ja vistos)
    def __str__(self):
        return f"{self.sabor} - R$ {self.preco}"  # aula 03: representacao do objeto
    @classmethod
    def carregar_todos(cls, caminho=ARQ_PIZZAS):  # aula 17: arquivo -> lista
        itens = []
        try:
            with open(caminho, "r", encoding="utf-8") as f:
                for linha in f:
                    sabor, preco = linha.strip().split(";")  # aula 18: desfaz o split
                    itens.append(cls(sabor, preco))
        except FileNotFoundError:
            pass
        return itens
    @classmethod
    def salvar_todos(cls, pizzas, caminho=ARQ_PIZZAS):  # aula 23: reescreve TUDO em "w"
        with open(caminho, "w", encoding="utf-8") as f:
            for p in pizzas:
                f.write(f"{p.sabor};{p.preco}\n")

class TelaClientes(tk.Frame):  # aula 23 (NOVO): a tela vira um tk.Frame (aula 05) -- era o App(tk.Tk) da EV2
    def __init__(self, pai):
        super().__init__(pai)  # aula 07: super() liga ao tk.Frame
        self.clientes = Cliente.carregar_todos()  # aula 17: READ -- a lista nasce cheia do arquivo
        self.total_texto = tk.StringVar()  # aula 23: StringVar do total
        self._montar_widgets()  # aula 02: metodos organizam a montagem
        self.atualizar_lista()  # aula 23: desenha a lista inicial
    def _montar_widgets(self):
        tk.Label(self, text="Clientes").pack(pady=6)  # aula 22: Label + pack no proprio Frame (self)
        quadro_form = tk.Frame(self)  # aula 05: um Frame agrupa o formulario (composicao)
        quadro_form.pack(pady=4)
        tk.Label(quadro_form, text="Nome:").grid(row=0, column=0, sticky="e", padx=4, pady=4)  # aula 22: grid DENTRO do quadro_form
        self.campo_nome = tk.Entry(quadro_form, width=24)
        self.campo_nome.grid(row=0, column=1, padx=4, pady=4)
        tk.Label(quadro_form, text="E-mail:").grid(row=1, column=0, sticky="e", padx=4, pady=4)
        self.campo_email = tk.Entry(quadro_form, width=24)
        self.campo_email.grid(row=1, column=1, padx=4, pady=4)
        tk.Button(quadro_form, text="Adicionar", command=self.adicionar).grid(row=2, column=0, columnspan=2, pady=6)  # aula 21: command= -- o observer do botao
        self.lista = tk.Listbox(self, width=44, height=8)  # aula 23: o Listbox (atributo)
        self.lista.pack(padx=10, pady=5)
        tk.Label(self, textvariable=self.total_texto).pack()  # aula 23: Label ligado ao StringVar
        quadro_acoes = tk.Frame(self)  # aula 05: outro Frame, para as acoes
        quadro_acoes.pack(pady=6)
        tk.Button(quadro_acoes, text="Editar", command=self.editar).pack(side="left", padx=5)  # aula 21: observer do Editar
        tk.Button(quadro_acoes, text="Excluir", command=self.excluir).pack(side="left", padx=5)  # aula 21: observer do Excluir
    def atualizar_lista(self):  # aula 23: redesenha o Listbox (READ)
        self.lista.delete(0, tk.END)  # aula 23: limpa tudo
        for c in self.clientes:
            self.lista.insert(tk.END, str(c))  # aula 23: __str__ por linha (aula 03)
        self.total_texto.set(f"Total: {len(self.clientes)} cliente(s)")  # aula 18: len()
    def _indice_selecionado(self):  # aula 02: auxiliar reutilizavel (editar e excluir)
        selecao = self.lista.curselection()  # aula 23: indices marcados
        if not selecao:
            messagebox.showerror("Sem selecao", "Selecione um cliente na lista.")  # aula 23: avisa se nada selecionado
            return None
        return selecao[0]
    def adicionar(self):  # aula 21: observer -- CREATE
        nome = self.campo_nome.get()
        email = self.campo_email.get()
        if nome == "" or email == "":  # aula 14: valida ANTES de criar
            messagebox.showerror("Campos vazios", "Preencha nome e e-mail.")
            return
        self.clientes.append(Cliente(nome, email))  # aula 18: append na lista
        Cliente.salvar_todos(self.clientes)  # aula 23: reescreve o arquivo ("w")
        self.atualizar_lista()  # aula 23: redesenha
        self.campo_nome.delete(0, tk.END)  # aula 22: limpa os campos
        self.campo_email.delete(0, tk.END)
    def excluir(self):  # aula 21: observer -- DELETE
        indice = self._indice_selecionado()
        if indice is None:
            return
        cliente = self.clientes[indice]
        if not messagebox.askyesno("Excluir", f"Excluir {cliente.nome}?"):  # aula 23: askyesno -> bool (aula 01)
            return
        del self.clientes[indice]  # aula 23: del lista[i]
        Cliente.salvar_todos(self.clientes)  # aula 23: regrava ("w")
        self.atualizar_lista()
    def editar(self):  # aula 21: observer -- UPDATE
        indice = self._indice_selecionado()
        if indice is None:
            return
        cliente = self.clientes[indice]
        janela_edicao = tk.Toplevel(self)  # aula 23: segunda janela (Toplevel)
        janela_edicao.title("Editar cliente")
        janela_edicao.geometry("300x150")
        tk.Label(janela_edicao, text="Nome:").grid(row=0, column=0, sticky="e", padx=5, pady=5)
        campo_nome = tk.Entry(janela_edicao, width=24)
        campo_nome.grid(row=0, column=1, padx=5, pady=5)
        campo_nome.insert(0, cliente.nome)  # aula 23: insert(0, ...) pre-preenche
        tk.Label(janela_edicao, text="E-mail:").grid(row=1, column=0, sticky="e", padx=5, pady=5)
        campo_email = tk.Entry(janela_edicao, width=24)
        campo_email.grid(row=1, column=1, padx=5, pady=5)
        campo_email.insert(0, cliente.email)
        def salvar_edicao():  # aula 21: closure -- enxerga o cliente desta edicao
            novo_nome = campo_nome.get()
            novo_email = campo_email.get()
            if novo_nome == "" or novo_email == "":
                messagebox.showerror("Campos vazios", "Preencha nome e e-mail.")
                return
            cliente.nome = novo_nome
            cliente.email = novo_email
            Cliente.salvar_todos(self.clientes)  # aula 23: PRECISA reescrever!
            self.atualizar_lista()
            janela_edicao.destroy()  # aula 23: fecha a Toplevel
        tk.Button(janela_edicao, text="Salvar", command=salvar_edicao).grid(row=2, column=0, columnspan=2, pady=10)  # aula 21: observer do Salvar da Toplevel

class TelaPizzas(tk.Frame):  # aula 23: a TELA irma -- MESMA estrutura, campos sabor/preco
    def __init__(self, pai):
        super().__init__(pai)  # aula 07: super() liga ao tk.Frame
        self.pizzas = Pizza.carregar_todos()  # aula 17: READ das pizzas
        self.total_texto = tk.StringVar()
        self._montar_widgets()
        self.atualizar_lista()
    def _montar_widgets(self):
        tk.Label(self, text="Pizzas").pack(pady=6)
        quadro_form = tk.Frame(self)
        quadro_form.pack(pady=4)
        tk.Label(quadro_form, text="Sabor:").grid(row=0, column=0, sticky="e", padx=4, pady=4)
        self.campo_sabor = tk.Entry(quadro_form, width=24)
        self.campo_sabor.grid(row=0, column=1, padx=4, pady=4)
        tk.Label(quadro_form, text="Preco:").grid(row=1, column=0, sticky="e", padx=4, pady=4)
        self.campo_preco = tk.Entry(quadro_form, width=24)
        self.campo_preco.grid(row=1, column=1, padx=4, pady=4)
        tk.Button(quadro_form, text="Adicionar", command=self.adicionar).grid(row=2, column=0, columnspan=2, pady=6)
        self.lista = tk.Listbox(self, width=44, height=8)  # aula 23: o Listbox das pizzas
        self.lista.pack(padx=10, pady=5)
        tk.Label(self, textvariable=self.total_texto).pack()
        quadro_acoes = tk.Frame(self)
        quadro_acoes.pack(pady=6)
        tk.Button(quadro_acoes, text="Editar", command=self.editar).pack(side="left", padx=5)
        tk.Button(quadro_acoes, text="Excluir", command=self.excluir).pack(side="left", padx=5)
    def atualizar_lista(self):
        self.lista.delete(0, tk.END)
        for p in self.pizzas:
            self.lista.insert(tk.END, str(p))
        self.total_texto.set(f"Total: {len(self.pizzas)} pizza(s)")
    def _indice_selecionado(self):
        selecao = self.lista.curselection()
        if not selecao:
            messagebox.showerror("Sem selecao", "Selecione uma pizza na lista.")
            return None
        return selecao[0]
    def adicionar(self):  # aula 21: observer -- CREATE (pizza)
        sabor = self.campo_sabor.get()
        preco = self.campo_preco.get()
        if sabor == "" or preco == "":
            messagebox.showerror("Campos vazios", "Preencha sabor e preco.")
            return
        self.pizzas.append(Pizza(sabor, preco))  # aula 18: append
        Pizza.salvar_todos(self.pizzas)  # aula 23: reescreve ("w")
        self.atualizar_lista()
        self.campo_sabor.delete(0, tk.END)
        self.campo_preco.delete(0, tk.END)
    def excluir(self):  # aula 21: observer -- DELETE
        indice = self._indice_selecionado()
        if indice is None:
            return
        pizza = self.pizzas[indice]
        if not messagebox.askyesno("Excluir", f"Excluir {pizza.sabor}?"):  # aula 23: askyesno -> bool
            return
        del self.pizzas[indice]  # aula 23: del lista[i]
        Pizza.salvar_todos(self.pizzas)
        self.atualizar_lista()
    def editar(self):  # aula 21: observer -- UPDATE
        indice = self._indice_selecionado()
        if indice is None:
            return
        pizza = self.pizzas[indice]
        janela_edicao = tk.Toplevel(self)  # aula 23: Toplevel de edicao
        janela_edicao.title("Editar pizza")
        janela_edicao.geometry("300x150")
        tk.Label(janela_edicao, text="Sabor:").grid(row=0, column=0, sticky="e", padx=5, pady=5)
        campo_sabor = tk.Entry(janela_edicao, width=24)
        campo_sabor.grid(row=0, column=1, padx=5, pady=5)
        campo_sabor.insert(0, pizza.sabor)
        tk.Label(janela_edicao, text="Preco:").grid(row=1, column=0, sticky="e", padx=5, pady=5)
        campo_preco = tk.Entry(janela_edicao, width=24)
        campo_preco.grid(row=1, column=1, padx=5, pady=5)
        campo_preco.insert(0, pizza.preco)
        def salvar_edicao():
            novo_sabor = campo_sabor.get()
            novo_preco = campo_preco.get()
            if novo_sabor == "" or novo_preco == "":
                messagebox.showerror("Campos vazios", "Preencha sabor e preco.")
                return
            pizza.sabor = novo_sabor
            pizza.preco = novo_preco
            Pizza.salvar_todos(self.pizzas)  # aula 23: regrava as pizzas
            self.atualizar_lista()
            janela_edicao.destroy()
        tk.Button(janela_edicao, text="Salvar", command=salvar_edicao).grid(row=2, column=0, columnspan=2, pady=10)

class App(tk.Tk):  # aula 07: o container -- a app INTEIRA herda de tk.Tk
    def __init__(self):
        super().__init__()  # aula 07: super() liga ao tk.Tk
        self.title("Cadastros")
        self.geometry("420x520")
        quadro_botoes = tk.Frame(self)  # aula 05: um Frame para os botoes de troca
        quadro_botoes.pack(pady=8)
        tk.Button(quadro_botoes, text="Clientes", command=self.mostrar_clientes).pack(side="left", padx=5)  # aula 21: botao Clientes -> mostrar_clientes
        tk.Button(quadro_botoes, text="Pizzas", command=self.mostrar_pizzas).pack(side="left", padx=5)  # aula 21: botao Pizzas -> mostrar_pizzas
        container = tk.Frame(self)  # aula 05: o container das telas empilhadas
        container.pack(fill="both", expand=True)  # aula 22: pack preenche a janela
        self.tela_clientes = TelaClientes(container)  # aula 23: instancia a TelaClientes (um objeto, aula 02)
        self.tela_pizzas = TelaPizzas(container)  # aula 23: instancia a TelaPizzas
        self.tela_clientes.grid(row=0, column=0, sticky="nsew")  # aula 23 (NOVO): telas na MESMA celula do grid (row=0, column=0)
        self.tela_pizzas.grid(row=0, column=0, sticky="nsew")  # aula 23: a irma, empilhada na MESMA celula
        self._montar_menu()  # aula 23: monta a barra de menu
        self.mostrar_clientes()  # aula 23: abre mostrando a tela de clientes
    def _montar_menu(self):
        barra = tk.Menu(self)  # aula 23: a barra de menu
        menu_cadastros = tk.Menu(barra, tearoff=0)  # aula 23: submenu (tearoff=0)
        menu_cadastros.add_command(label="Clientes", command=self.mostrar_clientes)  # aula 21: item de menu -> o MESMO metodo do botao
        menu_cadastros.add_command(label="Pizzas", command=self.mostrar_pizzas)  # aula 21: item Pizzas -> mesmo metodo do botao
        barra.add_cascade(label="Cadastros", menu=menu_cadastros)
        self.config(menu=barra)  # aula 23: instala a barra
    def mostrar_clientes(self):
        self.tela_clientes.tkraise()  # aula 23 (NOVO): tkraise() traz a tela A FRENTE
    def mostrar_pizzas(self):
        self.tela_pizzas.tkraise()  # aula 23: tkraise() na tela de pizzas

app = App()
app.mainloop()
# saida esperada:
# clicar em "Pizzas" (botao ou menu Cadastros > Pizzas) troca a tela na hora (tkraise)
# feche e reabra: clientes.txt e pizzas.txt guardaram os dois cadastros
Ele compartilha o clientes.txt com a EV1 e a EV2 (e cria um pizzas.txt novo ao lado). São ~90 linhas, mas cada trecho é uma peça que você já montou isolada nas camadas. Digite junto, não copie: os dedos aprendem o que o olho só reconhece.

resolução 3 · armadilha

Armadilha: empilhar na mesma célula (+tkraise) × pack um embaixo do outro

Trocar de tela só funciona se as telas ocuparem a MESMA célula. Com pack, elas ficam uma embaixo da outra — as duas visíveis — e o tkraise() não tem o que esconder.

pack — as duas coexistem, tkraise não esconde nada

empilha_errado
# as duas telas com pack:
self.tela_clientes.pack()
self.tela_pizzas.pack()
# ficam uma EMBAIXO da outra,
# as duas visiveis ao mesmo tempo.
# pack nao empilha na mesma celula,
# entao o tkraise() nao esconde nada:
self.tela_clientes.tkraise()   # sem efeito util

grid(row=0, column=0) nas duas + tkraise

empilha_certo
# as duas na MESMA celula:
self.tela_clientes.grid(row=0, column=0)
self.tela_pizzas.grid(row=0, column=0)
# empilhadas como cartas: uma
# cobre a outra. trocar de tela =
# trazer a escolhida A FRENTE:
self.tela_clientes.tkraise()   # cobre a de pizzas
O pack empilha coisas que COEXISTEM (uma embaixo da outra, todas visíveis); trocar de TELA é outra coisa — as telas ocupam a MESMA célula do grid e o tkraise() escolhe qual fica na frente. E a regra de ouro segue de pé: um gerente por container. O container das telas usa grid; dentro de cada tela, pack e grid à vontade.

resolução 3 · o resultado

A tela Clientes do app

O app aberto na tela Clientes: no topo os botões Clientes | Pizzas e o menu Cadastros; à frente, a tela de clientes com o formulário, o Listbox, o total e os botões Editar/Excluir.

Screenshot da janela 'Cadastros': no topo dois botões, 'Clientes' e 'Pizzas', e uma barra de menu com 'Cadastros'; à frente a tela Clientes, com um formulário (campos Nome e E-mail e botão Adicionar), um Listbox com os clientes cadastrados, o total de clientes abaixo e dois botões, 'Editar' e 'Excluir'.
É a TelaClientes(tk.Frame) trazida à frente por tkraise() na abertura. Repare que é o mesmo layout do gestor da EV2 — a única diferença é que agora ela é um Frame dentro de um app maior, não a janela inteira.

resolução 3 · o resultado

A tela Pizzas do app

A MESMA janela depois de clicar em Pizzas: a tela de pizzas veio à frente (tkraise()), com o formulário Sabor/Preço, o Listbox das pizzas e o total. Nada foi recriado — só trocou a carta de cima.

Screenshot da mesma janela 'Cadastros' após clicar em 'Pizzas': agora a tela Pizzas está à frente, com um formulário (campos Sabor e Preço e botão Adicionar), um Listbox com as pizzas cadastradas no formato 'sabor - R$ preço', o total abaixo e os botões 'Editar' e 'Excluir'.
As duas imagens juntas (Clientes e Pizzas) evidenciam a troca: a janela é a mesma, os botões e o menu são os mesmos — só mudou qual Frame está à frente. Isso é o app multi-tela: várias telas empilhadas na mesma célula, e o tkraise() escolhendo qual aparece.

recapitulando

Glossário rápido de hoje

item detalhe
Listbox Widget de lista de linhas selecionáveis e roláveis — o par visual da list (aula 18). .insert(tk.END, txt), .delete(0, tk.END) limpa, .delete(i) apaga uma, .curselection() dá os índices, .get(i) lê a linha.
índice do Listbox = índice da lista Se você povoou o Listbox na mesma ordem da lista Python, curselection()[0] é o índice de clientes[i]. Mexeu na lista? Redesenhe (preencher_lista) pra não descolar.
bind() widget.bind("", handler) liga um evento nomeado a um callback em QUALQUER widget — o command= generalizado (aula 21). O handler recebe o argumento evento.
<> O evento nomeado que dispara quando a seleção do Listbox muda. Outros: (duplo-clique), (Enter). Nomes entre <> (ou <<>> pros virtuais).
messagebox Caixas de diálogo prontas (from tkinter import messagebox): showinfo (aviso), showerror (erro) e askyesno (Sim/Não → devolve bool, alimenta o if). São modais: o programa espera a resposta.
StringVar Variável de controle observável. Label(..., textvariable=var) inscreve o rótulo; var.set(txt) atualiza todos os Labels ligados de uma vez. var.get() lê. O Observer (aula 21) além do command=.
Toplevel Uma segunda janela, filha da principal (tk.Toplevel(pai)). Tem title()/geometry() e recebe widgets (composição, aula 05), mas NÃO leva mainloop() próprio. Fecha com .destroy().
Entry.insert(0, txt) Escreve texto no Entry a partir do índice 0 — pré-preenche um campo. O inverso do delete(0, tk.END) da aula 22: um preenche, o outro esvazia.
App(tk.Tk) A app inteira como classe que HERDA de tk.Tk (aula 07). self é a janela; super().__init__() liga ao Tk; widgets viram atributos e callbacks viram métodos. app = App(); app.mainloop().
Menu A barra do topo: tk.Menu(self) é a barra, tk.Menu(barra, tearoff=0) um submenu, add_cascade pendura o submenu, add_command põe um item (com command=). self.config(menu=barra) instala.
salvar_todos (modo "w") @classmethod que reescreve o arquivo INTEIRO em "w" a partir da lista. Diferente do "a" (append) da aula 22: remover/editar precisam regravar tudo, não só anexar.
del lista[i] Remove um item da lista pelo índice — o mesmo del que apagava chave de dicionário na aula 18, agora numa list. Depois de mexer, regrave (salvar_todos) e redesenhe.
CRUD As quatro operações de dados: Create (adicionar), Read (listar), Update (editar), Delete (excluir). O gestor de hoje faz as quatro na tela, cada uma persistindo em disco.
Tela como Frame (TelaX(tk.Frame)) Uma tela vira uma classe que HERDA de tk.Frame (aula 07): monta seus próprios widgets (composição, aula 05) e vira um objeto que o container posiciona. É o App(tk.Tk) da EV2 um nível abaixo — um Frame, não a janela.
tkraise() widget.tkraise() traz um Frame à FRENTE da pilha. Com várias telas na MESMA célula do grid (row=0, column=0), trocar de tela é só dar tkraise() na que deve aparecer — a de cima cobre as outras.
app multi-tela Várias telas (Frames irmãos) empilhadas na mesma célula de um container, e um App(tk.Tk) que alterna entre elas com tkraise() — botões e itens de menu chamando o MESMO método. Um app, várias telas.

O que dominamos hoje

  • Troco o painel-Label por um Listbox de verdade: .insert(tk.END, ...) povoa, .delete(0, tk.END) limpa, .curselection() dá o índice marcado — e sei que o índice do Listbox é o índice da minha lista Python.
  • Reajo a eventos além do clique com bind("<>", handler): o command= generalizado (aula 21), com o handler recebendo o argumento evento.
  • Uso messagebox pra falar alto: showinfo/showerror avisam, e askyesno devolve um bool que barra ações destrutivas (o del só passa com o Sim).
  • Ligo rótulos a um StringVar (textvariable=) e atualizo tudo com um .set() — o Observer da aula 21 um nível acima do command=.
  • Abro uma segunda janela com Toplevel pra editar sem perder a principal, pré-preenchendo campos com insert(0, ...) e fechando com destroy().
  • Escrevo a app INTEIRA como class App(tk.Tk) (aula 07): self é a janela, super().__init__() liga ao Tk, widgets viram atributos e callbacks viram métodos — o Contador da aula 22 crescido.
  • Monto uma barra de Menu (tk.Menu + add_cascade + add_command) e fecho o CRUD visual: criar, listar, editar e excluir, cada operação regravando o arquivo com salvar_todos() ("w").
  • Organizo um app de VÁRIAS telas: cada tela é uma class TelaX(tk.Frame), todas empilhadas na mesma célula do grid, e um App(tk.Tk) alterna entre elas com tkraise() — o botão e o item de menu chamando o mesmo método.

exercícios

Mãos à obra: os exercícios

Quatro janelas em três níveis, todas INDIVIDUAIS e entregues em arquivo .py (o desafio pede também um PNG da SUA janela). Comece em sala; termine em casa. Mire pelo menos um de cada nível — e teste TODOS os botões e a seleção antes de entregar.

01

Fácil · Lista de tarefas

Uma janela com um Entry, um botão Adicionar (que faz lista.insert(tk.END, campo.get()) e limpa o campo) e um botão Limpar (que faz lista.delete(0, tk.END)). Um Listbox mostra as tarefas. Entregue o .py. Foco: Listbox + insert/delete, o par povoar/limpar.

02

Fácil/Médio · Selecionou, mostrou

Um Listbox já preenchido com uma lista fixa de nomes (crie a lista no código e povoe com um for). Ligue um bind("<<ListboxSelect>>", ...) que, ao selecionar uma linha, mostra Selecionado: <nome> num Label abaixo. Sem botão nenhum. Entregue o .py. Foco: bind na seleção + curselection/get.

03

Médio · Do print ao código

A partir do SCREENSHOT do próximo slide, reproduza a janela: um Entry em cima, um Listbox no meio e um Frame com os botões Adicionar e Remover lado a lado (pack(side="left") DENTRO do Frame). Adicionar insere o texto do campo; Remover apaga a linha selecionada (lista.delete(curselection()[0])). Entregue o .py. Foco: ler um layout e traduzi-lo — Listbox + Frame, um gerente por container.

04

Desafio · Mini-CRUD como App(tk.Tk)

Uma agenda simples escrita como class App(tk.Tk): um Entry + Adicionar (append numa lista self.itens e redesenha o Listbox), e um botão Excluir que só apaga o selecionado APÓS um messagebox.askyesno. Guarde o estado em self.itens e os callbacks como métodos. Não precisa de arquivo. Entregue o .py E um PNG da SUA janela aberta. Foco: App(tk.Tk) + askyesno protegendo o excluir — o coração do gestor, sozinho.

exercício 3 · do print ao código

A janela-alvo do Exercício 3

Esta é a janela de tarefas que o seu código deve reproduzir. Olhe como um dev: onde está o Entry? o Listbox? onde há um Frame agrupando botões? qual gerente em cada container?

Screenshot da janela 'Minhas tarefas': um campo de texto no topo, abaixo uma lista (Listbox) com algumas tarefas, e na base dois botões lado a lado, 'Adicionar' e 'Remover'.
Dicas de leitura: o campo, a lista e o grupo de botões empilhados = pack na janela; os dois botões lado a lado, centralizados = um Frame com pack(side="left") POR DENTRO. Adicionar usa campo.get() + lista.insert(tk.END, ...); Remover usa lista.curselection() + lista.delete(i). Um gerente por container, como na aula 22.

Antes de entregar: confira

  • O arquivo .py roda sem erro do começo ao fim: a janela abre e o mainloop() (ou App().mainloop()) é a última linha.
  • O Listbox é povoado por insert(tk.END, ...) e, quando a lista muda, eu redesenho (delete(0, tk.END) + reinsere) — sem duplicar linhas.
  • Toda leitura de seleção checa curselection() ANTES de usar o índice (tupla vazia = nada selecionado); e o índice do Listbox bate com o da minha lista.
  • Se usei bind, o handler aceita o parâmetro evento (def handler(evento):); se usei command=, é sem parênteses.
  • Ações destrutivas (excluir/limpar) passam por um messagebox.askyesno — nada some sem confirmação.
  • Cada container usa UM gerente só (pack OU grid); misturei só em Frames diferentes — sem o TclError de layout.
  • Nomes de variáveis, textos da tela e comentários em PT-BR. No desafio: app como class App(tk.Tk) e entreguei o .py + o PNG da janela aberta.
próxima aula

Aula 24 · 08/07

POO + Banco de dados (SQLite)

O gestor de hoje guarda os clientes num arquivo de texto — uma linha por cliente, lida e regravada inteira a cada mudança. Funciona pra dezenas; não pra milhares. Amanhã os objetos vão pra um BANCO DE DADOS de verdade: o SQLite (que também já vem com o Python), com tabelas, SQL e consultas — a persistência da aula 17 crescendo pro nível profissional.

SQLite SQL & tabelas persistência real