Lewati ke isi

Styling Interface dengan ttk dan Custom Theme

Dalam pengembangan aplikasi desktop dengan Python, tampilan yang menarik dan modern sangat penting untuk memberikan pengalaman pengguna yang baik. Tkinter standar memiliki keterbatasan dalam hal styling, namun dengan ttk (Themed Tkinter) dan custom theme, kita dapat membuat aplikasi yang lebih profesional dan modern.


Bagian 1: Dasar-dasar ttk dan Styling

1.1 Perbandingan Widget Tkinter vs ttk

Praktikum 1.1: Membuat aplikasi perbandingan dasar

import tkinter as tk
from tkinter import ttk
import tkinter.font as tkFont

class PerbandinganWidget:
    def __init__(self):
        self.root = tk.Tk()
        self.root.title("Perbandingan Widget Tkinter vs ttk")
        self.root.geometry("800x600")
        self.root.configure(bg='#f0f0f0')

        # Variabel untuk menyimpan data
        self.nama_var = tk.StringVar()
        self.email_var = tk.StringVar()
        self.gender_var = tk.StringVar(value="Laki-laki")
        self.hobi_vars = {
            'membaca': tk.BooleanVar(),
            'olahraga': tk.BooleanVar(),
            'musik': tk.BooleanVar()
        }

        self._buat_interface()

Setelah class PerbandinganWidget dan method __init__, lanjutkan dengan method untuk membuat interface:

    def _buat_interface(self):
        # Header
        header_frame = tk.Frame(self.root, bg='#2c3e50', height=80)
        header_frame.pack(fill='x', padx=10, pady=(10,5))
        header_frame.pack_propagate(False)

        title_label = tk.Label(header_frame, text="Perbandingan Widget Tkinter vs ttk",
                              font=('Arial', 16, 'bold'), fg='white', bg='#2c3e50')
        title_label.pack(expand=True)

Setelah header frame, lanjutkan dengan frame untuk widget standar:

        # Frame untuk widget Tkinter standar
        tk_frame = tk.LabelFrame(self.root, text="Widget Tkinter Standar",
                                font=('Arial', 12, 'bold'), bg='#f0f0f0', fg='#2c3e50')
        tk_frame.pack(side='left', fill='both', expand=True, padx=(10,5), pady=5)

        # Input nama dengan Tkinter standar
        tk.Label(tk_frame, text="Nama:", font=('Arial', 10), bg='#f0f0f0').pack(anchor='w', padx=10, pady=(10,0))
        self.tk_entry = tk.Entry(tk_frame, textvariable=self.nama_var, font=('Arial', 10))
        self.tk_entry.pack(fill='x', padx=10, pady=(0,10))

        # Combobox dengan Tkinter standar (menggunakan OptionMenu)
        tk.Label(tk_frame, text="Gender:", font=('Arial', 10), bg='#f0f0f0').pack(anchor='w', padx=10)
        self.tk_option = tk.OptionMenu(tk_frame, self.gender_var, "Laki-laki", "Perempuan")
        self.tk_option.pack(fill='x', padx=10, pady=(0,10))

        # Checkbutton dengan Tkinter standar
        tk.Label(tk_frame, text="Hobi:", font=('Arial', 10), bg='#f0f0f0').pack(anchor='w', padx=10)
        for hobi, var in self.hobi_vars.items():
            tk.Checkbutton(tk_frame, text=hobi.capitalize(), variable=var,
                          font=('Arial', 9), bg='#f0f0f0').pack(anchor='w', padx=20)

        # Button dengan Tkinter standar
        tk.Button(tk_frame, text="Submit (Tkinter)", command=self.submit_tk,
                 font=('Arial', 10), bg='#3498db', fg='white').pack(pady=20, padx=10)

Setelah frame widget standar, lanjutkan dengan frame untuk widget ttk:

        # Frame untuk widget ttk
        ttk_frame = tk.LabelFrame(self.root, text="Widget ttk (Themed)",
                                 font=('Arial', 12, 'bold'), bg='#f0f0f0', fg='#2c3e50')
        ttk_frame.pack(side='right', fill='both', expand=True, padx=(5,10), pady=5)

        # Input nama dengan ttk
        ttk.Label(ttk_frame, text="Nama:", font=('Arial', 10)).pack(anchor='w', padx=10, pady=(10,0))
        self.ttk_entry = ttk.Entry(ttk_frame, textvariable=self.nama_var, font=('Arial', 10))
        self.ttk_entry.pack(fill='x', padx=10, pady=(0,10))

        # Combobox dengan ttk
        ttk.Label(ttk_frame, text="Gender:", font=('Arial', 10)).pack(anchor='w', padx=10)
        self.ttk_combo = ttk.Combobox(ttk_frame, textvariable=self.gender_var,
                                     values=["Laki-laki", "Perempuan"], state="readonly")
        self.ttk_combo.pack(fill='x', padx=10, pady=(0,10))

        # Checkbutton dengan ttk
        ttk.Label(ttk_frame, text="Hobi:", font=('Arial', 10)).pack(anchor='w', padx=10)
        for hobi, var in self.hobi_vars.items():
            ttk.Checkbutton(ttk_frame, text=hobi.capitalize(), variable=var).pack(anchor='w', padx=20)

        # Button dengan ttk
        ttk.Button(ttk_frame, text="Submit (ttk)", command=self.submit_ttk).pack(pady=20, padx=10)

