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 < 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 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:
- Multiple choice questions dengan radiobutton
- Timer countdown untuk setiap soal
- Real-time score tracking dengan progress bar
- Validasi jawaban sebelum lanjut ke soal berikutnya
- Animasi visual untuk feedback benar/salah
- Event handling untuk keyboard shortcuts (Enter untuk submit, Esc untuk skip)
- 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%)