Guia de estudo

Aula 22 · Seg, 06/07

Interface gráfica com Tkinter — parte 1

Depois de 21 aulas de terminal, o seu código ganha janela: widgets, layout e o clique do usuário chamando a SUA função.

A aula 21 fechou com uma promessa: ligar uma função a um botão é inscrever um observer. Hoje a gente colhe. O Tkinter — a biblioteca de interface gráfica que já vem com o Python — coloca janelas, botões e campos na tela, e o command= de cada botão é exatamente o Observer em ação. Nada de Python novo: widget é objeto, janela é composição, callback é Observer.

Tkinter widgets & layout eventos

roteiro da aula

O que vamos aprender hoje

01

A primeira janela

tk.Tk(), title(), geometry() — e o mainloop(), o loop de eventos que mantém a janela viva esperando o usuário.

02

Widgets

Label, Button, Entry e Frame: os objetos que formam a tela. Cada um é uma classe que você instancia e configura.

03

Layout

Quem posiciona é o geometry manager: pack (empilha), grid (linhas × colunas) e place (quase nunca). Um gerente por container.

04

Eventos

command= registra o callback, o clique notifica, o mainloop despacha — a colheita do Observer da aula 21, ao vivo na tela.

ponte com a aula 21

De onde paramos — a promessa da aula 21

  • 🖥️ Por 21 aulas, todo programa nosso viveu no terminal: print() pra falar, input() pra ouvir. Funciona — mas nenhum usuário de verdade quer usar um sistema assim.
  • 📣 A aula 21 fechou com uma promessa: ligar uma função a um botão = inscrever um observer. Hoje a gente colhe: o command= do Tkinter é exatamente isso.
  • 🖱️ O botão é o Subject; a SUA função é o observer; o clique é a notificação. O padrão de anteontem é o motor de toda interface gráfica.
  • 🧩 E não tem Python novo hoje: widget é objeto (aula 02), configurar é passar parâmetros nomeados (aula 05), janela é composição (aula 05), callback é Observer (aula 21). Só muda ONDE aparece: na tela.

definição

O que é GUI — e o que é o Tkinter

  • 🪟 GUI (Graphical User Interface — interface gráfica): janelas, botões, campos e cliques no lugar de comandos digitados. É a cara de praticamente todo software que você usa.
  • 🐍 Tkinter é a biblioteca de GUI que JÁ VEM com o Python — biblioteca padrão, zero instalação: import tkinter as tk e pronto. Existem outras (PyQt, Kivy, interfaces web), mas o Tkinter é o caminho mais curto entre o seu código e uma janela.
  • 📦 Convenção da turma: import tkinter as tk — nunca from tkinter import *. O tk. na frente diz de onde cada coisa vem e evita conflito de nomes.
  • 🎨 Dentro do Tkinter existe também o módulo ttk, com versões de visual mais moderno dos widgets. Hoje ficamos no tk clássico — o ttk fica citado pra quando você quiser refinar.

a POO que você já sabe

Widgets são objetos — você já sabe POO de tela

  • 🧱 Widget é cada componente visual: rótulo, botão, campo, quadro. E cada widget é um OBJETO — tk.Button(...) é instanciar uma classe, igual Cachorro() na aula 02.
  • 🏷️ tk.Label(janela, text="Oi", fg="blue") — esses argumentos com nome são os parâmetros nomeados da aula 05. Configurar um widget é passar kwargs pro construtor.
  • 🧩 A janela TEM widgets; um Frame TEM widgets — composição (aula 05). Uma tela inteira é uma árvore de objetos compostos, do tk.Tk() raiz até o último botão.
  • 🔁 E os métodos: rotulo.config(...) reconfigura, campo.get() lê, botao.pack() posiciona. Interagir com a tela é chamar métodos de objetos — POO pura, agora visível.

primeira janela

A primeira janela em 5 linhas

Cinco linhas e o Python abre uma janela DE VERDADE no seu sistema — com barra de título, botão de fechar, tudo. Digite, rode, e veja: seu código saiu do terminal.

🧱
tk.Tk() instancia a janela raiz — o objeto que representa a janela principal. Toda app Tkinter tem exatamente UMA.
mainloop() é a última linha SEMPRE: dali em diante quem manda é o loop de eventos. O script "para" ali — de propósito — até a janela fechar.
janela_minima.py
import tkinter as tk

janela = tk.Tk()                          # cria a janela raiz (um OBJETO)
janela.title("Minha primeira janela")     # método do objeto janela
janela.geometry("300x150")                # largura x altura, em pixels

janela.mainloop()                         # entrega o controle ao loop de eventos
Cuidado com um erro clássico: chamar a variável de tk (tk = tk.Tk()). Isso SOMBREIA o módulo — na linha seguinte, tk.Label(...) explode com AttributeError: '_tkinter.tkapp' object has no attribute 'Label'. Use janela, raiz, app... qualquer nome que não seja tk.

primeira janela

É isto que aparece na tela

O resultado das 5 linhas: uma janela vazia de 300 × 150 pixels, com o título que a gente definiu — e os botões de fechar/minimizar que o sistema operacional dá de graça.

Screenshot de uma janela vazia do Tkinter no macOS, com 300 por 150 pixels e o título 'Minha primeira janela' na barra de título.
A moldura (barra de título, botões de fechar/minimizar, sombra) é do sistema operacional — no Windows e no Linux a MESMA janela aparece com a moldura de lá. O Tkinter cuida do conteúdo; o sistema cuida da moldura.

primeira janela · anatomia

Anatomia das 5 linhas

Cada linha da janela mínima tem um papel. Leia de cima pra baixo — é o esqueleto de TODA app Tkinter que você vai escrever daqui pra frente.

1

janela = tk.Tk()

Cria a janela raiz: um objeto da classe Tk. É o container de tudo — todo widget vai nascer "dentro" dela. Uma app, UMA raiz.

2

janela.title("...")

Método do objeto janela: define o texto da barra de título. Sem ele, a janela abre com o título genérico "tk".

3

janela.geometry("300x150")

Define o tamanho como uma string "largura x altura" em pixels (o x é a letra x mesmo). Sem geometry, a janela nasce do tamanho do conteúdo.

4

janela.mainloop()

Entrega o controle ao loop de eventos: a janela fica viva, esperando cliques e teclas. Tudo que vier DEPOIS desta linha só roda quando a janela fechar.

primeira janela · o coração

mainloop(): o loop que espera e avisa

  • 🔁 O mainloop() é um laço infinito dentro do Tkinter: ele fica esperando EVENTOS (clique, tecla, fechar janela) e, quando um acontece, AVISA quem se inscreveu pra reagir.
  • 📣 Soa familiar? É o Observer da aula 21 por baixo dos panos: o loop despacha cada evento pros observers registrados. Você NÃO chama suas funções — o mainloop chama por você.
  • 🤵 Analogia do garçom: o mainloop() é o garçom circulando pelo salão. Ele espera alguém levantar a mão (o evento) e atende QUEM levantou (chama o callback registrado). Depois volta a circular — pra sempre, até o restaurante fechar.
  • ⏸️ Por isso ele é a ÚLTIMA linha: monte a tela inteira ANTES, e entregue o controle no final. O script "congela" no mainloop() — e isso é o comportamento certo.

primeira janela · armadilha

Armadilha: esquecer (ou atropelar) o mainloop

Os dois erros clássicos da primeira semana de Tkinter: esquecer o mainloop() — e colocar código depois dele esperando que rode.

Sem mainloop — a janela some

sem_mainloop.py
import tkinter as tk

janela = tk.Tk()
janela.title("Cadê a janela?")
# ...e o script acaba aqui.
# Sem mainloop(), o Python chega ao
# fim do arquivo e ENCERRA — a janela
# fecha na hora (ou nem aparece).

Código depois do mainloop

depois_do_mainloop.py
import tkinter as tk

janela = tk.Tk()
janela.mainloop()

print("Oi!")   # só roda quando a
# janela FECHAR — o mainloop segura
# o controle até lá. Parece que o
# print "não funciona".
A regra: monte a tela inteira ANTES, e deixe o mainloop() como ÚLTIMA linha. O que precisa acontecer durante a vida da janela não vem depois do mainloop — vem dentro de callbacks, que é o assunto do bloco de eventos.