Setelah frame ttk, lanjutkan dengan method untuk handle submit:

    def submit_tk(self):
        hobi_terpilih = [hobi for hobi, var in self.hobi_vars.items() if var.get()]
        print(f"Tkinter Submit - Nama: {self.nama_var.get()}, Gender: {self.gender_var.get()}, Hobi: {hobi_terpilih}")

    def submit_ttk(self):
        hobi_terpilih = [hobi for hobi, var in self.hobi_vars.items() if var.get()]
        print(f"ttk Submit - Nama: {self.nama_var.get()}, Gender: {self.gender_var.get()}, Hobi: {hobi_terpilih}")

    def run(self):
        self.root.mainloop()

# Jalankan aplikasi
if __name__ == "__main__":
    app = PerbandinganWidget()
    app.run()

Tugas: Jalankan aplikasi dan bandingkan tampilan widget Tkinter standar dengan ttk. Coba isi form di kedua sisi, lalu klik tombol Submit untuk melihat perbedaan visual dan fungsionalitas. Perhatikan perbedaan styling, responsivitas, dan konsistensi tampilan.


1.2 Implementasi Theme Switching

Praktikum 1.2: Membuat aplikasi dengan kemampuan mengganti theme

import tkinter as tk
from tkinter import ttk

class ThemeSwitcher:
    def __init__(self):
        self.root = tk.Tk()
        self.root.title("Theme Switcher Demo")
        self.root.geometry("600x400")

        # Inisialisasi style
        self.style = ttk.Style()
        self.current_theme = tk.StringVar(value=self.style.theme_use())

        self._buat_interface()
        self._setup_themes()

Setelah method __init__, lanjutkan dengan method untuk membuat interface:

    def _buat_interface(self):
        # Frame utama
        main_frame = ttk.Frame(self.root, padding="20")
        main_frame.pack(fill='both', expand=True)

        # Header
        ttk.Label(main_frame, text="Theme Switcher Demo",
                 font=('Arial', 16, 'bold')).pack(pady=(0,20))

        # Frame untuk theme selection
        theme_frame = ttk.LabelFrame(main_frame, text="Pilih Theme", padding="10")
        theme_frame.pack(fill='x', pady=(0,20))

        # Combobox untuk memilih theme
        ttk.Label(theme_frame, text="Theme:").pack(anchor='w')
        self.theme_combo = ttk.Combobox(theme_frame, textvariable=self.current_theme,
                                       values=self.style.theme_names(), state="readonly")
        self.theme_combo.pack(fill='x', pady=(5,10))
        self.theme_combo.bind('<<ComboboxSelected>>', self.change_theme)

Setelah theme selection frame, lanjutkan dengan widget demo:

        # Frame untuk demo widgets
        demo_frame = ttk.LabelFrame(main_frame, text="Demo Widgets", padding="10")
        demo_frame.pack(fill='both', expand=True)

        # Entry widget
        ttk.Label(demo_frame, text="Entry Widget:").pack(anchor='w')
        self.demo_entry = ttk.Entry(demo_frame)
        self.demo_entry.pack(fill='x', pady=(5,10))
        self.demo_entry.insert(0, "Contoh text...")

        # Button widgets
        button_frame = ttk.Frame(demo_frame)
        button_frame.pack(fill='x', pady=(0,10))

        ttk.Button(button_frame, text="Normal Button").pack(side='left', padx=(0,5))
        ttk.Button(button_frame, text="Disabled Button", state='disabled').pack(side='left')

        # Progressbar
        ttk.Label(demo_frame, text="Progress Bar:").pack(anchor='w')
        self.progress = ttk.Progressbar(demo_frame, mode='determinate', value=70)
        self.progress.pack(fill='x', pady=(5,10))

        # Scale
        ttk.Label(demo_frame, text="Scale Widget:").pack(anchor='w')
        self.scale = ttk.Scale(demo_frame, from_=0, to=100, orient='horizontal')
        self.scale.pack(fill='x', pady=(5,10))
        self.scale.set(50)

Setelah demo widgets, lanjutkan dengan method untuk setup themes dan change theme:

    def _setup_themes(self):
        # Konfigurasi tambahan untuk beberapa theme
        available_themes = self.style.theme_names()
        print(f"Available themes: {available_themes}")

    def change_theme(self, event=None):
        selected_theme = self.current_theme.get()
        try:
            self.style.theme_use(selected_theme)
            print(f"Theme changed to: {selected_theme}")
        except tk.TclError as e:
            print(f"Error changing theme: {e}")

    def run(self):
        self.root.mainloop()

# Jalankan aplikasi
if __name__ == "__main__":
    app = ThemeSwitcher()
    app.run()

Tugas: Jalankan aplikasi dan coba ganti-ganti theme yang tersedia melalui combobox. Perhatikan bagaimana setiap theme mengubah tampilan widget secara otomatis. Coba interaksi dengan berbagai widget untuk melihat konsistensi theme di seluruh aplikasi.


Bagian 2: Styling Advanced dan Custom Theme

2.1 Custom Styling dengan ttk.Style

Praktikum 2.1: Membuat aplikasi dengan custom styling

import tkinter as tk
from tkinter import ttk

class CustomStyleApp:
    def __init__(self):
        self.root = tk.Tk()
        self.root.title("Custom Styling dengan ttk")
        self.root.geometry("700x500")
        self.root.configure(bg='#2c3e50')

        # Inisialisasi style
        self.style = ttk.Style()
        self._setup_custom_styles()
        self._buat_interface()

