Lewati ke isi

Event-Driven Programming dalam GUI Tkinter

Bagian 1: Konsep Dasar Event-Driven Programming

Event-driven programming adalah paradigma pemrograman yang sangat berbeda dari pemrograman prosedural yang biasa kita pelajari. Dalam paradigma ini, program tidak berjalan secara linear dari atas ke bawah, melainkan menunggu dan merespons kejadian (events) yang terjadi.

1.1 Memahami Perbedaan Paradigma

Dalam pemrograman prosedural tradisional, kita menulis kode yang dieksekusi secara berurutan. Program memiliki titik awal yang jelas, menjalankan instruksi satu per satu, dan berakhir pada titik tertentu. Pengguna harus mengikuti alur yang telah ditentukan programmer.

Sebaliknya, dalam event-driven programming, program berjalan dalam sebuah loop tak terbatas yang disebut "event loop". Loop ini terus mendengarkan berbagai kejadian yang mungkin terjadi, seperti klik mouse, penekanan keyboard, atau perubahan nilai input. Ketika event terjadi, program akan menjalankan fungsi yang sesuai dengan event tersebut.

1.2 Komponen Utama Event-Driven Programming

Ada tiga komponen fundamental yang harus dipahami:

Event (Kejadian): Ini adalah aksi atau kejadian yang terjadi dalam aplikasi. Event bisa berupa interaksi pengguna seperti klik tombol, pengetikan teks, atau pergerakan mouse. Event juga bisa berupa kejadian sistem seperti timer yang berakhir atau perubahan status aplikasi.

Event Handler (Penangani Event): Ini adalah fungsi atau method yang akan dijalankan ketika event tertentu terjadi. Event handler berisi logika bisnis yang menentukan bagaimana aplikasi merespons event tersebut.

Event Loop (Loop Event): Ini adalah mekanisme yang terus berjalan untuk mendeteksi dan memproses event yang terjadi. Dalam Tkinter, event loop dijalankan dengan method mainloop().

1.3 Praktikum 1: Membandingkan Kedua Paradigma

Mari kita buat contoh sederhana untuk memahami perbedaan antara kedua paradigma ini dengan membuat aplikasi kalkulator sederhana.

Praktikum 1.1: Simulasi Pendekatan Prosedural

Buat file baru bernama kalkulator_demo.py dan ketik kode berikut:

# Simulasi pendekatan prosedural
def kalkulator_prosedural():
    print("=== KALKULATOR PROSEDURAL ===")
    print("Program berjalan berurutan, user harus mengikuti alur")

    while True:
        try:
            angka1 = float(input("Masukkan angka pertama: "))
            break
        except ValueError:
            print("Input harus berupa angka!")

    while True:
        operator = input("Masukkan operator (+, -, *, /): ")
        if operator in ['+', '-', '*', '/']:
            break
        print("Operator tidak valid!")

    while True:
        try:
            angka2 = float(input("Masukkan angka kedua: "))
            break
        except ValueError:
            print("Input harus berupa angka!")

    if operator == '+':
        hasil = angka1 + angka2
    elif operator == '-':
        hasil = angka1 - angka2
    elif operator == '*':
        hasil = angka1 * angka2
    elif operator == '/':
        if angka2 != 0:
            hasil = angka1 / angka2
        else:
            print("Error: Pembagian dengan nol!")
            return

    print(f"Hasil: {angka1} {operator} {angka2} = {hasil}")
    print("Program selesai")

# Uncomment baris berikut untuk menjalankan demo prosedural
# kalkulator_prosedural()

Tugas: Jalankan fungsi ini dan perhatikan bagaimana program memaksa user untuk mengikuti alur yang sudah ditentukan. User tidak bisa melompat ke langkah lain atau mengubah input sebelumnya tanpa mengulangi dari awal.

Praktikum 1.2: Implementasi Event-Driven dengan Tkinter

Sekarang mari kita buat kalkulator yang sama menggunakan pendekatan event-driven. Tambahkan kode berikut di file yang sama:

import tkinter as tk
from tkinter import messagebox

class KalkulatorEventDriven:
    def __init__(self):
        self.window = tk.Tk()
        self.window.title("Kalkulator Event-Driven")
        self.window.geometry("300x400")
        self.window.configure(bg="lightgray")

        # Variabel untuk menyimpan state
        self.current_input = ""
        self.operator = ""
        self.first_number = 0

        self.buat_interface()

    def buat_interface(self):
        # Display untuk menampilkan angka
        self.display = tk.Entry(
            self.window, 
            font=("Arial", 16), 
            justify="right",
            state="readonly",
            bg="white"
        )
        self.display.pack(fill=tk.X, padx=10, pady=10)

        # Frame untuk tombol
        button_frame = tk.Frame(self.window, bg="lightgray")
        button_frame.pack(fill=tk.BOTH, expand=True, padx=10, pady=5)

Tugas: Jalankan kode ini dan perhatikan bahwa jendela muncul dengan display, meskipun belum ada tombol.

Praktikum 1.3: Menambahkan Tombol Angka

Lanjutkan dengan menambahkan tombol-tombol angka:

        # Tombol angka (0-9)
        for i in range(3):
            for j in range(3):
                angka = i * 3 + j + 1
                btn = tk.Button(
                    button_frame,
                    text=str(angka),
                    font=("Arial", 14),
                    width=5,
                    height=2,
                    command=lambda n=angka: self.input_angka(n)
                )
                btn.grid(row=i, column=j, padx=2, pady=2)

        # Tombol 0
        btn_0 = tk.Button(
            button_frame,
            text="0",
            font=("Arial", 14),
            width=5,
            height=2,
            command=lambda: self.input_angka(0)
        )
        btn_0.grid(row=3, column=1, padx=2, pady=2)

Sekarang tambahkan method untuk menangani input angka:

    def input_angka(self, angka):
        """Event handler untuk input angka"""
        self.current_input += str(angka)
        self.update_display()

    def update_display(self):
        """Method untuk memperbarui tampilan display"""
        self.display.config(state="normal")
        self.display.delete(0, tk.END)
        self.display.insert(0, self.current_input)
        self.display.config(state="readonly")

Tugas: Jalankan aplikasi dan coba klik tombol angka. Perhatikan bagaimana angka muncul di display setiap kali tombol diklik.

Praktikum 1.4: Menambahkan Tombol Operator

Tambahkan tombol-tombol operator di sebelah kanan tombol angka:

        # Tombol operator
        operators = ['+', '-', '*', '/']
        for i, op in enumerate(operators):
            btn_op = tk.Button(
                button_frame,
                text=op,
                font=("Arial", 14),
                width=5,
                height=2,
                bg="orange",
                command=lambda o=op: self.input_operator(o)
            )
            btn_op.grid(row=i, column=3, padx=2, pady=2)

        # Tombol sama dengan
        btn_equals = tk.Button(
            button_frame,
            text="=",
            font=("Arial", 14),
            width=5,
            height=2,
            bg="lightblue",
            command=self.hitung_hasil
        )
        btn_equals.grid(row=3, column=2, padx=2, pady=2)

        # Tombol clear
        btn_clear = tk.Button(
            button_frame,
            text="C",
            font=("Arial", 14),
            width=5,
            height=2,
            bg="red",
            fg="white",
            command=self.clear_all
        )
        btn_clear.grid(row=3, column=0, padx=2, pady=2)