widgets · panorama

Os 4 widgets de hoje

Uma janela vazia não serve pra nada. Estes quatro objetos cobrem 90% das telas que você vai montar — e cada um é uma classe do tk.

01

tk.Label

O rótulo: mostra um texto (ou imagem) na tela. Não reage a clique — só informa. É o print() do mundo visual.

02

tk.Button

O botão: mostra um texto e, no clique, chama a função registrada no command=. É onde o Observer da aula 21 entra em cena.

03

tk.Entry

O campo de UMA linha onde o usuário digita. O .get() devolve o que foi digitado — SEMPRE como string. É o input() do mundo visual.

04

tk.Frame

O quadro: um widget invisível que AGRUPA outros widgets. Composição (aula 05) pra organizar a tela em blocos.

widgets · Label e Button

Label e Button: o primeiro clique que reage

Um Label dá as boas-vindas e um Button reage ao clique. Repare no command=ao_clicar — SEM parênteses: a gente entrega a função pro botão. Isto é inscrever um observer (aula 21).

📌
.pack() é o que COLOCA o widget na tela: sem um gerente de layout, o widget existe mas não aparece. A gente destrincha o pack a fundo no próximo bloco (layout).
📣
command=ao_clicar é o Observer da aula 21: o botão é o Subject, ao_clicar é o observer inscrito, o clique é a notificação — e quem chama a função é o mainloop.
🖨️
O print ainda sai no TERMINAL — janela e terminal convivem. Daqui a pouco a resposta vai aparecer NA tela, com .config().
ola_botao.py
import tkinter as tk

def ao_clicar():                          # callback: será o observer do botão
    print("Botão clicado!")

janela = tk.Tk()                          # a janela raiz (um OBJETO)
janela.title("Olá, botão")
janela.geometry("300x150")

rotulo = tk.Label(janela, text="Bem-vindo à aula 22!")  # Label é OBJETO; janela = o PAI
rotulo.pack(pady=10)                      # .pack() = gerente de layout: põe na tela

botao = tk.Button(janela, text="Clique aqui", command=ao_clicar)  # command = inscreve o observer
botao.pack()

janela.mainloop()                         # loop de eventos: é ele que chama ao_clicar
# cada clique no botão imprime no terminal: Botão clicado!
O primeiro argumento de todo widget é o PAI: tk.Label(janela, ...) diz "este rótulo mora dentro da janela".

widgets · Label e Button

A janela do ola_botao.py

O Label em cima, o Button embaixo — empilhados pelo pack(). Cada clique no botão imprime Botão clicado! no terminal que abriu a janela.

Screenshot de uma janela Tkinter com o título 'Olá, botão', mostrando o texto 'Bem-vindo à aula 22!' e, abaixo, um botão com o rótulo 'Clique aqui'.
Teste na sua máquina: clique 3 vezes e olhe o terminal — três linhas "Botão clicado!". A janela não mostra nada de novo (ainda): quem reage é o print. Colocar a reação NA janela é o que o Contador vai fazer no bloco de eventos.

widgets · Entry

Entry: o usuário digita, o .get() devolve

O Entry é o input() do mundo visual: um campo de uma linha. A diferença: você lê QUANDO QUISER, chamando campo.get() — em geral, dentro do callback de um botão.

🔤
.get() devolve SEMPRE string — mesmo que o usuário digite 25, chega "25". Igualzinho ao input() que vocês conhecem desde a aula 01.
⏱️
O .get() é chamado DENTRO do callback — na hora do clique. Se você chamar na montagem da tela, pega o campo ainda vazio.
campo_nome.py
import tkinter as tk

def cumprimentar():
    nome = campo_nome.get()       # .get() devolve SEMPRE string
    print(f"Olá, {nome}!")

janela = tk.Tk()
janela.title("Campo de nome")
janela.geometry("300x150")

rotulo = tk.Label(janela, text="Digite seu nome:")   # rótulo fixo
rotulo.pack(pady=5)

campo_nome = tk.Entry(janela, width=25)   # Entry = caixa de texto; width= é kwarg
campo_nome.pack()

botao = tk.Button(janela, text="Cumprimentar", command=cumprimentar)  # command = observer
botao.pack(pady=10)

janela.mainloop()
# digitando "Ana" e clicando: Olá, Ana!
Precisa de número? Converta: int(campo.get()) ou float(campo.get()) — e lembre que conversão pode falhar: int("vinte e cinco") levanta ValueError: invalid literal for int() with base 10: 'vinte e cinco'. O try/except da aula 14 volta à cena quando a GUI encontra o usuário real.

widgets · Entry

A janela do campo_nome.py

Rótulo, campo e botão empilhados. O usuário digita no Entry; o clique dispara o cumprimentar(), que lê o campo com .get() e imprime no terminal.

Screenshot de uma janela Tkinter com o título 'Campo de nome', mostrando o texto 'Digite seu nome:', um campo de entrada de texto e um botão 'Cumprimentar'.
O fluxo completo: o usuário digita "Ana" → clica em Cumprimentar → o mainloop avisa o callback → cumprimentar() chama campo_nome.get() → "Olá, Ana!" no terminal. Evento, notificação, reação — o vocabulário do Observer descrevendo a tela.

widgets · Frame

Frame: um widget que TEM widgets

O Frame é um quadro que agrupa widgets — composição (aula 05) na tela: a janela TEM quadros, cada quadro TEM os seus widgets. É assim que uma tela grande vira blocos organizados.

🧩
Repare no PAI de cada widget: o rótulo e o campo nascem DENTRO de quadro_dados; os botões, DENTRO de quadro_botoes. A árvore de composição da tela.
🖼️
bd=2, relief="groove" desenha uma moldura no quadro de dados — útil pra ENXERGAR o Frame. O quadro_botoes não tem moldura: Frame é invisível por padrão.
quadros.py
import tkinter as tk

janela = tk.Tk()
janela.title("Quadros")
janela.geometry("320x180")

# um Frame agrupa widgets — a janela COMPÕE o quadro, o quadro COMPÕE os campos
quadro_dados = tk.Frame(janela, bd=2, relief="groove")   # Frame é OBJETO; pai = janela
quadro_dados.pack(padx=10, pady=10, fill="x")

rotulo = tk.Label(quadro_dados, text="Dados do cliente")   # agora o PAI é o quadro
rotulo.pack()
campo_nome = tk.Entry(quadro_dados, width=25)
campo_nome.pack(pady=5)

quadro_botoes = tk.Frame(janela)          # segundo quadro, também filho da janela
quadro_botoes.pack(pady=10)

botao_salvar = tk.Button(quadro_botoes, text="Salvar")
botao_salvar.pack(side="left", padx=5)    # side="left" = lado a lado
botao_cancelar = tk.Button(quadro_botoes, text="Cancelar")
botao_cancelar.pack(side="left", padx=5)  # idem, ao lado do anterior

janela.mainloop()
Além de organizar, o Frame resolve um problema de layout que ainda vem nesta aula: cada Frame pode usar o SEU próprio gerente de layout. Botões lado a lado num Frame, formulário em grid em outro — sem briga.

widgets · Frame

A janela do quadros.py

Dois blocos: o quadro de dados (com moldura groove) agrupando rótulo e campo, e o quadro de botões alinhando Salvar e Cancelar lado a lado.

Screenshot de uma janela Tkinter com o título 'Quadros': um quadro com moldura contendo o texto 'Dados do cliente' e um campo de texto, e abaixo dois botões lado a lado, 'Salvar' e 'Cancelar'.
Essa estrutura — um bloco de campos em cima, uma barra de botões embaixo — é O layout clássico de formulário. Você vai reproduzi-la no exercício 3 (a ficha de matrícula) e em praticamente toda tela da parte 2.

layout · o problema