Setelah method __init__, lanjutkan dengan method untuk setup custom styles:

    def _setup_custom_styles(self):
        # Custom style untuk button
        self.style.configure('Custom.TButton',
                           background='#3498db',
                           foreground='white',
                           borderwidth=0,
                           focuscolor='none',
                           padding=(20, 10))

        self.style.map('Custom.TButton',
                      background=[('active', '#2980b9'),
                                ('pressed', '#21618c')])

        # Custom style untuk entry
        self.style.configure('Custom.TEntry',
                           fieldbackground='#ecf0f1',
                           borderwidth=2,
                           insertcolor='#2c3e50',
                           padding=(10, 8))

        self.style.map('Custom.TEntry',
                      focuscolor=[('focus', '#3498db')])

        # Custom style untuk label
        self.style.configure('Title.TLabel',
                           background='#2c3e50',
                           foreground='#ecf0f1',
                           font=('Arial', 18, 'bold'))

        self.style.configure('Subtitle.TLabel',
                           background='#2c3e50',
                           foreground='#bdc3c7',
                           font=('Arial', 12))

Setelah setup custom styles, lanjutkan dengan method untuk membuat interface:

    def _buat_interface(self):
        # Main container
        main_frame = ttk.Frame(self.root)
        main_frame.pack(fill='both', expand=True, padx=20, pady=20)

        # Header section
        header_frame = ttk.Frame(main_frame)
        header_frame.pack(fill='x', pady=(0,30))

        ttk.Label(header_frame, text="Custom Styling Demo",
                 style='Title.TLabel').pack()
        ttk.Label(header_frame, text="Aplikasi dengan styling kustom menggunakan ttk.Style",
                 style='Subtitle.TLabel').pack(pady=(5,0))

        # Form section
        form_frame = ttk.LabelFrame(main_frame, text="Form Input", padding="20")
        form_frame.pack(fill='x', pady=(0,20))

        # Nama input
        ttk.Label(form_frame, text="Nama Lengkap:").pack(anchor='w', pady=(0,5))
        self.nama_entry = ttk.Entry(form_frame, style='Custom.TEntry', width=40)
        self.nama_entry.pack(fill='x', pady=(0,15))

        # Email input
        ttk.Label(form_frame, text="Email:").pack(anchor='w', pady=(0,5))
        self.email_entry = ttk.Entry(form_frame, style='Custom.TEntry', width=40)
        self.email_entry.pack(fill='x', pady=(0,15))

Setelah form section, lanjutkan dengan button section dan method handlers:

        # Button section
        button_frame = ttk.Frame(form_frame)
        button_frame.pack(fill='x', pady=(10,0))

        ttk.Button(button_frame, text="Submit Data",
                  style='Custom.TButton', command=self.submit_data).pack(side='left', padx=(0,10))
        ttk.Button(button_frame, text="Reset Form",
                  style='Custom.TButton', command=self.reset_form).pack(side='left')

        # Result section
        self.result_frame = ttk.LabelFrame(main_frame, text="Hasil Input", padding="20")
        self.result_frame.pack(fill='both', expand=True)

        self.result_text = tk.Text(self.result_frame, height=8, bg='#ecf0f1',
                                  fg='#2c3e50', font=('Consolas', 10))
        self.result_text.pack(fill='both', expand=True)

    def submit_data(self):
        nama = self.nama_entry.get()
        email = self.email_entry.get()

        if nama and email:
            result = f"Data berhasil disubmit:\nNama: {nama}\nEmail: {email}\n" + "="*40 + "\n"
            self.result_text.insert('end', result)
            self.result_text.see('end')
        else:
            self.result_text.insert('end', "Error: Semua field harus diisi!\n" + "="*40 + "\n")

    def reset_form(self):
        self.nama_entry.delete(0, 'end')
        self.email_entry.delete(0, 'end')
        self.result_text.delete('1.0', 'end')

    def run(self):
        self.root.mainloop()

# Jalankan aplikasi
if __name__ == "__main__":
    app = CustomStyleApp()
    app.run()

Tugas: Jalankan aplikasi dan perhatikan perbedaan styling yang telah dikustomisasi. Coba isi form dengan data, klik Submit Data untuk melihat hasilnya, lalu gunakan Reset Form untuk membersihkan. Amati bagaimana custom styling memberikan tampilan yang lebih profesional.


2.2 Responsive Design dengan ttk

Praktikum 2.2: Membuat aplikasi yang responsif terhadap perubahan ukuran window

import tkinter as tk
from tkinter import ttk

class ResponsiveApp:
    def __init__(self):
        self.root = tk.Tk()
        self.root.title("Responsive Design dengan ttk")
        self.root.geometry("800x600")
        self.root.minsize(600, 400)

        # Variabel untuk tracking ukuran window
        self.current_width = 800
        self.current_height = 600

        self._setup_styles()
        self._buat_interface()
        self._bind_events()

Setelah method __init__, lanjutkan dengan setup styles untuk responsive design:

    def _setup_styles(self):
        self.style = ttk.Style()

        # Style untuk desktop (default)
        self.style.configure('Desktop.TLabel',
                           font=('Arial', 12),
                           padding=(10, 5))

        # Style untuk tablet
        self.style.configure('Tablet.TLabel',
                           font=('Arial', 10),
                           padding=(8, 4))

        # Style untuk mobile
        self.style.configure('Mobile.TLabel',
                           font=('Arial', 9),
                           padding=(5, 3))

        # Button styles
        self.style.configure('Desktop.TButton',
                           padding=(15, 8),
                           font=('Arial', 11))

        self.style.configure('Tablet.TButton',
                           padding=(12, 6),
                           font=('Arial', 10))

        self.style.configure('Mobile.TButton',
                           padding=(8, 4),
                           font=('Arial', 9))