Tambahkan method untuk menangani operator:

    def input_operator(self, op):
        """Event handler untuk input operator"""
        if self.current_input:
            self.first_number = float(self.current_input)
            self.operator = op
            self.current_input = ""
            self.update_display()

    def hitung_hasil(self):
        """Event handler untuk menghitung hasil"""
        if self.current_input and self.operator:
            try:
                second_number = float(self.current_input)

                if self.operator == '+':
                    result = self.first_number + second_number
                elif self.operator == '-':
                    result = self.first_number - second_number
                elif self.operator == '*':
                    result = self.first_number * second_number
                elif self.operator == '/':
                    if second_number != 0:
                        result = self.first_number / second_number
                    else:
                        messagebox.showerror("Error", "Pembagian dengan nol!")
                        return

                self.current_input = str(result)
                self.operator = ""
                self.first_number = 0
                self.update_display()

            except ValueError:
                messagebox.showerror("Error", "Input tidak valid!")

    def clear_all(self):
        """Event handler untuk clear semua"""
        self.current_input = ""
        self.operator = ""
        self.first_number = 0
        self.update_display()

    def jalankan(self):
        """Method untuk menjalankan aplikasi"""
        self.window.mainloop()

# Untuk menjalankan kalkulator event-driven
if __name__ == "__main__":
    kalkulator = KalkulatorEventDriven()
    kalkulator.jalankan()

Tugas: Jalankan aplikasi kalkulator lengkap. Coba lakukan beberapa operasi matematika dan perhatikan bagaimana setiap klik tombol langsung memberikan respons. Bandingkan dengan versi prosedural - di sini user bebas mengklik tombol apa saja kapan saja.


Bagian 2: Event Handling dalam Tkinter

Event handling adalah jantung dari aplikasi GUI. Dalam bagian ini, kita akan mempelajari berbagai jenis event dan cara menanganinya dengan efektif.

2.1 Jenis-Jenis Event

Tkinter mendukung berbagai macam event yang dapat kita tangani. Event-event ini dibagi menjadi beberapa kategori:

Mouse Events: Event yang berkaitan dengan aktivitas mouse seperti klik, double-click, drag, dan pergerakan mouse.

Keyboard Events: Event yang berkaitan dengan penekanan tombol keyboard, baik tombol karakter maupun tombol khusus.

Window Events: Event yang berkaitan dengan jendela aplikasi seperti resize, minimize, maximize, dan close.

Widget Events: Event yang spesifik untuk widget tertentu seperti perubahan nilai pada Entry atau selection pada Listbox.

2.2 Method Binding Event

Ada dua cara utama untuk menangani event dalam Tkinter:

Command Parameter: Cara paling sederhana, digunakan untuk event default widget (biasanya klik untuk Button).

Bind Method: Cara yang lebih fleksibel, memungkinkan kita menangani berbagai jenis event pada widget apa pun.

2.3 Praktikum 2: Aplikasi Paint Sederhana

Mari kita buat aplikasi paint sederhana untuk memahami berbagai jenis event handling.

Praktikum 2.1: Setup Aplikasi Paint

Buat file baru bernama paint_app.py:

import tkinter as tk
from tkinter import colorchooser, messagebox, filedialog

class PaintApp:
    def __init__(self):
        self.window = tk.Tk()
        self.window.title("Paint App - Event Handling Demo")
        self.window.geometry("800x600")

        # Variabel untuk painting
        self.last_x = None
        self.last_y = None
        self.pen_color = "black"
        self.pen_size = 2
        self.is_drawing = False

        self.buat_interface()
        self.bind_events()

    def buat_interface(self):
        # Frame untuk toolbar
        toolbar = tk.Frame(self.window, bg="lightgray", height=50)
        toolbar.pack(fill=tk.X, side=tk.TOP)
        toolbar.pack_propagate(False)

        # Canvas untuk menggambar
        self.canvas = tk.Canvas(
            self.window, 
            bg="white", 
            cursor="pencil"
        )
        self.canvas.pack(fill=tk.BOTH, expand=True)

Tugas: Jalankan kode ini dan pastikan jendela dengan toolbar dan canvas putih muncul.

Praktikum 2.2: Menambahkan Toolbar

Tambahkan tombol-tombol di toolbar:

        # Tombol pilih warna
        btn_color = tk.Button(
            toolbar,
            text="Pilih Warna",
            command=self.pilih_warna,
            bg="lightblue"
        )
        btn_color.pack(side=tk.LEFT, padx=5, pady=5)

        # Label dan slider untuk ukuran pen
        tk.Label(toolbar, text="Ukuran:", bg="lightgray").pack(side=tk.LEFT, padx=5)

        self.size_var = tk.IntVar(value=2)
        size_scale = tk.Scale(
            toolbar,
            from_=1,
            to=10,
            orient=tk.HORIZONTAL,
            variable=self.size_var,
            command=self.ubah_ukuran
        )
        size_scale.pack(side=tk.LEFT, padx=5)

        # Tombol clear
        btn_clear = tk.Button(
            toolbar,
            text="Clear",
            command=self.clear_canvas,
            bg="red",
            fg="white"
        )
        btn_clear.pack(side=tk.LEFT, padx=5, pady=5)

        # Label info
        self.info_label = tk.Label(
            toolbar,
            text=f"Warna: {self.pen_color} | Ukuran: {self.pen_size}",
            bg="lightgray"
        )
        self.info_label.pack(side=tk.RIGHT, padx=10)

Tambahkan method untuk menangani toolbar:

    def pilih_warna(self):
        """Event handler untuk memilih warna"""
        color = colorchooser.askcolor(title="Pilih Warna Pen")
        if color[1]:  # Jika user tidak cancel
            self.pen_color = color[1]
            self.update_info()

    def ubah_ukuran(self, value):
        """Event handler untuk mengubah ukuran pen"""
        self.pen_size = int(value)
        self.update_info()

    def clear_canvas(self):
        """Event handler untuk membersihkan canvas"""
        if messagebox.askyesno("Konfirmasi", "Hapus semua gambar?"):
            self.canvas.delete("all")

    def update_info(self):
        """Method untuk update info di toolbar"""
        self.info_label.config(
            text=f"Warna: {self.pen_color} | Ukuran: {self.pen_size}"
        )

Tugas: Jalankan aplikasi dan test semua tombol di toolbar. Pastikan color picker, slider, dan clear button berfungsi.

Praktikum 2.3: Mouse Event Handling

Sekarang kita akan menambahkan kemampuan menggambar dengan mouse:

    def bind_events(self):
        """Method untuk binding semua events"""
        # Mouse events untuk menggambar
        self.canvas.bind("<Button-1>", self.start_draw)
        self.canvas.bind("<B1-Motion>", self.draw)
        self.canvas.bind("<ButtonRelease-1>", self.stop_draw)

        # Mouse events untuk info posisi
        self.canvas.bind("<Motion>", self.show_position)

        # Keyboard events
        self.window.bind("<Control-s>", self.save_image)
        self.window.bind("<Control-o>", self.open_image)
        self.window.bind("<Control-n>", self.new_canvas)

        # Window events
        self.window.protocol("WM_DELETE_WINDOW", self.on_closing)

Tambahkan method untuk menangani mouse events:

    def start_draw(self, event):
        """Event handler saat mulai menggambar (mouse press)"""
        self.last_x = event.x
        self.last_y = event.y
        self.is_drawing = True

    def draw(self, event):
        """Event handler saat menggambar (mouse drag)"""
        if self.is_drawing and self.last_x and self.last_y:
            # Gambar garis dari posisi terakhir ke posisi sekarang
            self.canvas.create_line(
                self.last_x, self.last_y,
                event.x, event.y,
                width=self.pen_size,
                fill=self.pen_color,
                capstyle=tk.ROUND,
                smooth=tk.TRUE
            )
            self.last_x = event.x
            self.last_y = event.y

    def stop_draw(self, event):
        """Event handler saat berhenti menggambar (mouse release)"""
        self.is_drawing = False
        self.last_x = None
        self.last_y = None

    def show_position(self, event):
        """Event handler untuk menampilkan posisi mouse"""
        if hasattr(self, 'pos_label'):
            self.pos_label.destroy()

        self.pos_label = tk.Label(
            self.window,
            text=f"Posisi: ({event.x}, {event.y})",
            bg="yellow"
        )
        self.pos_label.place(x=event.x + 10, y=event.y + 10)

        # Hapus label setelah 1 detik
        self.window.after(1000, lambda: self.pos_label.destroy() if hasattr(self, 'pos_label') else None)

Tugas: Jalankan aplikasi dan coba menggambar dengan mouse. Perhatikan bagaimana garis terbentuk saat Anda drag mouse, dan posisi mouse ditampilkan saat bergerak.

Praktikum 2.4: Keyboard Event Handling

Tambahkan method untuk menangani keyboard shortcuts:

    def save_image(self, event=None):
        """Event handler untuk save (Ctrl+S)"""
        filename = filedialog.asksaveasfilename(
            defaultextension=".ps",
            filetypes=[("PostScript files", "*.ps"), ("All files", "*.*")]
        )
        if filename:
            self.canvas.postscript(file=filename)
            messagebox.showinfo("Info", f"Gambar disimpan sebagai {filename}")

    def open_image(self, event=None):
        """Event handler untuk open (Ctrl+O)"""
        messagebox.showinfo("Info", "Fitur buka gambar belum diimplementasi")

    def new_canvas(self, event=None):
        """Event handler untuk canvas baru (Ctrl+N)"""
        if messagebox.askyesno("Canvas Baru", "Buat canvas baru? Gambar saat ini akan hilang."):
            self.canvas.delete("all")

    def on_closing(self):
        """Event handler saat jendela akan ditutup"""
        if messagebox.askokcancel("Keluar", "Yakin ingin keluar? Gambar yang belum disimpan akan hilang."):
            self.window.destroy()

    def jalankan(self):
        """Method untuk menjalankan aplikasi"""
        self.window.mainloop()

# Untuk menjalankan aplikasi
if __name__ == "__main__":
    app = PaintApp()
    app.jalankan()

Tugas: Jalankan aplikasi paint lengkap. Coba semua fitur: menggambar, ganti warna, ubah ukuran, clear, dan keyboard shortcuts (Ctrl+S, Ctrl+N). Perhatikan bagaimana setiap event memberikan respons yang berbeda.


Bagian 3: Widget Interaktif dan State Management

Dalam bagian ini, kita akan mempelajari cara membuat widget yang lebih interaktif dan mengelola state aplikasi dengan baik.

3.1 Variable Control dalam Tkinter

Tkinter menyediakan beberapa jenis variabel kontrol yang memungkinkan kita untuk menghubungkan data dengan widget secara otomatis:

StringVar: Untuk menyimpan dan mengelola data string. Biasanya digunakan dengan Entry, Label, atau Radiobutton.

IntVar: Untuk menyimpan dan mengelola data integer. Sering digunakan dengan Checkbutton, Radiobutton, atau Scale.

DoubleVar: Untuk menyimpan data floating point. Berguna untuk Scale atau Entry yang menerima angka desimal.

BooleanVar: Untuk menyimpan data boolean (True/False). Ideal untuk Checkbutton.

3.2 Trace Method untuk Real-time Updates

Method trace_add() memungkinkan kita untuk "mengamati" perubahan pada variabel kontrol dan menjalankan fungsi callback setiap kali variabel berubah. Ini sangat berguna untuk validasi real-time, update tampilan dinamis, atau sinkronisasi data.

3.3 Praktikum 3: Aplikasi Konverter Suhu

Mari kita buat aplikasi konverter suhu yang mendemonstrasikan penggunaan variabel kontrol dan real-time updates.

Praktikum 3.1: Setup Aplikasi Konverter

Buat file baru bernama konverter_suhu.py:

import tkinter as tk
from tkinter import ttk
import math

class KonverterSuhu:
    def __init__(self):
        self.window = tk.Tk()
        self.window.title("Konverter Suhu - State Management Demo")
        self.window.geometry("500x400")
        self.window.configure(bg="lightblue")

        # Variabel kontrol
        self.celsius_var = tk.DoubleVar()
        self.fahrenheit_var = tk.DoubleVar()
        self.kelvin_var = tk.DoubleVar()
        self.rankine_var = tk.DoubleVar()

        # Flag untuk mencegah infinite loop saat update
        self.updating = False

        self.buat_interface()
        self.setup_traces()

    def buat_interface(self):
        # Judul
        title_label = tk.Label(
            self.window,
            text="KONVERTER SUHU UNIVERSAL",
            font=("Arial", 16, "bold"),
            bg="lightblue"
        )
        title_label.pack(pady=20)

        # Frame utama
        main_frame = tk.Frame(self.window, bg="lightblue")
        main_frame.pack(fill=tk.BOTH, expand=True, padx=20)

Tugas: Jalankan kode ini dan pastikan jendela dengan judul muncul.

Praktikum 3.2: Membuat Input Fields

Tambahkan input fields untuk setiap skala suhu:

        # Celsius
        celsius_frame = tk.Frame(main_frame, bg="white", relief=tk.RAISED, bd=2)
        celsius_frame.pack(fill=tk.X, pady=5)

        tk.Label(
            celsius_frame,
            text="Celsius (°C):",
            font=("Arial", 12, "bold"),
            bg="white",
            width=15,
            anchor="w"
        ).pack(side=tk.LEFT, padx=10, pady=10)

        self.celsius_entry = tk.Entry(
            celsius_frame,
            textvariable=self.celsius_var,
            font=("Arial", 12),
            width=20,
            justify="center"
        )
        self.celsius_entry.pack(side=tk.RIGHT, padx=10, pady=10)

        # Fahrenheit
        fahrenheit_frame = tk.Frame(main_frame, bg="white", relief=tk.RAISED, bd=2)
        fahrenheit_frame.pack(fill=tk.X, pady=5)

        tk.Label(
            fahrenheit_frame,
            text="Fahrenheit (°F):",
            font=("Arial", 12, "bold"),
            bg="white",
            width=15,
            anchor="w"
        ).pack(side=tk.LEFT, padx=10, pady=10)

        self.fahrenheit_entry = tk.Entry(
            fahrenheit_frame,
            textvariable=self.fahrenheit_var,
            font=("Arial", 12),
            width=20,
            justify="center"
        )
        self.fahrenheit_entry.pack(side=tk.RIGHT, padx=10, pady=10)

        # Kelvin
        kelvin_frame = tk.Frame(main_frame, bg="white", relief=tk.RAISED, bd=2)
        kelvin_frame.pack(fill=tk.X, pady=5)

        tk.Label(
            kelvin_frame,
            text="Kelvin (K):",
            font=("Arial", 12, "bold"),
            bg="white",
            width=15,
            anchor="w"
        ).pack(side=tk.LEFT, padx=10, pady=10)

        self.kelvin_entry = tk.Entry(
            kelvin_frame,
            textvariable=self.kelvin_var,
            font=("Arial", 12),
            width=20,
            justify="center"
        )
        self.kelvin_entry.pack(side=tk.RIGHT, padx=10, pady=10)

        # Rankine
        rankine_frame = tk.Frame(main_frame, bg="white", relief=tk.RAISED, bd=2)
        rankine_frame.pack(fill=tk.X, pady=5)

        tk.Label(
            rankine_frame,
            text="Rankine (°R):",
            font=("Arial", 12, "bold"),
            bg="white",
            width=15,
            anchor="w"
        ).pack(side=tk.LEFT, padx=10, pady=10)

        self.rankine_entry = tk.Entry(
            rankine_frame,
            textvariable=self.rankine_var,
            font=("Arial", 12),
            width=20,
            justify="center"
        )
        self.rankine_entry.pack(side=tk.RIGHT, padx=10, pady=10)

Tugas: Jalankan dan lihat empat input field untuk berbagai skala suhu.

Praktikum 3.3: Setup Trace untuk Real-time Conversion

Tambahkan method untuk mengatur trace:

    def setup_traces(self):
        """Setup trace untuk semua variabel"""
        self.celsius_var.trace_add("write", self.from_celsius)
        self.fahrenheit_var.trace_add("write", self.from_fahrenheit)
        self.kelvin_var.trace_add("write", self.from_kelvin)
        self.rankine_var.trace_add("write", self.from_rankine)