Quem posiciona os widgets? O geometry manager

  • 👻 Criar o widget NÃO o coloca na tela: tk.Label(janela, text="Oi") sozinho existe na memória, mas não aparece. Quem decide ONDE ele entra é um gerente de layout (geometry manager).
  • 📐 O Tkinter tem três: pack() — empilha os widgets pelos lados; grid() — organiza em linhas × colunas; place() — coordenadas exatas em pixel.
  • 🧠 A filosofia: você NÃO calcula pixels. Descreve a intenção ("neste lado", "nesta linha e coluna") e o gerente posiciona — e REposiciona quando a janela muda de tamanho.
  • ⚖️ Regra que evita dor de cabeça (e que tem um slide de armadilha só pra ela): UM gerente por container. Dentro da mesma janela ou do mesmo Frame, não misture pack com grid.

layout · pack

pack: empilhando pelos lados

O pack() empilha: side= escolhe o lado, fill= estica no eixo, expand=True ocupa o espaço que sobrar. Quatro rótulos coloridos mostram o mecanismo:

📚
side= aceita "top" (padrão), "bottom", "left" e "right". A ordem dos pack() importa: cada widget pega o espaço na vez dele.
↔️
fill="x" estica na horizontal, "y" na vertical, "both" nos dois — e expand=True faz o widget reivindicar o espaço LIVRE da janela.
pack_lados.py
import tkinter as tk

janela = tk.Tk()
janela.title("pack: lados")
janela.geometry("360x200")

topo = tk.Label(janela, text="topo (side='top')", bg="#378add", fg="white")  # bg=, fg= = kwargs
topo.pack(side="top", fill="x")           # gerente de layout: encosta no topo, estica na horizontal

rodape = tk.Label(janela, text="rodapé (side='bottom')", bg="#5dd88a")
rodape.pack(side="bottom", fill="x")      # encosta embaixo

esquerda = tk.Label(janela, text="esquerda", bg="#f0c060")
esquerda.pack(side="left", fill="y")      # encosta à esquerda, estica na vertical

centro = tk.Label(janela, text="centro (expand=True)", bg="#d15555", fg="white")
centro.pack(side="left", fill="both", expand=True)   # expand=True ocupa o espaço que sobra

janela.mainloop()
pack é ótimo pra estruturas simples: uma barra no topo, um rodapé, botões lado a lado. Quando a tela vira um formulário (rótulo + campo, linha por linha), alinhar com pack fica ingrato — é a deixa do grid, no próximo slide.

layout · pack

A janela do pack_lados.py

Cada cor mostra uma decisão de layout: topo e rodapé com fill="x", a lateral com fill="y", e o centro com expand=True ocupando todo o resto.

Screenshot de uma janela Tkinter dividida em quatro áreas coloridas: uma faixa azul no topo, uma faixa verde no rodapé, uma coluna amarela à esquerda e uma área vermelha ocupando o centro expandido.
Redimensione a janela no seu computador e observe: o gerente recalcula tudo — o topo continua colado em cima, o centro continua ocupando a sobra. Você descreveu a INTENÇÃO; o pack faz a conta.

layout · grid

grid: o formulário em linhas e colunas

O grid() pensa como planilha: row= e column= posicionam cada widget na célula. É O gerente de formulários — rótulos na coluna 0, campos na coluna 1:

🧲
sticky="e" cola o widget no lado LESTE (east — direita) da célula: é o que alinha os dois-pontos dos rótulos rente aos campos.
↔️
columnspan=2 faz o botão atravessar as duas colunas — por isso ele fica centralizado embaixo do formulário, em vez de preso a uma coluna.
formulario_grid.py
import tkinter as tk

janela = tk.Tk()
janela.title("Formulário")

rotulo_nome = tk.Label(janela, text="Nome:")
rotulo_nome.grid(row=0, column=0, sticky="e", padx=5, pady=5)   # .grid() = gerente por linha/coluna
campo_nome = tk.Entry(janela, width=25)
campo_nome.grid(row=0, column=1, padx=5, pady=5)   # rótulo e campo na mesma linha, colunas 0 e 1

rotulo_email = tk.Label(janela, text="E-mail:")
rotulo_email.grid(row=1, column=0, sticky="e", padx=5, pady=5)  # idem, agora na linha 1
campo_email = tk.Entry(janela, width=25)
campo_email.grid(row=1, column=1, padx=5, pady=5)

botao_salvar = tk.Button(janela, text="Salvar")
botao_salvar.grid(row=2, column=0, columnspan=2, pady=10)   # columnspan=2 = ocupa as duas colunas

janela.mainloop()
Sem geometry(), a janela nasce do tamanho exato do conteúdo — o grid calcula. E repare que padx/pady existem nos dois gerentes: respiro entre os widgets, em pixels.

layout · grid

A janela do formulario_grid.py

Duas colunas: rótulos alinhados à direita (sticky="e"), campos à esquerda, e o botão atravessando as duas colunas com columnspan=2.

Screenshot de uma janela Tkinter com o título 'Formulário': duas linhas com os rótulos 'Nome:' e 'E-mail:' alinhados à direita, cada um seguido de um campo de texto, e um botão 'Salvar' centralizado abaixo.
Grave esta forma: é o formulário que a gente vai usar no cadastro do exercício ao vivo, na ficha de matrícula do exercício 3 e na tela de login do desafio. Formulário = grid.

layout · escolhendo

pack, grid ou place? O guia rápido

item detalhe
pack Empilhar poucos widgets numa direção: barra no topo, rodapé, botões lado a lado. Controles: side=, fill=, expand=. Simples e direto.
grid Formulários e telas em linhas × colunas — o caso mais comum. Controles: row=, column=, sticky=, columnspan=. NA DÚVIDA, USE GRID.
place Coordenadas exatas em pixel (x=, y=). Quase nunca: a tela quebra quando a janela redimensiona ou a fonte muda. Saiba que existe — e evite.
A regra de ouro UM gerente por container. Dentro da mesma janela ou do mesmo Frame, escolha pack OU grid — nunca os dois. Frames DIFERENTES podem usar gerentes diferentes.

layout · armadilha

Armadilha: pack e grid no MESMO container

Este é o bug clássico de layout — e o Tkinter não perdoa: misturou os dois gerentes (pack e grid) no MESMO container? Ele levanta um erro na hora e o programa para.

O código que quebra

mistura.py
import tkinter as tk

janela = tk.Tk()

rotulo = tk.Label(janela, text="Oi")
rotulo.pack()                 # pack na janela...

campo = tk.Entry(janela)
campo.grid(row=0, column=1)   # ...e grid na MESMA janela

janela.mainloop()

O que o Tkinter diz

o erro
_tkinter.TclError: cannot use
geometry manager grid inside .
which already has slaves
managed by pack

(Em Macs antigos com Tk 8.5, em vez
do erro a janela pode CONGELAR —
mesma causa, sintoma diferente.)
A defesa é a regra de ouro: UM gerente por container. Quer misturar? Frames diferentes: o formulário em grid num Frame, os botões em pack noutro — cada container com o seu gerente, sem briga. Se a janela quebrar com esse TclError, a PRIMEIRA suspeita é essa mistura.

eventos · a colheita do Observer

A colheita do Observer: command= É o padrão da aula 21

  • 📣 command=minha_funcao é REGISTRAR um observer no botão: "quando o clique acontecer, me avisa chamando esta função". O mesmo inscrever() que você escreveu no Canal da aula 21.
  • 🔔 O clique é a NOTIFICAÇÃO: o mainloop percebe o evento e chama a função inscrita. Você nunca chama o callback — ele É chamado. Por isso o nome: callback, "chamada de volta".
  • 🧠 Os três papéis do diagrama da aula 21, vivos na tela: o botão é o Subject, a sua função é o observer, e o mainloop é quem despacha as notificações.
  • A frase-âncora da aula inteira: widget é objeto; janela é composição; callback é Observer. Nada de Python novo — a POO das aulas 02, 05 e 21 acabou de ganhar uma tela.

eventos · armadilha

Armadilha: command=f() com parênteses = botão morto

O erro nº 1 de quem começa com Tkinter. A diferença é UM par de parênteses — e ela decide se o botão funciona ou nasce morto.

Com parênteses — errado

botao_morto.py
def ao_clicar():
    print("Botão clicado!")

botao = tk.Button(
    janela,
    text="Clique",
    command=ao_clicar(),  # 💥 CHAMA agora!
)
# na MONTAGEM da tela já imprime:
# Botão clicado!
# ...e registra o RETORNO: None.
# Clicar depois não faz NADA.

Sem parênteses — certo

botao_vivo.py
def ao_clicar():
    print("Botão clicado!")

botao = tk.Button(
    janela,
    text="Clique",
    command=ao_clicar,    # a FUNÇÃO em si
)
# nada roda na montagem; a função
# fica REGISTRADA no botão.
# cada clique: Botão clicado!
É a função como VALOR da aula 18: command= quer a função em si (pra guardar e chamar depois), não o resultado dela. O sintoma do erro é característico: o print sai UMA vez, na abertura da janela, e o botão nunca mais responde. "E se o meu callback PRECISA de argumento?" — próximo slide.

eventos · callback com argumento

Callback com argumento: entra o lambda

command=escolher("Calabresa") cairia na armadilha dos parênteses — executaria na montagem. A saída: embrulhar a chamada num lambda (aula 18): uma função sem nome que, SÓ no clique, chama escolher() com o argumento certo.

🎁
O lambda é o embrulho: command= recebe uma FUNÇÃO (o lambda), e a chamada escolher("Calabresa") só acontece quando o lambda for executado — no clique.
♻️
Uma função escolher() só, N botões: cada lambda registra o argumento do seu botão. Sem copiar e colar escolher_calabresa, escolher_mussarela...
sabores.py
import tkinter as tk

def escolher(sabor):                      # recebe o sabor como argumento
    print(f"Sabor escolhido: {sabor}")

janela = tk.Tk()
janela.title("Pizzaria")

# lambda: função anônima que, no clique, chama escolher() com o argumento
botao1 = tk.Button(janela, text="Calabresa",
                   command=lambda: escolher("Calabresa"))   # o lambda é o observer
botao1.pack(pady=2)

botao2 = tk.Button(janela, text="Mussarela",
                   command=lambda: escolher("Mussarela"))   # idem, com outro sabor
botao2.pack(pady=2)

janela.mainloop()
# clique em "Calabresa":  Sabor escolhido: Calabresa
# clique em "Mussarela":  Sabor escolhido: Mussarela
É o mesmo lambda do sorted(key=...) da aula 18 — uma função pequena, criada na hora, passada como valor. Lá ele era a estratégia do Strategy; aqui é o observer do botão. As duas pontas do que você aprendeu na aula 21.

eventos · método como callback

O callback pode ser um MÉTODO: o Contador

Quando o callback precisa de MEMÓRIA entre um clique e outro, a resposta é POO: guarde o estado num atributo e registre um MÉTODO como observer. E repare no .config(): agora a reação aparece NA tela.

🧬
O estado vive em self.cliques — um atributo (aula 02), não uma variável global. Cada clique incrementa e o valor SOBREVIVE entre as notificações.
🔄
self.rotulo.config(text=...) reconfigura um widget VIVO: o texto do rótulo muda na janela aberta. Primeira vez que a resposta aparece na tela, não no terminal.
contador.py
import tkinter as tk

class Contador:
    def __init__(self, janela):           # janela = o PAI dos widgets
        self.cliques = 0                  # estado guardado no objeto
        self.rotulo = tk.Label(janela, text="Cliques: 0")   # widget vira atributo
        self.rotulo.pack(pady=10)
        self.botao = tk.Button(janela, text="Contar",
                               command=self.ao_clicar)   # método como observer
        self.botao.pack()

    def ao_clicar(self):                  # método bound: já carrega o self
        self.cliques += 1
        self.rotulo.config(text=f"Cliques: {self.cliques}")   # atualiza a TELA
        print(f"Cliques: {self.cliques}")

janela = tk.Tk()
janela.title("Contador")
janela.geometry("300x150")
contador = Contador(janela)               # instanciar a classe monta a tela
janela.mainloop()
# três cliques imprimem: Cliques: 1 / Cliques: 2 / Cliques: 3
# e o rótulo na janela acompanha: "Cliques: 3"
command=self.ao_clicar registra um método LIGADO ao objeto: quando o mainloop notificar, o self certo vai junto. Esse desenho — widgets como atributos, callbacks como métodos — é o embrião do "jeito profissional": na parte 2, a app INTEIRA vira uma classe (herdando de tk.Tk).

eventos · amarrando com a POO

Conexão: eventos são a POO que você já sabe

  • 🧱 Objetos e métodos (aula 02): cada widget é um objeto instanciado com kwargs; .pack(), .get(), .config() são métodos dele. O Contador guarda estado em self.cliques — atributo, não global.
  • 📦 Função como valor (aula 18): command=f entrega a função sem executar; lambda embrulha a chamada com argumento. O mesmo mecanismo do sorted(key=...).
  • 📣 Observer (aula 21): command= inscreve, o clique notifica, o mainloop despacha. Você desenhou esse diagrama anteontem — hoje ele tem botão e janela.
  • 🧩 Composição (aula 05): a janela TEM frames, frames TÊM widgets, o Contador TEM rótulo e botão. Toda tela é uma árvore de objetos compostos.

da tela pro objeto

O formulário vira objeto: a GUI encontra as SUAS classes

  • 🖊️ Até agora, o .get() virava print. O passo que muda o jogo: o que o usuário digita vira um OBJETO de uma classe NOSSA — com atributos, métodos e __str__.
  • 🏗️ O callback lê os Entrys e chama o construtor: Cliente(campo_nome.get(), campo_email.get()). A tela é só a porta de entrada; quem carrega os dados pelo sistema é o objeto.
  • 🔗 Essa é a arquitetura de qualquer sistema real: a INTERFACE coleta, o OBJETO representa. Na parte 2, esses objetos vão pra uma lista (aula 18) e aparecem num Listbox — o começo do CRUD visual.

da tela pro objeto

novo_cliente.py: do Entry pro objeto Cliente

Tudo desta aula num arquivo só: a classe Cliente com __str__ (aula 03), o formulário em grid, e o callback salvar — o observer do botão — construindo o objeto a partir da tela.

🏗️
Cliente(campo_nome.get(), campo_email.get()) — dois .get() viram os argumentos do construtor. A ponte tela → objeto em UMA linha.
🪞
O print(cliente) sai legível por causa do __str__ (aula 03). Sem ele, você veria <__main__.Cliente object at 0x...>.
novo_cliente.py
import tkinter as tk

class Cliente:
    def __init__(self, nome, email):
        self.nome = nome
        self.email = email

    def __str__(self):                    # __str__ = representação do objeto
        return f"Cliente: {self.nome} <{self.email}>"

def salvar():                             # observer do botão Salvar
    # a tela COLETA; o objeto REPRESENTA
    cliente = Cliente(campo_nome.get(), campo_email.get())   # .get() lê os Entry
    print(cliente)                        # print usa o __str__

janela = tk.Tk()
janela.title("Novo cliente")

rotulo_nome = tk.Label(janela, text="Nome:")
rotulo_nome.grid(row=0, column=0, sticky="e", padx=5, pady=5)   # formulário no grid
campo_nome = tk.Entry(janela, width=25)
campo_nome.grid(row=0, column=1, padx=5, pady=5)

rotulo_email = tk.Label(janela, text="E-mail:")
rotulo_email.grid(row=1, column=0, sticky="e", padx=5, pady=5)  # idem para o e-mail
campo_email = tk.Entry(janela, width=25)
campo_email.grid(row=1, column=1, padx=5, pady=5)

botao_salvar = tk.Button(janela, text="Salvar", command=salvar)   # command = inscreve salvar
botao_salvar.grid(row=2, column=0, columnspan=2, pady=10)

janela.mainloop()
# preenchendo "Ana Souza" / "ana@email.com" e clicando em Salvar:
# Cliente: Ana Souza <ana@email.com>
Falta uma coisa pra isso virar um cadastro de verdade: e se o usuário clicar com os campos vazios? Validação — é exatamente o que o exercício ao vivo adiciona agora.

ao vivo · vamos construir juntos

Exercício ao Vivo: cadastro.py em camadas

Vamos montar juntos um cadastro de clientes completo: formulário em grid, validação de campos vazios e um status colorido NA PRÓPRIA janela — verde no sucesso, vermelho no erro. Cada camada entra, roda e só então vem a próxima.

1

Camada 1 — a classe e a janela