Setelah setup styles, lanjutkan dengan method untuk membuat interface:

    def _buat_interface(self):
        # Main container dengan scrollable frame
        self.canvas = tk.Canvas(self.root)
        self.scrollbar = ttk.Scrollbar(self.root, orient="vertical", command=self.canvas.yview)
        self.scrollable_frame = ttk.Frame(self.canvas)

        self.scrollable_frame.bind(
            "<Configure>",
            lambda e: self.canvas.configure(scrollregion=self.canvas.bbox("all"))
        )

        self.canvas.create_window((0, 0), window=self.scrollable_frame, anchor="nw")
        self.canvas.configure(yscrollcommand=self.scrollbar.set)

        # Header section
        self.header_frame = ttk.Frame(self.scrollable_frame)
        self.header_frame.pack(fill='x', padx=20, pady=20)

        self.title_label = ttk.Label(self.header_frame, text="Responsive Design Demo",
                                    style='Desktop.TLabel')
        self.title_label.pack()

        self.size_label = ttk.Label(self.header_frame, text=f"Window Size: {self.current_width}x{self.current_height}",
                                   style='Desktop.TLabel')
        self.size_label.pack(pady=(5,0))

Setelah header section, lanjutkan dengan content sections:

        # Content grid yang akan berubah layout
        self.content_frame = ttk.Frame(self.scrollable_frame)
        self.content_frame.pack(fill='both', expand=True, padx=20, pady=(0,20))

        # Cards yang akan di-reposition berdasarkan ukuran window
        self.cards = []
        for i in range(6):
            card = ttk.LabelFrame(self.content_frame, text=f"Card {i+1}", padding="15")

            ttk.Label(card, text=f"Content untuk card {i+1}",
                     style='Desktop.TLabel').pack()
            ttk.Button(card, text=f"Action {i+1}",
                      style='Desktop.TButton').pack(pady=(10,0))

            self.cards.append(card)

        # Control panel
        self.control_frame = ttk.LabelFrame(self.scrollable_frame, text="Controls", padding="15")
        self.control_frame.pack(fill='x', padx=20, pady=(0,20))

        ttk.Button(self.control_frame, text="Simulate Mobile (400x600)",
                  command=lambda: self.resize_window(400, 600),
                  style='Desktop.TButton').pack(side='left', padx=(0,10))

        ttk.Button(self.control_frame, text="Simulate Tablet (600x800)",
                  command=lambda: self.resize_window(600, 800),
                  style='Desktop.TButton').pack(side='left', padx=(0,10))

        ttk.Button(self.control_frame, text="Simulate Desktop (800x600)",
                  command=lambda: self.resize_window(800, 600),
                  style='Desktop.TButton').pack(side='left')

        # Pack canvas dan scrollbar
        self.canvas.pack(side="left", fill="both", expand=True)
        self.scrollbar.pack(side="right", fill="y")

        # Initial layout
        self._update_layout()

Setelah interface setup, lanjutkan dengan method untuk handle responsive behavior:

    def _bind_events(self):
        self.root.bind('<Configure>', self._on_window_resize)

    def _on_window_resize(self, event):
        if event.widget == self.root:
            self.current_width = self.root.winfo_width()
            self.current_height = self.root.winfo_height()
            self.size_label.config(text=f"Window Size: {self.current_width}x{self.current_height}")
            self._update_layout()

    def _update_layout(self):
        # Clear current layout
        for card in self.cards:
            card.pack_forget()

        # Determine layout based on window width
        if self.current_width < 500:  # Mobile
            self._apply_mobile_layout()
        elif self.current_width < 700:  # Tablet
            self._apply_tablet_layout()
        else:  # Desktop
            self._apply_desktop_layout()

    def _apply_mobile_layout(self):
        # Single column layout
        for card in self.cards:
            card.pack(fill='x', pady=(0,10))
        self._update_styles('Mobile')

    def _apply_tablet_layout(self):
        # Two column layout
        for i, card in enumerate(self.cards):
            if i % 2 == 0:
                card.pack(side='left', fill='both', expand=True, padx=(0,5), pady=(0,10))
            else:
                card.pack(side='right', fill='both', expand=True, padx=(5,0), pady=(0,10))
        self._update_styles('Tablet')

    def _apply_desktop_layout(self):
        # Three column layout
        for i, card in enumerate(self.cards):
            row = i // 3
            col = i % 3
            if col == 0:
                card.pack(side='left', fill='both', expand=True, padx=(0,5), pady=(0,10))
            elif col == 1:
                card.pack(side='left', fill='both', expand=True, padx=(5,5), pady=(0,10))
            else:
                card.pack(side='left', fill='both', expand=True, padx=(5,0), pady=(0,10))
        self._update_styles('Desktop')

    def _update_styles(self, size):
        style_suffix = f'{size}.TLabel'
        button_style = f'{size}.TButton'

        self.title_label.config(style=style_suffix)
        self.size_label.config(style=style_suffix)

        # Update all labels and buttons in cards
        for card in self.cards:
            for child in card.winfo_children():
                if isinstance(child, ttk.Label):
                    child.config(style=style_suffix)
                elif isinstance(child, ttk.Button):
                    child.config(style=button_style)

    def resize_window(self, width, height):
        self.root.geometry(f"{width}x{height}")

    def run(self):
        self.root.mainloop()