Tambahkan method untuk konversi dari Celsius:

    def from_celsius(self, *args):
        """Konversi dari Celsius ke skala lain"""
        if self.updating:
            return

        try:
            celsius = self.celsius_var.get()
            self.updating = True

            # Konversi ke Fahrenheit
            fahrenheit = (celsius * 9/5) + 32
            self.fahrenheit_var.set(round(fahrenheit, 2))

            # Konversi ke Kelvin
            kelvin = celsius + 273.15
            self.kelvin_var.set(round(kelvin, 2))

            # Konversi ke Rankine
            rankine = (celsius + 273.15) * 9/5
            self.rankine_var.set(round(rankine, 2))

            self.updating = False

        except tk.TclError:
            # Terjadi saat input tidak valid (kosong atau bukan angka)
            pass
        except Exception as e:
            self.updating = False

Tugas: Jalankan aplikasi dan coba ketik angka di field Celsius. Perhatikan bagaimana field lain otomatis terupdate.

Praktikum 3.4: Menambahkan Konversi dari Skala Lain

Tambahkan method konversi dari Fahrenheit:

    def from_fahrenheit(self, *args):
        """Konversi dari Fahrenheit ke skala lain"""
        if self.updating:
            return

        try:
            fahrenheit = self.fahrenheit_var.get()
            self.updating = True

            # Konversi ke Celsius
            celsius = (fahrenheit - 32) * 5/9
            self.celsius_var.set(round(celsius, 2))

            # Konversi ke Kelvin
            kelvin = celsius + 273.15
            self.kelvin_var.set(round(kelvin, 2))

            # Konversi ke Rankine
            rankine = fahrenheit + 459.67
            self.rankine_var.set(round(rankine, 2))

            self.updating = False

        except tk.TclError:
            pass
        except Exception as e:
            self.updating = False

    def from_kelvin(self, *args):
        """Konversi dari Kelvin ke skala lain"""
        if self.updating:
            return

        try:
            kelvin = self.kelvin_var.get()
            self.updating = True

            # Konversi ke Celsius
            celsius = kelvin - 273.15
            self.celsius_var.set(round(celsius, 2))

            # Konversi ke Fahrenheit
            fahrenheit = (celsius * 9/5) + 32
            self.fahrenheit_var.set(round(fahrenheit, 2))

            # Konversi ke Rankine
            rankine = kelvin * 9/5
            self.rankine_var.set(round(rankine, 2))

            self.updating = False

        except tk.TclError:
            pass
        except Exception as e:
            self.updating = False

    def from_rankine(self, *args):
        """Konversi dari Rankine ke skala lain"""
        if self.updating:
            return

        try:
            rankine = self.rankine_var.get()
            self.updating = True

            # Konversi ke Kelvin
            kelvin = rankine * 5/9
            self.kelvin_var.set(round(kelvin, 2))

            # Konversi ke Celsius
            celsius = kelvin - 273.15
            self.celsius_var.set(round(celsius, 2))

            # Konversi ke Fahrenheit
            fahrenheit = rankine - 459.67
            self.fahrenheit_var.set(round(fahrenheit, 2))

            self.updating = False

        except tk.TclError:
            pass
        except Exception as e:
            self.updating = False

Tugas: Test konversi dari semua skala suhu. Coba input di field mana pun dan lihat field lain terupdate otomatis.

Praktikum 3.5: Menambahkan Fitur Tambahan

Tambahkan tombol reset dan informasi tambahan:

        # Tombol reset
        btn_reset = tk.Button(
            main_frame,
            text="Reset Semua",
            font=("Arial", 12, "bold"),
            bg="red",
            fg="white",
            command=self.reset_all
        )
        btn_reset.pack(pady=20)

        # Frame untuk informasi tambahan
        info_frame = tk.Frame(main_frame, bg="lightyellow", relief=tk.GROOVE, bd=2)
        info_frame.pack(fill=tk.X, pady=10)

        tk.Label(
            info_frame,
            text="INFORMASI SUHU",
            font=("Arial", 12, "bold"),
            bg="lightyellow"
        ).pack(pady=5)

        self.info_label = tk.Label(
            info_frame,
            text="Masukkan nilai suhu di salah satu field",
            font=("Arial", 10),
            bg="lightyellow",
            justify=tk.LEFT
        )
        self.info_label.pack(pady=5)

Tambahkan method untuk reset dan update info:

    def reset_all(self):
        """Reset semua field"""
        self.updating = True
        self.celsius_var.set(0)
        self.fahrenheit_var.set(0)
        self.kelvin_var.set(0)
        self.rankine_var.set(0)
        self.updating = False
        self.update_info()

    def update_info(self):
        """Update informasi tambahan"""
        try:
            celsius = self.celsius_var.get()

            info_text = f"Suhu saat ini: {celsius}°C\n"

            if celsius == 0:
                info_text += "• Titik beku air (kondisi normal)"
            elif celsius == 100:
                info_text += "• Titik didih air (kondisi normal)"
            elif celsius == -273.15:
                info_text += "• Suhu absolut nol"
            elif celsius &lt; 0:
                info_text += "• Di bawah titik beku air"
            elif celsius > 100:
                info_text += "• Di atas titik didih air"
            else:
                info_text += "• Suhu normal"

            self.info_label.config(text=info_text)

        except:
            self.info_label.config(text="Masukkan nilai suhu yang valid")

Modifikasi semua method konversi untuk memanggil update_info() di akhir:

    # Tambahkan baris ini di akhir setiap method konversi (sebelum except)
    self.update_info()

Tambahkan method untuk menjalankan aplikasi:

    def jalankan(self):
        """Method untuk menjalankan aplikasi"""
        self.window.mainloop()

# Untuk menjalankan aplikasi
if __name__ == "__main__":
    app = KonverterSuhu()
    app.jalankan()

Tugas: Jalankan aplikasi konverter suhu lengkap. Test semua fitur dan perhatikan bagaimana informasi tambahan berubah sesuai dengan nilai suhu yang diinput.


Bagian 4: Timer dan Animasi

Dalam bagian ini, kita akan mempelajari cara membuat aplikasi yang dinamis dengan menggunakan timer dan animasi sederhana.

4.1 Method after() untuk Timer

Method after() adalah cara utama untuk membuat timer dalam Tkinter. Method ini memungkinkan kita untuk menjalankan fungsi setelah delay tertentu atau secara berulang. Sintaksnya adalah widget.after(delay_ms, function).

Timer sangat berguna untuk membuat animasi, update data secara berkala, atau membuat countdown. Berbeda dengan time.sleep() yang akan memblokir GUI, after() tidak mengganggu responsivitas aplikasi.

4.2 Animasi Sederhana dengan Canvas

Canvas adalah widget yang sangat powerful untuk membuat grafik dan animasi. Kita dapat menggambar berbagai bentuk, menggerakkan objek, dan membuat efek visual yang menarik.

4.3 Praktikum 4: Aplikasi Stopwatch dengan Animasi

Mari kita buat aplikasi stopwatch yang mendemonstrasikan penggunaan timer dan animasi.

Praktikum 4.1: Setup Aplikasi Stopwatch

Buat file baru bernama stopwatch_app.py:

import tkinter as tk
import math
import time