A classe Cliente com __str__ (o objeto que a tela vai produzir) e a janela raiz: título "Cadastro de clientes", geometry("340x180").

2

Camada 2 — o formulário com grid

Dois pares Label + Entry (Nome, E-mail) nas linhas 0 e 1, rótulos com sticky="e"; o botão Salvar com columnspan=2 na linha 2; e um rotulo_status VAZIO na linha 3 — reservado pras mensagens.

3

Camada 3 — o callback salvar()

Valida: campo vazio → rotulo_status.config(text="Preencha todos os campos!", fg="red") e return. Válido → cria o Cliente, print(cliente), status VERDE com o nome cadastrado e delete(0, tk.END) limpando os dois campos.

4

Testar os DOIS caminhos

Clicar com tudo vazio (status vermelho, nada no terminal) e clicar preenchido (print no terminal, status verde, campos limpos). Callback com dois caminhos = teste com dois cliques.

resolução · camadas 1 e 2

Camadas 1 e 2: a classe, a janela e o formulário

Primeiro o esqueleto: a classe Cliente e o formulário em grid. A novidade fica na última linha — um Label de texto VAZIO, esperando as mensagens de status.

🧱
Mesmo formulário do novo_cliente.py — rótulos sticky="e", campos na coluna 1. A repetição é proposital: essa forma tem que sair no reflexo.
🪧
O rotulo_status nasce com text="": invisível até a primeira mensagem. O callback vai escrever nele com .config() — o truque do Contador.
cadastro.py
import tkinter as tk

# Camada 1: a classe que a tela vai produzir
class Cliente:
    def __init__(self, nome, email):
        self.nome = nome
        self.email = email

    def __str__(self):                    # __str__ = representação do objeto
        return f"Cliente: {self.nome} <{self.email}>"

# Camada 2: a janela e o formulário
janela = tk.Tk()
janela.title("Cadastro de clientes")
janela.geometry("340x180")

rotulo_nome = tk.Label(janela, text="Nome:")
rotulo_nome.grid(row=0, column=0, sticky="e", padx=5, pady=5)   # rótulo + campo no grid
campo_nome = tk.Entry(janela, width=25)
campo_nome.grid(row=0, column=1, padx=5, pady=5)

rotulo_email = tk.Label(janela, text="E-mail:")
rotulo_email.grid(row=1, column=0, sticky="e", padx=5, pady=5)  # idem para o e-mail
campo_email = tk.Entry(janela, width=25)
campo_email.grid(row=1, column=1, padx=5, pady=5)

rotulo_status = tk.Label(janela, text="")      # começa vazio: é o painel de avisos
rotulo_status.grid(row=3, column=0, columnspan=2)
A linha 2 do grid ficou reservada: é onde o botão Salvar entra na próxima camada, junto com o callback que ele registra.

resolução · camada 3

Camada 3: o callback salvar(), com validação

O coração do cadastro: o observer do botão Salvar. Dois caminhos — o return antecipado no erro (padrão da aula 14: valide ANTES de criar o objeto) e o caminho feliz que cria, imprime, avisa e limpa.

🛡️
A validação vem PRIMEIRO: campo vazio → mensagem vermelha e return. O objeto Cliente só nasce com dados válidos — a lição da aula 14 aplicada na GUI.
🧹
campo.delete(0, tk.END) apaga do caractere 0 até o fim: os campos ficam limpos pro próximo cadastro. O par do .get() — um lê, o outro esvazia.
cadastro.py
# Camada 3: o callback — o observer do botão Salvar
def salvar():
    nome = campo_nome.get()               # .get() lê o Entry
    email = campo_email.get()
    if nome == "" or email == "":
        rotulo_status.config(text="Preencha todos os campos!", fg="red")   # .config() atualiza a TELA
        return                        # caminho do erro: para AQUI
    cliente = Cliente(nome, email)        # cria o objeto a partir da tela
    print(cliente)                    # Cliente: Ana Souza <ana@email.com>
    rotulo_status.config(text=f"{cliente.nome} cadastrado!", fg="green")
    campo_nome.delete(0, tk.END)      # limpa os campos pro próximo cadastro
    campo_email.delete(0, tk.END)

botao_salvar = tk.Button(janela, text="Salvar", command=salvar)   # command = inscreve salvar
botao_salvar.grid(row=2, column=0, columnspan=2, pady=10)

janela.mainloop()                         # loop de eventos: espera cliques
Repare que salvar() usa campo_nome, campo_email e rotulo_status sem recebê-los: o Python resolve os nomes na hora da CHAMADA (o clique), quando tudo já existe. No arquivo final, def salvar() aparece antes dos widgets — e funciona por isso.

resolução · o código completo

Tudo junto: cadastro.py

As três camadas no arquivo final — é exatamente isto que você roda. Classe, callback com validação, formulário, status. Quarenta linhas e é um sistema com cara de sistema.

🗺️
Leia o arquivo como um mapa da aula: widgets-objetos, formulário em grid (um gerente só!), command=salvar sem parênteses, .get()/.config()/.delete() no callback.
🏗️
E a ponte da POO: a tela coleta, valida — e só então o Cliente nasce. A interface serve ao objeto, não o contrário.
cadastro.py
import tkinter as tk

class Cliente:                            # o modelo (objeto que a tela produz)
    def __init__(self, nome, email):
        self.nome = nome
        self.email = email

    def __str__(self):                    # __str__ = representação do objeto
        return f"Cliente: {self.nome} <{self.email}>"

def salvar():                             # o observer do botão Salvar
    nome = campo_nome.get()               # .get() lê os Entry
    email = campo_email.get()
    if nome == "" or email == "":
        rotulo_status.config(text="Preencha todos os campos!", fg="red")
        return
    cliente = Cliente(nome, email)
    print(cliente)                    # Cliente: Ana Souza <ana@email.com>
    rotulo_status.config(text=f"{cliente.nome} cadastrado!", fg="green")   # .config() atualiza a TELA
    campo_nome.delete(0, tk.END)      # limpa os campos pro próximo cadastro
    campo_email.delete(0, tk.END)

janela = tk.Tk()                          # a tela
janela.title("Cadastro de clientes")
janela.geometry("340x180")

rotulo_nome = tk.Label(janela, text="Nome:")
rotulo_nome.grid(row=0, column=0, sticky="e", padx=5, pady=5)   # formulário no grid
campo_nome = tk.Entry(janela, width=25)
campo_nome.grid(row=0, column=1, padx=5, pady=5)

rotulo_email = tk.Label(janela, text="E-mail:")
rotulo_email.grid(row=1, column=0, sticky="e", padx=5, pady=5)  # idem para o e-mail
campo_email = tk.Entry(janela, width=25)
campo_email.grid(row=1, column=1, padx=5, pady=5)

botao_salvar = tk.Button(janela, text="Salvar", command=salvar)   # command = inscreve o observer
botao_salvar.grid(row=2, column=0, columnspan=2, pady=10)

rotulo_status = tk.Label(janela, text="")   # painel de avisos, começa vazio
rotulo_status.grid(row=3, column=0, columnspan=2)

janela.mainloop()                         # loop de eventos
Digite este arquivo junto — não copie: os dedos aprendem o que o olho só reconhece.

resolução · testando

O teste dos dois caminhos

Callback com if se testa clicando duas vezes — uma por caminho. O que acontece em cada clique, no terminal E na janela:

Clicar com os campos vazios

caminho do erro
No terminal: nada é impresso.

Na janela, o status mostra,
em VERMELHO:

  Preencha todos os campos!

O return interrompeu o salvar()
antes de criar o Cliente — o
objeto inválido nem chegou a
existir.

Preencher Ana Souza / ana@email.com

caminho feliz
No terminal:

  Cliente: Ana Souza <ana@email.com>

Na janela, o status fica VERDE:

  Ana Souza cadastrado!

...e os dois campos são limpos
pelo delete(0, tk.END), prontos
pro próximo cadastro.
Dois cliques, dois caminhos, zero surpresa: é o critério de pronto do exercício. Guarde o padrão — validar na entrada, avisar na tela, limpar pro próximo — porque a tela de login do desafio pede exatamente esses reflexos.

resolução · o resultado

