Neden önemli?
Anadolu Market'in deposunda çalışan ekip her sabah bir Excel dosyasını açıp aynı üç hesabı elle yapıyor. Python betiğini yazdın ama ekip terminal kullanmıyor. Çözüm küçük bir masaüstü penceresi: iki kutu, bir düğme, bir sonuç etiketi. Kurumlarda bu tür iç araçlar (internal tools) çok yaygındır: fiyat etiketi basıcı, vardiya planlayıcı, kur hesaplayıcı. Python'un yanında gelen tkinter, bunları ek kurulum olmadan yazmanı sağlar.
Bu derste iki şeyi birlikte öğreneceksin: grafik arayüzün (GUI) temel kavramları (pencere, widget, yerleşim, olay, callback, olay döngüsü) ve daha önemlisi mantığı arayüzden ayırmak. İyi yazılmış bir GUI uygulamasında hesap kodu pencereden habersizdir; bu yüzden test edilebilir ve aynı kod yarın bir web sayfasına taşınabilir.
Kurulum ve ilk pencere
Windows ve macOS'te python.org yükleyicisi tkinter'ı içerir. Linux'ta çoğu dağıtım ayrı paket ister. Kurulumu sınamak için terminalde küçük bir deneme penceresi açabilirsin:
# Linux (Debian/Ubuntu) sudo apt install python3-tk # Her işletim sisteminde deneme penceresi python -m tkinter
En küçük uygulama: bir pencere, bir etiket, bir düğme.
import tkinter as tk
pencere = tk.Tk()
pencere.title("Anadolu Market")
pencere.geometry("320x120")
etiket = tk.Label(pencere, text="Merhaba depo ekibi!")
etiket.pack(pady=10)
def selamla():
etiket.config(text="Düğmeye basıldı")
tk.Button(pencere, text="Bas", command=selamla).pack()
pencere.mainloop() # olay döngüsü: pencere kapanana kadar burada kalır
ilk_pencere.py olarak kaydet ve python ilk_pencere.py ile çalıştır. VS Code'da da "Run Python File" yeterlidir; tkinter için pip install gerekmez.Widget'lar
| Widget | Ne işe yarar? | Sık kullanılan |
|---|---|---|
Label | Metin ya da resim gösterir | text=, textvariable=, config() |
Entry | Tek satır girdi kutusu | get(), delete(0, "end"), insert() |
Button | Tıklanınca fonksiyon çağırır | command= |
Listbox | Seçilebilir satır listesi | insert("end", x), curselection(), delete(i) |
Checkbutton, Radiobutton | Seçenekler | variable= (BooleanVar/StringVar) |
Frame | Widget'ları gruplayan kap | İç içe yerleşim |
ttk.Combobox | Açılır liste | values=, get() |
tkinter.ttk modülü aynı widget'ların işletim sistemine daha uygun görünen "temalı" sürümlerini sunar; yeni projelerde ttk.Button, ttk.Entry tercih edilir.
Yerleşim: pack, grid, place
| Yöntem | Mantık | Ne zaman? |
|---|---|---|
pack() | Widget'ları üstten alta (ya da yan yana) dizer | Basit, tek sütunlu düzen |
grid(row=, column=) | Satır/sütun tablosu; columnspan, sticky | Formlar, hesap makinesi tuşları |
place(x=, y=) | Piksel koordinatı | Nadiren; pencere boyutu değişince bozulur |
pack, bazılarını grid ile yerleştirirsen tkinter ikisini uzlaştırmaya çalışırken donar ya da TclError verir. Farklı yöntem gerekiyorsa ayrı Frame'ler kullan.Grid, hesap makinesi gibi düzenlerde etiket listesinden satır/sütun hesaplamayı gerektirir; bu tamamen sıradan Python'dur:
tuslar = ["7", "8", "9", "/", "4", "5", "6", "*", "1", "2", "3", "-", "0", ".", "=", "+"]
sutun_sayisi = 4
for i, tus in enumerate(tuslar):
satir, sutun = divmod(i, sutun_sayisi)
satir += 1 # 0. satır ekrana ayrıldı
print(f"{tus!r:5} -> grid(row={satir}, column={sutun})")
if i == 5:
print("...")
break
Olaylar ve callback'ler
GUI programı yukarıdan aşağı akmaz; olay güdümlüdür (event-driven). Sen fonksiyonları kaydedersin, kullanıcı tıklayınca ya da tuşa basınca tkinter onları çağırır. Bu fonksiyonlara callback denir. En sık hata, fonksiyonu kaydetmek yerine çağırmaktır:
| Yazım | Ne olur? |
|---|---|
command=hesapla | Fonksiyon nesnesi kaydedilir; tıklanınca çalışır. Doğru. |
command=hesapla() | Fonksiyon pencere kurulurken HEMEN bir kez çalışır, dönüş değeri (çoğu zaman None) kaydedilir; tıklama hiçbir şey yapmaz. |
command=lambda: ekle("7") | Argümanlı çağrı için sarmalayıcı; tıklanınca ekle("7") çalışır. |
entry.bind("<Return>", f) | Klavye/fare olayı; f(event) biçiminde bir olay nesnesiyle çağrılır. |
Bunu tkinter'sız, sahte bir düğme sınıfıyla burada görebilirsin:
class Dugme: # tk.Button'ın davranışını taklit eder
def __init__(self, metin, command=None):
self.metin = metin
self.command = command
def tikla(self):
if self.command is not None:
self.command()
else:
print(f"[{self.metin}] tıklandı ama bağlı komut yok")
def hesapla():
print("Hesaplandı!")
return None
dogru = Dugme("Hesapla", command=hesapla)
yanlis = Dugme("Hesapla", command=hesapla()) # burada bir kez çalışır!
print("--- kullanıcı tıklıyor ---")
dogru.tikla()
yanlis.tikla()
mainloop: olay döngüsü
mainloop() sonsuz bir döngüdür: işletim sisteminden olayları (tıklama, tuş, yeniden çizim, zamanlayıcı) bir kuyruktan alır ve ilgili callback'i çağırır. Bu yüzden callback'ler kısa olmalıdır; 10 saniyelik bir hesap yaparsan döngü o süre boyunca olay işleyemez ve pencere "yanıt vermiyor" olur. Mekanizma kabaca şöyledir:
from collections import deque
kuyruk = deque()
kayitli = {} # olay adı -> callback listesi
def bind(olay, fonk):
kayitli.setdefault(olay, []).append(fonk)
def mainloop():
while kuyruk: # gerçekte: pencere kapanana dek
olay, veri = kuyruk.popleft()
for fonk in kayitli.get(olay, []):
fonk(veri)
bind("tikla", lambda v: print("Sepete eklendi:", v))
bind("tus", lambda v: print("Basılan tuş:", v))
bind("tus", lambda v: print(" (arama kutusu güncellendi)"))
kuyruk.extend([("tus", "a"), ("tikla", "Termos"), ("kapat", None), ("tus", "b")])
mainloop()
print("Pencere kapandı")
Zamanlayıcı için tkinter pencere.after(1000, fonk) sunar: "1 saniye sonra fonk'u kuyruğa koy". Saat, ilerleme çubuğu, periyodik yenileme hep bununla yapılır; time.sleep ise döngüyü dondurur.
StringVar: widget ile değişkeni bağlamak
tk.StringVar, IntVar, DoubleVar, BooleanVar bir değeri tutar ve ona bağlı widget'ları otomatik günceller. Label(textvariable=sonuc) yazarsan sonuc.set("...") etiketi değiştirir; trace_add("write", f) ile değer her yazıldığında bir fonksiyon çalıştırabilirsin (canlı doğrulama). Bu, gözlemci (observer) kalıbıdır:
class Degisken: # tk.StringVar'ın sade taklidi
def __init__(self, deger=""):
self._deger = deger
self._izleyiciler = []
def get(self):
return self._deger
def set(self, yeni):
self._deger = yeni
for f in self._izleyiciler:
f(yeni)
def trace_add(self, f):
self._izleyiciler.append(f)
tutar = Degisken()
tutar.trace_add(lambda v: print("Etiket:", v or "(boş)"))
tutar.trace_add(lambda v: print("Geçerli mi?", v.replace(",", ".", 1).replace(".", "", 1).isdigit()))
tutar.set("250,50")
tutar.set("abc")
messagebox ve bahşiş hesaplayıcı
tkinter.messagebox hazır diyalog pencereleri sunar: showinfo, showwarning, showerror, askyesno. Şimdi küçük ama tam bir uygulama yazalım. Önce mantık (model): saf Python, tarayıcıda çalışır.
def bahsis_hesapla(tutar, yuzde, kisi):
if tutar <= 0 or not 0 <= yuzde <= 100 or kisi < 1:
raise ValueError("Geçersiz girdi")
bahsis = round(tutar * yuzde / 100, 2)
toplam = round(tutar + bahsis, 2)
return {"bahsis": bahsis, "toplam": toplam, "kisi_basi": round(toplam / kisi, 2)}
print(bahsis_hesapla(1250.0, 10, 4))
print(bahsis_hesapla(480, 15, 3))
try:
bahsis_hesapla(100, 120, 2)
except ValueError as e:
print("Hata:", e)
Sonra arayüz: yalnız girdiyi okur, modeli çağırır, sonucu gösterir.
import tkinter as tk
from tkinter import ttk, messagebox
from bahsis_model import bahsis_hesapla # yukarıdaki fonksiyon
pencere = tk.Tk()
pencere.title("Bahşiş Hesaplayıcı")
tutar, yuzde, kisi, sonuc = tk.StringVar(), tk.StringVar(value="10"), tk.StringVar(value="1"), tk.StringVar()
for satir, (etiket, degisken) in enumerate([("Tutar (TL)", tutar), ("Bahşiş %", yuzde), ("Kişi", kisi)]):
ttk.Label(pencere, text=etiket).grid(row=satir, column=0, sticky="w", padx=8, pady=4)
ttk.Entry(pencere, textvariable=degisken, width=12).grid(row=satir, column=1, padx=8)
def hesapla(event=None):
try:
s = bahsis_hesapla(float(tutar.get().replace(",", ".")), float(yuzde.get()), int(kisi.get()))
except ValueError:
messagebox.showerror("Hata", "Lütfen geçerli sayılar girin.")
return
sonuc.set(f"Bahşiş {s['bahsis']:.2f} | Toplam {s['toplam']:.2f} | Kişi başı {s['kisi_basi']:.2f}")
ttk.Button(pencere, text="Hesapla", command=hesapla).grid(row=3, column=0, columnspan=2, pady=6)
ttk.Label(pencere, textvariable=sonuc).grid(row=4, column=0, columnspan=2, pady=4)
pencere.bind("<Return>", hesapla) # Enter tuşu da hesaplasın
pencere.mainloop()
bahsis_model.py, arayüzü bahsis_app.py olarak aynı klasöre kaydet ve python bahsis_app.py çalıştır. hesapla(event=None) imzası sayesinde aynı fonksiyon hem düğmeden (argümansız) hem Enter tuşundan (olay nesnesiyle) çağrılabilir.MVC: mantığı arayüzden ayırmak
Model–View–Controller (Model–Görünüm–Denetleyici) bu ayrımı adlandırır. Model iş kuralıdır (hesap, doğrulama, veri). View pencere ve widget'lardır, kural içermez. Controller görünümden girdiyi alır, modeli çağırır, sonucu görünüme yazar. Controller'ı, get()/set() sunan herhangi bir nesneyle çalışacak biçimde yazarsan onu gerçek pencere olmadan, sahte bir görünümle test edebilirsin. Bu dersin mini projesi tam olarak bu.
Hesap makinesi bir durum makinesidir
Hesap makinesinin ekranı, o ana kadar basılan tuşlara bağlı bir durumdur: ekrandaki sayı, bekleyen işlem, biriken değer ve "yeni sayı mı başlıyor?" bayrağı. Her tuş bu durumu değiştirir. Arayüzdeki 16 düğmenin her biri yalnızca makine.bas("7") çağırır ve ekran'ı etikete yazar.
class MiniHesap:
def __init__(self):
self.ekran, self.birikim, self.islem, self.yeni = "0", None, None, True
def bas(self, tus):
if tus.isdigit():
self.ekran = tus if self.yeni or self.ekran == "0" else self.ekran + tus
self.yeni = False
elif tus in "+-":
self.esittir()
self.birikim, self.islem, self.yeni = float(self.ekran), tus, True
elif tus == "=":
self.esittir()
def esittir(self):
if self.islem and not self.yeni:
b, s = self.birikim, float(self.ekran)
sonuc = b + s if self.islem == "+" else b - s
self.ekran = str(int(sonuc)) if sonuc == int(sonuc) else str(sonuc)
self.islem, self.yeni = None, True
m = MiniHesap()
for tus in "12+30-5=":
m.bas(tus)
print(f"{tus} -> ekran {m.ekran}")
Döngüde düğme oluşturma tuzağı
Tuşları döngüyle oluştururken command=lambda: ekle(tus) yazarsan bütün düğmeler son tuşu ekler. Lambda tus'un değerini değil adını hatırlar; tıklandığında döngü çoktan bitmiş ve tus son değerde kalmıştır (geç bağlama, late binding). Çözüm: değeri varsayılan argümanla dondurmak.
ekran = []
yanlis = [lambda: ekran.append(t) for t in "123"]
dogru = [lambda t=t: ekran.append(t) for t in "123"]
for f in yanlis:
f()
print("yanlış:", "".join(ekran))
ekran.clear()
for f in dogru:
f()
print("doğru :", "".join(ekran))
Alternatifler
| Araç | Güçlü yanı | Dikkat | Kurulum |
|---|---|---|---|
| tkinter (+ttk) | Python ile gelir, hafif, öğrenmesi kolay | Görünüm sade; karmaşık arayüzde zahmetli | Yok |
| customtkinter | tkinter üstünde modern, koyu tema | Topluluk projesi | pip install customtkinter |
| PySide6 (Qt) | Profesyonel, zengin widget, Qt Designer | Büyük paket; öğrenme eğrisi | pip install PySide6 |
| PyQt6 | PySide6 ile benzer API | GPL/ticari lisans; kapalı kaynakta dikkat | pip install PyQt6 |
| Flet | Flutter tabanlı; masaüstü, web ve mobil tek kod | Görece yeni; sürüm değişiklikleri | pip install flet |
| Kivy | Dokunmatik ve mobil odaklı | Kendine özgü görünüm | pip install kivy |
Uygulamayı Python kurulu olmayan bir bilgisayara dağıtmak için pip install pyinstaller ve pyinstaller --onefile --windowed bahsis_app.py ile tek dosyalık çalıştırılabilir üretebilirsin.
Uzun işlerde donmayan arayüz
Rapor oluşturmak gibi uzun bir işi callback içinde yaparsan pencere donar. Çözüm: işi threading.Thread ile arka planda çalıştırmak ve sonucu bir queue.Queue'ya koymak; arayüz ise pencere.after(100, kontrol_et) ile kuyruğu periyodik olarak yoklar. Altın kural: widget'lara yalnız ana iş parçacığı dokunur; arka plandaki thread'den label.config(...) çağırmak rastgele çökmelere yol açar.
Olay nesnesi ve "break"
bind ile bağlanan fonksiyon bir event alır: event.char, event.keysym, event.x, event.widget. Fonksiyon "break" döndürürse olayın diğer işleyicilere (ör. Entry'nin harfi yazması) gitmesi durur; yalnız rakam kabul eden bir kutu böyle yapılır.
Test edilebilir GUI
Controller'ı sahte görünümle test etmek ilk adımdır. Gerçek widget testleri için pytest-qt (Qt) ya da tkinter'da widget.event_generate kullanılabilir, ama bunlar yavaştır ve ekran gerektirir. Kuralların yüzde doksanını model ve controller testleri kapsamalı.