Tkinter é a biblioteca de interface gráfica da biblioteca padrão do Python: zero instalação, import tkinter as tk e pronto. Uma GUI (Graphical User Interface) troca o terminal por janelas, botões e campos — e no Tkinter cada um desses componentes (widgets) é um objeto que você instancia, configura com parâmetros nomeados e opera por métodos.
import tkinter as tk
janela = tk.Tk() # a janela raiz (UMA por app)
janela.title("Minha primeira janela")
janela.geometry("300x150") # "largura x altura" em pixels
janela.mainloop() # última linha, SEMPRE Analogia do garçom: o mainloop circula pelo salão esperando alguém levantar a mão (evento); quando levanta, atende quem levantou (chama o callback) e volta a circular.
Criar um widget não o coloca na tela: quem posiciona é um gerente de layout. Você não calcula pixels — descreve a intenção (lado, linha/coluna) e o gerente posiciona e REposiciona quando a janela muda de tamanho. São três: pack, grid e place.
| Gerente | Quando usar | Controles |
|---|---|---|
| pack | Empilhar numa direção: barras, botões lado a lado | side=, fill=, expand= |
| grid | Formulários, linhas × colunas — na dúvida, use grid | row=, column=, sticky=, columnspan= |
| place | Coordenadas exatas — quase nunca (quebra ao redimensionar) | x=, y= |
Misturar pack e grid no MESMO container levanta o erro _tkinter.TclError: cannot use geometry manager grid inside . which already has slaves managed by pack — o Tkinter para na hora. (Em Macs antigos com Tk 8.5 a janela pode CONGELAR em vez de dar o erro; mesma causa.) Regra de ouro: um gerente por container.
Um callback é a função que você registra pra reagir a um evento — o observer da aula 21. command=minha_funcao inscreve o observer no botão (o Subject); o clique é a notificação; e quem chama a função é o mainloop. Você nunca chama o callback — ele é 'chamado de volta' (callback) pelo loop de eventos.
# 1) função simples — SEM parênteses:
botao = tk.Button(janela, text="Clique", command=ao_clicar)
# 2) com argumento — embrulhe num lambda (aula 18):
botao = tk.Button(janela, text="Calabresa",
command=lambda: escolher("Calabresa"))
# 3) com estado — um MÉTODO do objeto:
self.botao = tk.Button(janela, text="Contar",
command=self.ao_clicar) A frase-âncora: widget é objeto; janela é composição; callback é Observer. Nada de Python novo — a POO das aulas 02, 05 e 21 acabou de ganhar uma tela.
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.
roteiro da aula
tk.Tk(), title(), geometry() — e o mainloop(), o loop de eventos que mantém a janela viva esperando o usuário.
Label, Button, Entry e Frame: os objetos que formam a tela. Cada um é uma classe que você instancia e configura.
Quem posiciona é o geometry manager: pack (empilha), grid (linhas × colunas) e place (quase nunca). Um gerente por container.
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
print() pra falar, input() pra ouvir. Funciona — mas nenhum usuário de verdade quer usar um sistema assim. command= do Tkinter é exatamente isso. definiçã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. import tkinter as tk — nunca from tkinter import *. O tk. na frente diz de onde cada coisa vem e evita conflito de nomes. 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
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. tk.Tk() raiz até o último botão. 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
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.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 primeira janela
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.
primeira janela · anatomia
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.
Cria a janela raiz: um objeto da classe Tk. É o container de tudo — todo widget vai nascer "dentro" dela. Uma app, UMA raiz.
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".
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.
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() é 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. 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. mainloop() — e isso é o comportamento certo. primeira janela · armadilha
Os dois erros clássicos da primeira semana de Tkinter: esquecer o mainloop() — e colocar código depois dele esperando que rode.
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). 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". widgets · panorama
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.
O rótulo: mostra um texto (ou imagem) na tela. Não reage a clique — só informa. É o print() do mundo visual.
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.
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.
O quadro: um widget invisível que AGRUPA outros widgets. Composição (aula 05) pra organizar a tela em blocos.
widgets · Label e Button
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.print ainda sai no TERMINAL — janela e terminal convivem. Daqui a pouco a resposta vai aparecer NA tela, com .config().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! widgets · Label e Button
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.
widgets · Entry
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..get() é chamado DENTRO do callback — na hora do clique. Se você chamar na montagem da tela, pega o campo ainda vazio.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! widgets · Entry
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.
widgets · Frame
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.
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.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() widgets · Frame
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.
layout · o problema
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). pack() — empilha os widgets pelos lados; grid() — organiza em linhas × colunas; place() — coordenadas exatas em pixel. pack com grid. layout · pack
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.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() layout · pack
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.
layout · grid
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.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() layout · grid
Duas colunas: rótulos alinhados à direita (sticky="e"), campos à esquerda, e o botão atravessando as duas colunas com columnspan=2.
layout · escolhendo
| 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
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.
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() _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.) TclError, a PRIMEIRA suspeita é essa mistura.eventos · a colheita do Observer
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. 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". eventos · armadilha
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.
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. 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! eventos · callback com argumento
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.
lambda é o embrulho: command= recebe uma FUNÇÃO (o lambda), e a chamada escolher("Calabresa") só acontece quando o lambda for executado — no clique.escolher() só, N botões: cada lambda registra o argumento do seu botão. Sem copiar e colar escolher_calabresa, escolher_mussarela...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 eventos · método como callback
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.
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.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" eventos · amarrando com a POO
.pack(), .get(), .config() são métodos dele. O Contador guarda estado em self.cliques — atributo, não global. command=f entrega a função sem executar; lambda embrulha a chamada com argumento. O mesmo mecanismo do sorted(key=...). command= inscreve, o clique notifica, o mainloop despacha. Você desenhou esse diagrama anteontem — hoje ele tem botão e janela. da tela pro objeto
.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__. Cliente(campo_nome.get(), campo_email.get()). A tela é só a porta de entrada; quem carrega os dados pelo sistema é o objeto. Listbox — o começo do CRUD visual. da tela pro objeto
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.print(cliente) sai legível por causa do __str__ (aula 03). Sem ele, você veria <__main__.Cliente object at 0x...>.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> ao vivo · vamos construir juntos
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.
A classe Cliente com __str__ (o objeto que a tela vai produzir) e a janela raiz: título "Cadastro de clientes", geometry("340x180").
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.
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.
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
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.
novo_cliente.py — rótulos sticky="e", campos na coluna 1. A repetição é proposital: essa forma tem que sair no reflexo.rotulo_status nasce com text="": invisível até a primeira mensagem. O callback vai escrever nele com .config() — o truque do Contador.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) resolução · camada 3
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.
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.# 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 resolução · o código completo
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.
grid (um gerente só!), command=salvar sem parênteses, .get()/.config()/.delete() no callback.Cliente nasce. A interface serve ao objeto, não o contrário.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 resolução · testando
Callback com if se testa clicando duas vezes — uma por caminho. O que acontece em cada clique, no terminal E na janela:
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. 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. resolução · o resultado
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.
ao vivo · vamos construir juntos
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.
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.
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).
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.
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
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.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 resolução · camada 2
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.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.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() resolução · camada 3
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.# 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 resolução · o código completo
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á.
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).clientes.txt. A memória volátil da aula 17, resolvida.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) resolução · armadilha
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.
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. 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. "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
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á.
recapitulando
| 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. |
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.
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).
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.
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.
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
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?
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.