O cadastro depois de um cadastro

A janela logo após salvar a Ana: status verde com o nome cadastrado e os campos já limpos pro próximo cliente — o caminho feliz congelado em um screenshot.

Screenshot da janela 'Cadastro de clientes' após um cadastro bem-sucedido: campos Nome e E-mail vazios, botão Salvar, e a mensagem 'Ana Souza cadastrado!' em verde na parte inferior.
Compare com o seu: se o status não aparece, confira o rotulo_status na linha 3 do grid; se o clique não faz nada, confira o command=salvar (SEM parênteses); se travou sem mensagem... você misturou pack com grid.

ao vivo · vamos construir juntos

Exercício ao Vivo 2: o cadastro que NÃO esquece

O cadastro anterior tinha um furo: fechou a janela, os clientes sumiram — a memória volátil que a aula 17 apontou. Agora a gente resolve: cada cliente vai pra uma LISTA na tela (aula 18) E pra um ARQUIVO no disco (aula 17). De novo em camadas — cada uma entra, roda, e só então vem a próxima.

1

Camada 1 — a classe Cliente ganha memória

A Cliente recebe dois métodos da aula 17: salvar(), que ANEXA uma linha ao clientes.txt (modo "a"), e o @classmethod carregar_todos(), que LÊ o arquivo de volta e devolve a lista de objetos — com try/except FileNotFoundError pra 1ª execução.

2

Camada 2 — a lista aparece na tela

No topo, clientes = Cliente.carregar_todos(): a lista já NASCE cheia com o que foi salvo antes. Um Frame-painel embaixo do formulário mostra a coleção, e atualizar_painel() a redesenha com "\n".join(...) e len() (aula 18).

3

Camada 3 — cadastrar() e o botão Ordenar

O cadastrar() valida, append na lista (aula 18) E cliente.salvar() no arquivo (aula 17). Um segundo botão, Ordenar por nome, chama clientes.sort(key=lambda c: c.nome) — os dois lado a lado num Frame.

4

Rodar DUAS vezes — o payoff

Cadastre Ana e Bruno, FECHE a janela e rode de novo: o painel abre já com os dois. Os dados sobreviveram ao fim do programa — a memória volátil da aula 17, finalmente resolvida. Esse é o critério de pronto.

resolução · camada 1

Camada 1: a classe Cliente ganha memória

A Cliente do cadastro anterior só existia na memória do programa — fechou, sumiu. Agora ela ganha dois métodos da aula 17: salvar() (anexa ao arquivo) e carregar_todos() (lê o arquivo de volta). O objeto passa a saber se guardar e se recuperar.

💾
salvar() abre o arquivo em modo "a" (append) e ANEXA uma linha — a persistência da aula 17. O "a" não apaga o que já está lá; cada cliente vira uma linha nome;email. Com Path(__file__).parent (aula 17), o clientes.txt nasce AO LADO do .py — rode de onde rodar.
📂
carregar_todos() é @classmethod: lê o arquivo, desfaz o split(";") e monta a lista de objetos (aula 18). O try/except FileNotFoundError (aula 17) segura a 1ª execução, quando o clientes.txt ainda não existe.
cadastro_lista.py
import tkinter as tk
from pathlib import Path

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

# Camada 1: a classe Cliente agora TEM memória (colheita da aula 17)
class Cliente:
    def __init__(self, nome, email):
        self.nome = nome
        self.email = email

    def __str__(self):                            # __str__ = representação do objeto
        return f"{self.nome} <{self.email}>"

    def salvar(self, caminho=ARQUIVO):            # aula 17: anexa ESTE cliente ao arquivo
        with open(caminho, "a", encoding="utf-8") as f:   # "a" = append (NÃO apaga o que já existe)
            f.write(f"{self.nome};{self.email}\n")         # campos com ";", \n encerra a linha

    @classmethod
    def carregar_todos(cls, caminho=ARQUIVO):     # aula 17: lê o arquivo -> devolve a lista
        clientes = []
        try:
            with open(caminho, "r", encoding="utf-8") as f:   # "r" = leitura
                for linha in f:                       # uma linha por cliente
                    nome, email = linha.strip().split(";")   # desfaz o que o salvar() juntou
                    clientes.append(cls(nome, email))  # aula 18: monta a lista de objetos
        except FileNotFoundError:                 # 1ª execução: o arquivo ainda não existe
            pass                                  # começa com lista vazia, sem quebrar
        return clientes
Repare no __str__: agora devolve só "nome ", sem o prefixo "Cliente:" — é esse texto que vai aparecer na lista do painel, uma linha por cliente. A classe que a tela produz agora carrega a própria persistência: gravar e carregar são responsabilidade DELA, não da interface.

resolução · camada 2

Camada 2: a lista aparece na tela

Com a classe pronta, a coleção entra na tela. A novidade está na primeira linha: clientes já NASCE cheia — carregar_todos() lê o arquivo antes de qualquer clique. E um Frame-painel embaixo do formulário mostra a lista, redesenhada por atualizar_painel().

🧮
atualizar_painel() reconstrói o painel a partir da lista: "\n".join(str(c) for c in clientes) monta o texto (aula 18), len(clientes) conta — e .config() (aula 22) redesenha os Labels vivos. Uma função, a fonte única da verdade na tela.
🧩
O painel é um Frame que COMPÕE dois Labels — rotulo_total e rotulo_lista (composição, aula 05). Nada de Listbox ainda: a coleção aparece num Label, dentro de um Frame. O widget de lista de verdade fica pra parte 2.
cadastro_lista.py
import tkinter as tk

# Camada 2: a lista aparece na tela
# a lista já NASCE com o que foi salvo antes (payoff da aula 17: a memória não some)
clientes = Cliente.carregar_todos()

def atualizar_painel():                       # redesenha o painel a partir da lista
    texto = "\n".join(str(c) for c in clientes)   # aula 18: "\n".join + comprehension
    rotulo_lista.config(text=texto)           # .config() atualiza o Label vivo
    rotulo_total.config(text=f"Total: {len(clientes)} cliente(s)")   # len() = quantos há

janela = tk.Tk()                              # a tela
janela.title("Cadastro de clientes")
janela.geometry("360x320")

rotulo_nome = tk.Label(janela, text="Nome:")
rotulo_nome.grid(row=0, column=0, sticky="e", padx=5, pady=5)   # formulário no grid
campo_nome = tk.Entry(janela, width=25)
campo_nome.grid(row=0, column=1, padx=5, pady=5)

rotulo_email = tk.Label(janela, text="E-mail:")
rotulo_email.grid(row=1, column=0, sticky="e", padx=5, pady=5)  # idem para o e-mail
campo_email = tk.Entry(janela, width=25)
campo_email.grid(row=1, column=1, padx=5, pady=5)

rotulo_status = tk.Label(janela, text="")     # painel de avisos, começa vazio
rotulo_status.grid(row=3, column=0, columnspan=2)

# o painel da lista: um Frame que COMPÕE dois Labels (composição, aula 05)
quadro_painel = tk.Frame(janela, bd=2, relief="groove")
quadro_painel.grid(row=4, column=0, columnspan=2, padx=10, pady=10, sticky="we")
rotulo_total = tk.Label(quadro_painel, text="Total: 0 cliente(s)")   # filho do quadro
rotulo_total.pack()
rotulo_lista = tk.Label(quadro_painel, text="", justify="left")      # a lista renderizada
rotulo_lista.pack()
clientes = Cliente.carregar_todos() na primeira linha é o payoff da aula 17: a lista abre com o que foi salvo em execuções anteriores — a memória não some mais quando a janela fecha. O rotulo_lista usa justify="left" pra alinhar as linhas à esquerda.

resolução · camada 3

Camada 3: cadastrar() e o botão Ordenar

O coração da versão 2: o cadastrar() agora guarda em DOIS lugares — na lista (memória) e no arquivo (disco) — e um segundo botão, Ordenar, reorganiza a coleção pelo nome. Dois observers dividindo o mesmo formulário.