# Jalankan aplikasi
if __name__ == "__main__":
    app = ResponsiveApp()
    app.run()

Tugas: Jalankan aplikasi dan coba ubah ukuran window dengan drag corner atau gunakan tombol simulasi. Perhatikan bagaimana layout berubah dari desktop (3 kolom) ke tablet (2 kolom) ke mobile (1 kolom). Amati juga perubahan ukuran font dan padding yang menyesuaikan dengan ukuran layar.


Bagian 3: Advanced Techniques dan Animasi

3.1 Animasi dan Transisi dengan ttk

Praktikum 3.1: Membuat aplikasi dengan efek animasi smooth

import tkinter as tk
from tkinter import ttk
import threading
import time

class AnimatedApp:
    def __init__(self):
        self.root = tk.Tk()
        self.root.title("Animasi dan Transisi dengan ttk")
        self.root.geometry("800x600")
        self.root.configure(bg='#1a1a1a')

        # Animation variables
        self.animation_running = False
        self.progress_value = 0
        self.fade_alpha = 1.0

        self._setup_styles()
        self._buat_interface()

Setelah method __init__, lanjutkan dengan setup styles untuk animasi:

    def _setup_styles(self):
        self.style = ttk.Style()

        # Dark theme styles
        self.style.configure('Dark.TFrame',
                           background='#2d2d2d',
                           relief='flat')

        self.style.configure('Dark.TLabel',
                           background='#2d2d2d',
                           foreground='#ffffff',
                           font=('Arial', 11))

        self.style.configure('Title.TLabel',
                           background='#2d2d2d',
                           foreground='#00ff88',
                           font=('Arial', 16, 'bold'))

        self.style.configure('Animated.TButton',
                           background='#00ff88',
                           foreground='#1a1a1a',
                           borderwidth=0,
                           focuscolor='none',
                           padding=(20, 10),
                           font=('Arial', 10, 'bold'))

        self.style.map('Animated.TButton',
                      background=[('active', '#00cc6a'),
                                ('pressed', '#009954')])

Setelah setup styles, lanjutkan dengan method untuk membuat interface:

    def _buat_interface(self):
        # Main container
        self.main_frame = ttk.Frame(self.root, style='Dark.TFrame')
        self.main_frame.pack(fill='both', expand=True, padx=20, pady=20)

        # Header dengan animasi fade
        self.header_frame = ttk.Frame(self.main_frame, style='Dark.TFrame')
        self.header_frame.pack(fill='x', pady=(0,30))

        self.title_label = ttk.Label(self.header_frame, text="🎬 Animation Demo",
                                    style='Title.TLabel')
        self.title_label.pack()

        # Progress animation section
        self.progress_frame = ttk.LabelFrame(self.main_frame, text="Progress Animation", padding="20")
        self.progress_frame.pack(fill='x', pady=(0,20))

        ttk.Label(self.progress_frame, text="Animated Progress Bar:",
                 style='Dark.TLabel').pack(anchor='w', pady=(0,10))

        self.progress_bar = ttk.Progressbar(self.progress_frame, mode='determinate',
                                          length=400, style='TProgressbar')
        self.progress_bar.pack(fill='x', pady=(0,15))

        # Control buttons untuk progress
        progress_controls = ttk.Frame(self.progress_frame, style='Dark.TFrame')
        progress_controls.pack(fill='x')

        ttk.Button(progress_controls, text="Start Animation",
                  style='Animated.TButton', command=self.start_progress_animation).pack(side='left', padx=(0,10))
        ttk.Button(progress_controls, text="Stop Animation",
                  style='Animated.TButton', command=self.stop_animation).pack(side='left', padx=(0,10))
        ttk.Button(progress_controls, text="Reset",
                  style='Animated.TButton', command=self.reset_progress).pack(side='left')

Setelah progress section, lanjutkan dengan sliding panel animation:

        # Sliding panel animation
        self.slide_frame = ttk.LabelFrame(self.main_frame, text="Sliding Panel", padding="20")
        self.slide_frame.pack(fill='x', pady=(0,20))

        # Container untuk sliding panel
        self.slide_container = ttk.Frame(self.slide_frame, style='Dark.TFrame', height=200)
        self.slide_container.pack(fill='x', pady=(0,15))
        self.slide_container.pack_propagate(False)

        # Panel yang akan slide
        self.sliding_panel = ttk.Frame(self.slide_container, style='Dark.TFrame')
        self.sliding_panel.place(x=-300, y=0, width=300, height=200)

        # Content dalam sliding panel
        panel_content = ttk.Frame(self.sliding_panel, style='Dark.TFrame')
        panel_content.pack(fill='both', expand=True, padx=20, pady=20)

        ttk.Label(panel_content, text="🎯 Sliding Panel Content",
                 style='Title.TLabel').pack()
        ttk.Label(panel_content, text="Panel ini dapat slide masuk dan keluar",
                 style='Dark.TLabel').pack(pady=(10,0))

        # Controls untuk sliding
        slide_controls = ttk.Frame(self.slide_frame, style='Dark.TFrame')
        slide_controls.pack(fill='x')

        ttk.Button(slide_controls, text="Slide In",
                  style='Animated.TButton', command=self.slide_in).pack(side='left', padx=(0,10))
        ttk.Button(slide_controls, text="Slide Out",
                  style='Animated.TButton', command=self.slide_out).pack(side='left')

