Ana içeriğe geç
Kesit Python
Orta · Otomasyon, web ve araçlar

Masaüstü arayüz: tkinter ve MVC

tkinter ile pencere, widget, grid/pack yerleşimi, olay ve callback, StringVar ve messagebox kullanmayı; bahşiş hesaplayıcı ve hesap makinesi örnekleriyle mantığı arayüzden ayıran MVC düzenini ve tkinter alternatiflerini öğrenirsin.

Uygulamada aç, 19 alıştırmayı çözÜcretsiz, hesap gerekmez, kurulum yok.

Bu sayfa dersin okuma sürümüdür. Metin bir kutuyu çalıştırmanı ya da adıma geçmeni söylüyorsa dersi uygulamada aç.

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.

tkinter tarayıcıda çalışmazBu sayfadaki Python motoru tarayıcıda çalışır ve pencere açamaz. Arayüz kodlarını gri kutularda (çalıştır düğmesi olmadan) gösteriyoruz; bunları kendi bilgisayarında dene. Alıştırmalar ise arayüzden bağımsız mantık katmanını yazdırır ve burada çalışır.

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
Kendi bilgisayarında deneKodu 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

WidgetNe işe yarar?Sık kullanılan
LabelMetin ya da resim gösterirtext=, textvariable=, config()
EntryTek satır girdi kutusuget(), delete(0, "end"), insert()
ButtonTıklanınca fonksiyon çağırırcommand=
ListboxSeçilebilir satır listesiinsert("end", x), curselection(), delete(i)
Checkbutton, RadiobuttonSeçeneklervariable= (BooleanVar/StringVar)
FrameWidget'ları gruplayan kapİç içe yerleşim
ttk.ComboboxAçılır listevalues=, 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öntemMantıkNe zaman?
pack()Widget'ları üstten alta (ya da yan yana) dizerBasit, tek sütunlu düzen
grid(row=, column=)Satır/sütun tablosu; columnspan, stickyFormlar, hesap makinesi tuşları
place(x=, y=)Piksel koordinatıNadiren; pencere boyutu değişince bozulur
Aynı kapta pack ve grid karıştırmaBir pencere ya da Frame içinde bazı widget'ları 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ımNe olur?
command=hesaplaFonksiyon 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()
Kendi bilgisayarında deneMantığı 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ıDikkatKurulum
tkinter (+ttk)Python ile gelir, hafif, öğrenmesi kolayGörünüm sade; karmaşık arayüzde zahmetliYok
customtkintertkinter üstünde modern, koyu temaTopluluk projesipip install customtkinter
PySide6 (Qt)Profesyonel, zengin widget, Qt DesignerBüyük paket; öğrenme eğrisipip install PySide6
PyQt6PySide6 ile benzer APIGPL/ticari lisans; kapalı kaynakta dikkatpip install PyQt6
FletFlutter tabanlı; masaüstü, web ve mobil tek kodGörece yeni; sürüm değişiklikleripip install flet
KivyDokunmatik ve mobil odaklıKendine özgü görünümpip 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.

Gerçek işteŞirket içinde masaüstü araçlar genellikle bir uzmanın tekrar eden işini otomatikleştirmek için doğar: depoda etiket basma, muhasebede dosya birleştirme, İK'da izin hesabı. Bu araçların ömrü çoğu zaman beklenenden uzun olur ve bir gün web'e taşınması istenir. Mantığı arayüzden ayırdıysan taşıma, yalnız görünüm katmanını yeniden yazmaktır; ayırmadıysan baştan yazmaktır. Ayrıca ayrık mantık birim testiyle güvenceye alınır; GUI'yi elle tıklayarak test etmek ise yavaş ve güvenilmezdir.
Uzmanlaş

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ı.

Alıştırmalara geç (19)