🔗
cadastrar() faz os dois: clientes.append(cliente) guarda na LISTA (memória, aula 18) e cliente.salvar() grava no ARQUIVO (disco, aula 17). Lista pra ver agora, arquivo pra lembrar depois — validando ANTES de criar (aula 14).
🔤
ordenar() chama clientes.sort(key=lambda c: c.nome) — ordena a lista in-place pelo nome (aula 18) e atualizar_painel() redesenha. É o mesmo key=lambda do sorted() da aula 18, agora atrás de um botão.
cadastro_lista.py
# Camada 3: os callbacks (observers) e os botões
def cadastrar():                              # observer do botão Salvar
    nome = campo_nome.get()                   # .get() lê o Entry
    email = campo_email.get()
    if nome == "" or email == "":             # valida ANTES de criar (lição da aula 14)
        rotulo_status.config(text="Preencha todos os campos!", fg="red")
        return
    cliente = Cliente(nome, email)            # a tela vira objeto
    clientes.append(cliente)                  # aula 18: guarda na lista (memória)
    cliente.salvar()                          # aula 17: grava no arquivo (disco)
    atualizar_painel()                        # redesenha a lista na tela
    rotulo_status.config(text=f"{cliente.nome} cadastrado!", fg="green")
    campo_nome.delete(0, tk.END)              # limpa os campos pro próximo
    campo_email.delete(0, tk.END)

def ordenar():                                # observer do botão Ordenar
    clientes.sort(key=lambda c: c.nome)       # aula 18: ordena in-place, com key
    atualizar_painel()

# os dois botões lado a lado num Frame (pack DENTRO; um gerente por container)
quadro_botoes = tk.Frame(janela)
quadro_botoes.grid(row=2, column=0, columnspan=2, pady=10)
botao_salvar = tk.Button(quadro_botoes, text="Salvar", command=cadastrar)   # inscreve o observer
botao_salvar.pack(side="left", padx=5)
botao_ordenar = tk.Button(quadro_botoes, text="Ordenar por nome", command=ordenar)
botao_ordenar.pack(side="left", padx=5)

atualizar_painel()                            # mostra já o que veio do arquivo
janela.mainloop()                             # loop de eventos
Os dois botões vivem num Frame próprio (quadro_botoes): pack(side="left") DENTRO dele, grid pra posicionar o Frame na janela — cada container com o seu gerente, a regra de ouro do layout. E o atualizar_painel() da última linha mostra, já na abertura, o que veio do arquivo.

resolução · o código completo

Tudo junto: cadastro_lista.py

As três camadas no arquivo final — o cadastro que sobrevive ao fechar. Classe com persistência, lista, painel e dois botões. Rode, cadastre, feche, reabra: os clientes continuam lá.

🗺️
O arquivo inteiro num mapa: a classe com memória (salvar/carregar_todos, aula 17), a lista clientes (aula 18), o painel-Frame (composição, aula 05) e os dois observers cadastrar/ordenar (o command= da aula 21).
🔁
As duas últimas linhas contam a história: rode, cadastre Ana e Bruno, FECHE e rode de novo — o painel abre com os dois, vindos do clientes.txt. A memória volátil da aula 17, resolvida.
cadastro_lista.py
import tkinter as tk
from pathlib import Path

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

class Cliente:                                # o modelo — agora com memória
    def __init__(self, nome, email):
        self.nome = nome
        self.email = email

    def __str__(self):                        # __str__ = representação do objeto
        return f"{self.nome} <{self.email}>"

    def salvar(self, caminho=ARQUIVO):     # aula 17: anexa uma linha ao arquivo
        with open(caminho, "a", encoding="utf-8") as f:   # "a" = append, não apaga
            f.write(f"{self.nome};{self.email}\n")

    @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(";")
                    clientes.append(cls(nome, email))
        except FileNotFoundError:             # sem arquivo ainda: lista vazia
            pass
        return clientes

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

def atualizar_painel():                       # redesenha o painel a partir da lista
    texto = "\n".join(str(c) for c in clientes)   # aula 18: join + comprehension
    rotulo_lista.config(text=texto)
    rotulo_total.config(text=f"Total: {len(clientes)} cliente(s)")   # len() da lista

def cadastrar():                              # observer do botão Salvar
    nome = campo_nome.get()
    email = campo_email.get()
    if nome == "" or email == "":             # valida ANTES de criar (aula 14)
        rotulo_status.config(text="Preencha todos os campos!", fg="red")
        return
    cliente = Cliente(nome, email)
    clientes.append(cliente)                  # memória: aula 18
    cliente.salvar()                          # disco: aula 17
    atualizar_painel()
    rotulo_status.config(text=f"{cliente.nome} cadastrado!", fg="green")
    campo_nome.delete(0, tk.END)              # limpa pro próximo
    campo_email.delete(0, tk.END)

def ordenar():                                # observer do botão Ordenar
    clientes.sort(key=lambda c: c.nome)       # ordena in-place, com key (aula 18)
    atualizar_painel()

janela = tk.Tk()                              # a tela
janela.title("Cadastro de clientes")
janela.geometry("360x320")

rotulo_nome = tk.Label(janela, text="Nome:")
rotulo_nome.grid(row=0, column=0, sticky="e", padx=5, pady=5)   # formulário no grid
campo_nome = tk.Entry(janela, width=25)
campo_nome.grid(row=0, column=1, padx=5, pady=5)

rotulo_email = tk.Label(janela, text="E-mail:")
rotulo_email.grid(row=1, column=0, sticky="e", padx=5, pady=5)  # idem para o e-mail
campo_email = tk.Entry(janela, width=25)
campo_email.grid(row=1, column=1, padx=5, pady=5)

quadro_botoes = tk.Frame(janela)              # botões lado a lado (pack DENTRO)
quadro_botoes.grid(row=2, column=0, columnspan=2, pady=10)
botao_salvar = tk.Button(quadro_botoes, text="Salvar", command=cadastrar)
botao_salvar.pack(side="left", padx=5)
botao_ordenar = tk.Button(quadro_botoes, text="Ordenar por nome", command=ordenar)
botao_ordenar.pack(side="left", padx=5)

rotulo_status = tk.Label(janela, text="")     # painel de avisos, começa vazio
rotulo_status.grid(row=3, column=0, columnspan=2)

quadro_painel = tk.Frame(janela, bd=2, relief="groove")   # o painel da lista (composição)
quadro_painel.grid(row=4, column=0, columnspan=2, padx=10, pady=10, sticky="we")
rotulo_total = tk.Label(quadro_painel, text="Total: 0 cliente(s)")
rotulo_total.pack()
rotulo_lista = tk.Label(quadro_painel, text="", justify="left")
rotulo_lista.pack()

atualizar_painel()                            # já mostra o que veio do arquivo
janela.mainloop()
# 1ª execução: painel "Total: 0 cliente(s)"; cadastre Ana e Bruno
# feche e rode de novo: o painel abre com os 2 (vieram do clientes.txt)
O clientes.txt nasce ao lado do script, na primeira gravação. Só widgets Label, Button, Entry e Frame — sem Listbox (esse é o começo da parte 2, amanhã). Digite junto, não copie: os dedos aprendem o que o olho só reconhece.

resolução · armadilha

Anexa ou apaga? O "a" e o "w" do salvar()

Uma letra no open() decide se o cadastro ACUMULA ou se ESQUECE tudo a cada clique. É a mesma distinção de modos da aula 17 — e aqui ela é a diferença entre um cadastro que funciona e um que perde dados em silêncio.

Abrir em "w" — apaga tudo

o erro
open(caminho, "w", ...)

O modo "w" (write) TRUNCA o
arquivo: cada gravação zera o
que já estava lá.

Resultado: só o ÚLTIMO cliente
sobrevive. Ana some quando Bruno
é salvo — e ninguém vê o erro
até reabrir a janela.

Abrir em "a" — anexa

o certo
open(caminho, "a", ...)

O modo "a" (append) escreve NO
FIM do arquivo: cada cliente
vira uma linha nova, sem tocar
nas anteriores.

É o que o salvar() usa: a
memória em disco CRESCE a cada
cadastro, não se apaga.
É a mesma escolha da aula 17: "w" começa do zero, "a" continua de onde parou. Todo cadastro que ACUMULA pede "a" — trocar por "w" é o bug silencioso que só aparece no segundo cliente, quando o primeiro já sumiu.

resolução · o resultado