Setelah sliding panel, lanjutkan dengan fade animation dan methods:

        # Fade animation section
        self.fade_frame = ttk.LabelFrame(self.main_frame, text="Fade Animation", padding="20")
        self.fade_frame.pack(fill='both', expand=True)

        # Content yang akan fade
        self.fade_content = ttk.Frame(self.fade_frame, style='Dark.TFrame')
        self.fade_content.pack(fill='both', expand=True, pady=(0,15))

        ttk.Label(self.fade_content, text="✨ Fade Content",
                 style='Title.TLabel').pack(pady=20)
        ttk.Label(self.fade_content, text="Content ini dapat fade in/out dengan smooth transition",
                 style='Dark.TLabel').pack()

        # Fade controls
        fade_controls = ttk.Frame(self.fade_frame, style='Dark.TFrame')
        fade_controls.pack(fill='x')

        ttk.Button(fade_controls, text="Fade Out",
                  style='Animated.TButton', command=self.fade_out).pack(side='left', padx=(0,10))
        ttk.Button(fade_controls, text="Fade In",
                  style='Animated.TButton', command=self.fade_in).pack(side='left')

    # Animation methods
    def start_progress_animation(self):
        if not self.animation_running:
            self.animation_running = True
            threading.Thread(target=self._animate_progress, daemon=True).start()

    def _animate_progress(self):
        while self.animation_running and self.progress_value < 100:
            self.progress_value += 1
            self.progress_bar['value'] = self.progress_value
            time.sleep(0.05)  # 50ms delay untuk smooth animation

        if self.progress_value >= 100:
            self.animation_running = False

    def stop_animation(self):
        self.animation_running = False

    def reset_progress(self):
        self.animation_running = False
        self.progress_value = 0
        self.progress_bar['value'] = 0

Setelah progress animation methods, lanjutkan dengan sliding dan fade methods:

    def slide_in(self):
        threading.Thread(target=self._animate_slide_in, daemon=True).start()

    def _animate_slide_in(self):
        start_x = -300
        end_x = 0
        steps = 30

        for i in range(steps + 1):
            current_x = start_x + (end_x - start_x) * (i / steps)
            self.sliding_panel.place(x=current_x, y=0, width=300, height=200)
            time.sleep(0.02)  # 20ms untuk smooth animation

    def slide_out(self):
        threading.Thread(target=self._animate_slide_out, daemon=True).start()

    def _animate_slide_out(self):
        start_x = 0
        end_x = -300
        steps = 30

        for i in range(steps + 1):
            current_x = start_x + (end_x - start_x) * (i / steps)
            self.sliding_panel.place(x=current_x, y=0, width=300, height=200)
            time.sleep(0.02)

    def fade_out(self):
        threading.Thread(target=self._animate_fade_out, daemon=True).start()

    def _animate_fade_out(self):
        steps = 20
        for i in range(steps + 1):
            alpha = 1.0 - (i / steps)
            # Simulate fade dengan mengubah state
            if alpha < 0.5:
                for child in self.fade_content.winfo_children():
                    child.configure(state='disabled' if hasattr(child, 'configure') else None)
            time.sleep(0.05)

        # Hide content
        self.fade_content.pack_forget()

    def fade_in(self):
        self.fade_content.pack(fill='both', expand=True, pady=(0,15))
        threading.Thread(target=self._animate_fade_in, daemon=True).start()

    def _animate_fade_in(self):
        steps = 20
        for i in range(steps + 1):
            alpha = i / steps
            # Restore state
            if alpha > 0.5:
                for child in self.fade_content.winfo_children():
                    if hasattr(child, 'configure'):
                        try:
                            child.configure(state='normal')
                        except:
                            pass
            time.sleep(0.05)

    def run(self):
        self.root.mainloop()

# Jalankan aplikasi
if __name__ == "__main__":
    app = AnimatedApp()
    app.run()

Tugas: Jalankan aplikasi dan coba semua animasi yang tersedia. Mulai dengan Start Animation untuk progress bar, lalu coba Slide In/Out untuk panel, dan Fade In/Out untuk content. Perhatikan bagaimana setiap animasi berjalan smooth dengan threading untuk mencegah UI freeze.


3.2 Dashboard Interaktif dengan Real-time Updates

Praktikum 3.2: Membuat dashboard dengan update data real-time

import tkinter as tk
from tkinter import ttk
import threading
import time
import random
from datetime import datetime

class InteractiveDashboard:
    def __init__(self):
        self.root = tk.Tk()
        self.root.title("Interactive Dashboard - Real-time Updates")
        self.root.geometry("1000x700")
        self.root.configure(bg='#0f1419')

        # Data variables
        self.is_running = False
        self.cpu_data = []
        self.memory_data = []
        self.network_data = []
        self.max_data_points = 50

        # Metrics
        self.current_cpu = 0
        self.current_memory = 0
        self.current_network = 0
        self.total_requests = 0
        self.active_users = 0

        self._setup_styles()
        self._buat_interface()

Setelah method __init__, lanjutkan dengan setup styles untuk dashboard:

    def _setup_styles(self):
        self.style = ttk.Style()

        # Dashboard theme
        self.style.configure('Dashboard.TFrame',
                           background='#1e2328',
                           relief='flat')

        self.style.configure('Card.TFrame',
                           background='#252a31',
                           relief='raised',
                           borderwidth=1)

        self.style.configure('Dashboard.TLabel',
                           background='#1e2328',
                           foreground='#ffffff',
                           font=('Arial', 10))

        self.style.configure('Title.TLabel',
                           background='#1e2328',
                           foreground='#00d4ff',
                           font=('Arial', 14, 'bold'))

        self.style.configure('Metric.TLabel',
                           background='#252a31',
                           foreground='#00ff88',
                           font=('Arial', 20, 'bold'))

        self.style.configure('MetricLabel.TLabel',
                           background='#252a31',
                           foreground='#ffffff',
                           font=('Arial', 9))

        self.style.configure('Control.TButton',
                           background='#00d4ff',
                           foreground='#0f1419',
                           borderwidth=0,
                           padding=(15, 8),
                           font=('Arial', 10, 'bold'))