class StopwatchApp:
    def __init__(self):
        self.window = tk.Tk()
        self.window.title("Stopwatch dengan Animasi")
        self.window.geometry("600x500")
        self.window.configure(bg="black")

        # Variabel untuk stopwatch
        self.start_time = 0
        self.elapsed_time = 0
        self.is_running = False
        self.timer_job = None

        # Variabel untuk animasi
        self.animation_job = None
        self.rotation_angle = 0

        self.buat_interface()
        self.start_animation()

    def buat_interface(self):
        # Frame untuk display waktu
        time_frame = tk.Frame(self.window, bg="black")
        time_frame.pack(pady=20)

        # Label untuk menampilkan waktu
        self.time_label = tk.Label(
            time_frame,
            text="00:00:00",
            font=("Digital-7", 48, "bold"),
            fg="lime",
            bg="black"
        )
        self.time_label.pack()

        # Label untuk milidetik
        self.ms_label = tk.Label(
            time_frame,
            text="000",
            font=("Digital-7", 24),
            fg="yellow",
            bg="black"
        )
        self.ms_label.pack()

Tugas: Jalankan kode ini dan lihat display waktu dengan font digital.

Praktikum 4.2: Menambahkan Canvas untuk Animasi

Tambahkan canvas dengan animasi lingkaran berputar:

        # Canvas untuk animasi
        self.canvas = tk.Canvas(
            self.window,
            width=300,
            height=300,
            bg="black",
            highlightthickness=0
        )
        self.canvas.pack(pady=20)

        # Gambar lingkaran luar (static)
        self.canvas.create_oval(
            50, 50, 250, 250,
            outline="white",
            width=3,
            tags="outer_circle"
        )

        # Gambar titik-titik penanda waktu
        self.buat_penanda_waktu()

        # Jarum detik (akan beranimasi)
        self.jarum_detik = self.canvas.create_line(
            150, 150, 150, 70,
            fill="red",
            width=3,
            tags="second_hand"
        )

        # Titik tengah
        self.canvas.create_oval(
            145, 145, 155, 155,
            fill="white",
            outline="white"
        )

Tambahkan method untuk membuat penanda waktu:

    def buat_penanda_waktu(self):
        """Membuat penanda waktu di sekeliling lingkaran"""
        center_x, center_y = 150, 150
        radius = 90

        for i in range(60):
            angle = math.radians(i * 6 - 90)  # -90 untuk mulai dari atas

            if i % 5 == 0:  # Penanda jam (setiap 5 detik)
                inner_radius = radius - 15
                width = 3
                color = "white"
            else:  # Penanda detik
                inner_radius = radius - 8
                width = 1
                color = "gray"

            # Titik luar
            x1 = center_x + radius * math.cos(angle)
            y1 = center_y + radius * math.sin(angle)

            # Titik dalam
            x2 = center_x + inner_radius * math.cos(angle)
            y2 = center_y + inner_radius * math.sin(angle)

            self.canvas.create_line(
                x1, y1, x2, y2,
                fill=color,
                width=width
            )

Tugas: Jalankan dan lihat canvas dengan lingkaran dan penanda waktu seperti jam analog.

Praktikum 4.3: Menambahkan Kontrol Stopwatch

Tambahkan tombol-tombol kontrol:

        # Frame untuk tombol kontrol
        control_frame = tk.Frame(self.window, bg="black")
        control_frame.pack(pady=20)

        # Tombol Start/Stop
        self.start_stop_btn = tk.Button(
            control_frame,
            text="START",
            font=("Arial", 14, "bold"),
            bg="green",
            fg="white",
            width=10,
            command=self.toggle_stopwatch
        )
        self.start_stop_btn.pack(side=tk.LEFT, padx=10)

        # Tombol Reset
        self.reset_btn = tk.Button(
            control_frame,
            text="RESET",
            font=("Arial", 14, "bold"),
            bg="red",
            fg="white",
            width=10,
            command=self.reset_stopwatch
        )
        self.reset_btn.pack(side=tk.LEFT, padx=10)

        # Tombol Lap
        self.lap_btn = tk.Button(
            control_frame,
            text="LAP",
            font=("Arial", 14, "bold"),
            bg="blue",
            fg="white",
            width=10,
            command=self.record_lap,
            state=tk.DISABLED
        )
        self.lap_btn.pack(side=tk.LEFT, padx=10)

Tambahkan area untuk menampilkan lap times:

        # Frame untuk lap times
        lap_frame = tk.LabelFrame(
            self.window,
            text="Lap Times",
            font=("Arial", 12, "bold"),
            fg="white",
            bg="black"
        )
        lap_frame.pack(fill=tk.BOTH, expand=True, padx=20, pady=10)

        # Listbox untuk menampilkan lap times
        self.lap_listbox = tk.Listbox(
            lap_frame,
            font=("Courier", 11),
            bg="black",
            fg="lime",
            selectbackground="gray"
        )
        self.lap_listbox.pack(fill=tk.BOTH, expand=True, padx=5, pady=5)

        # Variabel untuk lap times
        self.lap_times = []
        self.lap_count = 0

Tugas: Jalankan dan lihat interface lengkap dengan tombol kontrol dan area lap times.

Praktikum 4.4: Implementasi Fungsi Stopwatch

Tambahkan method untuk menangani stopwatch:

    def toggle_stopwatch(self):
        """Toggle start/stop stopwatch"""
        if not self.is_running:
            self.start_stopwatch()
        else:
            self.stop_stopwatch()

    def start_stopwatch(self):
        """Mulai stopwatch"""
        self.is_running = True
        self.start_time = time.time() - self.elapsed_time

        # Update tampilan tombol
        self.start_stop_btn.config(text="STOP", bg="red")
        self.lap_btn.config(state=tk.NORMAL)

        # Mulai timer
        self.update_time()

    def stop_stopwatch(self):
        """Stop stopwatch"""
        self.is_running = False

        # Update tampilan tombol
        self.start_stop_btn.config(text="START", bg="green")
        self.lap_btn.config(state=tk.DISABLED)

        # Hentikan timer
        if self.timer_job:
            self.window.after_cancel(self.timer_job)

    def reset_stopwatch(self):
        """Reset stopwatch"""
        self.stop_stopwatch()
        self.elapsed_time = 0

        # Reset tampilan
        self.time_label.config(text="00:00:00")
        self.ms_label.config(text="000")

        # Reset lap times
        self.lap_times.clear()
        self.lap_count = 0
        self.lap_listbox.delete(0, tk.END)

        # Reset jarum detik
        self.update_second_hand(0)

    def record_lap(self):
        """Catat lap time"""
        if self.is_running:
            self.lap_count += 1
            current_time = self.elapsed_time

            # Hitung lap time (selisih dengan lap sebelumnya)
            if self.lap_times:
                lap_time = current_time - self.lap_times[-1][1]
            else:
                lap_time = current_time

            # Simpan lap time
            self.lap_times.append((self.lap_count, current_time, lap_time))

            # Format dan tampilkan
            total_formatted = self.format_time(current_time)
            lap_formatted = self.format_time(lap_time)

            lap_text = f"Lap {self.lap_count:2d}: {lap_formatted} (Total: {total_formatted})"
            self.lap_listbox.insert(tk.END, lap_text)

            # Scroll ke bawah
            self.lap_listbox.see(tk.END)

Praktikum 4.5: Update Timer dan Animasi