O cadastro que lembra dos clientes

A janela da versão 2: o formulário em cima, os botões Salvar e Ordenar no meio, e embaixo o painel com a lista de clientes já cadastrados e o total. Feche e reabra — a lista continua lá.

Screenshot da janela 'Cadastro de clientes' na versão com lista: no topo, o formulário com os campos Nome e E-mail; abaixo, os botões 'Salvar' e 'Ordenar por nome' lado a lado; e na parte inferior, um quadro com moldura listando dois ou três clientes já cadastrados (nome e e-mail por linha) e a mensagem 'Total: N cliente(s)'.
O teste que prova a persistência: cadastre alguns clientes, FECHE a janela por completo e rode o arquivo de novo — o painel abre já preenchido, porque carregar_todos() leu o clientes.txt. Foi exatamente isso que faltava no cadastro anterior: agora os objetos não morrem com a janela.

recapitulando

Glossário rápido de hoje

item detalhe
widget Cada componente visual da interface (rótulo, botão, campo, quadro) — e um OBJETO: instanciado de uma classe do tk, configurado por kwargs, operado por métodos.
janela raiz — tk.Tk() O objeto da janela principal. Toda app tem exatamente UMA; todos os widgets nascem dentro dela (ou de um Frame dela). Nunca chame a variável de tk.
mainloop() O loop de eventos: espera eventos e despacha cada um pro callback registrado. Última linha do script — dali em diante, quem chama suas funções é ele.
evento Algo que acontece na interface: clique, tecla, fechar janela. É a 'notificação' do Observer — o mainloop percebe e avisa quem se inscreveu.
callback A função registrada pra reagir a um evento — o OBSERVER da aula 21. Você não a chama; ela é 'chamada de volta' pelo mainloop.
command= O parâmetro do Button onde o observer se inscreve. Recebe a FUNÇÃO (sem parênteses) ou um lambda quando há argumento.
geometry manager Quem posiciona os widgets na tela: pack, grid ou place. Sem chamar um deles, o widget existe mas não aparece. UM gerente por container.
pack() Empilha pelos lados: side= escolhe o lado, fill= estica, expand=True ocupa a sobra. Bom pra barras e botões lado a lado.
grid() Linhas × colunas: row=, column=, sticky= alinha na célula, columnspan= atravessa colunas. O gerente de formulários — na dúvida, use grid.
place() Coordenadas exatas em pixel. Quase nunca: quebra quando a janela redimensiona. Saiba que existe — e prefira os outros dois.
Entry.get() Lê o que o usuário digitou — SEMPRE string. Precisa de número? int()/float(), lembrando que conversão pode levantar ValueError (aula 14).
Frame Widget invisível que agrupa widgets — composição (aula 05). Cada Frame pode usar o próprio geometry manager, sem brigar com os vizinhos.
.config() Reconfigura um widget VIVO: rotulo.config(text="...", fg="green") muda texto e cor com a janela aberta. É como o callback responde NA tela.

O que dominamos hoje

  • Sei o que é uma GUI e o que é o Tkinter: a biblioteca de interface que já vem com o Python — import tkinter as tk, zero instalação.
  • Crio uma janela com tk.Tk(), title() e geometry(), e sei que o mainloop() é o loop de eventos que espera e despacha — SEMPRE a última linha do script.
  • Uso os 4 widgets como objetos: Label mostra, Button reage, Entry recebe (e .get() devolve SEMPRE string), Frame agrupa por composição (aula 05).
  • Posiciono com pack (empilhar) e grid (formulários — na dúvida, grid), sei que place é exceção, e sigo a regra de ouro: UM gerente por container (misturar = janela congelada).
  • Colhi o Observer da aula 21: command=funcao (SEM parênteses) inscreve o observer, o clique notifica, o mainloop despacha. Com argumento, lambda (aula 18); com estado, método self.ao_clicar e .config() respondendo na tela.
  • Transformo o que o usuário digita em objeto de classe MINHA: o callback lê os Entrys, valida ANTES de construir (aula 14) e chama o construtor.
  • Sei a frase-âncora da aula: widget é objeto; janela é composição; callback é Observer.

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 clique em TODOS os botões antes de entregar.

01

Fácil · Sons do Pet Shop

Uma janela com um Label de título ("Sons do Pet Shop") e DOIS botões: Latir, que imprime Au au! no terminal, e Miar, que imprime Miau!. Layout com pack() (título em cima, botões embaixo). Não esqueça o mainloop() no final. Entregue o arquivo .py. Foco: primeira janela + command= sem parênteses (um callback por botão).

02

Fácil/Médio · Cadastro de produto

Crie a classe Produto(nome, preco) com __str__ devolvendo no formato Produto: Ração Premium — R$ 89.90. Monte um formulário em grid (Nome e Preço) com um botão Salvar cujo callback cria o Produto e o imprime no terminal. Pegadinha: o preço chega do .get() como STRING — converta com float() e formate com :.2f no __str__. Entregue o arquivo .py. Foco: Entry → objeto + conversão de tipos.

03

Médio · Do print ao código

O caminho inverso: a partir do SCREENSHOT do próximo slide (a "Ficha de matrícula"), reproduza a janela — 3 linhas de formulário (Nome, Curso, Turma) com Label + Entry em grid, e um Frame embaixo com os botões Matricular e Limpar lado a lado (pack(side="left") DENTRO do Frame). Os botões podem só imprimir uma mensagem. Entregue o arquivo .py. Foco: ler um layout pronto e traduzi-lo — grid + Frame, um gerente por container.

04

Desafio · Tela de login com validação

Uma tela com Usuário e Senha (use tk.Entry(janela, show="*") pra esconder a senha), botão Entrar e um Label de mensagem. Se usuário e senha baterem com os valores corretos (guarde "aluno"/"senai123" em constantes no código — didático, nunca faça isso num sistema real) → Login realizado com sucesso! em VERDE; errado OU vazio → Usuário ou senha incorretos. em VERMELHO. Teste os dois caminhos. Entregue o .py E um PNG da SUA janela aberta (print da tela, você quem tira). Foco: callback com dois caminhos + .config() colorido — o padrão do cadastro, sozinho.

exercício 3 · do print ao código

A janela-alvo do Exercício 3

Esta é a "Ficha de matrícula" que o seu código deve reproduzir. Olhe como um dev: quantas linhas? quantas colunas? onde há um Frame? qual gerente em cada container?

Screenshot da janela 'Ficha de matrícula': três linhas de formulário com os rótulos Nome, Curso e Turma alinhados à direita, cada um com um campo de texto ao lado, e abaixo dois botões lado a lado, 'Matricular' e 'Limpar'.
Dicas de leitura: rótulos alinhados à direita rente aos campos = grid com sticky="e"; botões lado a lado, centralizados = um Frame (posicionado com grid + columnspan) usando pack(side="left") POR DENTRO. Gerentes diferentes em containers diferentes: pode — essa é exatamente a regra.

Antes de entregar: confira

  • O arquivo .py roda sem erro do começo ao fim: a janela abre, e o mainloop() é a última linha.
  • Cada container usa UM gerente só (pack OU grid); se misturei, foi em Frames diferentes — e a janela abre sem o TclError de gerente de layout.
  • Todo command= registra a função SEM parênteses (ou com lambda quando há argumento) — e eu cliquei em TODOS os botões pra provar que reagem.
  • Tudo que veio de Entry.get() foi tratado como string — converti com float()/int() onde precisei de número.
  • Nomes de variáveis, textos da tela e comentários em PT-BR.
  • No desafio: testei os DOIS caminhos do login (certo → verde, errado/vazio → vermelho) e entreguei o .py + o PNG da janela aberta.
próxima aula

Aula 23 · 07/07

Interface gráfica com Tkinter — parte 2

A janela de hoje cadastra UM cliente por vez — e os objetos somem no terminal. Amanhã eles ficam: Listbox pra listar as coleções da aula 18 NA TELA, Menu, MessageBox pra erros e confirmações, e o CRUD visual completo. E o salto de arquitetura: a app inteira vira uma CLASSE, herdando de tk.Tk — o jeito profissional que o Contador já ensaiou.

Listbox Menu & MessageBox CRUD visual