Setelah setup styles, lanjutkan dengan method untuk membuat interface:

    def _buat_interface(self):
        # Main container
        self.main_frame = ttk.Frame(self.root, style='Dashboard.TFrame')
        self.main_frame.pack(fill='both', expand=True, padx=10, pady=10)

        # Header
        header_frame = ttk.Frame(self.main_frame, style='Dashboard.TFrame')
        header_frame.pack(fill='x', pady=(0,20))

        ttk.Label(header_frame, text="📊 System Dashboard",
                 style='Title.TLabel').pack(side='left')

        self.status_label = ttk.Label(header_frame, text="● Stopped",
                                     style='Dashboard.TLabel')
        self.status_label.pack(side='right')

        self.time_label = ttk.Label(header_frame, text="",
                                   style='Dashboard.TLabel')
        self.time_label.pack(side='right', padx=(0,20))

        # Metrics cards row
        metrics_frame = ttk.Frame(self.main_frame, style='Dashboard.TFrame')
        metrics_frame.pack(fill='x', pady=(0,20))

        # CPU Card
        self.cpu_card = ttk.Frame(metrics_frame, style='Card.TFrame', padding="15")
        self.cpu_card.pack(side='left', fill='both', expand=True, padx=(0,10))

        ttk.Label(self.cpu_card, text="CPU Usage", style='MetricLabel.TLabel').pack()
        self.cpu_metric = ttk.Label(self.cpu_card, text="0%", style='Metric.TLabel')
        self.cpu_metric.pack(pady=(5,0))

        self.cpu_progress = ttk.Progressbar(self.cpu_card, mode='determinate', length=200)
        self.cpu_progress.pack(pady=(10,0), fill='x')

Setelah CPU card, lanjutkan dengan memory dan network cards:

        # Memory Card
        self.memory_card = ttk.Frame(metrics_frame, style='Card.TFrame', padding="15")
        self.memory_card.pack(side='left', fill='both', expand=True, padx=(5,5))

        ttk.Label(self.memory_card, text="Memory Usage", style='MetricLabel.TLabel').pack()
        self.memory_metric = ttk.Label(self.memory_card, text="0%", style='Metric.TLabel')
        self.memory_metric.pack(pady=(5,0))

        self.memory_progress = ttk.Progressbar(self.memory_card, mode='determinate', length=200)
        self.memory_progress.pack(pady=(10,0), fill='x')

        # Network Card
        self.network_card = ttk.Frame(metrics_frame, style='Card.TFrame', padding="15")
        self.network_card.pack(side='left', fill='both', expand=True, padx=(10,0))

        ttk.Label(self.network_card, text="Network I/O", style='MetricLabel.TLabel').pack()
        self.network_metric = ttk.Label(self.network_card, text="0 MB/s", style='Metric.TLabel')
        self.network_metric.pack(pady=(5,0))

        self.network_progress = ttk.Progressbar(self.network_card, mode='determinate', length=200)
        self.network_progress.pack(pady=(10,0), fill='x')

        # Statistics row
        stats_frame = ttk.Frame(self.main_frame, style='Dashboard.TFrame')
        stats_frame.pack(fill='x', pady=(0,20))

        # Requests Card
        requests_card = ttk.Frame(stats_frame, style='Card.TFrame', padding="15")
        requests_card.pack(side='left', fill='both', expand=True, padx=(0,10))

        ttk.Label(requests_card, text="Total Requests", style='MetricLabel.TLabel').pack()
        self.requests_metric = ttk.Label(requests_card, text="0", style='Metric.TLabel')
        self.requests_metric.pack(pady=(5,0))

        # Users Card
        users_card = ttk.Frame(stats_frame, style='Card.TFrame', padding="15")
        users_card.pack(side='left', fill='both', expand=True, padx=(10,0))

        ttk.Label(users_card, text="Active Users", style='MetricLabel.TLabel').pack()
        self.users_metric = ttk.Label(users_card, text="0", style='Metric.TLabel')
        self.users_metric.pack(pady=(5,0))

Setelah statistics cards, lanjutkan dengan chart area dan controls:

        # Chart area (simulated dengan text widget)
        chart_frame = ttk.LabelFrame(self.main_frame, text="Real-time Monitoring", padding="15")
        chart_frame.pack(fill='both', expand=True, pady=(0,20))

        self.chart_text = tk.Text(chart_frame, height=15, bg='#0f1419', fg='#00ff88',
                                 font=('Consolas', 9), state='disabled')
        self.chart_text.pack(fill='both', expand=True)

        # Scrollbar untuk chart
        chart_scroll = ttk.Scrollbar(chart_frame, orient="vertical", command=self.chart_text.yview)
        chart_scroll.pack(side="right", fill="y")
        self.chart_text.configure(yscrollcommand=chart_scroll.set)

        # Control buttons
        control_frame = ttk.Frame(self.main_frame, style='Dashboard.TFrame')
        control_frame.pack(fill='x')

        ttk.Button(control_frame, text="▶ Start Monitoring",
                  style='Control.TButton', command=self.start_monitoring).pack(side='left', padx=(0,10))
        ttk.Button(control_frame, text="⏸ Stop Monitoring",
                  style='Control.TButton', command=self.stop_monitoring).pack(side='left', padx=(0,10))
        ttk.Button(control_frame, text="🗑 Clear Data",
                  style='Control.TButton', command=self.clear_data).pack(side='left', padx=(0,10))
        ttk.Button(control_frame, text="📊 Generate Report",
                  style='Control.TButton', command=self.generate_report).pack(side='right')

        # Start time updates
        self._update_time()

