Tkinter è la libreria standard di Python per creare interfacce grafiche (GUI). È inclusa automaticamente in Python, quindi non devi installare nulla di extra. Il nome viene da "Tk Interface" — è un wrapper attorno al toolkit grafico Tk.
Alla fine del corso avrai costruito una Calcolatrice completa con interfaccia grafica, gestione degli errori e tastiera virtuale.
Tkinter è già incluso in Python. Per verificare che tutto funzioni, apri il terminale e digita:
# Verifica installazione Python python --version # Verifica che Tkinter sia disponibile python -m tkinter
Se si apre una piccola finestra di test, Tkinter è pronto! Se hai errori su Linux, installa il pacchetto:
sudo apt-get install python3-tk
Ogni programma Tkinter ha questa struttura fondamentale:
import tkinter as tk # 1. Importa Tkinter root = tk.Tk() # 2. Crea la finestra principale # 3. Aggiungi qui i tuoi widget... root.mainloop() # 4. Avvia il ciclo degli eventi
mainloop() è fondamentale: tiene la finestra aperta e ascolta gli eventi (click, tasti premuti, ecc.). Senza di esso la finestra si chiuderebbe immediatamente.
import tkinter as tktk.Tk()root.mainloop()A cosa serve mainloop()?
Quale delle seguenti è la struttura corretta di un programma Tkinter minimo?
tk.Tk() → mainloop() → import tkinterimport tkinter → tk.Tk() → widget → mainloop()tk.Tk() → import tkinter → mainloop()mainloop() → import tkinter → tk.Tk()La finestra principale (root) è il contenitore di tutta la tua applicazione. Puoi personalizzarla in molti modi:
import tkinter as tk root = tk.Tk() # Titolo della finestra root.title("La mia prima app") # Dimensioni: larghezza x altezza root.geometry("500x350") # Posizione: 500x350 partendo da (100, 100) root.geometry("500x350+100+100") # Colore di sfondo root.configure(bg="#f0f0f0") # Impedisci il ridimensionamento root.resizable(False, False) # Dimensione minima root.minsize(300, 200) root.mainloop()
Una Label mostra del testo (o un'immagine) nella finestra:
import tkinter as tk root = tk.Tk() root.title("Hello World!") root.geometry("400x200") # Crea una Label label = tk.Label( root, # contenitore padre text="Ciao, Tkinter! 👋", font=("Arial", 20, "bold"), # (font, dimensione, stile) fg="#2563eb", # colore testo (foreground) bg="#eff6ff" # colore sfondo ) # Mostra la label nella finestra label.pack(pady=50) root.mainloop()
text — il testo da visualizzarefont — tupla: (famiglia, dimensione, stile)fg / foreground — colore del testobg / background — colore dello sfondowidth / height — dimensioni in caratteri/pixelpadx / pady — padding interno orizzontale/verticalepack, grid, o place), altrimenti non apparirà. Vedremo questi in dettaglio nella lezione 4.
Come si imposta il colore del testo di una Label?
color="rosso"fg="#ff0000"text_color="red"background="red"I widget sono gli elementi visivi dell'interfaccia: bottoni, caselle di testo, checkbox, ecc. Ecco i più importanti:
import tkinter as tk def saluta(): print("Bottone cliccato!") root = tk.Tk() root.geometry("300x200") btn = tk.Button( root, text="Clicca qui!", command=saluta, # funzione da chiamare al click bg="#2563eb", fg="white", font=("Arial", 12), padx=20, pady=8, relief="flat", # stile bordo: flat, raised, sunken cursor="hand2" # cursore al passaggio del mouse ) btn.pack(pady=60) root.mainloop()
import tkinter as tk root = tk.Tk() # Crea l'Entry entry = tk.Entry(root, width=30, font=("Arial", 14)) entry.pack(pady=20) # Testo placeholder manuale entry.insert(0, "Scrivi qualcosa...") def leggi_testo(): testo = entry.get() # legge il valore print(f"Hai scritto: {testo}") def svuota(): entry.delete(0, tk.END) # cancella tutto tk.Button(root, text="Leggi", command=leggi_testo).pack() tk.Button(root, text="Svuota", command=svuota).pack() root.mainloop()
import tkinter as tk root = tk.Tk() # Checkbutton — variabile collegata BooleanVar var_check = tk.BooleanVar() check = tk.Checkbutton(root, text="Accetta termini", variable=var_check) check.pack() # Radiobutton — variabile collegata StringVar var_radio = tk.StringVar(value="python") tk.Radiobutton(root, text="Python", variable=var_radio, value="python").pack() tk.Radiobutton(root, text="Java", variable=var_radio, value="java").pack() tk.Radiobutton(root, text="C++", variable=var_radio, value="cpp").pack() def mostra(): print(var_check.get(), var_radio.get()) tk.Button(root, text="Mostra selezione", command=mostra).pack(pady=10) root.mainloop()
Come si legge il valore inserito in un Entry?
entry.value()entry.textentry.get()entry.read()A differenza di Entry (una riga), il widget Text permette di inserire e visualizzare testo su più righe. È utile per editor, note, log di output:
import tkinter as tk root = tk.Tk() # Crea il Text (larghezza in caratteri, altezza in righe) txt = tk.Text(root, width=40, height=8, font=("Consolas", 12)) txt.pack(padx=10, pady=10) # Inserisci testo da codice txt.insert(tk.END, "Prima riga\nSeconda riga\n") # Leggi tutto il contenuto contenuto = txt.get("1.0", tk.END) # "1.0" = riga 1, colonna 0 # Leggi una riga specifica riga2 = txt.get("2.0", "2.end") # Cancella tutto txt.delete("1.0", tk.END) # Rendi il testo di sola lettura txt.config(state=tk.DISABLED) root.mainloop()
La Listbox mostra una lista di elementi selezionabili. Spesso viene abbinata a una Scrollbar per liste lunghe:
import tkinter as tk root = tk.Tk() # Frame con listbox + scrollbar frame = tk.Frame(root) frame.pack(padx=10, pady=10) scrollbar = tk.Scrollbar(frame, orient="vertical") lb = tk.Listbox(frame, height=6, yscrollcommand=scrollbar.set, selectmode=tk.SINGLE) # o tk.MULTIPLE scrollbar.config(command=lb.yview) lb.pack(side="left") scrollbar.pack(side="right", fill="y") # Aggiungi elementi linguaggi = ["Python", "Java", "C++", "JavaScript", "Go", "Rust"] for lang in linguaggi: lb.insert(tk.END, lang) def leggi_selezione(): idx = lb.curselection() # tupla degli indici selezionati if idx: print(lb.get(idx[0])) # testo dell'elemento selezionato tk.Button(root, text="Mostra selezionato", command=leggi_selezione).pack(pady=6) root.mainloop()
I Geometry Managers decidono dove e come i widget vengono posizionati. Ce ne sono 3:
Mette i widget uno dopo l'altro (verticalmente o orizzontalmente).
import tkinter as tk root = tk.Tk() # side: TOP (default), BOTTOM, LEFT, RIGHT tk.Label(root, text="In alto", bg="#fca5a5").pack(side="top", fill="x", pady=5) tk.Label(root, text="Sinistra", bg="#86efac").pack(side="left", fill="y", padx=5) tk.Label(root, text="Destra", bg="#93c5fd").pack(side="right", fill="y", padx=5) tk.Label(root, text="Centro", bg="#fde68a").pack(expand=True) # fill: "x", "y", "both", None # expand: True/False — occupa spazio extra disponibile root.mainloop()
Il più potente per form e layout strutturati:
import tkinter as tk root = tk.Tk() root.title("Form con Grid") # Etichette — colonna 0 tk.Label(root, text="Nome:").grid(row=0, column=0, sticky="e", padx=10, pady=8) tk.Label(root, text="Email:").grid(row=1, column=0, sticky="e", padx=10, pady=8) tk.Label(root, text="Età:").grid(row=2, column=0, sticky="e", padx=10, pady=8) # Entry — colonna 1 nome = tk.Entry(root, width=25) email = tk.Entry(root, width=25) eta = tk.Entry(root, width=25) nome.grid(row=0, column=1, pady=8) email.grid(row=1, column=1, pady=8) eta.grid(row=2, column=1, pady=8) # Bottone su 2 colonne tk.Button(root, text="Invia").grid(row=3, column=0, columnspan=2, pady=12) # sticky: "n","s","e","w" o combinazioni "nsew" — allineamento # columnspan: unisce più colonne root.mainloop()
Posiziona i widget con coordinate esatte (x, y). Utile per layout custom ma meno flessibile:
tk.Label(root, text="Precisione assoluta").place(x=100, y=50) # oppure con percentuali tk.Label(root, text="Centrato").place(relx=0.5, rely=0.5, anchor="center")
pack() e grid() nello stesso contenitore padre. Puoi mescolarli in Frame diversi, ma non nello stesso.
Quale geometry manager è più adatto per creare un form con etichette e campi allineati?
pack()grid()place()Tkinter usa un sistema ad eventi: quando l'utente interagisce (click, tasto premuto, movimento del mouse), viene generato un evento. Puoi rispondere in due modi:
import tkinter as tk root = tk.Tk() contatore = [0] # lista per mutabilità in closure lbl = tk.Label(root, text="Clic: 0", font=("Arial", 16)) lbl.pack(pady=20) def incrementa(): contatore[0] += 1 lbl.config(text=f"Clic: {contatore[0]}") tk.Button(root, text="Clicca!", command=incrementa).pack() root.mainloop()
import tkinter as tk root = tk.Tk() lbl = tk.Label(root, text="Interagisci!", font=("Arial", 14)) lbl.pack(pady=30, padx=30) # Evento click sinistro del mouse def al_click(event): lbl.config(text=f"Click su ({event.x}, {event.y})") # Tasto premuto sulla tastiera def al_tasto(event): lbl.config(text=f"Tasto: {event.keysym}") # Mouse sopra il widget def al_hover(event): lbl.config(bg="#dbeafe") def al_leave(event): lbl.config(bg="SystemButtonFace") root.bind("<Button-1>", al_click) # click sinistro root.bind("<Key>", al_tasto) # qualsiasi tasto lbl.bind("<Enter>", al_hover) # mouse entra lbl.bind("<Leave>", al_leave) # mouse esce root.mainloop()
<Button-1> — click sinistro mouse<Button-3> — click destro mouse<Double-Button-1> — doppio click<Key> — qualsiasi tasto tastiera<Return> — tasto Invio<Escape> — tasto Esc<Enter> — mouse entra nel widget<Leave> — mouse esce dal widget<Configure> — finestra ridimensionataSe devi passare parametri alla funzione nel command=, usa lambda:
def cambia_colore(widget, colore): widget.config(bg=colore) # ✅ Corretto — usa lambda tk.Button(root, text="Rosso", command=lambda: cambia_colore(label, "red")).pack() # ❌ Sbagliato — chiama subito la funzione tk.Button(root, text="Verde", command=cambia_colore(label, "green")).pack()
Quale stringa rappresenta l'evento "tasto Invio premuto"?
<Enter-Key><Key-Enter><Return><Newline>Tkinter ha variabili speciali che si sincronizzano automaticamente con i widget. Quando cambia la variabile, il widget si aggiorna; quando l'utente modifica il widget, la variabile si aggiorna.
tk.StringVar() — per testotk.IntVar() — per interitk.DoubleVar() — per decimalitk.BooleanVar() — per True/Falseimport tkinter as tk root = tk.Tk() # StringVar collegata all'Entry nome_var = tk.StringVar() entry = tk.Entry(root, textvariable=nome_var) entry.pack(pady=10) # Label che mostra il valore automaticamente lbl = tk.Label(root, textvariable=nome_var, fg="blue") lbl.pack() # Modificare la variabile da codice def imposta(): nome_var.set("Mario Rossi") # aggiorna entry e label! def leggi(): print(nome_var.get()) # legge il valore tk.Button(root, text="Imposta", command=imposta).pack() tk.Button(root, text="Leggi", command=leggi).pack() root.mainloop()
import tkinter as tk root = tk.Tk() testo = tk.StringVar() # Callback chiamata ogni volta che la var cambia def on_change(*args): val = testo.get() lunghezza.config(text=f"Caratteri: {len(val)}") # "w" = write (quando viene scritto) testo.trace("w", on_change) tk.Entry(root, textvariable=testo, width=30).pack(pady=10) lunghezza = tk.Label(root, text="Caratteri: 0") lunghezza.pack() root.mainloop()
StringVar, IntVar, ecc. quando hai bisogno che un widget si aggiorni automaticamente al cambiare dei dati, o quando vuoi collegare più widget allo stesso valore.
Come si aggiorna il valore di una StringVar?
var.update("valore")var.set("valore")var = "valore"var.write("valore")import tkinter as tk root = tk.Tk() root.title("Finestra Principale") def apri_finestra(): top = tk.Toplevel(root) # finestra secondaria top.title("Finestra Secondaria") top.geometry("300x150") top.transient(root) # legata alla finestra padre top.grab_set() # blocca la finestra padre (modale) tk.Label(top, text="Ciao dalla finestra secondaria!").pack(pady=20) tk.Button(top, text="Chiudi", command=top.destroy).pack() root.wait_window(top) # aspetta che si chiuda tk.Button(root, text="Apri finestra", command=apri_finestra).pack(pady=50) root.mainloop()
Tkinter include dialog box pronti all'uso:
from tkinter import messagebox, simpledialog, filedialog # Messaggi messagebox.showinfo("Info", "Operazione completata!") messagebox.showwarning("Attenzione", "Controlla i dati") messagebox.showerror("Errore", "Qualcosa è andato storto") # Domanda sì/no — restituisce True/False risposta = messagebox.askyesno("Conferma", "Vuoi continuare?") # Input di testo nome = simpledialog.askstring("Input", "Come ti chiami?") eta = simpledialog.askinteger("Input", "Quanti anni hai?") # Apri file percorso = filedialog.askopenfilename( title="Seleziona file", filetypes=[("File di testo", "*.txt"), ("Tutti", "*.*")] ) # Salva file salva = filedialog.asksaveasfilename(defaultextension=".txt")
import tkinter as tk from tkinter import messagebox root = tk.Tk() # Barra del menu principale menubar = tk.Menu(root) # Menu File file_menu = tk.Menu(menubar, tearoff=0) file_menu.add_command(label="Nuovo", accelerator="Ctrl+N") file_menu.add_command(label="Apri", accelerator="Ctrl+O") file_menu.add_separator() file_menu.add_command(label="Esci", command=root.quit) menubar.add_cascade(label="File", menu=file_menu) # Menu Aiuto help_menu = tk.Menu(menubar, tearoff=0) help_menu.add_command(label="Info", command=lambda: messagebox.showinfo("Info", "v1.0")) menubar.add_cascade(label="Aiuto", menu=help_menu) root.config(menu=menubar) root.mainloop()
Quale widget si usa per creare finestre secondarie in Tkinter?
tk.Window()tk.Toplevel()tk.Frame()tk.Dialog()Costruiremo una calcolatrice grafica completa con: display numerico, tastiera virtuale con tutti gli operatori, gestione degli errori (divisione per zero, ecc.), effetti hover sui tasti, e supporto alla tastiera fisica.
Iniziamo con la struttura dell'applicazione:
import tkinter as tk from tkinter import messagebox class Calcolatrice: def __init__(self, root): self.root = root self.root.title("🧮 Calcolatrice") self.root.geometry("320x480") self.root.resizable(False, False) self.root.configure(bg="#1e1e2e") # Stato interno self.espressione = "" self.display_var = tk.StringVar(value="0") self.nuovo_numero = True # dopo un operatore self.crea_display() self.crea_tasti() self.bind_tastiera() # Avvia l'applicazione if __name__ == "__main__": root = tk.Tk() app = Calcolatrice(root) root.mainloop()
def crea_display(self): # Frame display frame_display = tk.Frame(self.root, bg="#1e1e2e") frame_display.pack(fill="x", padx=12, pady=12) # Riga espressione (piccola, grigia) self.lbl_expr = tk.Label( frame_display, text="", font=("Consolas", 12), fg="#6c7086", bg="#1e1e2e", anchor="e" ) self.lbl_expr.pack(fill="x") # Riga risultato (grande) self.lbl_display = tk.Label( frame_display, textvariable=self.display_var, font=("Consolas", 36, "bold"), fg="#cdd6f4", bg="#1e1e2e", anchor="e" ) self.lbl_display.pack(fill="x") # Linea separatrice tk.Frame(self.root, bg="#313244", height=1).pack(fill="x", padx=12)
def crea_tasti(self): # Layout: (testo, riga, colonna, colspan, tipo) tasti = [ ("C", 0, 0, 1, "fn"), ("±", 0, 1, 1, "fn"), ("%", 0, 2, 1, "op"), ("÷", 0, 3, 1, "op"), ("7", 1, 0, 1, "num"), ("8", 1, 1, 1, "num"), ("9", 1, 2, 1, "num"), ("×", 1, 3, 1, "op"), ("4", 2, 0, 1, "num"), ("5", 2, 1, 1, "num"), ("6", 2, 2, 1, "num"), ("-", 2, 3, 1, "op"), ("1", 3, 0, 1, "num"), ("2", 3, 1, 1, "num"), ("3", 3, 2, 1, "num"), ("+", 3, 3, 1, "op"), ("0", 4, 0, 2, "num"), (".", 4, 2, 1, "num"), ("=", 4, 3, 1, "eq"), ] colori = { "num": ("#313244", "#45475a"), # normale, hover "op": ("#f38ba8", "#eba0ac"), "fn": ("#45475a", "#585b70"), "eq": ("#a6e3a1", "#94e2d5"), } frame_tasti = tk.Frame(self.root, bg="#1e1e2e") frame_tasti.pack(fill="both", expand=True, padx=12, pady=12) for c in range(4): frame_tasti.columnconfigure(c, weight=1) for r in range(5): frame_tasti.rowconfigure(r, weight=1) for (testo, riga, col, span, tipo) in tasti: bg, hover = colori[tipo] fg = "#1e1e2e" if tipo in ("eq", "op") else "#cdd6f4" btn = tk.Button( frame_tasti, text=testo, font=("Consolas", 18, "bold"), bg=bg, fg=fg, activebackground=hover, relief="flat", bd=0, cursor="hand2", command=lambda t=testo: self.premi(t) ) btn.grid(row=riga, column=col, columnspan=span, sticky="nsew", padx=3, pady=3) # Effetto hover btn.bind("<Enter>", lambda e, b=btn, h=hover: b.config(bg=h)) btn.bind("<Leave>", lambda e, b=btn, n=bg: b.config(bg=n))
def premi(self, tasto): if tasto == "C": self.espressione = "" self.display_var.set("0") self.lbl_expr.config(text="") elif tasto == "±": try: val = float(self.display_var.get()) self.display_var.set(str(-val)) except: pass elif tasto == "%": try: val = float(self.display_var.get()) self.display_var.set(str(val / 100)) except: pass elif tasto == "=": try: expr = self.espressione + self.display_var.get() # Converti simboli in operatori Python expr = expr.replace("×", "*").replace("÷", "/") self.lbl_expr.config(text=expr + " =") risultato = eval(expr) # Mostra intero se possibile if risultato == int(risultato): risultato = int(risultato) self.display_var.set(str(risultato)) self.espressione = "" self.nuovo_numero = True except ZeroDivisionError: self.display_var.set("Errore: ÷0") self.espressione = "" except: self.display_var.set("Errore") self.espressione = "" elif tasto in ("+", "-", "×", "÷"): self.espressione += self.display_var.get() + tasto self.lbl_expr.config(text=self.espressione) self.nuovo_numero = True else: # Numero o punto if self.nuovo_numero: self.display_var.set(tasto) self.nuovo_numero = False else: attuale = self.display_var.get() if tasto == "." and "." in attuale: return # non inserire due punti if attuale == "0" and tasto != ".": self.display_var.set(tasto) else: self.display_var.set(attuale + tasto) def bind_tastiera(self): # Supporto tastiera fisica for k in "0123456789.": self.root.bind(f"<Key-{k}>", lambda e, t=k: self.premi(t)) self.root.bind("<Return>", lambda e: self.premi("=")) self.root.bind("<Escape>", lambda e: self.premi("C")) self.root.bind("<plus>", lambda e: self.premi("+")) self.root.bind("<minus>", lambda e: self.premi("-")) self.root.bind("<asterisk>", lambda e: self.premi("×")) self.root.bind("<slash>", lambda e: self.premi("÷"))
import tkinter as tk class Calcolatrice: def __init__(self, root): self.root = root self.root.title("🧮 Calcolatrice") self.root.geometry("320x480") self.root.resizable(False, False) self.root.configure(bg="#1e1e2e") self.espressione = "" self.display_var = tk.StringVar(value="0") self.nuovo_numero = True self.crea_display() self.crea_tasti() self.bind_tastiera() def crea_display(self): f = tk.Frame(self.root, bg="#1e1e2e") f.pack(fill="x", padx=12, pady=12) self.lbl_expr = tk.Label(f, text="", font=("Consolas",12), fg="#6c7086", bg="#1e1e2e", anchor="e") self.lbl_expr.pack(fill="x") tk.Label(f, textvariable=self.display_var, font=("Consolas",36,"bold"), fg="#cdd6f4", bg="#1e1e2e", anchor="e").pack(fill="x") tk.Frame(self.root, bg="#313244", height=1).pack(fill="x", padx=12) def crea_tasti(self): layout = [ ("C",0,0,1,"fn"),( "±",0,1,1,"fn"),("%",0,2,1,"op"),("÷",0,3,1,"op"), ("7",1,0,1,"num"),("8",1,1,1,"num"),("9",1,2,1,"num"),("×",1,3,1,"op"), ("4",2,0,1,"num"),("5",2,1,1,"num"),("6",2,2,1,"num"),("-",2,3,1,"op"), ("1",3,0,1,"num"),("2",3,1,1,"num"),("3",3,2,1,"num"),("+",3,3,1,"op"), ("0",4,0,2,"num"),(".",4,2,1,"num"),("=",4,3,1,"eq"), ] C = {"num":("#313244","#45475a"),"op":("#f38ba8","#eba0ac"), "fn":("#45475a","#585b70"), "eq":("#a6e3a1","#94e2d5")} f = tk.Frame(self.root, bg="#1e1e2e") f.pack(fill="both", expand=True, padx=12, pady=12) [f.columnconfigure(i,weight=1) for i in range(4)] [f.rowconfigure(i,weight=1) for i in range(5)] for (t,r,c,s,tipo) in layout: bg,hov = C[tipo] fg = "#1e1e2e" if tipo in ("eq","op") else "#cdd6f4" b = tk.Button(f, text=t, font=("Consolas",18,"bold"), bg=bg,fg=fg,activebackground=hov,relief="flat",bd=0, cursor="hand2",command=lambda x=t: self.premi(x)) b.grid(row=r,column=c,columnspan=s,sticky="nsew",padx=3,pady=3) b.bind("<Enter>",lambda e,b=b,h=hov: b.config(bg=h)) b.bind("<Leave>",lambda e,b=b,n=bg: b.config(bg=n)) def premi(self, t): if t == "C": self.espressione = ""; self.display_var.set("0") self.lbl_expr.config(text=""); self.nuovo_numero = True elif t == "±": try: self.display_var.set(str(-float(self.display_var.get()))) except: pass elif t == "%": try: self.display_var.set(str(float(self.display_var.get())/100)) except: pass elif t == "=": try: expr = (self.espressione+self.display_var.get()).replace("×","*").replace("÷","/") self.lbl_expr.config(text=expr+" =") r = eval(expr) self.display_var.set(str(int(r) if r==int(r) else r)) self.espressione = ""; self.nuovo_numero = True except ZeroDivisionError: self.display_var.set("Errore: ÷0"); self.espressione = "" except: self.display_var.set("Errore"); self.espressione = "" elif t in "+-×÷": self.espressione += self.display_var.get()+t self.lbl_expr.config(text=self.espressione); self.nuovo_numero = True else: if self.nuovo_numero: self.display_var.set(t); self.nuovo_numero = False else: v = self.display_var.get() if t=="." and "." in v: return self.display_var.set(t if v=="0" and t!="." else v+t) def bind_tastiera(self): for k in "0123456789.": self.root.bind(f"<{k}>", lambda e,t=k: self.premi(t)) self.root.bind("<Return>", lambda e: self.premi("=")) self.root.bind("<Escape>", lambda e: self.premi("C")) self.root.bind("<plus>", lambda e: self.premi("+")) self.root.bind("<minus>", lambda e: self.premi("-")) self.root.bind("<asterisk>", lambda e: self.premi("×")) self.root.bind("<slash>", lambda e: self.premi("÷")) if __name__ == "__main__": root = tk.Tk() Calcolatrice(root) root.mainloop()
open().
Ottimo lavoro! Hai imparato:
ttk (Themed Tk) è un modulo di Tkinter che fornisce widget con un aspetto nativo del sistema operativo e supporto a temi personalizzati. I widget ttk sostituiscono quelli classici con versioni più moderne e stilizzate.
tk.Button per controllo totale sui colori. Usa ttk.Button quando vuoi l'aspetto nativo e i temi. Per stili avanzati, ttk usa Style() invece di opzioni dirette.
import tkinter as tk from tkinter import ttk root = tk.Tk() root.title("Widget ttk") root.geometry("400x300") # ttk.Button — aspetto nativo ttk.Button(root, text="Bottone ttk").pack(pady=10) # ttk.Entry ttk.Entry(root, width=25).pack(pady=5) # ttk.Combobox — menù a tendina combo = ttk.Combobox(root, values=["Python", "Java", "C++"]) combo.set("Seleziona linguaggio") combo.pack(pady=5) # ttk.Progressbar pb = ttk.Progressbar(root, length=200, mode="determinate") pb.pack(pady=10) pb["value"] = 65 # 65% # ttk.Scale — slider ttk.Scale(root, from_=0, to=100, orient="horizontal").pack(pady=5) # ttk.Notebook — schede/tab nb = ttk.Notebook(root) tab1 = ttk.Frame(nb) tab2 = ttk.Frame(nb) nb.add(tab1, text="Scheda 1") nb.add(tab2, text="Scheda 2") nb.pack(expand=True, fill="both", pady=10) root.mainloop()
import tkinter as tk from tkinter import ttk root = tk.Tk() root.configure(bg="#1a1a2e") # Crea oggetto Style style = ttk.Style() # Scegli un tema di base print(style.theme_names()) # ('winnative', 'clam', 'alt', 'default', ...) style.theme_use("clam") # tema più personalizzabile # Personalizza il Button style.configure( "Accent.TButton", # nome stile personalizzato background="#7c6af7", foreground="white", font=("Syne", 12, "bold"), padding=(12, 6), borderwidth=0 ) style.map("Accent.TButton", background=[("active", "#9b8bff")] # hover ) # Applica lo stile ttk.Button(root, text="Bottone Accent", style="Accent.TButton").pack(pady=30) # Personalizza Label style.configure("Dark.TLabel", background="#1a1a2e", foreground="#cdd6f4", font=("Consolas", 14) ) ttk.Label(root, text="Label scura", style="Dark.TLabel").pack() root.mainloop()
Come si applica uno stile personalizzato a un widget ttk?
ttk.Button(root, bg="#333")ttk.Button(root, style="MioStile.TButton")ttk.Button(root, theme="dark")ttk.Button(root, color="blue")Vuoi mostrare dati tabellari (tipo una rubrica) in Tkinter. Quale widget ttk è più adatto?
ttk.Comboboxttk.Progressbarttk.Treeviewttk.NotebookIl Canvas è una tela su cui puoi disegnare forme geometriche, testo, immagini e persino creare giochi 2D. È il widget più potente di Tkinter per la grafica.
import tkinter as tk root = tk.Tk() root.title("Canvas Demo") # Crea il canvas canvas = tk.Canvas(root, width=500, height=400, bg="#1e1e2e") canvas.pack() # Rettangolo: (x1,y1, x2,y2) canvas.create_rectangle(50, 50, 150, 120, fill="#7c6af7", outline="#9b8bff", width=2) # Ovale/Cerchio canvas.create_oval(200, 50, 320, 170, fill="#a6e3a1", outline="white") # Linea canvas.create_line(50, 200, 450, 200, fill="#f38ba8", width=3, dash=(10, 5)) # Poligono (triangolo) canvas.create_polygon(250, 230, 200, 320, 300, 320, fill="#f7a26a", outline="white", width=2) # Testo sul canvas canvas.create_text(250, 370, text="Canvas Tkinter!", font=("Consolas", 16, "bold"), fill="#cdd6f4") root.mainloop()
Ogni oggetto disegnato ha un ID che puoi usare per modificarlo o eliminarlo:
import tkinter as tk root = tk.Tk() canvas = tk.Canvas(root, width=400, height=300, bg="#1e1e2e") canvas.pack() # L'ID viene restituito alla creazione cerchio = canvas.create_oval(150, 100, 250, 200, fill="#7c6af7") def sposta(): # Sposta di 10px a destra e 5px in basso canvas.move(cerchio, 10, 5) def cambia_colore(): canvas.itemconfig(cerchio, fill="#f38ba8") def elimina(): canvas.delete(cerchio) def pulisci_tutto(): canvas.delete("all") # cancella TUTTO tk.Button(root, text="Sposta", command=sposta).pack(side="left", padx=4) tk.Button(root, text="Cambia colore", command=cambia_colore).pack(side="left", padx=4) tk.Button(root, text="Elimina", command=elimina).pack(side="left", padx=4) root.mainloop()
import tkinter as tk root = tk.Tk() root.title("Mini Paint") colore_attuale = ["#7c6af7"] spessore = [4] prev_x = prev_y = [None] canvas = tk.Canvas(root, width=600, height=400, bg="white", cursor="crosshair") canvas.pack() def disegna(event): if prev_x[0] is not None: canvas.create_line( prev_x[0], prev_y[0], event.x, event.y, fill=colore_attuale[0], width=spessore[0], capstyle=tk.ROUND, smooth=True ) prev_x[0], prev_y[0] = event.x, event.y def reset_prev(event): prev_x[0] = prev_y[0] = None canvas.bind("<B1-Motion>", disegna) canvas.bind("<ButtonRelease-1>", reset_prev) # Barra strumenti colori barra = tk.Frame(root, bg="#f0f0f0") barra.pack(fill="x") colori = ["#7c6af7", "#f38ba8", "#a6e3a1", "#f7a26a", "black", "white"] for c in colori: tk.Button(barra, bg=c, width=3, command=lambda x=c: colore_attuale.__setitem__(0, x)).pack(side="left", padx=2, pady=4) tk.Button(barra, text="🗑 Pulisci", command=lambda: canvas.delete("all")).pack(side="right", padx=8) root.mainloop()
Come si cancellano TUTTI gli oggetti da un Canvas?
canvas.clear()canvas.remove_all()canvas.delete("all")canvas.destroy()Per modificare il colore di un oggetto già disegnato sul Canvas, quale metodo usi?
canvas.config(id, fill="red")canvas.itemconfig(id, fill="red")canvas.update(id, fill="red")canvas.set(id, fill="red")after(ms, funzione) esegue una funzione dopo un certo numero di millisecondi. Chiamandolo ricorsivamente crei un loop di animazione — il cuore di qualsiasi animazione in Tkinter.
time.sleep() blocca il mainloop e congela l'intera interfaccia. Usa sempre after() per i ritardi nelle GUI.
import tkinter as tk root = tk.Tk() root.title("Palla Rimbalzante") canvas = tk.Canvas(root, width=500, height=400, bg="#1e1e2e") canvas.pack() # Posizione e velocità x, y = 250, 200 dx, dy = 4, 3 r = 25 palla = canvas.create_oval(x-r, y-r, x+r, y+r, fill="#7c6af7", outline="") def anima(): global x, y, dx, dy x += dx y += dy # Rimbalzo sui bordi if x - r <= 0 or x + r >= 500: dx = -dx if y - r <= 0 or y + r >= 400: dy = -dy # Aggiorna posizione canvas.coords(palla, x-r, y-r, x+r, y+r) # Chiama se stessa ogni 16ms (~60fps) root.after(16, anima) anima() # avvia il loop root.mainloop()
import tkinter as tk root = tk.Tk() root.title("Countdown") root.configure(bg="#1e1e2e") secondi = [10] timer_id = [None] lbl = tk.Label(root, text="10", font=("Consolas", 72, "bold"), fg="#a6e3a1", bg="#1e1e2e") lbl.pack(pady=30, padx=60) def tick(): secondi[0] -= 1 lbl.config(text=str(secondi[0])) if secondi[0] <= 3: lbl.config(fg="#f38ba8") # rosso negli ultimi secondi if secondi[0] > 0: timer_id[0] = root.after(1000, tick) else: lbl.config(text="⏰ Fine!", fg="#f7a26a") def avvia(): secondi[0] = 10 lbl.config(text="10", fg="#a6e3a1") tick() def ferma(): if timer_id[0]: root.after_cancel(timer_id[0]) # cancella il prossimo tick tk.Button(root, text="▶ Avvia", command=avvia, bg="#a6e3a1", fg="#1e1e2e", font=("Syne",12,"bold")).pack(side="left", padx=20, pady=10) tk.Button(root, text="⏹ Ferma", command=ferma, bg="#f38ba8", fg="#1e1e2e", font=("Syne",12,"bold")).pack(side="left", padx=4) root.mainloop()
from tkinter import ttk import tkinter as tk root = tk.Tk() valore = tk.DoubleVar() pb = ttk.Progressbar(root, variable=valore, maximum=100, length=300) pb.pack(pady=30, padx=30) lbl = tk.Label(root, text="0%", font=("Consolas", 14)) lbl.pack() def avanza(): v = valore.get() if v < 100: valore.set(v + 1) lbl.config(text=f"{int(v+1)}%") root.after(30, avanza) else: lbl.config(text="✅ Completato!") tk.Button(root, text="Avvia caricamento", command=lambda: [valore.set(0), avanza()]).pack(pady=10) root.mainloop()
Vuoi eseguire una funzione aggiorna() ogni 500ms. Quale codice è corretto?
time.sleep(0.5); aggiorna()root.after(500, aggiorna)root.timer(500, aggiorna)root.loop(aggiorna, 500)Questo codice vuole animare una palla ma la finestra si blocca. Perché?
import time
def anima():
while True:
canvas.move(palla, 2, 0)
time.sleep(0.016)
canvas.move non esistetime.sleep() blocca il mainloop di Tkinterroot.mainloop()SQLite è un database integrato in Python (no installazione), perfetto per app desktop. Combinato con Tkinter puoi creare applicazioni con dati persistenti — rubrica, gestione studenti, inventario, ecc.
import sqlite3 funziona con qualsiasi Python 3.x.
import sqlite3 # Connessione (crea il file se non esiste) conn = sqlite3.connect("rubrica.db") cur = conn.cursor() # CREATE — crea la tabella cur.execute(""" CREATE TABLE IF NOT EXISTS contatti ( id INTEGER PRIMARY KEY AUTOINCREMENT, nome TEXT NOT NULL, email TEXT, tel TEXT ) """) # INSERT — aggiungi un contatto cur.execute("INSERT INTO contatti (nome, email, tel) VALUES (?, ?, ?)", ("Mario Rossi", "[email protected]", "333-1234567")) # SELECT — leggi tutti i contatti cur.execute("SELECT * FROM contatti") righe = cur.fetchall() for riga in righe: print(riga) # UPDATE cur.execute("UPDATE contatti SET tel=? WHERE nome=?", ("333-9999999", "Mario Rossi")) # DELETE cur.execute("DELETE FROM contatti WHERE id=?", (1,)) conn.commit() # salva le modifiche conn.close() # chiudi la connessione
import tkinter as tk from tkinter import ttk, messagebox import sqlite3 class RubricaApp: def __init__(self, root): self.root = root self.root.title("📒 Rubrica") self.root.geometry("620x480") self.root.configure(bg="#f8fafc") self.init_db() self.crea_ui() self.carica_contatti() def init_db(self): # Inizializza il database self.conn = sqlite3.connect("rubrica.db") self.cur = self.conn.cursor() self.cur.execute(""" CREATE TABLE IF NOT EXISTS contatti ( id INTEGER PRIMARY KEY AUTOINCREMENT, nome TEXT NOT NULL, email TEXT, tel TEXT )""") self.conn.commit() def crea_ui(self): # Frame form in alto form = tk.LabelFrame(self.root, text=" Nuovo Contatto ", font=("Arial",10,"bold"), bg="#f8fafc") form.pack(fill="x", padx=16, pady=12) etichette = ["Nome:", "Email:", "Telefono:"] self.campi = {} for i, et in enumerate(etichette): tk.Label(form, text=et, bg="#f8fafc").grid(row=i, column=0, sticky="e", padx=8, pady=5) chiave = et[:-1].lower() e = ttk.Entry(form, width=30) e.grid(row=i, column=1, pady=5, padx=4) self.campi[chiave] = e btn_frame = tk.Frame(form, bg="#f8fafc") btn_frame.grid(row=3, column=0, columnspan=2, pady=8) tk.Button(btn_frame, text="➕ Aggiungi", command=self.aggiungi, bg="#7c6af7", fg="white", relief="flat", padx=12).pack(side="left", padx=4) tk.Button(btn_frame, text="🗑 Elimina selezionato", command=self.elimina, bg="#f38ba8", fg="white", relief="flat", padx=12).pack(side="left", padx=4) # Tabella contatti (Treeview) cols = ("ID", "Nome", "Email", "Telefono") self.tree = ttk.Treeview(self.root, columns=cols, show="headings", height=12) for c in cols: self.tree.heading(c, text=c) self.tree.column(c, width=60 if c=="ID" else 160) self.tree.pack(fill="both", expand=True, padx=16, pady=4) def carica_contatti(self): for i in self.tree.get_children(): self.tree.delete(i) self.cur.execute("SELECT * FROM contatti ORDER BY nome") for riga in self.cur.fetchall(): self.tree.insert("", tk.END, values=riga) def aggiungi(self): nome = self.campi["nome"].get().strip() email = self.campi["email"].get().strip() tel = self.campi["telefono"].get().strip() if not nome: messagebox.showwarning("Attenzione", "Il nome è obbligatorio!") return self.cur.execute("INSERT INTO contatti (nome,email,tel) VALUES (?,?,?)", (nome, email, tel)) self.conn.commit() for e in self.campi.values(): e.delete(0, tk.END) self.carica_contatti() def elimina(self): sel = self.tree.selection() if not sel: messagebox.showinfo("Info", "Seleziona un contatto da eliminare") return id_cont = self.tree.item(sel[0])["values"][0] if messagebox.askyesno("Conferma", "Eliminare questo contatto?"): self.cur.execute("DELETE FROM contatti WHERE id=?", (id_cont,)) self.conn.commit() self.carica_contatti() if __name__ == "__main__": root = tk.Tk() RubricaApp(root) root.mainloop()
SELECT * WHERE nome LIKE ?, oppure un pulsante "Modifica" che pre-popola il form con i dati del contatto selezionato. Puoi anche esportare in CSV con il modulo csv della libreria standard.
Perché si usano i ? (placeholder) nelle query SQLite invece di formattare la stringa direttamente?
Dopo aver eseguito una INSERT, cosa devi chiamare per salvare definitivamente i dati?
conn.save()cur.flush()conn.commit()conn.close()Continua con la prossima lezione per avanzare nel corso.
Nelle app reali non si scrive tutto in un unico file. Si usa il pattern MVC (Model-View-Controller) o almeno una separazione chiara tra logica e interfaccia. Questo rende il codice manutenibile e scalabile.
# Struttura consigliata per app Tkinter medio-grandi mia_app/ ├── main.py # entry point ├── model/ │ ├── __init__.py │ └── database.py # logica dati ├── view/ │ ├── __init__.py │ ├── main_view.py # finestra principale │ └── dialogs.py # finestre secondarie ├── controller/ │ ├── __init__.py │ └── app_ctrl.py # logica applicazione └── assets/ └── style.tcl # tema personalizzato
import tkinter as tk from tkinter import ttk from view.main_view import MainView from controller.app_ctrl import AppController class Application(tk.Tk): def __init__(self): super().__init__() self.title("La Mia App") self.geometry("900x600") self.minsize(700, 450) # Applica tema ttk self.style = ttk.Style(self) self.style.theme_use("clam") self._configura_tema() # Crea controller (che crea la view) self.controller = AppController(self) def _configura_tema(self): self.style.configure(".", font=("Segoe UI", 10), background="#f0f2f5" ) self.style.configure("TButton", padding=(10, 5), relief="flat", background="#1877f2", foreground="white" ) self.style.map("TButton", background=[("active", "#166fe5"), ("disabled", "#cccccc")] ) if __name__ == "__main__": app = Application() app.mainloop()
Puoi creare un tema scuro professionale configurando tutti i widget:
from tkinter import ttk def applica_tema_scuro(root): style = ttk.Style(root) style.theme_use("clam") # Colori BG = "#1e1e2e" BG2 = "#313244" FG = "#cdd6f4" ACCENT = "#89b4fa" MUTED = "#585b70" RED = "#f38ba8" GREEN = "#a6e3a1" # Sfondo globale style.configure(".", background=BG, foreground=FG, fieldbackground=BG2, troughcolor=BG2, selectbackground=ACCENT, selectforeground=BG, font=("Consolas", 10) ) # Frame e Label style.configure("TFrame", background=BG) style.configure("TLabel", background=BG, foreground=FG) style.configure("TLabelframe", background=BG, foreground=ACCENT, bordercolor=BG2) style.configure("TLabelframe.Label", background=BG, foreground=ACCENT, font=("Consolas", 10, "bold")) # Button style.configure("TButton", background=BG2, foreground=FG, bordercolor=MUTED, padding=(8, 4), relief="flat") style.map("TButton", background=[("active", ACCENT), ("pressed", ACCENT)], foreground=[("active", BG)]) # Accent Button style.configure("Accent.TButton", background=ACCENT, foreground=BG, font=("Consolas", 10, "bold")) style.map("Accent.TButton", background=[("active", "#74c7ec")]) # Entry style.configure("TEntry", fieldbackground=BG2, foreground=FG, insertcolor=FG, bordercolor=MUTED, padding=5) style.map("TEntry", bordercolor=[("focus", ACCENT)]) # Combobox style.configure("TCombobox", fieldbackground=BG2, foreground=FG, selectbackground=ACCENT, selectforeground=BG) # Notebook (tab) style.configure("TNotebook", background=BG, bordercolor=BG2) style.configure("TNotebook.Tab", background=BG2, foreground=MUTED, padding=(10, 4)) style.map("TNotebook.Tab", background=[("selected", BG)], foreground=[("selected", ACCENT)]) # Treeview style.configure("Treeview", background=BG2, foreground=FG, fieldbackground=BG2, rowheight=26) style.configure("Treeview.Heading", background=BG, foreground=ACCENT, font=("Consolas", 10, "bold")) style.map("Treeview", background=[("selected", ACCENT)], foreground=[("selected", BG)]) # Progressbar style.configure("TProgressbar", troughcolor=BG2, background=ACCENT, thickness=8) # Scrollbar style.configure("TScrollbar", troughcolor=BG, background=MUTED, arrowcolor=MUTED) style.map("TScrollbar", background=[("active", ACCENT)]) root.configure(bg=BG) return style
import tkinter as tk from tkinter import ttk root = tk.Tk() root.geometry("600x400") # ── PanedWindow — pannelli ridimensionabili ── paned = ttk.PanedWindow(root, orient="horizontal") paned.pack(fill="both", expand=True) left_frame = ttk.Frame(paned, width=200) right_frame = ttk.Frame(paned) paned.add(left_frame, weight=1) paned.add(right_frame, weight=3) # ── Spinbox — selettore numerico ── ttk.Label(left_frame, text="Età:").pack(pady=5) spinbox = ttk.Spinbox(left_frame, from_=1, to=120, width=8, wrap=True) spinbox.pack(pady=3) # ── OptionMenu — menù a tendina semplice ── lingua = tk.StringVar(value="Italiano") ttk.Label(left_frame, text="Lingua:").pack(pady=5) menu = ttk.OptionMenu(left_frame, lingua, "Italiano", "Inglese", "Francese", "Spagnolo") menu.pack(pady=3) # ── Scale — slider ── volume = tk.IntVar(value=50) ttk.Label(left_frame, text="Volume:").pack(pady=5) ttk.Scale(left_frame, from_=0, to=100, variable=volume, orient="horizontal").pack(pady=3, fill="x", padx=10) # ── Text con scrollbar integrata ── txt_frame = ttk.Frame(right_frame) txt_frame.pack(fill="both", expand=True, padx=10, pady=10) txt = tk.Text(txt_frame, wrap="word", font=("Consolas", 11)) scrollbar = ttk.Scrollbar(txt_frame, command=txt.yview) txt.configure(yscrollcommand=scrollbar.set) txt.pack(side="left", fill="both", expand=True) scrollbar.pack(side="right", fill="y") root.mainloop()
import tkinter as tk from tkinter import ttk class AppState: """Stato condiviso tra tutti i componenti.""" def __init__(self): self.utente_loggato = None self.tema = "chiaro" self.lingua = "it" self._callbacks = {} # osservatori per il pattern Observer def on_change(self, evento, callback): """Registra un listener per un evento di stato.""" self._callbacks.setdefault(evento, []).append(callback) def emit(self, evento, data=None): """Notifica tutti i listener.""" for cb in self._callbacks.get(evento, []): cb(data) class MainApp(tk.Tk): def __init__(self): super().__init__() self.state = AppState() # unica sorgente di verità self._crea_ui() def _crea_ui(self): lbl = ttk.Label(self, text="Nessun utente") lbl.pack(pady=20) # Ascolta cambiamenti di login self.state.on_change("login", lambda u: lbl.config(text=f"Benvenuto, {u}!")) ttk.Button(self, text="Simula login", command=lambda: self.state.emit("login", "Mario")).pack() MainApp().mainloop()
import tkinter as tk root = tk.Tk() canvas = tk.Canvas(root, width=500, height=400, bg="#f0f2f5") canvas.pack() # Crea un rettangolo trascinabile rect = canvas.create_rectangle(50, 50, 150, 100, fill="#1877f2", outline="#0d5bba", width=2) label = canvas.create_text(100, 75, text="Trascina!", fill="white") drag_data = {"x": 0, "y": 0} def start_drag(event): drag_data["x"] = event.x drag_data["y"] = event.y def do_drag(event): dx = event.x - drag_data["x"] dy = event.y - drag_data["y"] canvas.move(rect, dx, dy) canvas.move(label, dx, dy) drag_data["x"] = event.x drag_data["y"] = event.y canvas.tag_bind(rect, "<Button-1>", start_drag) canvas.tag_bind(rect, "<B1-Motion>", do_drag) canvas.tag_bind(label, "<Button-1>", start_drag) canvas.tag_bind(label, "<B1-Motion>", do_drag) root.mainloop()
Nel pattern MVC applicato a Tkinter, quale componente gestisce i dati?
Quale metodo di ttk.Style si usa per cambiare il colore di sfondo di un TButton quando è in stato "active"?
style.configure("TButton", hover="#color")style.map("TButton", background=[("active","#color")])style.bind("TButton", active="#color")style.state("TButton", active="#color")Microsoft SQL Server è un sistema di gestione di database relazionali (RDBMS) enterprise, usato in aziende di tutto il mondo. A differenza di SQLite (file locale), SQL Server gira come servizio su un server e supporta connessioni multiple, transazioni avanzate, stored procedure e molto altro.
Per connettere Python a SQL Server si usa la libreria pyodbc insieme al driver ODBC di Microsoft:
# Installa pyodbc pip install pyodbc # Installa anche pandas (opzionale, per visualizzare dati) pip install pandas # Su Windows: scarica il driver ODBC da Microsoft # https://learn.microsoft.com/sql/connect/odbc/download-odbc-driver-for-sql-server # Verifica driver installati python -c "import pyodbc; print(pyodbc.drivers())"
import pyodbc # ── Autenticazione Windows (Trusted Connection) ── conn_str_windows = ( "DRIVER={ODBC Driver 18 for SQL Server};" "SERVER=localhost;" # o nome server/IP "DATABASE=MioDatabase;" "Trusted_Connection=yes;" "TrustServerCertificate=yes;" ) # ── Autenticazione SQL (username/password) ── conn_str_sql = ( "DRIVER={ODBC Driver 18 for SQL Server};" "SERVER=192.168.1.100,1433;" # IP,porta "DATABASE=MioDatabase;" "UID=sa;" "PWD=MiaPassword123!;" "TrustServerCertificate=yes;" ) # Connessione try: conn = pyodbc.connect(conn_str_windows) print("✅ Connesso a SQL Server!") cur = conn.cursor() # Versione SQL Server cur.execute("SELECT @@VERSION") print(cur.fetchone()[0]) except pyodbc.Error as e: print(f"❌ Errore: {e}") finally: if "conn" in dir(): conn.close()
import pyodbc from contextlib import contextmanager class SQLServerManager: def __init__(self, server, database, uid=None, pwd=None): if uid: self.conn_str = ( f"DRIVER={{ODBC Driver 18 for SQL Server}};" f"SERVER={server};DATABASE={database};" f"UID={uid};PWD={pwd};TrustServerCertificate=yes;" ) else: self.conn_str = ( f"DRIVER={{ODBC Driver 18 for SQL Server}};" f"SERVER={server};DATABASE={database};" f"Trusted_Connection=yes;TrustServerCertificate=yes;" ) self._conn = None def connetti(self): self._conn = pyodbc.connect(self.conn_str, autocommit=False) return self def disconnetti(self): if self._conn: self._conn.close() def esegui(self, query, params=None): """Esegui INSERT/UPDATE/DELETE con commit automatico.""" cur = self._conn.cursor() cur.execute(query, params or []) self._conn.commit() return cur def leggi(self, query, params=None): """Esegui SELECT e ritorna tutte le righe.""" cur = self._conn.cursor() cur.execute(query, params or []) return cur.fetchall() def leggi_uno(self, query, params=None): cur = self._conn.cursor() cur.execute(query, params or []) return cur.fetchone() def crea_tabella(self): self.esegui(""" IF NOT EXISTS ( SELECT * FROM sysobjects WHERE name='Dipendenti' AND xtype='U' ) CREATE TABLE Dipendenti ( Id INT IDENTITY(1,1) PRIMARY KEY, Nome NVARCHAR(100) NOT NULL, Cognome NVARCHAR(100) NOT NULL, Email NVARCHAR(200) UNIQUE, Reparto NVARCHAR(100), Stipendio DECIMAL(10,2), DataAssunzione DATE DEFAULT GETDATE() ) """)
# Usando SQLServerManager definito sopra db = SQLServerManager("localhost", "AziendaDB") db.connetti() db.crea_tabella() # ── CREATE ── db.esegui( "INSERT INTO Dipendenti (Nome, Cognome, Email, Reparto, Stipendio) " "VALUES (?, ?, ?, ?, ?)", ("Marco", "Rossi", "[email protected]", "IT", 35000.00) ) # ── READ — tutti ── dipendenti = db.leggi("SELECT * FROM Dipendenti ORDER BY Cognome") for d in dipendenti: print(f"{d.Id} | {d.Nome} {d.Cognome} | {d.Reparto} | €{d.Stipendio}") # ── READ — con filtro ── it_team = db.leggi( "SELECT * FROM Dipendenti WHERE Reparto = ? AND Stipendio > ?", ("IT", 30000) ) # ── UPDATE ── db.esegui( "UPDATE Dipendenti SET Stipendio = ? WHERE Id = ?", (38000.00, 1) ) # ── DELETE ── db.esegui("DELETE FROM Dipendenti WHERE Id = ?", (5,)) # ── Stored Procedure ── db.esegui("EXEC sp_AggiornaStipendi @percentuale=?", (5,)) db.disconnetti()
Costruiamo un'app completa di gestione dipendenti con ricerca in tempo reale, inserimento, modifica ed eliminazione:
import tkinter as tk from tkinter import ttk, messagebox import pyodbc class GestioneDipendenti(tk.Tk): def __init__(self): super().__init__() self.title("👥 Gestione Dipendenti") self.geometry("1000x600") self.configure(bg="#f8fafc") # Connessione DB self.db = SQLServerManager("localhost", "AziendaDB") try: self.db.connetti() self.db.crea_tabella() except Exception as e: messagebox.showerror("DB Error", f"Connessione fallita:\n{e}") self.destroy(); return # Variabili form self.var_nome = tk.StringVar() self.var_cognome = tk.StringVar() self.var_email = tk.StringVar() self.var_reparto = tk.StringVar() self.var_stipendio = tk.StringVar() self.var_cerca = tk.StringVar() self.id_selezionato = None self._build_ui() self._carica_dati() # Cerca in tempo reale self.var_cerca.trace("w", lambda *_: self._carica_dati()) self.protocol("WM_DELETE_WINDOW", self._chiudi) def _build_ui(self): # ── Toolbar in cima ── toolbar = tk.Frame(self, bg="#1877f2", height=46) toolbar.pack(fill="x"); toolbar.pack_propagate(False) tk.Label(toolbar, text="👥 Gestione Dipendenti", fg="white", bg="#1877f2", font=("Segoe UI", 13, "bold")).pack(side="left", padx=16) # Cerca tk.Label(toolbar, text="🔍", bg="#1877f2", fg="white").pack(side="right", padx=(0,4)) ttk.Entry(toolbar, textvariable=self.var_cerca, width=20).pack(side="right", pady=8, padx=4) # ── Layout principale ── main = tk.Frame(self, bg="#f8fafc") main.pack(fill="both", expand=True, padx=16, pady=16) # Form a sinistra form = tk.LabelFrame(main, text=" 📝 Dipendente ", font=("Segoe UI", 10, "bold"), bg="#f8fafc") form.pack(side="left", fill="y", padx=(0,12), ipadx=10, ipady=6) campi = [("Nome", self.var_nome), ("Cognome", self.var_cognome), ("Email", self.var_email), ("Reparto", self.var_reparto), ("Stipendio", self.var_stipendio)] for i, (lbl, var) in enumerate(campi): tk.Label(form, text=lbl+":", bg="#f8fafc", anchor="w").grid(row=i, column=0, sticky="w", pady=4) ttk.Entry(form, textvariable=var, width=22).grid( row=i, column=1, pady=4, padx=6) # Bottoni azione btn_frame = tk.Frame(form, bg="#f8fafc") btn_frame.grid(row=len(campi), column=0, columnspan=2, pady=12) for (txt, cmd, bg) in [ ("➕ Aggiungi", self._aggiungi, "#42b72a"), ("✏️ Modifica", self._modifica, "#1877f2"), ("🗑️ Elimina", self._elimina, "#fa383e"), ("🔄 Reset", self._reset_form,"#65676b") ]: tk.Button(btn_frame, text=txt, command=cmd, bg=bg, fg="white", relief="flat", padx=10, pady=5, cursor="hand2").pack(fill="x", pady=2) # Tabella a destra cols = ("ID", "Nome", "Cognome", "Email", "Reparto", "Stipendio") self.tree = ttk.Treeview(main, columns=cols, show="headings") for c, w in zip(cols, [50, 120, 120, 200, 100, 90]): self.tree.heading(c, text=c) self.tree.column(c, width=w, anchor="center") vsb = ttk.Scrollbar(main, orient="vertical", command=self.tree.yview) self.tree.configure(yscrollcommand=vsb.set) self.tree.pack(side="left", fill="both", expand=True) vsb.pack(side="left", fill="y") self.tree.bind("<<TreeviewSelect>>", self._seleziona) def _carica_dati(self): cerca = self.var_cerca.get().strip() if cerca: q = ("SELECT Id, Nome, Cognome, Email, Reparto, Stipendio " "FROM Dipendenti WHERE Nome LIKE ? OR Cognome LIKE ? OR Reparto LIKE ? " "ORDER BY Cognome") righe = self.db.leggi(q, (f"%{cerca}%",)*3) else: righe = self.db.leggi( "SELECT Id, Nome, Cognome, Email, Reparto, Stipendio FROM Dipendenti ORDER BY Cognome") for i in self.tree.get_children(): self.tree.delete(i) for r in righe: self.tree.insert("", tk.END, values=( r.Id, r.Nome, r.Cognome, r.Email, r.Reparto, f"€{r.Stipendio:,.2f}" if r.Stipendio else "" )) def _seleziona(self, event): sel = self.tree.selection() if not sel: return vals = self.tree.item(sel[0])["values"] self.id_selezionato = vals[0] self.var_nome.set(vals[1]) self.var_cognome.set(vals[2]) self.var_email.set(vals[3] or "") self.var_reparto.set(vals[4] or "") self.var_stipendio.set(vals[5].replace("€","").replace(",","") if vals[5] else "") def _aggiungi(self): if not self.var_nome.get() or not self.var_cognome.get(): messagebox.showwarning("Attenzione", "Nome e Cognome obbligatori!"); return try: self.db.esegui( "INSERT INTO Dipendenti (Nome,Cognome,Email,Reparto,Stipendio) VALUES (?,?,?,?,?)", (self.var_nome.get(), self.var_cognome.get(), self.var_email.get() or None, self.var_reparto.get() or None, float(self.var_stipendio.get()) if self.var_stipendio.get() else None)) self._carica_dati(); self._reset_form() except Exception as e: messagebox.showerror("Errore", str(e)) def _modifica(self): if not self.id_selezionato: messagebox.showinfo("Info", "Seleziona un dipendente dalla tabella"); return try: self.db.esegui( "UPDATE Dipendenti SET Nome=?,Cognome=?,Email=?,Reparto=?,Stipendio=? WHERE Id=?", (self.var_nome.get(), self.var_cognome.get(), self.var_email.get() or None, self.var_reparto.get() or None, float(self.var_stipendio.get()) if self.var_stipendio.get() else None, self.id_selezionato)) self._carica_dati(); self._reset_form() except Exception as e: messagebox.showerror("Errore", str(e)) def _elimina(self): if not self.id_selezionato: messagebox.showinfo("Info", "Seleziona un dipendente"); return if messagebox.askyesno("Conferma", "Eliminare questo dipendente?"): self.db.esegui("DELETE FROM Dipendenti WHERE Id=?", (self.id_selezionato,)) self._carica_dati(); self._reset_form() def _reset_form(self): for v in [self.var_nome, self.var_cognome, self.var_email, self.var_reparto, self.var_stipendio]: v.set("") self.id_selezionato = None def _chiudi(self): self.db.disconnetti() self.destroy() if __name__ == "__main__": GestioneDipendenti().mainloop()
# ── JOIN tra tabelle ── query_join = """ SELECT d.Nome, d.Cognome, r.NomeReparto, r.Budget FROM Dipendenti d INNER JOIN Reparti r ON d.Reparto = r.Id WHERE r.Budget > 100000 ORDER BY d.Cognome """ # ── Aggregazioni ── query_stats = """ SELECT Reparto, COUNT(*) AS NumDipendenti, AVG(Stipendio) AS StipendioMedio, MAX(Stipendio) AS StipendioMax, SUM(Stipendio) AS CostoTotale FROM Dipendenti GROUP BY Reparto HAVING COUNT(*) > 2 ORDER BY CostoTotale DESC """ # ── Paginazione (SQL Server 2012+) ── pagina = 1 per_pagina = 20 query_page = f""" SELECT * FROM Dipendenti ORDER BY Id OFFSET {(pagina-1) * per_pagina} ROWS FETCH NEXT {per_pagina} ROWS ONLY """ # ── Transazione esplicita ── try: cur = conn.cursor() cur.execute("BEGIN TRANSACTION") cur.execute("UPDATE Conti SET Saldo = Saldo - ? WHERE Id = ?", (500, 1)) cur.execute("UPDATE Conti SET Saldo = Saldo + ? WHERE Id = ?", (500, 2)) conn.commit() # salva SOLO se entrambe OK print("✅ Trasferimento riuscito") except: conn.rollback() # annulla tutto se errore print("❌ Transazione annullata")
Le Stored Procedure sono blocchi di SQL precompilati sul server. Sono più veloci delle query normali e centralizzano la logica di business nel database.
import pyodbc conn = pyodbc.connect(conn_str) cur = conn.cursor() # ── Chiamare una Stored Procedure senza parametri ── cur.execute("EXEC sp_GetAllDipendenti") righe = cur.fetchall() # ── Con parametri di input ── cur.execute("EXEC sp_GetDipendentiByReparto @Reparto=?", ("IT",)) righe = cur.fetchall() # ── Con parametro di output ── cur.execute(""" DECLARE @Totale INT; EXEC sp_ContaDipendenti @Reparto=?, @Totale=@Totale OUTPUT; SELECT @Totale; """, ("IT",)) totale = cur.fetchval() print(f"Dipendenti IT: {totale}") # ── Esempio: creare una SP direttamente da Python ── cur.execute(""" IF OBJECT_ID('sp_GetDipendentiByReparto') IS NOT NULL DROP PROCEDURE sp_GetDipendentiByReparto """) cur.execute(""" CREATE PROCEDURE sp_GetDipendentiByReparto @Reparto NVARCHAR(100) AS BEGIN SELECT Id, Nome, Cognome, Email, Stipendio FROM Dipendenti WHERE Reparto = @Reparto ORDER BY Cognome, Nome END """) conn.commit()
import pyodbc from tkinter import messagebox def esegui_sicuro(conn, query, params=None): """Wrapper con gestione errori completa.""" try: cur = conn.cursor() cur.execute(query, params or []) conn.commit() return True, cur except pyodbc.IntegrityError as e: conn.rollback() # Violazione UNIQUE, FK, NOT NULL ecc. messagebox.showerror("Errore dati", f"Dato duplicato o non valido:\n{e.args[1]}") return False, None except pyodbc.OperationalError as e: conn.rollback() # Connessione persa, timeout ecc. messagebox.showerror("Errore connessione", f"Problema con il database:\n{e.args[1]}\n\nRiprova tra poco.") return False, None except pyodbc.ProgrammingError as e: conn.rollback() # Errore SQL (tabella non trovata, sintassi ecc.) messagebox.showerror("Errore SQL", str(e)) return False, None # Codici di errore SQL Server comuni ERRORI_SQL = { 2627: "Valore duplicato: esiste già un record con questi dati", 547: "Violazione chiave esterna: riferimento non valido", 515: "Campo obbligatorio mancante (NOT NULL)", 208: "Tabella non trovata", 4060: "Database non trovato o accesso negato", 18456:"Login fallito — credenziali errate", } def descrivi_errore(e): if hasattr(e, "args") and e.args: codice = e.args[0] return ERRORI_SQL.get(codice, str(e)) return str(e)
Una funzionalità comune nelle app gestionali è esportare i dati in Excel o CSV:
import csv, os from tkinter import filedialog, messagebox from datetime import datetime def esporta_csv(righe, intestazioni, titolo="dati"): """Esporta dati in CSV con dialogo salvataggio.""" nome_default = f"{titolo}_{datetime.now().strftime('%Y%m%d_%H%M')}.csv" percorso = filedialog.asksaveasfilename( defaultextension=".csv", initialfile=nome_default, filetypes=[("CSV", "*.csv"), ("Tutti", "*.*")] ) if not percorso: return with open(percorso, "w", newline="", encoding="utf-8-sig") as f: writer = csv.writer(f, delimiter=";") writer.writerow(intestazioni) writer.writerows(righe) messagebox.showinfo("Esportazione", f"✅ {len(righe)} righe esportate in:\n{percorso}") os.startfile(percorso) # apre il file (Windows) # Installa openpyxl per Excel: pip install openpyxl try: import openpyxl from openpyxl.styles import Font, PatternFill, Alignment def esporta_excel(righe, intestazioni, titolo="report"): percorso = filedialog.asksaveasfilename( defaultextension=".xlsx", initialfile=f"{titolo}.xlsx", filetypes=[("Excel", "*.xlsx")] ) if not percorso: return wb = openpyxl.Workbook() ws = wb.active ws.title = titolo # Intestazioni con stile header_fill = PatternFill("solid", fgColor="1877F2") for col, nome in enumerate(intestazioni, 1): cell = ws.cell(1, col, nome) cell.font = Font(bold=True, color="FFFFFF") cell.fill = header_fill cell.alignment = Alignment(horizontal="center") # Dati for r, riga in enumerate(righe, 2): for c, val in enumerate(riga, 1): ws.cell(r, c, val) # Larghezza colonne automatica for col in ws.columns: max_w = max(len(str(c.value or "")) for c in col) ws.column_dimensions[col[0].column_letter].width = min(max_w + 4, 40) wb.save(percorso) os.startfile(percorso) except ImportError: print("Installa openpyxl per esportare in Excel: pip install openpyxl")
Mettendo insieme tutto — Tkinter, ttk, SQL Server, CRUD, esportazione — ecco la struttura di un gestionale aziendale completo:
# Struttura del progetto finale gestionale/ ├── main.py # entry point e Application ├── config.py # stringa connessione, costanti ├── db/ │ ├── manager.py # SQLServerManager │ ├── dipendenti_repo.py # CRUD Dipendenti │ └── reparti_repo.py # CRUD Reparti ├── ui/ │ ├── main_window.py # finestra principale con menu │ ├── dipendenti_tab.py # scheda gestione dipendenti │ ├── reparti_tab.py # scheda gestione reparti │ ├── report_tab.py # scheda report e grafici │ └── login_dialog.py # finestra di login ├── utils/ │ ├── esporta.py # CSV / Excel │ ├── validazioni.py # controllo dati form │ └── tema.py # tema ttk personalizzato └── assets/ └── icone/ # file .png per toolbar
import tkinter as tk from tkinter import ttk, messagebox import pyodbc from db.manager import SQLServerManager from ui.main_window import MainWindow from ui.login_dialog import LoginDialog from utils.tema import applica_tema from config import SERVER, DATABASE class GestionaleApp(tk.Tk): def __init__(self): super().__init__() self.withdraw() # nasconde finestra durante login self.title("Gestionale Aziendale") self.geometry("1200x700") self.minsize(900, 550) applica_tema(self) # Login login = LoginDialog(self) self.wait_window(login) if not getattr(login, "confermato", False): self.destroy(); return # Connessione DB try: self.db = SQLServerManager( SERVER, DATABASE, uid=login.uid, pwd=login.pwd ).connetti() except pyodbc.Error as e: messagebox.showerror("Connessione", f"Impossibile connettersi al database:\n{e}") self.destroy(); return # Mostra finestra principale self.deiconify() MainWindow(self, self.db) self.protocol("WM_DELETE_WINDOW", self._chiudi) def _chiudi(self): if messagebox.askyesno("Esci", "Chiudere il gestionale?"): if hasattr(self, "db"): self.db.disconnetti() self.destroy() if __name__ == "__main__": GestionaleApp().mainloop()
Hai imparato tutto il necessario per costruire applicazioni desktop professionali con Python. Ecco il riepilogo completo:
Quale libreria Python si usa per connettersi a SQL Server tramite ODBC?
sqlite3pymssqlpyodbcsqlalchemyQuesto codice è pericoloso. Perché?
nome = entry.get()
cur.execute("SELECT * FROM Utenti WHERE Nome = '" + nome + "'")
?entry.get() non funziona con pyodbccommit()Come si chiama una Stored Procedure con parametro in pyodbc?
cur.call("sp_Nome", ("valore",))cur.execute("EXEC sp_Nome @Param=?", ("valore",))cur.procedure("sp_Nome", "valore")conn.stored_proc("sp_Nome", "valore")In una transazione con pyodbc, cosa succede se chiami conn.rollback()?
Matplotlib è la libreria Python più potente per la visualizzazione di dati. Integrata con Tkinter, permette di creare dashboard interattive, grafici scientifici e applicazioni di data analysis con GUI professionale.
pip install matplotlib numpy
NumPy è raccomandato per manipolazione dati numerici e calcoli matematici efficienti.
Per integrare Matplotlib in Tkinter, usiamo il backend TkAgg e la classe FigureCanvasTkAgg che crea un widget Tkinter contenente il grafico.
import tkinter as tk from matplotlib.figure import Figure from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg import numpy as np # Finestra principale root = tk.Tk() root.title("Matplotlib in Tkinter") root.geometry("900x600") # Crea figura Matplotlib (non usare plt.figure) fig = Figure( figsize=(8, 5), # larghezza x altezza in pollici dpi=100, # risoluzione facecolor='white' # colore sfondo ) # Aggiungi subplot (area per grafico) ax = fig.add_subplot(111) # 1 riga, 1 colonna, grafico #1 # Incorpora la figura in Tkinter canvas = FigureCanvasTkAgg(fig, master=root) canvas.draw() canvas.get_tk_widget().pack(fill=tk.BOTH, expand=True) root.mainloop()
import matplotlib.pyplot as plt in applicazioni Tkinter. Usa sempre Figure e FigureCanvasTkAgg per evitare conflitti tra mainloop di pyplot e Tkinter.
Il grafico più comune per mostrare trend nel tempo o relazioni continue tra variabili.
import tkinter as tk from matplotlib.figure import Figure from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg import numpy as np root = tk.Tk() root.title("Line Plot - Temperatura Mensile") root.geometry("1000x700") root.configure(bg="#f5f5f5") # Dati esempio: temperatura media mensile mesi = ['Gen', 'Feb', 'Mar', 'Apr', 'Mag', 'Giu', 'Lug', 'Ago', 'Set', 'Ott', 'Nov', 'Dic'] temp_2023 = [5, 7, 12, 15, 20, 25, 28, 27, 22, 16, 10, 6] temp_2024 = [4, 6, 11, 16, 21, 26, 30, 29, 24, 17, 11, 7] # Crea figura fig = Figure(figsize=(10, 6), dpi=100) ax = fig.add_subplot(111) # Plot di due linee con stili diversi ax.plot(mesi, temp_2023, marker='o', # marcatori cerchio linestyle='-', # linea continua linewidth=2.5, # spessore linea color='#3b82f6', # blu label='2023', markersize=8) ax.plot(mesi, temp_2024, marker='s', # marcatori quadrati linestyle='--', # linea tratteggiata linewidth=2.5, color='#ef4444', # rosso label='2024', markersize=8) # Personalizzazione grafico ax.set_title('Temperatura Media Mensile', fontsize=18, fontweight='bold', pad=20) ax.set_xlabel('Mese', fontsize=14, fontweight='bold') ax.set_ylabel('Temperatura (°C)', fontsize=14, fontweight='bold') ax.legend(loc='upper left', fontsize=12, framealpha=0.9) ax.grid(True, alpha=0.3, linestyle='--') # Migliora l'aspetto fig.tight_layout() # Incorpora in Tkinter canvas = FigureCanvasTkAgg(fig, master=root) canvas.draw() canvas.get_tk_widget().pack(fill=tk.BOTH, expand=True, padx=20, pady=20) root.mainloop()
'-' continua · '--' tratteggiata · '-.' punto-trattino · ':' puntini'o' cerchio · 's' quadrato · '^' triangolo · 'D' diamante · '*' stella · '+' croce
Ideale per confrontare categorie discrete o mostrare distribuzioni.
import tkinter as tk from matplotlib.figure import Figure from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg import numpy as np root = tk.Tk() root.title("Bar Chart - Vendite per Prodotto") root.geometry("1000x700") # Dati vendite prodotti = ['Laptop', 'Tablet', 'Smartphone', 'Monitor', 'Tastiera'] q1_vendite = [120, 85, 200, 75, 150] q2_vendite = [140, 95, 220, 65, 160] # Crea figura fig = Figure(figsize=(10, 6), dpi=100) ax = fig.add_subplot(111) # Posizioni barre x = np.arange(len(prodotti)) # [0, 1, 2, 3, 4] larghezza = 0.35 # Crea barre affiancate barre1 = ax.bar(x - larghezza/2, q1_vendite, larghezza, label='Q1 2024', color='#10b981', edgecolor='#059669', linewidth=1.5) barre2 = ax.bar(x + larghezza/2, q2_vendite, larghezza, label='Q2 2024', color='#3b82f6', edgecolor='#1d4ed8', linewidth=1.5) # Aggiungi valori sopra le barre def aggiungi_etichette(barre): for barra in barre: altezza = barra.get_height() ax.text(barra.get_x() + barra.get_width()/2, altezza, f'{int(altezza)}', ha='center', va='bottom', fontsize=10, fontweight='bold') aggiungi_etichette(barre1) aggiungi_etichette(barre2) # Personalizzazione ax.set_title('Vendite per Prodotto - Q1 vs Q2', fontsize=18, fontweight='bold', pad=20) ax.set_xlabel('Prodotto', fontsize=14, fontweight='bold') ax.set_ylabel('Unità Vendute', fontsize=14, fontweight='bold') ax.set_xticks(x) ax.set_xticklabels(prodotti, fontsize=12) ax.legend(fontsize=12) ax.grid(axis='y', alpha=0.3, linestyle='--') fig.tight_layout() # Incorpora in Tkinter canvas = FigureCanvasTkAgg(fig, master=root) canvas.draw() canvas.get_tk_widget().pack(fill=tk.BOTH, expand=True, padx=20, pady=20) root.mainloop()
Mostra la distribuzione di frequenza di dati continui, utile per analisi statistiche.
import tkinter as tk from matplotlib.figure import Figure from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg import numpy as np root = tk.Tk() root.title("Istogramma - Distribuzione Età Utenti") root.geometry("1000x700") # Genera dati esempio: età di 1000 utenti (distribuzione normale) np.random.seed(42) eta_utenti = np.random.normal(loc=35, scale=12, size=1000) eta_utenti = np.clip(eta_utenti, 18, 70) # limita età tra 18-70 fig = Figure(figsize=(10, 6), dpi=100) ax = fig.add_subplot(111) # Crea istogramma n, bins, patches = ax.hist( eta_utenti, bins=30, # numero di intervalli color='#8b5cf6', edgecolor='#6d28d9', alpha=0.7, linewidth=1.5 ) # Aggiungi linea di media media = np.mean(eta_utenti) ax.axvline(media, color='#ef4444', linestyle='--', linewidth=2.5, label=f'Media: {media:.1f} anni') # Personalizzazione ax.set_title('Distribuzione Età Utenti', fontsize=18, fontweight='bold', pad=20) ax.set_xlabel('Età (anni)', fontsize=14, fontweight='bold') ax.set_ylabel('Frequenza', fontsize=14, fontweight='bold') ax.legend(fontsize=12) ax.grid(alpha=0.3, linestyle='--') # Statistiche aggiuntive stats_text = f'N = {len(eta_utenti)}\nStd = {np.std(eta_utenti):.1f}' ax.text(0.02, 0.95, stats_text, transform=ax.transAxes, fontsize=11, verticalalignment='top', bbox=dict(boxstyle='round', facecolor='white', alpha=0.8)) fig.tight_layout() canvas = FigureCanvasTkAgg(fig, master=root) canvas.draw() canvas.get_tk_widget().pack(fill=tk.BOTH, expand=True, padx=20, pady=20) root.mainloop()
Visualizza proporzioni e percentuali di un insieme di categorie.
import tkinter as tk from matplotlib.figure import Figure from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg root = tk.Tk() root.title("Pie Chart - Quote di Mercato") root.geometry("1000x700") # Dati market share aziende = ['Azienda A', 'Azienda B', 'Azienda C', 'Azienda D', 'Altri'] quote = [35, 28, 18, 12, 7] colori = ['#3b82f6', '#10b981', '#f59e0b', '#ef4444', '#8b5cf6'] fig = Figure(figsize=(10, 8), dpi=100) ax = fig.add_subplot(111) # Stacca la fetta più grande esplosione = (0.1, 0, 0, 0, 0) # stacca solo la prima fetta # Crea grafico a torta wedges, testi, autotesti = ax.pie( quote, labels=aziende, autopct='%1.1f%%', # mostra percentuali startangle=90, # inizia dall'alto colors=colori, explode=esplosione, shadow=True, # aggiungi ombra textprops={'fontsize': 12, 'fontweight': 'bold'} ) # Migliora leggibilità percentuali for autotesto in autotesti: autotesto.set_color('white') autotesto.set_fontsize(11) autotesto.set_fontweight('bold') ax.set_title('Quote di Mercato 2024', fontsize=20, fontweight='bold', pad=30) # Assicura che la torta sia circolare ax.axis('equal') fig.tight_layout() canvas = FigureCanvasTkAgg(fig, master=root) canvas.draw() canvas.get_tk_widget().pack(fill=tk.BOTH, expand=True, padx=20, pady=20) root.mainloop()
Mostra la relazione tra due variabili continue, ideale per identificare correlazioni.
import tkinter as tk from matplotlib.figure import Figure from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg import numpy as np root = tk.Tk() root.title("Scatter Plot - Altezza vs Peso") root.geometry("1000x700") # Genera dati correlati (altezza cm vs peso kg) np.random.seed(42) n_persone = 200 altezza = np.random.normal(170, 10, n_persone) # media 170cm peso = 0.6 * altezza + np.random.normal(0, 5, n_persone) - 30 # correlazione positiva # Colori basati sul BMI bmi = peso / ((altezza/100)**2) colori = ['#10b981' if 18.5 <= b <= 25 else '#ef4444' for b in bmi] fig = Figure(figsize=(10, 6), dpi=100) ax = fig.add_subplot(111) # Scatter plot con dimensioni variabili scatter = ax.scatter( altezza, peso, c=colori, s=50, # dimensione punti alpha=0.6, edgecolors='black', linewidths=0.5 ) # Aggiungi linea di tendenza z = np.polyfit(altezza, peso, 1) # regressione lineare p = np.poly1d(z) ax.plot(altezza, p(altezza), "--", color='#3b82f6', linewidth=2.5, label=f'Tendenza: y={z[0]:.2f}x{z[1]:+.2f}') # Personalizzazione ax.set_title('Correlazione Altezza-Peso', fontsize=18, fontweight='bold', pad=20) ax.set_xlabel('Altezza (cm)', fontsize=14, fontweight='bold') ax.set_ylabel('Peso (kg)', fontsize=14, fontweight='bold') ax.legend(fontsize=12) ax.grid(True, alpha=0.3, linestyle='--') # Legenda colori from matplotlib.patches import Patch legenda_elementi = [ Patch(facecolor='#10b981', label='BMI Normale'), Patch(facecolor='#ef4444', label='BMI Fuori Range') ] ax.legend(handles=legenda_elementi, loc='upper left', fontsize=11) fig.tight_layout() canvas = FigureCanvasTkAgg(fig, master=root) canvas.draw() canvas.get_tk_widget().pack(fill=tk.BOTH, expand=True, padx=20, pady=20) root.mainloop()
Visualizza dati matriciali con gradiente di colori per identificare pattern.
import tkinter as tk from matplotlib.figure import Figure from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg import numpy as np root = tk.Tk() root.title("Heatmap - Traffico Sito Web") root.geometry("1100x700") # Dati: traffico per giorno e ora giorni = ['Lun', 'Mar', 'Mer', 'Gio', 'Ven', 'Sab', 'Dom'] ore = [f'{h:02d}:00' for h in range(0, 24, 2)] # ogni 2 ore # Genera dati casuali (picchi nelle ore lavorative e weekend) np.random.seed(42) traffico = np.random.randint(50, 150, size=(len(giorni), len(ore))) # Simula picchi ore lavorative (9-18) traffico[:, 4:9] += 100 # ore 8-18 # Simula picchi weekend traffico[5:, :] += 50 fig = Figure(figsize=(11, 6), dpi=100) ax = fig.add_subplot(111) # Crea heatmap im = ax.imshow(traffico, cmap='YlOrRd', # colormap giallo-arancio-rosso aspect='auto', interpolation='nearest') # Imposta tick personalizzati ax.set_xticks(np.arange(len(ore))) ax.set_yticks(np.arange(len(giorni))) ax.set_xticklabels(ore, fontsize=10) ax.set_yticklabels(giorni, fontsize=11) # Ruota le etichette delle ore ax.tick_params(axis='x', rotation=45) # Aggiungi valori nelle celle for i in range(len(giorni)): for j in range(len(ore)): testo = ax.text(j, i, int(traffico[i, j]), ha="center", va="center", color="white" if traffico[i, j] > 150 else "black", fontsize=9, fontweight='bold') # Aggiungi barra dei colori cbar = fig.colorbar(im, ax=ax) cbar.set_label('Visite/h', rotation=270, labelpad=20, fontsize=12, fontweight='bold') ax.set_title('Traffico Sito Web per Giorno e Ora', fontsize=18, fontweight='bold', pad=20) ax.set_xlabel('Ora del Giorno', fontsize=13, fontweight='bold') ax.set_ylabel('Giorno della Settimana', fontsize=13, fontweight='bold') fig.tight_layout() canvas = FigureCanvasTkAgg(fig, master=root) canvas.draw() canvas.get_tk_widget().pack(fill=tk.BOTH, expand=True, padx=20, pady=20) root.mainloop()
'viridis' · 'plasma' · 'coolwarm' · 'YlOrRd' · 'RdYlGn' · 'Blues' · 'Greys''_r' per invertire: 'viridis_r'
Organizza più grafici in una griglia per confronti side-by-side.
import tkinter as tk from matplotlib.figure import Figure from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg import numpy as np root = tk.Tk() root.title("Dashboard Multi-Grafico") root.geometry("1400x900") root.configure(bg="#f5f5f5") # Dati esempio np.random.seed(42) mesi = ['G', 'F', 'M', 'A', 'M', 'G', 'L', 'A', 'S', 'O', 'N', 'D'] vendite = np.random.randint(50, 150, 12) profitti = vendite * np.random.uniform(0.15, 0.25, 12) clienti = np.random.randint(100, 500, 12) # Crea figura con griglia 2x2 fig = Figure(figsize=(14, 8), dpi=100) # ━━━ Grafico 1: Line Plot (in alto a sinistra) ━━━ ax1 = fig.add_subplot(2, 2, 1) ax1.plot(mesi, vendite, marker='o', linewidth=2.5, color='#3b82f6', label='Vendite') ax1.set_title('Trend Vendite Mensili', fontsize=14, fontweight='bold', pad=10) ax1.set_ylabel('Vendite (k€)', fontweight='bold') ax1.grid(True, alpha=0.3, linestyle='--') ax1.legend() # ━━━ Grafico 2: Bar Chart (in alto a destra) ━━━ ax2 = fig.add_subplot(2, 2, 2) ax2.bar(mesi, profitti, color='#10b981', edgecolor='#059669', linewidth=1.5) ax2.set_title('Profitti Mensili', fontsize=14, fontweight='bold', pad=10) ax2.set_ylabel('Profitto (k€)', fontweight='bold') ax2.grid(axis='y', alpha=0.3, linestyle='--') # ━━━ Grafico 3: Scatter Plot (in basso a sinistra) ━━━ ax3 = fig.add_subplot(2, 2, 3) ax3.scatter(vendite, profitti, s=100, c='#8b5cf6', alpha=0.6, edgecolors='black') ax3.set_title('Vendite vs Profitti', fontsize=14, fontweight='bold', pad=10) ax3.set_xlabel('Vendite (k€)', fontweight='bold') ax3.set_ylabel('Profitti (k€)', fontweight='bold') ax3.grid(True, alpha=0.3, linestyle='--') # ━━━ Grafico 4: Pie Chart (in basso a destra) ━━━ ax4 = fig.add_subplot(2, 2, 4) categorie = ['Q1', 'Q2', 'Q3', 'Q4'] valori = [sum(vendite[:3]), sum(vendite[3:6]), sum(vendite[6:9]), sum(vendite[9:])] colors_pie = ['#3b82f6', '#10b981', '#f59e0b', '#ef4444'] ax4.pie(valori, labels=categorie, autopct='%1.1f%%', colors=colors_pie, startangle=90, textprops={'fontweight': 'bold'}) ax4.set_title('Vendite per Trimestre', fontsize=14, fontweight='bold', pad=10) # Titolo generale fig.suptitle('Dashboard Aziendale 2024', fontsize=20, fontweight='bold', y=0.98) fig.tight_layout(rect=[0, 0, 1, 0.96]) # lascia spazio al suptitle canvas = FigureCanvasTkAgg(fig, master=root) canvas.draw() canvas.get_tk_widget().pack(fill=tk.BOTH, expand=True, padx=15, pady=15) root.mainloop()
Crea grafici che si aggiornano dinamicamente senza bloccare la GUI.
import tkinter as tk from matplotlib.figure import Figure from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg import numpy as np from collections import deque root = tk.Tk() root.title("Monitor CPU Real-Time") root.geometry("1000x600") root.configure(bg="#0d0d0d") # Setup figura dark mode fig = Figure(figsize=(10, 5), dpi=100, facecolor="#141414") ax = fig.add_subplot(111) ax.set_facecolor("#0d0d0d") ax.set_ylim(0, 100) ax.set_xlim(0, 60) ax.set_ylabel("CPU %", color="#aaa", fontsize=12, fontweight='bold') ax.set_xlabel("Secondi", color="#aaa", fontsize=12, fontweight='bold') ax.tick_params(colors="#555") ax.grid(alpha=0.15, color="#333", linestyle='--') canvas = FigureCanvasTkAgg(fig, master=root) canvas.get_tk_widget().pack(fill="both", expand=True) # Buffer circolare (ultimi 60 valori) N = 60 dati_cpu = deque([0] * N, maxlen=N) linea, = ax.plot([], [], color="#00bcd4", linewidth=2.5, label="CPU Usage") fill_area = [None] # Etichetta valore corrente valore_testo = ax.text(0.02, 0.95, '', transform=ax.transAxes, fontsize=16, color='#00bcd4', fontweight='bold', verticalalignment='top') ax.legend(loc='upper right', fontsize=11, framealpha=0.3) def aggiorna_grafico(): # Simula valore CPU (sostituisci con psutil.cpu_percent() per dati reali) nuovo_valore = np.random.uniform(10, 90) dati_cpu.append(nuovo_valore) x = list(range(N)) y = list(dati_cpu) # Aggiorna linea linea.set_data(x, y) # Aggiorna area riempita if fill_area[0]: fill_area[0].remove() fill_area[0] = ax.fill_between(x, y, alpha=0.2, color="#00bcd4") # Aggiorna testo valore corrente valore_testo.set_text(f'CPU: {nuovo_valore:.1f}%') canvas.draw_idle() root.after(500, aggiorna_grafico) # aggiorna ogni 500ms aggiorna_grafico() root.mainloop()
import tkinter as tk from matplotlib.figure import Figure from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg import numpy as np root = tk.Tk() root.title("Animazione Onde Sinusoidali") root.geometry("1000x700") fig = Figure(figsize=(10, 6), dpi=100) ax = fig.add_subplot(111) # Setup iniziale x = np.linspace(0, 4*np.pi, 500) ax.set_xlim(0, 4*np.pi) ax.set_ylim(-2.5, 2.5) ax.set_xlabel('x', fontsize=13, fontweight='bold') ax.set_ylabel('y', fontsize=13, fontweight='bold') ax.set_title('Onde Sinusoidali Animate', fontsize=16, fontweight='bold', pad=15) ax.grid(True, alpha=0.3, linestyle='--') # Crea 3 linee per 3 onde diverse linea1, = ax.plot([], [], color='#3b82f6', linewidth=2.5, label='sin(x)') linea2, = ax.plot([], [], color='#ef4444', linewidth=2.5, label='sin(2x)') linea3, = ax.plot([], [], color='#10b981', linewidth=2.5, label='sin(x/2)') ax.legend(loc='upper right', fontsize=11) canvas = FigureCanvasTkAgg(fig, master=root) canvas.get_tk_widget().pack(fill=tk.BOTH, expand=True, padx=20, pady=20) # Variabile fase fase = [0] def anima_onde(): fase[0] += 0.1 # Calcola valori y y1 = np.sin(x + fase[0]) y2 = np.sin(2*x + fase[0]) y3 = np.sin(x/2 + fase[0]) # Aggiorna linee linea1.set_data(x, y1) linea2.set_data(x, y2) linea3.set_data(x, y3) canvas.draw_idle() root.after(50, anima_onde) # 20 FPS anima_onde() root.mainloop()
import tkinter as tk from matplotlib.figure import Figure from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg import numpy as np from collections import deque import datetime root = tk.Tk() root.title("Live Stock Ticker") root.geometry("1200x700") fig = Figure(figsize=(12, 6), dpi=100) ax = fig.add_subplot(111) # Dati iniziali N = 100 prezzo_base = 150.0 prezzi = deque([prezzo_base] * N, maxlen=N) timestamp = deque([''] * N, maxlen=N) linea, = ax.plot([], [], color='#3b82f6', linewidth=2.5) ax.set_ylabel('Prezzo ($)', fontsize=13, fontweight='bold') ax.set_title('AAPL - Apple Inc.', fontsize=16, fontweight='bold', pad=15) ax.grid(True, alpha=0.3, linestyle='--') # Etichetta prezzo corrente prezzo_label = tk.Label(root, text='', font=('Arial', 24, 'bold'), bg='white', fg='#10b981') prezzo_label.pack(pady=10) canvas = FigureCanvasTkAgg(fig, master=root) canvas.get_tk_widget().pack(fill=tk.BOTH, expand=True, padx=20, pady=10) def aggiorna_ticker(): # Simula cambio di prezzo (random walk) cambio = np.random.uniform(-2, 2) nuovo_prezzo = prezzi[-1] + cambio prezzi.append(nuovo_prezzo) # Timestamp ora = datetime.datetime.now().strftime('%H:%M:%S') timestamp.append(ora) # Aggiorna grafico x = list(range(N)) y = list(prezzi) linea.set_data(x, y) ax.set_xlim(0, N-1) ax.set_ylim(min(y) - 5, max(y) + 5) # Colora in base a trend colore = '#10b981' if cambio >= 0 else '#ef4444' simbolo = '▲' if cambio >= 0 else '▼' # Aggiorna label prezzo_label.config( text=f'${nuovo_prezzo:.2f} {simbolo} {abs(cambio):.2f}', fg=colore ) canvas.draw_idle() root.after(1000, aggiorna_ticker) # aggiorna ogni secondo aggiorna_ticker() root.mainloop()
draw_idle() invece di draw() · limita FPS a 15-30 · usa deque invece di liste · evita plot complessi con troppi dati
Aggiungi toolbar navigazione e gestisci eventi mouse per grafici interattivi.
import tkinter as tk from matplotlib.figure import Figure from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg, NavigationToolbar2Tk import numpy as np root = tk.Tk() root.title("Grafico Interattivo con Toolbar") root.geometry("1100x750") # Frame principale frame = tk.Frame(root) frame.pack(fill=tk.BOTH, expand=True, padx=15, pady=15) # Genera dati np.random.seed(42) x = np.linspace(0, 10, 100) y1 = np.sin(x) + np.random.normal(0, 0.2, 100) y2 = np.cos(x) + np.random.normal(0, 0.2, 100) fig = Figure(figsize=(10, 6), dpi=100) ax = fig.add_subplot(111) # Plot linee linea1, = ax.plot(x, y1, 'o-', color='#3b82f6', label='sin(x)', markersize=5) linea2, = ax.plot(x, y2, 's-', color='#ef4444', label='cos(x)', markersize=5) ax.set_xlabel('x', fontsize=13, fontweight='bold') ax.set_ylabel('y', fontsize=13, fontweight='bold') ax.set_title('Grafico Interattivo - Clicca sui punti!', fontsize=16, fontweight='bold', pad=15) ax.legend(fontsize=12) ax.grid(True, alpha=0.3, linestyle='--') # Crea canvas canvas = FigureCanvasTkAgg(fig, master=frame) canvas.draw() canvas.get_tk_widget().pack(fill=tk.BOTH, expand=True) # Aggiungi toolbar navigazione (zoom, pan, save) toolbar = NavigationToolbar2Tk(canvas, frame) toolbar.update() # Label per mostrare coordinate coord_label = tk.Label(root, text='Coordinate: ', font=('Arial', 11), anchor='w') coord_label.pack(fill=tk.X, padx=15, pady=(0, 10)) # Gestione evento click def on_click(event): if event.inaxes != ax: return coord_label.config(text=f'Coordinate: x={event.xdata:.2f}, y={event.ydata:.2f}') # Aggiungi marker sul punto cliccato ax.plot(event.xdata, event.ydata, '*', color='#f59e0b', markersize=15, alpha=0.8) canvas.draw() # Gestione evento mouse hover def on_hover(event): if event.inaxes != ax: coord_label.config(text='Coordinate: ') return coord_label.config(text=f'Coordinate: x={event.xdata:.2f}, y={event.ydata:.2f}') # Collega eventi canvas.mpl_connect('button_press_event', on_click) canvas.mpl_connect('motion_notify_event', on_hover) # Bottoni controllo btn_frame = tk.Frame(root) btn_frame.pack(pady=10) def reset_grafico(): ax.clear() ax.plot(x, y1, 'o-', color='#3b82f6', label='sin(x)', markersize=5) ax.plot(x, y2, 's-', color='#ef4444', label='cos(x)', markersize=5) ax.set_xlabel('x', fontsize=13, fontweight='bold') ax.set_ylabel('y', fontsize=13, fontweight='bold') ax.set_title('Grafico Interattivo - Clicca sui punti!', fontsize=16, fontweight='bold') ax.legend(fontsize=12) ax.grid(True, alpha=0.3, linestyle='--') canvas.draw() tk.Button(btn_frame, text="🔄 Reset", command=reset_grafico, bg='#8b5cf6', fg='white', font=('Arial', 11, 'bold'), padx=20, pady=8, relief='flat').pack(side=tk.LEFT, padx=5) root.mainloop()
Permetti agli utenti di salvare i grafici in vari formati con alta risoluzione.
from tkinter import filedialog, messagebox def salva_grafico(fig): """Apre dialog per salvare il grafico in vari formati""" percorso = filedialog.asksaveasfilename( defaultextension=".png", filetypes=[ ("PNG Image", "*.png"), ("PDF Document", "*.pdf"), ("SVG Vector", "*.svg"), ("JPEG Image", "*.jpg *.jpeg"), ("All Files", "*.*") ], initialfile="grafico", title="Salva Grafico" ) if percorso: try: fig.savefig( percorso, dpi=300, # alta risoluzione bbox_inches="tight", # taglia bordi bianchi facecolor=fig.get_facecolor(), edgecolor='none', transparent=False ) messagebox.showinfo( "Successo", f"✅ Grafico salvato con successo:\n{percorso}" ) except Exception as e: messagebox.showerror("Errore", f"❌ Errore nel salvataggio:\n{str(e)}") # Esempio di uso in una GUI import tkinter as tk from matplotlib.figure import Figure from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg root = tk.Tk() root.title("Salva Grafico") fig = Figure(figsize=(8, 5), dpi=100) ax = fig.add_subplot(111) ax.plot([1, 2, 3, 4], [1, 4, 2, 3], 'o-', linewidth=2) ax.set_title('Esempio Grafico', fontsize=14, fontweight='bold') canvas = FigureCanvasTkAgg(fig, master=root) canvas.get_tk_widget().pack(fill=tk.BOTH, expand=True, padx=20, pady=20) # Bottone salva tk.Button(root, text="💾 Salva Grafico", command=lambda: salva_grafico(fig), bg='#3b82f6', fg='white', font=('Arial', 12, 'bold'), padx=25, pady=10, relief='flat').pack(pady=15) root.mainloop()
Mettiamo insieme tutto quello che abbiamo imparato in una dashboard professionale con dati real-time, grafici multipli e controlli interattivi.
import tkinter as tk from tkinter import ttk, messagebox, filedialog from matplotlib.figure import Figure from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg import numpy as np from collections import deque import datetime class DashboardAziendale: def __init__(self, root): self.root = root self.root.title("Dashboard Aziendale Real-Time") self.root.geometry("1600x1000") # Stato self.is_running = False self.tema_dark = False # Dati (buffer ultimi 50 valori) self.vendite = deque([100] * 50, maxlen=50) self.clienti = deque([200] * 50, maxlen=50) self.profitto = deque([50] * 50, maxlen=50) self.costi = deque([30] * 50, maxlen=50) self.setup_ui() def setup_ui(self): # ━━━ TOP PANEL - Controlli ━━━ top_frame = tk.Frame(self.root, bg='#2c3e50', height=80) top_frame.pack(fill=tk.X, side=tk.TOP) top_frame.pack_propagate(False) # Titolo tk.Label(top_frame, text="📊 DASHBOARD AZIENDALE", font=('Arial', 24, 'bold'), bg='#2c3e50', fg='white').pack(side=tk.LEFT, padx=30) # Pulsanti controllo btn_frame = tk.Frame(top_frame, bg='#2c3e50') btn_frame.pack(side=tk.RIGHT, padx=30) self.btn_play = tk.Button(btn_frame, text="▶️ Start", command=self.toggle_run, bg='#27ae60', fg='white', font=('Arial', 11, 'bold'), padx=20, pady=8, relief='flat') self.btn_play.pack(side=tk.LEFT, padx=5) tk.Button(btn_frame, text="🔄 Reset", command=self.reset_dati, bg='#e74c3c', fg='white', font=('Arial', 11, 'bold'), padx=20, pady=8, relief='flat').pack(side=tk.LEFT, padx=5) tk.Button(btn_frame, text="💾 Salva", command=self.salva_dashboard, bg='#3498db', fg='white', font=('Arial', 11, 'bold'), padx=20, pady=8, relief='flat').pack(side=tk.LEFT, padx=5) tk.Button(btn_frame, text="🎨 Tema", command=self.toggle_tema, bg='#9b59b6', fg='white', font=('Arial', 11, 'bold'), padx=20, pady=8, relief='flat').pack(side=tk.LEFT, padx=5) # ━━━ STATS PANEL - Statistiche ━━━ stats_frame = tk.Frame(self.root, bg='#ecf0f1', height=100) stats_frame.pack(fill=tk.X, side=tk.TOP, padx=20, pady=(20, 10)) stats_frame.pack_propagate(False) self.stat_labels = {} stats = [ ("Vendite Totali", "vendite", "#3498db"), ("Clienti Attivi", "clienti", "#27ae60"), ("Profitto Medio", "profitto", "#f39c12"), ("Costi Operativi", "costi", "#e74c3c") ] for i, (titolo, chiave, colore) in enumerate(stats): frame = tk.Frame(stats_frame, bg=colore, relief='raised', bd=2) frame.pack(side=tk.LEFT, fill=tk.BOTH, expand=True, padx=8, pady=8) tk.Label(frame, text=titolo, font=('Arial', 11, 'bold'), bg=colore, fg='white').pack(pady=(8, 2)) lbl_valore = tk.Label(frame, text="0", font=('Arial', 20, 'bold'), bg=colore, fg='white') lbl_valore.pack(pady=(2, 8)) self.stat_labels[chiave] = lbl_valore # ━━━ GRAFICI ━━━ self.setup_grafici() def setup_grafici(self): # Frame contenitore grafici grafici_frame = tk.Frame(self.root, bg='white') grafici_frame.pack(fill=tk.BOTH, expand=True, padx=20, pady=10) # Crea figura con griglia 2x2 self.fig = Figure(figsize=(15, 8), dpi=100) # ━━━ Grafico 1: Vendite nel Tempo ━━━ self.ax1 = self.fig.add_subplot(2, 2, 1) self.linea_vendite, = self.ax1.plot([], [], color='#3498db', linewidth=2.5) self.ax1.set_title('Trend Vendite Real-Time', fontweight='bold', fontsize=13) self.ax1.set_ylabel('Vendite (k€)', fontweight='bold') self.ax1.grid(True, alpha=0.3, linestyle='--') # ━━━ Grafico 2: Clienti Attivi ━━━ self.ax2 = self.fig.add_subplot(2, 2, 2) self.linea_clienti, = self.ax2.plot([], [], color='#27ae60', linewidth=2.5) self.ax2.set_title('Clienti Attivi', fontweight='bold', fontsize=13) self.ax2.set_ylabel('N° Clienti', fontweight='bold') self.ax2.grid(True, alpha=0.3, linestyle='--') # ━━━ Grafico 3: Profitti vs Costi ━━━ self.ax3 = self.fig.add_subplot(2, 2, 3) self.barre_profitto = None self.barre_costi = None self.ax3.set_title('Profitti vs Costi (Ultimi 10)', fontweight='bold', fontsize=13) self.ax3.set_ylabel('Importo (k€)', fontweight='bold') self.ax3.grid(axis='y', alpha=0.3, linestyle='--') # ━━━ Grafico 4: Distribuzione Vendite (Pie) ━━━ self.ax4 = self.fig.add_subplot(2, 2, 4) self.ax4.set_title('Distribuzione Entrate', fontweight='bold', fontsize=13) self.fig.tight_layout(pad=3) # Incorpora in Tkinter self.canvas = FigureCanvasTkAgg(self.fig, master=grafici_frame) self.canvas.get_tk_widget().pack(fill=tk.BOTH, expand=True) def aggiorna_dati(self): # Genera nuovi dati simulati self.vendite.append(self.vendite[-1] + np.random.uniform(-5, 10)) self.clienti.append(self.clienti[-1] + np.random.randint(-10, 20)) self.profitto.append(max(10, self.profitto[-1] + np.random.uniform(-3, 5))) self.costi.append(max(5, self.costi[-1] + np.random.uniform(-2, 3))) # Aggiorna statistiche self.stat_labels['vendite'].config(text=f"{self.vendite[-1]:.1f}k€") self.stat_labels['clienti'].config(text=f"{int(self.clienti[-1])}") self.stat_labels['profitto'].config(text=f"{self.profitto[-1]:.1f}k€") self.stat_labels['costi'].config(text=f"{self.costi[-1]:.1f}k€") # Aggiorna grafici self.aggiorna_grafici() def aggiorna_grafici(self): x = list(range(len(self.vendite))) # Grafico 1: Vendite self.linea_vendite.set_data(x, list(self.vendite)) self.ax1.set_xlim(0, len(self.vendite) - 1) self.ax1.set_ylim(min(self.vendite) - 10, max(self.vendite) + 10) # Grafico 2: Clienti self.linea_clienti.set_data(x, list(self.clienti)) self.ax2.set_xlim(0, len(self.clienti) - 1) self.ax2.set_ylim(min(self.clienti) - 20, max(self.clienti) + 20) # Grafico 3: Barre Profitto/Costi (ultimi 10) self.ax3.clear() ultimi_10 = slice(-10, None) x_barre = np.arange(10) larghezza = 0.35 self.ax3.bar(x_barre - larghezza/2, list(self.profitto)[ultimi_10], larghezza, label='Profitto', color='#f39c12', edgecolor='#d68910') self.ax3.bar(x_barre + larghezza/2, list(self.costi)[ultimi_10], larghezza, label='Costi', color='#e74c3c', edgecolor='#c0392b') self.ax3.set_title('Profitti vs Costi (Ultimi 10)', fontweight='bold', fontsize=13) self.ax3.set_ylabel('Importo (k€)', fontweight='bold') self.ax3.legend() self.ax3.grid(axis='y', alpha=0.3, linestyle='--') # Grafico 4: Pie Chart Distribuzione self.ax4.clear() categorie = ['Prodotto A', 'Prodotto B', 'Prodotto C', 'Servizi'] valori = [35, 28, 22, 15] colors = ['#3498db', '#27ae60', '#f39c12', '#9b59b6'] self.ax4.pie(valori, labels=categorie, autopct='%1.1f%%', colors=colors, startangle=90, textprops={'fontweight': 'bold'}) self.ax4.set_title('Distribuzione Entrate', fontweight='bold', fontsize=13) self.canvas.draw_idle() def toggle_run(self): self.is_running = not self.is_running if self.is_running: self.btn_play.config(text="⏸️ Pause", bg='#e67e22') self.loop_aggiornamento() else: self.btn_play.config(text="▶️ Start", bg='#27ae60') def loop_aggiornamento(self): if self.is_running: self.aggiorna_dati() self.root.after(2000, self.loop_aggiornamento) # ogni 2 secondi def reset_dati(self): self.vendite.clear() self.clienti.clear() self.profitto.clear() self.costi.clear() self.vendite.extend([100] * 50) self.clienti.extend([200] * 50) self.profitto.extend([50] * 50) self.costi.extend([30] * 50) self.aggiorna_grafici() messagebox.showinfo("Reset", "📊 Dati resettati con successo!") def salva_dashboard(self): percorso = filedialog.asksaveasfilename( defaultextension=".png", filetypes=[("PNG", "*.png"), ("PDF", "*.pdf")], initialfile="dashboard" ) if percorso: self.fig.savefig(percorso, dpi=300, bbox_inches='tight') messagebox.showinfo("Salvato", f"💾 Dashboard salvata:\n{percorso}") def toggle_tema(self): self.tema_dark = not self.tema_dark if self.tema_dark: bg_color = '#1a1a1a' fg_color = '#cccccc' self.fig.patch.set_facecolor('#2a2a2a') for ax in [self.ax1, self.ax2, self.ax3, self.ax4]: ax.set_facecolor('#1a1a1a') ax.tick_params(colors=fg_color) ax.xaxis.label.set_color(fg_color) ax.yaxis.label.set_color(fg_color) ax.title.set_color(fg_color) else: self.fig.patch.set_facecolor('white') for ax in [self.ax1, self.ax2, self.ax3, self.ax4]: ax.set_facecolor('white') ax.tick_params(colors='black') ax.xaxis.label.set_color('black') ax.yaxis.label.set_color('black') ax.title.set_color('black') self.canvas.draw() if __name__ == "__main__": root = tk.Tk() app = DashboardAziendale(root) root.mainloop()
Quale classe si usa per incorporare un grafico Matplotlib in una finestra Tkinter?
MatplotlibTkFigureCanvasTkAggTkCanvas(fig)plt.show(master=root)Per aggiornare un grafico ogni 500ms senza bloccare la GUI, quale coppia di metodi si usa?
time.sleep(0.5) + canvas.draw()root.after(500, aggiorna) + canvas.draw_idle()threading.Timer(0.5, aggiorna) + canvas.draw()root.update() + fig.refresh()Per creare una griglia di 2 righe e 3 colonne di grafici, quale argomento si passa a add_subplot() per il grafico in posizione (riga 2, colonna 2)?
add_subplot(2, 2, 3)add_subplot(2, 3, 5)add_subplot(2, 3, 2)add_subplot(3, 2, 4)Hai padroneggiato tutto lo stack Python Desktop:
Prossimi passi suggeriti: