O Listbox é o par visual da list da aula 18: linhas selecionáveis e roláveis. Você o povoa a partir da sua lista Python e, se mantiver a mesma ordem, o índice do Listbox é o índice da lista — a chave pra achar o objeto que o usuário selecionou.
lista = tk.Listbox(pai, width=40, height=8)
lista.insert(tk.END, "texto") # adiciona no fim
lista.delete(0, tk.END) # limpa TUDO (ou delete(i) uma)
selecao = lista.curselection() # tupla de indices marcados
if selecao:
i = selecao[0] # indice do Listbox == indice da lista
print(lista.get(i)) # le o texto da linha i A lista Python é a fonte da verdade; o Listbox é só o espelho. Toda vez que a lista muda (add, remove, sort), redesenhe o Listbox — assim o índice nunca descola do objeto.
O messagebox traz caixas modais prontas pra avisar e perguntar — o programa espera a resposta. O askyesno devolve um bool que alimenta um if direto, ideal pra confirmar antes de apagar. O StringVar é uma variável observável: ligue um Label a ela e mudá-la redesenha o rótulo sozinho.
from tkinter import messagebox
messagebox.showinfo("Titulo", "Aviso") # so avisa
messagebox.showerror("Erro", "Deu ruim") # so avisa (erro)
if messagebox.askyesno("Excluir", "Tem certeza?"):
... # True = Sim, False = Nao (um bool!)
total = tk.StringVar()
total.set("Total: 0")
tk.Label(janela, textvariable=total).pack()
total.set("Total: 3") # o Label se redesenha sozinho O askyesno é o bool da aula 01 vindo de um clique do usuário; o StringVar é o Observer da aula 21 um nível acima do command=. De novo: nada de Python novo, só POO ganhando tela.
Quando a tela cresce, funções soltas e globais viram bagunça. A solução profissional é a app herdar de tk.Tk (aula 07): self vira a janela, o estado vira atributo e os callbacks viram métodos. E o Toplevel abre uma segunda janela (pra editar, por exemplo) sem perder a principal — o mesmo mainloop cuida das duas.
class App(tk.Tk):
def __init__(self):
super().__init__() # aula 07: liga ao tk.Tk (vem PRIMEIRO)
self.title("Minha app") # self E a janela
self.itens = [] # estado = atributo
self.lista = tk.Listbox(self) # widget = atributo
self.lista.pack()
tk.Button(self, text="OK",
command=self.acao).pack() # callback = metodo
def acao(self):
...
app = App()
app.mainloop() O Contador da aula 22 já era o embrião: widgets como atributos, callback como método. App(tk.Tk) é isso levado ao limite — a própria janela virou o objeto.
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.
roteiro da aula
O widget que LISTA coleções na tela e deixa selecionar. E o bind(): reagir à seleção, generalizando o command= pra qualquer evento.
messagebox pra erros e confirmações (askyesno vira um bool), e o StringVar: um rótulo que se atualiza sozinho quando o valor muda.
Uma segunda janela pra editar sem perder a principal, e o salto de arquitetura: a app INTEIRA herda de tk.Tk (aula 07).
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
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á. Label dentro de um Frame, redesenhado na mão por atualizar_painel() com "\n".join(...). Funciona pra MOSTRAR — mas é só texto. super() (07), o Observer (21). Muda ONDE aparece: numa interface que finalmente deixa MEXER nos dados. o problema de hoje
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. Listbox (lista selecionável), bind() (reagir à seleção), messagebox (confirmar), Toplevel (editar noutra janela) e StringVar (rótulos que se atualizam). tk.Tk (aula 07) — o Contador da aula 22 já ensaiou o caminho. listbox · o problema
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. 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. 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. curselection()[0] vira o índice de clientes[i]. Guarde isso. listbox · primeiro código
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.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 listbox · primeiro código
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.
bind · o problema
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. 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. bind recebe um argumento, o evento — def ao_selecionar(evento):. O command= não passa nada; o bind passa o objeto-evento (que traz detalhes de quem disparou). <>: "<<ListboxSelect>>" (mudou a seleção), "<Double-Button-1>" (duplo-clique), "<Return>" (tecla Enter). Você inscreve um observer pra cada um. bind · em código
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).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 bind · comparando
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.
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(): 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): 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
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.
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.
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.
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.
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
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.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 resolução 1 · camada 2
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.# 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) resolução 1 · camada 3
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.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.# 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 resolução 1 · o código completo
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.
carregar_todos/salvar_todos, aula 17), a lista clientes (aula 18), o Listbox + bind (aula 23) e o observer remover (command=, aula 21).salvar_todos() regravou o clientes.txt sem ele. Lista e disco em sincronia.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") resolução 1 · armadilha
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.
# 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! # 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. 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
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.
messagebox · o problema
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. messagebox: from tkinter import messagebox. São janelinhas modais — aparecem na frente, e o programa ESPERA a resposta antes de continuar. 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. 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
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.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" messagebox · o resultado
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.
stringvar · variável observável
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=..set() só atualiza todos de uma vez — sem caçar cada .config(). No gestor, o total de clientes vira um StringVar.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... toplevel · o problema
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. 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. toplevel · em código
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.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 toplevel · o resultado
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.
app-classe · o salto
tk.Tk (aula 07). class App(tk.Tk): — agora self É a janela, e super().__init__() liga ao tk.Tk por baixo. 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). app = App() monta tudo, app.mainloop() roda (método herdado). app-classe · o molde
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.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.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" app-classe · comparando
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).
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):
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() 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
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.
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.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 ao vivo 2 · vamos construir juntos
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.
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.
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.
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.
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
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.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 resolução 2 · camada 2
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).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.# 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) resolução 2 · camada 3
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.# 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 resolução 2 · camada 4
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.# 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) resolução 2 · o código completo
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.
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.salvar_todos().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) resolução 2 · armadilha
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.
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. 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. 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
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.
resolução 2 · o resultado
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.
multi-tela · o problema
Toplevel pra cada não é navegação, é poluição de janelas. Falta um jeito de TROCAR de tela no mesmo lugar. 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). 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. 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 (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.
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.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.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 ao vivo 3 · vamos construir juntos
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.
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.
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.
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.
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
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.
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.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.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
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.
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.App não sabe nada dos detalhes — só posiciona a tela e a traz à frente.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
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.
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.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).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
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.
mostrar_clientes (e idem pra Pizzas). Duas portas, um só observer (aula 21) — a navegação inteira é isso.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.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
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.
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).salvar_todos() no seu arquivo. Duas coleções, dois arquivos, um app.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 resolução 3 · armadilha
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.
# 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 # 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 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
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.
resolução 3 · o resultado
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.
recapitulando
| 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(" |
| < | O evento nomeado que dispara quando a seleção do Listbox muda. Outros: |
| 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. |
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.
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.
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.
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.
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
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?
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.