Setelah interface setup, lanjutkan dengan monitoring methods:

    def _update_time(self):
        current_time = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
        self.time_label.config(text=current_time)
        self.root.after(1000, self._update_time)

    def start_monitoring(self):
        if not self.is_running:
            self.is_running = True
            self.status_label.config(text="● Running", foreground='#00ff88')
            threading.Thread(target=self._monitoring_loop, daemon=True).start()

    def stop_monitoring(self):
        self.is_running = False
        self.status_label.config(text="● Stopped", foreground='#ff4444')

    def _monitoring_loop(self):
        while self.is_running:
            # Generate random data
            self.current_cpu = random.randint(10, 90)
            self.current_memory = random.randint(20, 80)
            self.current_network = random.uniform(0.1, 10.0)

            # Update requests and users
            self.total_requests += random.randint(1, 5)
            self.active_users = random.randint(50, 200)

            # Store data for chart
            self.cpu_data.append(self.current_cpu)
            self.memory_data.append(self.current_memory)
            self.network_data.append(self.current_network)

            # Limit data points
            if len(self.cpu_data) > self.max_data_points:
                self.cpu_data.pop(0)
                self.memory_data.pop(0)
                self.network_data.pop(0)

            # Update UI
            self._update_metrics()
            self._update_chart()

            time.sleep(1)  # Update every second

    def _update_metrics(self):
        # Update metric displays
        self.cpu_metric.config(text=f"{self.current_cpu}%")
        self.cpu_progress['value'] = self.current_cpu

        self.memory_metric.config(text=f"{self.current_memory}%")
        self.memory_progress['value'] = self.current_memory

        self.network_metric.config(text=f"{self.current_network:.1f} MB/s")
        self.network_progress['value'] = min(self.current_network * 10, 100)

        self.requests_metric.config(text=str(self.total_requests))
        self.users_metric.config(text=str(self.active_users))

Setelah monitoring methods, lanjutkan dengan chart dan utility methods:

    def _update_chart(self):
        if not self.cpu_data:
            return

        self.chart_text.config(state='normal')

        # Create simple ASCII chart
        timestamp = datetime.now().strftime("%H:%M:%S")
        cpu_bar = "█" * (self.current_cpu // 5)
        memory_bar = "█" * (self.current_memory // 5)
        network_bar = "█" * min(int(self.current_network), 20)

        chart_line = f"[{timestamp}] CPU: {cpu_bar:<20} {self.current_cpu}% | "
        chart_line += f"MEM: {memory_bar:<20} {self.current_memory}% | "
        chart_line += f"NET: {network_bar:<20} {self.current_network:.1f}MB/s\n"

        self.chart_text.insert('end', chart_line)
        self.chart_text.see('end')
        self.chart_text.config(state='disabled')

    def clear_data(self):
        self.cpu_data.clear()
        self.memory_data.clear()
        self.network_data.clear()
        self.total_requests = 0

        self.chart_text.config(state='normal')
        self.chart_text.delete('1.0', 'end')
        self.chart_text.config(state='disabled')

        # Reset metrics
        self.cpu_metric.config(text="0%")
        self.memory_metric.config(text="0%")
        self.network_metric.config(text="0 MB/s")
        self.requests_metric.config(text="0")
        self.users_metric.config(text="0")

        self.cpu_progress['value'] = 0
        self.memory_progress['value'] = 0
        self.network_progress['value'] = 0

    def generate_report(self):
        if not self.cpu_data:
            return

        # Generate summary report
        avg_cpu = sum(self.cpu_data) / len(self.cpu_data)
        avg_memory = sum(self.memory_data) / len(self.memory_data)
        avg_network = sum(self.network_data) / len(self.network_data)

        report = f"\n{'='*60}\n"
        report += f"SYSTEM PERFORMANCE REPORT - {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}\n"
        report += f"{'='*60}\n"
        report += f"Average CPU Usage: {avg_cpu:.1f}%\n"
        report += f"Average Memory Usage: {avg_memory:.1f}%\n"
        report += f"Average Network I/O: {avg_network:.1f} MB/s\n"
        report += f"Total Requests: {self.total_requests}\n"
        report += f"Current Active Users: {self.active_users}\n"
        report += f"Data Points Collected: {len(self.cpu_data)}\n"
        report += f"{'='*60}\n\n"

        self.chart_text.config(state='normal')
        self.chart_text.insert('end', report)
        self.chart_text.see('end')
        self.chart_text.config(state='disabled')

    def run(self):
        self.root.mainloop()

# Jalankan aplikasi
if __name__ == "__main__":
    app = InteractiveDashboard()
    app.run()

Tugas: Jalankan aplikasi dashboard dan klik Start Monitoring untuk memulai simulasi data real-time. Amati bagaimana metrics berubah setiap detik, progress bar terupdate, dan chart menampilkan data historis. Coba Generate Report untuk melihat summary, lalu Clear Data untuk reset. Perhatikan bagaimana threading memungkinkan UI tetap responsif selama update data.