Tambahkan method untuk update waktu dan animasi:

    def update_time(self):
        """Update tampilan waktu"""
        if self.is_running:
            current_time = time.time()
            self.elapsed_time = current_time - self.start_time

            # Update tampilan waktu
            time_str = self.format_time(self.elapsed_time)
            self.time_label.config(text=time_str)

            # Update milidetik
            ms = int((self.elapsed_time % 1) * 1000)
            self.ms_label.config(text=f"{ms:03d}")

            # Update jarum detik
            seconds = self.elapsed_time % 60
            self.update_second_hand(seconds)

            # Schedule next update
            self.timer_job = self.window.after(10, self.update_time)

    def format_time(self, seconds):
        """Format waktu ke string HH:MM:SS"""
        hours = int(seconds // 3600)
        minutes = int((seconds % 3600) // 60)
        secs = int(seconds % 60)
        return f"{hours:02d}:{minutes:02d}:{secs:02d}"

    def update_second_hand(self, seconds):
        """Update posisi jarum detik"""
        # Hitung sudut (0 detik = atas, 15 detik = kanan, dst)
        angle = math.radians(seconds * 6 - 90)  # -90 untuk mulai dari atas

        center_x, center_y = 150, 150
        length = 70

        end_x = center_x + length * math.cos(angle)
        end_y = center_y + length * math.sin(angle)

        # Update koordinat jarum
        self.canvas.coords(
            self.jarum_detik,
            center_x, center_y,
            end_x, end_y
        )

    def start_animation(self):
        """Mulai animasi latar belakang"""
        self.animate_background()

    def animate_background(self):
        """Animasi latar belakang (opsional)"""
        # Rotasi sudut untuk efek visual
        self.rotation_angle = (self.rotation_angle + 1) % 360

        # Update warna border berdasarkan status
        if self.is_running:
            color = f"#{int(127 + 127 * math.sin(math.radians(self.rotation_angle * 4))):02x}0000"
        else:
            color = "white"

        self.canvas.itemconfig("outer_circle", outline=color)

        # Schedule next animation frame
        self.animation_job = self.window.after(50, self.animate_background)

    def jalankan(self):
        """Method untuk menjalankan aplikasi"""
        self.window.mainloop()

# Untuk menjalankan aplikasi
if __name__ == "__main__":
    app = StopwatchApp()
    app.jalankan()

Tugas: Jalankan aplikasi stopwatch lengkap. Test semua fitur: start/stop, reset, lap times, dan perhatikan animasi jarum detik serta efek visual pada border lingkaran.


Bagian 5: Validasi dan Error Handling

Dalam bagian ini, kita akan mempelajari cara membuat aplikasi yang robust dengan validasi input yang baik dan penanganan error yang tepat.

5.1 Jenis-Jenis Validasi

Validasi adalah proses memastikan bahwa data yang diinput pengguna sesuai dengan format dan kriteria yang diharapkan. Ada beberapa jenis validasi:

Validasi Format: Memastikan input sesuai format yang diharapkan (email, nomor telepon, tanggal).

Validasi Range: Memastikan nilai numerik berada dalam rentang yang valid.

Validasi Required: Memastikan field wajib tidak kosong.

Validasi Custom: Validasi khusus sesuai aturan bisnis aplikasi.

5.2 Real-time vs Submit-time Validation

Real-time Validation: Validasi yang terjadi saat pengguna mengetik, memberikan feedback langsung.

Submit-time Validation: Validasi yang terjadi saat form di-submit, biasanya lebih komprehensif.

5.3 Praktikum 5: Aplikasi Registrasi dengan Validasi Lengkap

Mari kita buat aplikasi registrasi yang mendemonstrasikan berbagai teknik validasi dan error handling.

Praktikum 5.1: Setup Aplikasi Registrasi

Buat file baru bernama registrasi_app.py:

import tkinter as tk
from tkinter import messagebox, ttk
import re
from datetime import datetime, date

class RegistrasiApp:
    def __init__(self):
        self.window = tk.Tk()
        self.window.title("Form Registrasi - Validasi Demo")
        self.window.geometry("600x700")
        self.window.configure(bg="lightgray")

        # Variabel kontrol
        self.nama_var = tk.StringVar()
        self.email_var = tk.StringVar()
        self.phone_var = tk.StringVar()
        self.password_var = tk.StringVar()
        self.confirm_password_var = tk.StringVar()
        self.age_var = tk.IntVar()
        self.gender_var = tk.StringVar(value="Pria")
        self.agree_var = tk.BooleanVar()

        # Dictionary untuk menyimpan status validasi
        self.validation_status = {
            'nama': False,
            'email': False,
            'phone': False,
            'password': False,
            'confirm_password': False,
            'age': False,
            'agree': False
        }

        self.buat_interface()
        self.setup_validation()

    def buat_interface(self):
        # Judul
        title_label = tk.Label(
            self.window,
            text="FORM REGISTRASI PENGGUNA",
            font=("Arial", 18, "bold"),
            bg="lightgray",
            fg="darkblue"
        )
        title_label.pack(pady=20)

        # Main frame dengan scrollbar
        main_frame = tk.Frame(self.window, bg="lightgray")
        main_frame.pack(fill=tk.BOTH, expand=True, padx=20)

Tugas: Jalankan kode ini dan pastikan jendela dengan judul muncul.

Praktikum 5.2: Membuat Input Fields dengan Validasi Visual

Tambahkan input fields dengan indikator validasi:

        # Nama Lengkap
        self.buat_input_field(
            main_frame, "Nama Lengkap:", self.nama_var, 
            "nama", "Minimal 3 karakter, hanya huruf dan spasi"
        )

        # Email
        self.buat_input_field(
            main_frame, "Email:", self.email_var,
            "email", "Format: user@domain.com"
        )

        # Nomor Telepon
        self.buat_input_field(
            main_frame, "Nomor Telepon:", self.phone_var,
            "phone", "Format: 08xxxxxxxxxx (10-13 digit)"
        )

        # Password
        self.buat_input_field(
            main_frame, "Password:", self.password_var,
            "password", "Minimal 8 karakter, kombinasi huruf dan angka", show="*"
        )

        # Konfirmasi Password
        self.buat_input_field(
            main_frame, "Konfirmasi Password:", self.confirm_password_var,
            "confirm_password", "Harus sama dengan password", show="*"
        )

Tambahkan method untuk membuat input field:

    def buat_input_field(self, parent, label_text, variable, field_name, hint_text, show=None):
        """Method untuk membuat input field dengan validasi visual"""
        # Frame untuk field
        field_frame = tk.Frame(parent, bg="lightgray")
        field_frame.pack(fill=tk.X, pady=5)

        # Label
        label = tk.Label(
            field_frame,
            text=label_text,
            font=("Arial", 11, "bold"),
            bg="lightgray",
            width=20,
            anchor="w"
        )
        label.pack(side=tk.LEFT)

        # Frame untuk entry dan indikator
        entry_frame = tk.Frame(field_frame, bg="lightgray")
        entry_frame.pack(side=tk.LEFT, fill=tk.X, expand=True)

        # Entry
        entry = tk.Entry(
            entry_frame,
            textvariable=variable,
            font=("Arial", 11),
            width=25,
            show=show
        )
        entry.pack(side=tk.LEFT, padx=5)

        # Indikator validasi
        indicator = tk.Label(
            entry_frame,
            text="●",
            font=("Arial", 16),
            fg="red",
            bg="lightgray"
        )
        indicator.pack(side=tk.LEFT, padx=5)

        # Hint text
        hint_label = tk.Label(
            parent,
            text=hint_text,
            font=("Arial", 9),
            fg="gray",
            bg="lightgray",
            anchor="w"
        )
        hint_label.pack(fill=tk.X, padx=25)

        # Simpan referensi untuk update nanti
        setattr(self, f"{field_name}_indicator", indicator)
        setattr(self, f"{field_name}_entry", entry)
        setattr(self, f"{field_name}_hint", hint_label)

Tugas: Jalankan dan lihat input fields dengan indikator merah dan hint text.

Praktikum 5.3: Menambahkan Field Umur dan Gender

Tambahkan field untuk umur dan gender:

        # Umur dengan Spinbox
        age_frame = tk.Frame(main_frame, bg="lightgray")
        age_frame.pack(fill=tk.X, pady=5)

        tk.Label(
            age_frame,
            text="Umur:",
            font=("Arial", 11, "bold"),
            bg="lightgray",
            width=20,
            anchor="w"
        ).pack(side=tk.LEFT)

        age_spinbox = tk.Spinbox(
            age_frame,
            from_=13,
            to=100,
            textvariable=self.age_var,
            font=("Arial", 11),
            width=10
        )
        age_spinbox.pack(side=tk.LEFT, padx=5)

        self.age_indicator = tk.Label(
            age_frame,
            text="●",
            font=("Arial", 16),
            fg="red",
            bg="lightgray"
        )
        self.age_indicator.pack(side=tk.LEFT, padx=5)

        # Hint untuk umur
        tk.Label(
            main_frame,
            text="Minimal 13 tahun",
            font=("Arial", 9),
            fg="gray",
            bg="lightgray",
            anchor="w"
        ).pack(fill=tk.X, padx=25)

        # Gender dengan Radiobutton
        gender_frame = tk.Frame(main_frame, bg="lightgray")
        gender_frame.pack(fill=tk.X, pady=10)

        tk.Label(
            gender_frame,
            text="Jenis Kelamin:",
            font=("Arial", 11, "bold"),
            bg="lightgray",
            width=20,
            anchor="w"
        ).pack(side=tk.LEFT)

        radio_frame = tk.Frame(gender_frame, bg="lightgray")
        radio_frame.pack(side=tk.LEFT)

        tk.Radiobutton(
            radio_frame,
            text="Pria",
            variable=self.gender_var,
            value="Pria",
            bg="lightgray",
            font=("Arial", 10)
        ).pack(side=tk.LEFT, padx=5)

        tk.Radiobutton(
            radio_frame,
            text="Wanita",
            variable=self.gender_var,
            value="Wanita",
            bg="lightgray",
            font=("Arial", 10)
        ).pack(side=tk.LEFT, padx=5)

        # Checkbox persetujuan
        self.agree_checkbox = tk.Checkbutton(
            main_frame,
            text="Saya menyetujui syarat dan ketentuan yang berlaku",
            variable=self.agree_var,
            font=("Arial", 11),
            bg="lightgray",
            command=self.validate_agreement
        )
        self.agree_checkbox.pack(pady=20)

Tugas: Jalankan dan lihat field umur dengan spinbox dan radiobutton gender.

Praktikum 5.4: Setup Validasi Real-time

Tambahkan method untuk setup validasi:

    def setup_validation(self):
        """Setup validasi real-time untuk semua field"""
        self.nama_var.trace_add("write", lambda *args: self.validate_nama())
        self.email_var.trace_add("write",

```python
        self.nama_var.trace_add("write", lambda *args: self.validate_nama())
        self.email_var.trace_add("write", lambda *args: self.validate_email())
        self.phone_var.trace_add("write", lambda *args: self.validate_phone())
        self.password_var.trace_add("write", lambda *args: self.validate_password())
        self.confirm_password_var.trace_add("write", lambda *args: self.validate_confirm_password())
        self.age_var.trace_add("write", lambda *args: self.validate_age())

Tambahkan method validasi untuk setiap field:

    def validate_nama(self):
        """Validasi nama lengkap"""
        nama = self.nama_var.get().strip()

        # Cek panjang minimal
        if len(nama) < 3:
            self.set_validation_status('nama', False, "Nama terlalu pendek")
            return False

        # Cek hanya huruf dan spasi
        if not re.match(r'^[a-zA-Z\s]+$', nama):
            self.set_validation_status('nama', False, "Hanya huruf dan spasi diperbolehkan")
            return False

        # Cek tidak boleh hanya spasi
        if nama.replace(' ', '') == '':
            self.set_validation_status('nama', False, "Nama tidak boleh kosong")
            return False

        self.set_validation_status('nama', True, "Nama valid")
        return True

    def validate_email(self):
        """Validasi format email"""
        email = self.email_var.get().strip()

        if not email:
            self.set_validation_status('email', False, "Email tidak boleh kosong")
            return False

        # Pattern regex untuk email
        email_pattern = r'^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$'

        if not re.match(email_pattern, email):
            self.set_validation_status('email', False, "Format email tidak valid")
            return False

        self.set_validation_status('email', True, "Email valid")
        return True

    def validate_phone(self):
        """Validasi nomor telepon"""
        phone = self.phone_var.get().strip()

        if not phone:
            self.set_validation_status('phone', False, "Nomor telepon tidak boleh kosong")
            return False

        # Cek format nomor Indonesia
        if not re.match(r'^08\d{8,11}$', phone):
            self.set_validation_status('phone', False, "Format: 08xxxxxxxxxx (10-13 digit)")
            return False

        self.set_validation_status('phone', True, "Nomor telepon valid")
        return True

    def validate_password(self):
        """Validasi password"""
        password = self.password_var.get()

        if len(password) < 8:
            self.set_validation_status('password', False, "Password minimal 8 karakter")
            return False

        # Cek kombinasi huruf dan angka
        if not re.search(r'[a-zA-Z]', password) or not re.search(r'\d', password):
            self.set_validation_status('password', False, "Harus kombinasi huruf dan angka")
            return False

        self.set_validation_status('password', True, "Password valid")
        # Validasi ulang confirm password jika sudah diisi
        if self.confirm_password_var.get():
            self.validate_confirm_password()
        return True

    def validate_confirm_password(self):
        """Validasi konfirmasi password"""
        password = self.password_var.get()
        confirm = self.confirm_password_var.get()

        if not confirm:
            self.set_validation_status('confirm_password', False, "Konfirmasi password tidak boleh kosong")
            return False

        if password != confirm:
            self.set_validation_status('confirm_password', False, "Password tidak sama")
            return False

        self.set_validation_status('confirm_password', True, "Password cocok")
        return True

    def validate_age(self):
        """Validasi umur"""
        try:
            age = self.age_var.get()
            if age < 13:
                self.set_validation_status('age', False, "Umur minimal 13 tahun")
                return False
            elif age > 100:
                self.set_validation_status('age', False, "Umur maksimal 100 tahun")
                return False

            self.set_validation_status('age', True, "Umur valid")
            return True
        except:
            self.set_validation_status('age', False, "Umur harus berupa angka")
            return False

    def validate_agreement(self):
        """Validasi checkbox persetujuan"""
        if self.agree_var.get():
            self.validation_status['agree'] = True
        else:
            self.validation_status['agree'] = False

        self.update_submit_button()

Praktikum 5.5: Method Helper untuk Validasi

Tambahkan method helper untuk mengelola status validasi:

    def set_validation_status(self, field_name, is_valid, message):
        """Set status validasi dan update tampilan"""
        self.validation_status[field_name] = is_valid

        # Update indikator visual
        indicator = getattr(self, f"{field_name}_indicator")
        hint_label = getattr(self, f"{field_name}_hint")

        if is_valid:
            indicator.config(fg="green")
            hint_label.config(fg="green", text=f"✓ {message}")
        else:
            indicator.config(fg="red")
            hint_label.config(fg="red", text=f"✗ {message}")

        # Update tombol submit
        self.update_submit_button()

    def update_submit_button(self):
        """Update status tombol submit berdasarkan validasi"""
        all_valid = all(self.validation_status.values())

        if hasattr(self, 'submit_btn'):
            if all_valid:
                self.submit_btn.config(state=tk.NORMAL, bg="green")
            else:
                self.submit_btn.config(state=tk.DISABLED, bg="gray")

Praktikum 5.6: Menambahkan Tombol Submit dan Progress

Tambahkan tombol submit dan progress bar:

        # Progress bar untuk menunjukkan kelengkapan form
        progress_frame = tk.Frame(main_frame, bg="lightgray")
        progress_frame.pack(fill=tk.X, pady=10)

        tk.Label(
            progress_frame,
            text="Kelengkapan Form:",
            font=("Arial", 11, "bold"),
            bg="lightgray"
        ).pack(anchor="w")

        self.progress_var = tk.DoubleVar()
        self.progress_bar = ttk.Progressbar(
            progress_frame,
            variable=self.progress_var,
            maximum=100,
            length=400
        )
        self.progress_bar.pack(fill=tk.X, pady=5)

        self.progress_label = tk.Label(
            progress_frame,
            text="0% Complete",
            font=("Arial", 10),
            bg="lightgray"
        )
        self.progress_label.pack(anchor="w")

        # Tombol submit
        self.submit_btn = tk.Button(
            main_frame,
            text="DAFTAR SEKARANG",
            font=("Arial", 14, "bold"),
            bg="gray",
            fg="white",
            width=20,
            height=2,
            state=tk.DISABLED,
            command=self.submit_form
        )
        self.submit_btn.pack(pady=20)

Modifikasi method update_submit_button() untuk update progress:

    def update_submit_button(self):
        """Update status tombol submit dan progress bar"""
        valid_count = sum(1 for status in self.validation_status.values() if status)
        total_fields = len(self.validation_status)
        progress_percentage = (valid_count / total_fields) * 100

        # Update progress bar
        self.progress_var.set(progress_percentage)
        self.progress_label.config(text=f"{progress_percentage:.0f}% Complete ({valid_count}/{total_fields} fields)")

        # Update tombol submit
        if hasattr(self, 'submit_btn'):
            if progress_percentage == 100:
                self.submit_btn.config(state=tk.NORMAL, bg="green")
            else:
                self.submit_btn.config(state=tk.DISABLED, bg="gray")

Praktikum 5.7: Implementasi Submit dengan Error Handling

Tambahkan method untuk submit form:

    def submit_form(self):
        """Submit form dengan error handling lengkap"""
        try:
            # Validasi final semua field
            if not self.final_validation():
                return

            # Simulasi proses registrasi
            self.show_loading()

            # Kumpulkan data
            user_data = {
                'nama': self.nama_var.get().strip(),
                'email': self.email_var.get().strip(),
                'phone': self.phone_var.get().strip(),
                'age': self.age_var.get(),
                'gender': self.gender_var.get(),
                'registration_date': datetime.now().strftime("%Y-%m-%d %H:%M:%S")
            }

            # Simulasi delay proses
            self.window.after(2000, lambda: self.complete_registration(user_data))

        except Exception as e:
            self.hide_loading()
            messagebox.showerror("Error", f"Terjadi kesalahan: {str(e)}")

    def final_validation(self):
        """Validasi final sebelum submit"""
        errors = []

        # Validasi ulang semua field
        if not self.validate_nama():
            errors.append("Nama tidak valid")
        if not self.validate_email():
            errors.append("Email tidak valid")
        if not self.validate_phone():
            errors.append("Nomor telepon tidak valid")
        if not self.validate_password():
            errors.append("Password tidak valid")
        if not self.validate_confirm_password():
            errors.append("Konfirmasi password tidak valid")
        if not self.validate_age():
            errors.append("Umur tidak valid")
        if not self.agree_var.get():
            errors.append("Anda harus menyetujui syarat dan ketentuan")

        if errors:
            error_message = "Perbaiki kesalahan berikut:\n\n" + "\n".join(f"• {error}" for error in errors)
            messagebox.showerror("Validasi Gagal", error_message)
            return False

        return True

    def show_loading(self):
        """Tampilkan loading state"""
        self.submit_btn.config(text="MEMPROSES...", state=tk.DISABLED, bg="orange")

        # Disable semua input
        for widget_name in ['nama_entry', 'email_entry', 'phone_entry', 'password_entry', 'confirm_password_entry']:
            if hasattr(self, widget_name):
                getattr(self, widget_name).config(state=tk.DISABLED)

    def hide_loading(self):
        """Sembunyikan loading state"""
        self.submit_btn.config(text="DAFTAR SEKARANG", state=tk.NORMAL, bg="green")

        # Enable kembali semua input
        for widget_name in ['nama_entry', 'email_entry', 'phone_entry', 'password_entry', 'confirm_password_entry']:
            if hasattr(self, widget_name):
                getattr(self, widget_name).config(state=tk.NORMAL)

    def complete_registration(self, user_data):
        """Selesaikan proses registrasi"""
        self.hide_loading()

        # Tampilkan hasil registrasi
        success_message = f"""
REGISTRASI BERHASIL!

Data yang terdaftar:
• Nama: {user_data['nama']}
• Email: {user_data['email']}
• Telepon: {user_data['phone']}
• Umur: {user_data['age']} tahun
• Jenis Kelamin: {user_data['gender']}
• Tanggal Daftar: {user_data['registration_date']}

Selamat datang di aplikasi kami!
        """

        messagebox.showinfo("Registrasi Berhasil", success_message)

        # Reset form
        if messagebox.askyesno("Reset Form", "Ingin mendaftarkan pengguna lain?"):
            self.reset_form()
        else:
            self.window.quit()

    def reset_form(self):
        """Reset semua field form"""
        # Reset semua variabel
        self.nama_var.set("")
        self.email_var.set("")
        self.phone_var.set("")
        self.password_var.set("")
        self.confirm_password_var.set("")
        self.age_var.set(18)
        self.gender_var.set("Pria")
        self.agree_var.set(False)

        # Reset status validasi
        for key in self.validation_status:
            self.validation_status[key] = False

        # Reset tampilan indikator
        for field in ['nama', 'email', 'phone', 'password', 'confirm_password', 'age']:
            indicator = getattr(self, f"{field}_indicator")
            hint_label = getattr(self, f"{field}_hint")
            indicator.config(fg="red")
            hint_label.config(fg="gray")

        # Reset progress
        self.progress_var.set(0)
        self.progress_label.config(text="0% Complete (0/7 fields)")

        # Reset tombol
        self.submit_btn.config(state=tk.DISABLED, bg="gray")

    def jalankan(self):
        """Method untuk menjalankan aplikasi"""
        self.window.mainloop()

# Untuk menjalankan aplikasi
if __name__ == "__main__":
    app = RegistrasiApp()
    app.jalankan()

Tugas: Jalankan aplikasi registrasi lengkap. Test semua validasi dengan memasukkan data yang salah dan benar. Perhatikan bagaimana indikator berubah warna, progress bar terupdate, dan tombol submit hanya aktif ketika semua validasi terpenuhi. Coba juga proses submit dan lihat loading state serta hasil akhir.

Tugas Akhir

Buat aplikasi "Quiz Interaktif" yang menggabungkan semua konsep yang telah dipelajari:

Spesifikasi:

  1. Multiple choice questions dengan radiobutton
  2. Timer countdown untuk setiap soal
  3. Real-time score tracking dengan progress bar
  4. Validasi jawaban sebelum lanjut ke soal berikutnya
  5. Animasi visual untuk feedback benar/salah
  6. Event handling untuk keyboard shortcuts (Enter untuk submit, Esc untuk skip)
  7. State management untuk menyimpan progress quiz

Template Dasar:

import tkinter as tk
from tkinter import messagebox, ttk
import random
import time

class QuizApp:
    def __init__(self):
        self.window = tk.Tk()
        self.window.title("Quiz Interaktif - Tugas Akhir")
        self.window.geometry("700x500")

        # Data quiz
        self.questions = [
            {
                "question": "Apa itu event-driven programming?",
                "options": ["Program yang berjalan berurutan", "Program yang merespons kejadian", "Program tanpa GUI", "Program berbasis web"],
                "correct": 1
            },
            # Tambahkan 9 soal lagi
        ]

        # State management
        self.current_question = 0
        self.score = 0
        self.time_left = 30
        self.selected_answer = tk.IntVar()

        self.buat_interface()
        self.load_question()
        self.start_timer()

    def buat_interface(self):
        # TODO: Implementasi interface
        pass

    def load_question(self):
        # TODO: Load soal ke interface
        pass

    def start_timer(self):
        # TODO: Implementasi timer countdown
        pass

    def submit_answer(self):
        # TODO: Validasi dan proses jawaban
        pass

    def show_result(self):
        # TODO: Tampilkan hasil akhir
        pass

if __name__ == "__main__":
    app = QuizApp()
    app.jalankan()

Kriteria Penilaian:

  • Event handling dan interaktivitas (30%)
  • Timer dan animasi (25%)
  • Validasi dan state management (25%)
  • User interface dan user experience (20%)