File Management dalam GUI Tkinter
Bagian 1: File Dialogs dan Operasi File Dasar
File management adalah salah satu aspek penting dalam pengembangan aplikasi desktop. Pengguna perlu dapat membuka, menyimpan, dan mengelola file dengan mudah melalui interface yang intuitif. Dalam bagian ini, kita akan mempelajari cara menggunakan file dialogs dan melakukan operasi file dasar dalam aplikasi GUI.
1.1 Memahami File Dialogs
File dialog adalah jendela standar sistem operasi yang memungkinkan pengguna untuk memilih file atau direktori. Dialog ini memberikan interface yang familiar dan konsisten di semua aplikasi, sehingga pengguna tidak perlu mempelajari cara baru untuk setiap aplikasi.
Tkinter menyediakan beberapa jenis file dialog melalui modul filedialog:
Open File Dialog: Digunakan untuk memilih file yang akan dibuka. Dialog ini menampilkan struktur direktori dan memungkinkan pengguna untuk navigasi dan memilih file.
Save File Dialog: Digunakan untuk menentukan lokasi dan nama file yang akan disimpan. Dialog ini juga memungkinkan pengguna untuk membuat direktori baru.
Directory Dialog: Digunakan untuk memilih direktori atau folder, bukan file individual.
1.2 Keuntungan Menggunakan File Dialogs
Menggunakan file dialogs standar memberikan beberapa keuntungan:
Konsistensi: Pengguna sudah familiar dengan tampilan dan cara kerja dialog standar sistem operasi.
Fitur Lengkap: Dialog standar sudah dilengkapi dengan fitur seperti preview, sorting, filtering, dan navigasi yang canggih.
Keamanan: Dialog standar menangani validasi path dan permission secara otomatis.
Accessibility: Dialog standar sudah mendukung fitur accessibility untuk pengguna dengan kebutuhan khusus.
1.3 Praktikum 1: File Explorer Sederhana
Mari kita buat aplikasi file explorer sederhana untuk memahami penggunaan file dialogs.
Praktikum 1.1: Setup Aplikasi File Explorer
Buat file baru bernama file_explorer.py:
import tkinter as tk
from tkinter import filedialog, messagebox, ttk
import os
from datetime import datetime
import shutil
class FileExplorer:
def __init__(self):
self.window = tk.Tk()
self.window.title("File Explorer - File Management Demo")
self.window.geometry("800x600")
self.window.configure(bg="lightsteelblue")
# Variabel untuk menyimpan informasi file
self.current_file = None
self.current_directory = os.getcwd()
self.selected_files = []
self.buat_interface()
self.update_directory_info()
def buat_interface(self):
# Header dengan informasi direktori
header_frame = tk.Frame(self.window, bg="darkblue", height=60)
header_frame.pack(fill=tk.X)
header_frame.pack_propagate(False)
tk.Label(
header_frame,
text="FILE EXPLORER",
font=("Arial", 16, "bold"),
fg="white",
bg="darkblue"
).pack(pady=15)
# Frame untuk path dan navigasi
nav_frame = tk.Frame(self.window, bg="lightgray", height=40)
nav_frame.pack(fill=tk.X)
nav_frame.pack_propagate(False)
tk.Label(
nav_frame,
text="Current Directory:",
font=("Arial", 10, "bold"),
bg="lightgray"
).pack(side=tk.LEFT, padx=10, pady=10)
self.path_label = tk.Label(
nav_frame,
text=self.current_directory,
font=("Arial", 10),
bg="lightgray",
fg="blue",
anchor="w"
)
self.path_label.pack(side=tk.LEFT, fill=tk.X, expand=True, pady=10)
Tugas: Jalankan kode ini dan pastikan header dan navigation bar muncul dengan informasi direktori saat ini.
Praktikum 1.2: Menambahkan Toolbar dengan File Operations
Tambahkan toolbar dengan tombol-tombol untuk operasi file:
# Toolbar dengan tombol operasi file
toolbar = tk.Frame(self.window, bg="lightgray", relief=tk.RAISED, bd=1)
toolbar.pack(fill=tk.X, padx=2, pady=2)
# Tombol Open File
btn_open = tk.Button(
toolbar,
text="📁 Open File",
font=("Arial", 10),
bg="lightblue",
command=self.open_file,
width=12
)
btn_open.pack(side=tk.LEFT, padx=2, pady=2)
# Tombol Open Directory
btn_open_dir = tk.Button(
toolbar,
text="📂 Open Directory",
font=("Arial", 10),
bg="lightgreen",
command=self.open_directory,
width=15
)
btn_open_dir.pack(side=tk.LEFT, padx=2, pady=2)
# Tombol Create File
btn_create = tk.Button(
toolbar,
text="📄 Create File",
font=("Arial", 10),
bg="lightyellow",
command=self.create_file,
width=12
)
btn_create.pack(side=tk.LEFT, padx=2, pady=2)
# Tombol Delete File
btn_delete = tk.Button(
toolbar,
text="🗑️ Delete",
font=("Arial", 10),
bg="lightcoral",
command=self.delete_file,
width=10
)
btn_delete.pack(side=tk.LEFT, padx=2, pady=2)
# Tombol Refresh
btn_refresh = tk.Button(
toolbar,
text="🔄 Refresh",
font=("Arial", 10),
bg="lightcyan",
command=self.refresh_view,
width=10
)
btn_refresh.pack(side=tk.LEFT, padx=2, pady=2)
Tugas: Jalankan dan lihat toolbar dengan berbagai tombol operasi file.
Praktikum 1.3: Membuat Area Informasi File
Tambahkan area untuk menampilkan informasi file yang dipilih:
# Main content area
content_frame = tk.Frame(self.window, bg="lightsteelblue")
content_frame.pack(fill=tk.BOTH, expand=True, padx=5, pady=5)
# Frame kiri untuk file list
left_frame = tk.LabelFrame(
content_frame,
text="File Information",
font=("Arial", 12, "bold"),
bg="lightsteelblue"
)
left_frame.pack(side=tk.LEFT, fill=tk.BOTH, expand=True, padx=5)
# Text widget untuk menampilkan informasi file
self.info_text = tk.Text(
left_frame,
font=("Courier", 10),
bg="white",
wrap=tk.WORD,
height=15
)
# Scrollbar untuk info text
info_scrollbar = tk.Scrollbar(left_frame, orient=tk.VERTICAL, command=self.info_text.yview)
self.info_text.configure(yscrollcommand=info_scrollbar.set)
self.info_text.pack(side=tk.LEFT, fill=tk.BOTH, expand=True, padx=5, pady=5)
info_scrollbar.pack(side=tk.RIGHT, fill=tk.Y, pady=5)
# Frame kanan untuk preview
right_frame = tk.LabelFrame(
content_frame,
text="File Preview",
font=("Arial", 12, "bold"),
bg="lightsteelblue"
)
right_frame.pack(side=tk.RIGHT, fill=tk.BOTH, expand=True, padx=5)
# Text widget untuk preview file
self.preview_text = tk.Text(
right_frame,
font=("Courier", 9),
bg="lightyellow",
wrap=tk.WORD,
height=15,
state=tk.DISABLED
)
# Scrollbar untuk preview
preview_scrollbar = tk.Scrollbar(right_frame, orient=tk.VERTICAL, command=self.preview_text.yview)
self.preview_text.configure(yscrollcommand=preview_scrollbar.set)
self.preview_text.pack(side=tk.LEFT, fill=tk.BOTH, expand=True, padx=5, pady=5)
preview_scrollbar.pack(side=tk.RIGHT, fill=tk.Y, pady=5)
Tugas: Jalankan dan lihat layout dengan dua panel: informasi file dan preview.
Praktikum 1.4: Implementasi Open File Dialog
Tambahkan method untuk membuka file:
def open_file(self):
"""Method untuk membuka file menggunakan file dialog"""
# Definisikan jenis file yang didukung
file_types = [
("Text files", "*.txt"),
("Python files", "*.py"),
("JSON files", "*.json"),
("CSV files", "*.csv"),
("All files", "*.*")
]
# Tampilkan dialog open file
filename = filedialog.askopenfilename(
title="Select a file to open",
filetypes=file_types,
initialdir=self.current_directory
)
if filename: # Jika user memilih file (tidak cancel)
try:
self.current_file = filename
self.current_directory = os.path.dirname(filename)
self.update_directory_info()
self.show_file_info(filename)
self.preview_file(filename)
messagebox.showinfo("Success", f"File opened: {os.path.basename(filename)}")
except Exception as e:
messagebox.showerror("Error", f"Cannot open file: {str(e)}")
def show_file_info(self, filepath):
"""Method untuk menampilkan informasi detail file"""
try:
# Dapatkan informasi file
stat_info = os.stat(filepath)
file_size = stat_info.st_size
modified_time = datetime.fromtimestamp(stat_info.st_mtime)
created_time = datetime.fromtimestamp(stat_info.st_ctime)
# Format informasi
info = f"""FILE INFORMATION
{'='*50}
File Name: {os.path.basename(filepath)}
Full Path: {filepath}
Directory: {os.path.dirname(filepath)}
File Size: {self.format_file_size(file_size)}
Extension: {os.path.splitext(filepath)[1] or 'No extension'}
Created: {created_time.strftime('%Y-%m-%d %H:%M:%S')}
Modified: {modified_time.strftime('%Y-%m-%d %H:%M:%S')}
Permissions:
- Readable: {'Yes' if os.access(filepath, os.R_OK) else 'No'}
- Writable: {'Yes' if os.access(filepath, os.W_OK) else 'No'}
- Executable: {'Yes' if os.access(filepath, os.X_OK) else 'No'}
File Type: {self.get_file_type(filepath)}
"""
# Tampilkan di info text
self.info_text.delete(1.0, tk.END)
self.info_text.insert(1.0, info)
except Exception as e:
error_info = f"Error getting file information: {str(e)}"
self.info_text.delete(1.0, tk.END)
self.info_text.insert(1.0, error_info)
def format_file_size(self, size_bytes):
"""Method untuk format ukuran file ke format yang mudah dibaca"""
if size_bytes < 1024:
return f"{size_bytes} bytes"
elif size_bytes < 1024**2:
return f"{size_bytes/1024:.1f} KB"
elif size_bytes < 1024**3:
return f"{size_bytes/1024**2:.1f} MB"
else:
return f"{size_bytes/1024**3:.1f} GB"
def get_file_type(self, filepath):
"""Method untuk menentukan jenis file berdasarkan ekstensi"""
extension = os.path.splitext(filepath)[1].lower()
file_types = {
'.txt': 'Text Document',
'.py': 'Python Script',
'.json': 'JSON Data',
'.csv': 'CSV Data',
'.html': 'HTML Document',
'.css': 'CSS Stylesheet',
'.js': 'JavaScript',
'.jpg': 'JPEG Image',
'.png': 'PNG Image',
'.pdf': 'PDF Document',
'.docx': 'Word Document',
'.xlsx': 'Excel Spreadsheet'
}
return file_types.get(extension, 'Unknown File Type')
Tugas: Jalankan aplikasi dan coba buka berbagai jenis file. Perhatikan informasi detail yang ditampilkan di panel kiri.
Praktikum 1.5: Implementasi File Preview
Tambahkan method untuk preview file:
def preview_file(self, filepath):
"""Method untuk preview isi file"""
try:
file_size = os.path.getsize(filepath)
# Batasi preview untuk file besar
if file_size > 1024 * 1024: # 1MB
preview_content = f"File too large for preview ({self.format_file_size(file_size)})\n"
preview_content += "Please use appropriate application to view this file."
else:
# Coba baca file sebagai text
try:
with open(filepath, 'r', encoding='utf-8') as file:
content = file.read()
# Batasi jumlah karakter yang ditampilkan
if len(content) > 5000:
preview_content = content[:5000] + "\n\n... (Content truncated for preview)"
else:
preview_content = content
except UnicodeDecodeError:
# Jika tidak bisa dibaca sebagai text, coba sebagai binary
try:
with open(filepath, 'rb') as file:
binary_content = file.read(500) # Baca 500 bytes pertama
preview_content = "Binary file detected. Showing first 500 bytes as hex:\n\n"
preview_content += ' '.join(f'{byte:02x}' for byte in binary_content)
except Exception:
preview_content = "Cannot preview this file type."
# Tampilkan preview
self.preview_text.config(state=tk.NORMAL)
self.preview_text.delete(1.0, tk.END)
self.preview_text.insert(1.0, preview_content)
self.preview_text.config(state=tk.DISABLED)
except Exception as e:
error_preview = f"Error previewing file: {str(e)}"
self.preview_text.config(state=tk.NORMAL)
self.preview_text.delete(1.0, tk.END)
self.preview_text.insert(1.0, error_preview)
self.preview_text.config(state=tk.DISABLED)
Tugas: Test preview dengan membuka file teks, Python script, dan file binary. Perhatikan bagaimana aplikasi menangani berbagai jenis file.
Praktikum 1.6: Implementasi Open Directory
Tambahkan method untuk membuka direktori:
def open_directory(self):
"""Method untuk membuka direktori menggunakan directory dialog"""
directory = filedialog.askdirectory(
title="Select a directory",
initialdir=self.current_directory
)
if directory:
try:
self.current_directory = directory
self.update_directory_info()
self.show_directory_contents(directory)
messagebox.showinfo("Success", f"Directory opened: {directory}")
except Exception as e:
messagebox.showerror("Error", f"Cannot open directory: {str(e)}")
def show_directory_contents(self, directory):
"""Method untuk menampilkan isi direktori"""
try:
# Dapatkan daftar file dan folder
items = os.listdir(directory)
# Pisahkan folder dan file
folders = []
files = []
for item in items:
item_path = os.path.join(directory, item)
if os.path.isdir(item_path):
folders.append(item)
else:
files.append(item)
# Sort alphabetically
folders.sort()
files.sort()
# Format informasi direktori
dir_info = f"""DIRECTORY CONTENTS
{'='*50}
Directory: {directory}
Total Items: {len(items)} ({len(folders)} folders, {len(files)} files)
FOLDERS ({len(folders)}):
{'-'*30}
"""
for folder in folders:
folder_path = os.path.join(directory, folder)
try:
folder_size = self.get_directory_size(folder_path)
dir_info += f"📁 {folder} ({self.format_file_size(folder_size)})\n"
except:
dir_info += f"📁 {folder} (Size unknown)\n"
dir_info += f"\nFILES ({len(files)}):\n{'-'*30}\n"
for file in files:
file_path = os.path.join(directory, file)
try:
file_size = os.path.getsize(file_path)
file_ext = os.path.splitext(file)[1]
dir_info += f"📄 {file} ({self.format_file_size(file_size)}) {file_ext}\n"
except:
dir_info += f"📄 {file} (Size unknown)\n"
# Tampilkan di info text
self.info_text.delete(1.0, tk.END)
self.info_text.insert(1.0, dir_info)
# Clear preview
self.preview_text.config(state=tk.NORMAL)
self.preview_text.delete(1.0, tk.END)
self.preview_text.insert(1.0, "Select a file to preview its contents")
self.preview_text.config(state=tk.DISABLED)
except Exception as e:
error_info = f"Error reading directory: {str(e)}"
self.info_text.delete(1.0, tk.END)
self.info_text.insert(1.0, error_info)
def get_directory_size(self, directory):
"""Method untuk menghitung ukuran total direktori"""
total_size = 0
try:
for dirpath, dirnames, filenames in os.walk(directory):
for filename in filenames:
filepath = os.path.join(dirpath, filename)
try:
total_size += os.path.getsize(filepath)
except:
continue
except:
pass
return total_size
def update_directory_info(self):
"""Method untuk update informasi direktori di navigation bar"""
self.path_label.config(text=self.current_directory)
Tugas: Test open directory dan lihat bagaimana aplikasi menampilkan daftar file dan folder dengan informasi ukuran.
Praktikum 1.7: Implementasi Create dan Delete File
Tambahkan method untuk membuat dan menghapus file:
def create_file(self):
"""Method untuk membuat file baru"""
# Dialog untuk memilih lokasi dan nama file
filename = filedialog.asksaveasfilename(
title="Create new file",
defaultextension=".txt",
filetypes=[
("Text files", "*.txt"),
("Python files", "*.py"),
("JSON files", "*.json"),
("All files", "*.*")
],
initialdir=self.current_directory
)
if filename:
try:
# Buat file kosong
with open(filename, 'w', encoding='utf-8') as file:
file.write("") # File kosong
self.current_file = filename
self.current_directory = os.path.dirname(filename)
self.update_directory_info()
self.show_file_info(filename)
# Clear preview
self.preview_text.config(state=tk.NORMAL)
self.preview_text.delete(1.0, tk.END)
self.preview_text.insert(1.0, "New empty file created")
self.preview_text.config(state=tk.DISABLED)
messagebox.showinfo("Success", f"File created: {os.path.basename(filename)}")
except Exception as e:
messagebox.showerror("Error", f"Cannot create file: {str(e)}")
def delete_file(self):
"""Method untuk menghapus file"""
if not self.current_file:
messagebox.showwarning("Warning", "No file selected for deletion!")
return
# Konfirmasi penghapusan
filename = os.path.basename(self.current_file)
if messagebox.askyesno("Confirm Delete", f"Are you sure you want to delete '{filename}'?\n\nThis action cannot be undone."):
try:
os.remove(self.current_file)
# Clear displays
self.info_text.delete(1.0, tk.END)
self.info_text.insert(1.0, "File deleted successfully")
self.preview_text.config(state=tk.NORMAL)
self.preview_text.delete(1.0, tk.END)
self.preview_text.insert(1.0, "No file selected")
self.preview_text.config(state=tk.DISABLED)
self.current_file = None
messagebox.showinfo("Success", f"File '{filename}' has been deleted")
except Exception as e:
messagebox.showerror("Error", f"Cannot delete file: {str(e)}")
def refresh_view(self):
"""Method untuk refresh tampilan"""
if self.current_file and os.path.exists(self.current_file):
self.show_file_info(self.current_file)
self.preview_file(self.current_file)
elif os.path.exists(self.current_directory):
self.show_directory_contents(self.current_directory)
else:
# Jika direktori tidak ada, kembali ke home directory
self.current_directory = os.path.expanduser("~")
self.update_directory_info()
self.show_directory_contents(self.current_directory)
messagebox.showinfo("Refresh", "View refreshed successfully")
def jalankan(self):
"""Method untuk menjalankan aplikasi"""
self.window.mainloop()
# Untuk menjalankan aplikasi
if __name__ == "__main__":
app = FileExplorer()
app.jalankan()
Tugas: Test semua fitur file explorer: buka file, buka direktori, buat file baru, hapus file, dan refresh. Perhatikan bagaimana aplikasi menangani berbagai skenario dan memberikan feedback yang sesuai.
Bagian 2: Text Editor dengan File Management
Dalam bagian ini, kita akan membuat text editor yang lebih canggih dengan fitur file management yang lengkap. Text editor adalah salah satu aplikasi yang paling umum menggunakan file operations, sehingga cocok untuk mempelajari konsep file management secara praktis.
2.1 Fitur-Fitur Text Editor Modern
Text editor modern memiliki beberapa fitur standar yang harus ada:
File Operations: New, Open, Save, Save As, dan Recent Files.
Edit Operations: Undo, Redo, Cut, Copy, Paste, Find, Replace.
View Options: Word wrap, line numbers, syntax highlighting.
Status Information: File status, cursor position, file encoding.
2.2 State Management dalam Text Editor
Text editor perlu mengelola berbagai state:
File State: Apakah file sudah disimpan, sudah dimodifikasi, atau masih baru.
Edit State: History untuk undo/redo operations.
View State: Pengaturan tampilan seperti font, theme, dan layout.
2.3 Praktikum 2: Advanced Text Editor
Mari kita buat text editor yang canggih dengan fitur file management lengkap.
Praktikum 2.1: Setup Text Editor dengan Menu System
Buat file baru bernama advanced_text_editor.py:
import tkinter as tk
from tkinter import filedialog, messagebox, scrolledtext, font
import os
from datetime import datetime
class AdvancedTextEditor:
def __init__(self):
self.window = tk.Tk()
self.window.title("Advanced Text Editor")
self.window.geometry("900x700")
self.window.configure(bg="white")
# File management variables
self.current_file = None
self.is_modified = False
self.recent_files = []
self.max_recent_files = 10
# Editor settings
self.current_font = ("Consolas", 12)
self.word_wrap = True
self.show_line_numbers = True
# Undo/Redo stacks
self.undo_stack = []
self.redo_stack = []
self.buat_interface()
self.buat_menu_system()
self.bind_events()
self.update_title()
self.update_status()
def buat_interface(self):
# Toolbar
self.toolbar = tk.Frame(self.window, bg="lightgray", relief=tk.RAISED, bd=1)
self.toolbar.pack(fill=tk.X)
# Toolbar buttons
self.buat_toolbar_buttons()
# Main editor frame
editor_frame = tk.Frame(self.window)
editor_frame.pack(fill=tk.BOTH, expand=True, padx=2, pady=2)
# Line numbers frame (optional)
self.line_numbers_frame = tk.Frame(editor_frame, bg="lightgray", width=50)
# Text area dengan scrollbar
self.text_area = scrolledtext.ScrolledText(
editor_frame,
wrap=tk.WORD if self.word_wrap else tk.NONE,
undo=True,
font=self.current_font,
bg="white",
fg="black",
insertbackground="black",
selectbackground="lightblue"
)
self.text_area.pack(fill=tk.BOTH, expand=True)
# Status bar
self.status_bar = tk.Frame(self.window, relief=tk.SUNKEN, bd=1)
self.status_bar.pack(fill=tk.X, side=tk.BOTTOM)
# Status bar labels
self.status_label = tk.Label(
self.status_bar,
text="Ready",
anchor=tk.W,
font=("Arial", 9)
)
self.status_label.pack(side=tk.LEFT, padx=5)
self.position_label = tk.Label(
self.status_bar,
text="Line: 1, Column: 1",
anchor=tk.E,
font=("Arial", 9)
)
self.position_label.pack(side=tk.RIGHT, padx=5)
self.file_info_label = tk.Label(
self.status_bar,
text="Untitled",
anchor=tk.CENTER,
font=("Arial", 9)
)
self.file_info_label.pack(side=tk.RIGHT, padx=20)
Tugas: Jalankan kode ini dan lihat layout dasar text editor dengan toolbar dan status bar.
Praktikum 2.2: Membuat Toolbar Buttons
Tambahkan method untuk membuat tombol toolbar:
def buat_toolbar_buttons(self):
"""Method untuk membuat tombol-tombol di toolbar"""
# New file button
btn_new = tk.Button(
self.toolbar,
text="📄 New",
command=self.new_file,
relief=tk.FLAT,
bg="lightgray",
font=("Arial", 9)
)
btn_new.pack(side=tk.LEFT, padx=2, pady=2)
# Open file button
btn_open = tk.Button(
self.toolbar,
text="📁 Open",
command=self.open_file,
relief=tk.FLAT,
bg="lightgray",
font=("Arial", 9)
)
btn_open.pack(side=tk.LEFT, padx=2, pady=2)
# Save file button
btn_save = tk.Button(
self.toolbar,
text="💾 Save",
command=self.save_file,
relief=tk.FLAT,
bg="lightgray",
font=("Arial", 9)
)
btn_save.pack(side=tk.LEFT, padx=2, pady=2)
# Separator
separator1 = tk.Frame(self.toolbar, width=2, bg="gray", relief=tk.SUNKEN, bd=1)
separator1.pack(side=tk.LEFT, fill=tk.Y, padx=5, pady=2)
# Undo button
btn_undo = tk.Button(
self.toolbar,
text="↶ Undo",
command=self.undo_action,
relief=tk.FLAT,
bg="lightgray",
font=("Arial", 9)
)
btn_undo.pack(side=tk.LEFT, padx=2, pady=2)
# Redo button
btn_redo = tk.Button(
self.toolbar,
text="↷ Redo",
command=self.redo_action,
relief=tk.FLAT,
bg="lightgray",
font=("Arial", 9)
)
btn_redo.pack(side=tk.LEFT, padx=2, pady=2)
# Separator
separator2 = tk.Frame(self.toolbar, width=2, bg="gray", relief=tk.SUNKEN, bd=1)
separator2.pack(side=tk.LEFT, fill=tk.Y, padx=5, pady=2)
# Find button
btn_find = tk.Button(
self.toolbar,
text="🔍 Find",
command=self.show_find_dialog,
relief=tk.FLAT,
bg="lightgray",
font=("Arial", 9)
)
btn_find.pack(side=tk.LEFT, padx=2, pady=2)
# Font button
btn_font = tk.Button(
self.toolbar,
text="🔤 Font",
command=self.change_font,
relief=tk.FLAT,
bg="lightgray",
font=("Arial", 9)
)
btn_font.pack(side=tk.LEFT, padx=2, pady=2)
Tugas: Jalankan dan lihat toolbar dengan berbagai tombol yang sudah dikelompokkan dengan separator.
Praktikum 2.3: Membuat Menu System
Tambahkan method untuk membuat menu system yang lengkap:
def buat_menu_system(self):
"""Method untuk membuat menu system lengkap"""
menubar = tk.Menu(self.window)
self.window.config(menu=menubar)
# File Menu
file_menu = tk.Menu(menubar, tearoff=0)
menubar.add_cascade(label="File", menu=file_menu)
file_menu.add_command(label="New", command=self.new_file, accelerator="Ctrl+N")
file_menu.add_command(label="Open...", command=self.open_file, accelerator="Ctrl+O")
file_menu.add_separator()
file_menu.add_command(label="Save", command=self.save_file, accelerator="Ctrl+S")
file_menu.add_command(label="Save As...", command=self.save_as_file, accelerator="Ctrl+Shift+S")
file_menu.add_separator()
# Recent files submenu
self.recent_menu = tk.Menu(file_menu, tearoff=0)
file_menu.add_cascade(label="Recent Files", menu=self.recent_menu)
self.update_recent_menu()
file_menu.add_separator()
file_menu.add_command(label="Exit", command=self.on_closing, accelerator="Ctrl+Q")
# Edit Menu
edit_menu = tk.Menu(menubar, tearoff=0)
menubar.add_cascade(label="Edit", menu=edit_menu)
edit_menu.add_command(label="Undo", command=self.undo_action, accelerator="Ctrl+Z")
edit_menu.add_command(label="Redo", command=self.redo_action, accelerator="Ctrl+Y")
edit_menu.add_separator()
edit_menu.add_command(label="Cut", command=self.cut_text, accelerator="Ctrl+X")
edit_menu.add_command(label="Copy", command=self.copy_text, accelerator="Ctrl+C")
edit_menu.add_command(label="Paste", command=self.paste_text, accelerator="Ctrl+V")
edit_menu.add_separator()
edit_menu.add_command(label="Select All", command=self.select_all, accelerator="Ctrl+A")
edit_menu.add_command(label="Find...", command=self.show_find_dialog, accelerator="Ctrl+F")
edit_menu.add_command(label="Replace...", command=self.show_replace_dialog, accelerator="Ctrl+H")
# View Menu
view_menu = tk.Menu(menubar, tearoff=0)
menubar.add_cascade(label="View", menu=view_menu)
# Word wrap option
self.wrap_var = tk.BooleanVar(value=self.word_wrap)
view_menu.add_checkbutton(
label="Word Wrap",
variable=self.wrap_var,
command=self.toggle_word_wrap
)
# Line numbers option
self.line_numbers_var = tk.BooleanVar(value=self.show_line_numbers)
view_menu.add_checkbutton(
label="Show Line Numbers",
variable=self.line_numbers_var,
command=self.toggle_line_numbers
)
view_menu.add_separator()
view_menu.add_command(label="Change Font...", command=self.change_font)
# Tools Menu
tools_menu = tk.Menu(menubar, tearoff=0)
menubar.add_cascade(label="Tools", menu=tools_menu)
tools_menu.add_command(label="Word Count", command=self.show_word_count)
tools_menu.add_command(label="Character Count", command=self.show_char_count)
tools_menu.add_separator()
tools_menu.add_command(label="Insert Date/Time", command=self.insert_datetime)
# Help Menu
help_menu = tk.Menu(menubar, tearoff=0)
menubar.add_cascade(label="Help", menu=help_menu)
help_menu.add_command(label="About", command=self.show_about)
Tugas: Jalankan dan explore menu system yang lengkap. Perhatikan struktur menu dan submenu.
Praktikum 2.4: Implementasi File Operations
Tambahkan method untuk operasi file:
def new_file(self):
"""Method untuk membuat file baru"""
if self.is_modified:
if not self.confirm_save():
return
self.text_area.delete(1.0, tk.END)
self.current_file = None
self.is_modified = False
self.update_title()
self.update_status("New file created")
def open_file(self):
"""Method untuk membuka file"""
if self.is_modified:
if not self.confirm_save():
return
file_types = [
("Text files", "*.txt"),
("Python files", "*.py"),
("JavaScript files", "*.js"),
("HTML files", "*.html"),
("CSS files", "*.css"),
("JSON files", "*.json"),
("All files", "*.*")
]
filename = filedialog.askopenfilename(
title="Open File",
filetypes=file_types
)
if filename:
try:
with open(filename, 'r', encoding='utf-8') as file:
content = file.read()
self.text_area.delete(1.0, tk.END)
self.text_area.insert(1.0, content)
self.current_file = filename
self.is_modified = False
self.add_to_recent_files(filename)
self.update_title()
self.update_status(f"Opened: {os.path.basename(filename)}")
except Exception as e:
messagebox.showerror("Error", f"Cannot open file: {str(e)}")
def save_file(self):
"""Method untuk menyimpan file"""
if self.current_file:
try:
content = self.text_area.get(1.0, tk.END)
with open(self.current_file, 'w', encoding='utf-8') as file:
file.write(content)
self.is_modified = False
self.update_title()
self.update_status(f"Saved: {os.path.basename(self.current_file)}")
return True
except Exception as e:
messagebox.showerror("Error", f"Cannot save file: {str(e)}")
return False
else:
return self.save_as_file()
def save_as_file(self):
"""Method untuk save as"""
file_types = [
("Text files", "*.txt"),
("Python files", "*.py"),
("JavaScript files", "*.js"),
("HTML files", "*.html"),
("CSS files", "*.css"),
("JSON files", "*.json"),
("All files", "*.*")
]
filename = filedialog.asksaveasfilename(
title="Save As",
filetypes=file_types,
defaultextension=".txt"
)
if filename:
try:
content = self.text_area.get(1.0, tk.END)
with open(filename, 'w', encoding='utf-8') as file:
file.write(content)
self.current_file = filename
self.is_modified = False
self.add_to_recent_files(filename)
self.update_title()
self.update_status(f"Saved as: {os.path.basename(filename)}")
return True
except Exception as e:
messagebox.showerror("Error", f"Cannot save file: {str(e)}")
return False
return False
Tugas: Test operasi file dasar: new, open, save, dan save as. Perhatikan bagaimana status bar dan title window terupdate.
Praktikum 2.5: Recent Files Management
Tambahkan method untuk mengelola recent files:
def add_to_recent_files(self, filename):
"""Method untuk menambah file ke recent files list"""
# Hapus file dari list jika sudah ada
if filename in self.recent_files:
self.recent_files.remove(filename)
# Tambah di awal list
self.recent_files.insert(0, filename)
# Batasi jumlah recent files
if len(self.recent_files) > self.max_recent_files:
self.recent_files = self.recent_files[:self.max_recent_files]
# Update menu
self.update_recent_menu()
def update_recent_menu(self):
"""Method untuk update recent files menu"""
# Clear existing menu items
self.recent_menu.delete(0, tk.END)
if not self.recent_files:
self.recent_menu.add_command(label="No recent files", state=tk.DISABLED)
else:
for i, filename in enumerate(self.recent_files):
display_name = os.path.basename(filename)
if len(display_name) > 30:
display_name = display_name[:27] + "..."
self.recent_menu.add_command(
label=f"{i+1}. {display_name}",
command=lambda f=filename: self.open_recent_file(f)
)
self.recent_menu.add_separator()
self.recent_menu.add_command(
label="Clear Recent Files",
command=self.clear_recent_files
)
def open_recent_file(self, filename):
"""Method untuk membuka file dari recent files"""
if not os.path.exists(filename):
messagebox.showerror("Error", f"File not found: {filename}")
self.recent_files.remove(filename)
self.update_recent_menu()
return
if self.is_modified:
if not self.confirm_save():
return
try:
with open(filename, 'r', encoding='utf-8') as file:
content = file.read()
self.text_area.delete(1.0, tk.END)
self.text_area.insert(1.0, content)
self.current_file = filename
self.is_modified = False
self.add_to_recent_files(filename) # Move to top of recent list
self.update_title()
self.update_status(f"Opened: {os.path.basename(filename)}")
except Exception as e:
messagebox.showerror("Error", f"Cannot open file: {str(e)}")
def clear_recent_files(self):
"""Method untuk clear recent files list"""
if messagebox.askyesno("Clear Recent Files", "Clear all recent files from the list?"):
self.recent_files.clear()
self.update_recent_menu()
Tugas: Test recent files functionality dengan membuka beberapa file dan lihat bagaimana menu recent files terupdate.
Praktikum 2.6: Edit Operations
Tambahkan method untuk operasi edit:
def undo_action(self):
"""Method untuk undo"""
try:
self.text_area.edit_undo()
self.update_status("Undo performed")
except tk.TclError:
self.update_status("Nothing to undo")
def redo_action(self):
"""Method untuk redo"""
try:
self.text_area.edit_redo()
self.update_status("Redo performed")
except tk.TclError:
self.update_status("Nothing to redo")
def cut_text(self):
"""Method untuk cut text"""
try:
self.text_area.event_generate("<<Cut>>")
self.update_status("Text cut to clipboard")
except:
self.update_status("Cannot cut text")
def copy_text(self):
"""Method untuk copy text"""
try:
self.text_area.event_generate("<<Copy>>")
self.update_status("Text copied to clipboard")
except:
self.update_status("Cannot copy text")
def paste_text(self):
"""Method untuk paste text"""
try:
self.text_area.event_generate("<<Paste>>")
self.update_status("Text pasted from clipboard")
except:
self.update_status("Cannot paste text")
def select_all(self):
"""Method untuk select all text"""
self.text_area.tag_add(tk.SEL, "1.0", tk.END)
self.text_area.mark_set(tk.INSERT, "1.0")
self.text_area.see(tk.INSERT)
self.update_status("All text selected")
Tugas: Test semua operasi edit: undo, redo, cut, copy, paste, dan select all. Perhatikan feedback di status bar.
Praktikum 2.7: Find dan Replace Dialog
Tambahkan method untuk find dan replace:
def show_find_dialog(self):
"""Method untuk menampilkan dialog find"""
self.find_dialog = tk.Toplevel(self.window)
self.find_dialog.title("Find")
self.find_dialog.geometry("400x150")
self.find_dialog.transient(self.window)
self.find_dialog.grab_set()
# Find input
tk.Label(self.find_dialog, text="Find:").pack(pady=5)
self.find_entry = tk.Entry(self.find_dialog, width=40)
self.find_entry.pack(pady=5)
self.find_entry.focus()
# Options frame
options_frame = tk.Frame(self.find_dialog)
options_frame.pack(pady=5)
self.case_sensitive_var = tk.BooleanVar()
tk.Checkbutton(
options_frame,
text="Case sensitive",
variable=self.case_sensitive_var
).pack(side=tk.LEFT, padx=5)
self.whole_word_var = tk.BooleanVar()
tk.Checkbutton(
options_frame,
text="Whole word",
variable=self.whole_word_var
).pack(side=tk.LEFT, padx=5)
# Buttons frame
buttons_frame = tk.Frame(self.find_dialog)
buttons_frame.pack(pady=10)
tk.Button(
buttons_frame,
text="Find Next",
command=self.find_next,
width=10
).pack(side=tk.LEFT, padx=5)
tk.Button(
buttons_frame,
text="Find All",
command=self.find_all,
width=10
).pack(side=tk.LEFT, padx=5)
tk.Button(
buttons_frame,
text="Close",
command=self.find_dialog.destroy,
width=10
).pack(side=tk.LEFT, padx=5)
# Bind Enter key
self.find_entry.bind("<Return>", lambda e: self.find_next())
def find_next(self):
"""Method untuk find next occurrence"""
search_text = self.find_entry.get()
if not search_text:
return
# Get current cursor position
start_pos = self.text_area.index(tk.INSERT)
# Search options
case_sensitive = self.case_sensitive_var.get()
# Perform search
pos = self.text_area.search(
search_text,
start_pos,
tk.END,
nocase=not case_sensitive
)
if pos:
# Select found text
end_pos = f"{pos}+{len(search_text)}c"
self.text_area.tag_remove(tk.SEL, "1.0", tk.END)
self.text_area.tag_add(tk.SEL, pos, end_pos)
self.text_area.mark_set(tk.INSERT, end_pos)
self.text_area.see(pos)
self.update_status(f"Found: {search_text}")
else:
# Search from beginning
pos = self.text_area.search(
search_text,
"1.0",
start_pos,
nocase=not case_sensitive
)
if pos:
end_pos = f"{pos}+{len(search_text)}c"
self.text_area.tag_remove(tk.SEL, "1.0", tk.END)
self.text_area.tag_add(tk.SEL, pos, end_pos)
self.text_area.mark_set(tk.INSERT, end_pos)
self.text_area.see(pos)
self.update_status(f"Found: {search_text} (wrapped)")
else:
messagebox.showinfo("Find", f"'{search_text}' not found")
def find_all(self):
"""Method untuk find all occurrences"""
search_text = self.find_entry.get()
if not search_text:
return
# Remove previous highlights
self.text_area.tag_remove("found", "1.0", tk.END)
# Search all occurrences
start_pos = "1.0"
count = 0
while True:
pos = self.text_area.search(
search_text,
start_pos,
tk.END,
nocase=not self.case_sensitive_var.get()
)
if not pos:
break
end_pos = f"{pos}+{len(search_text)}c"
self.text_area.tag_add("found", pos, end_pos)
count += 1
start_pos = end_pos
# Configure highlight tag
self.text_area.tag_config("found", background="yellow", foreground="black")
if count > 0:
self.update_status(f"Found {count} occurrences of '{search_text}'")
else:
messagebox.showinfo("Find All", f"'{search_text}' not found")
def show_replace_dialog(self):
"""Method untuk menampilkan dialog replace"""
self.replace_dialog = tk.Toplevel(self.window)
self.replace_dialog.title("Replace")
self.replace_dialog.geometry("400x200")
self.replace_dialog.transient(self.window)
self.replace_dialog.grab_set()
# Find input
tk.Label(self.replace_dialog, text="Find:").pack(pady=5)
self.replace_find_entry = tk.Entry(self.replace_dialog, width=40)
self.replace_find_entry.pack(pady=5)
# Replace input
tk.Label(self.replace_dialog, text="Replace with:").pack(pady=5)
self.replace_with_entry = tk.Entry(self.replace_dialog, width=40)
self.replace_with_entry.pack(pady=5)
self.replace_find_entry.focus()
# Options frame
options_frame = tk.Frame(self.replace_dialog)
options_frame.pack(pady=5)
self.replace_case_var = tk.BooleanVar()
tk.Checkbutton(
options_frame,
text="Case sensitive",
variable=self.replace_case_var
).pack(side=tk.LEFT, padx=5)
# Buttons frame
buttons_frame = tk.Frame(self.replace_dialog)
buttons_frame.pack(pady=10)
tk.Button(
buttons_frame,
text="Replace",
command=self.replace_current,
width=12
).pack(side=tk.LEFT, padx=2)
tk.Button(
buttons_frame,
text="Replace All",
command=self.replace_all,
width=12
).pack(side=tk.LEFT, padx=2)
tk.Button(
buttons_frame,
text="Close",
command=self.replace_dialog.destroy,
width=12
).pack(side=tk.LEFT, padx=2)
def replace_current(self):
"""Method untuk replace current selection"""
find_text = self.replace_find_entry.get()
replace_text = self.replace_with_entry.get()
if not find_text:
return
try:
# Get current selection
selected_text = self.text_area.get(tk.SEL_FIRST, tk.SEL_LAST)
# Check if selection matches find text
if (self.replace_case_var.get() and selected_text == find_text) or \
(not self.replace_case_var.get() and selected_text.lower() == find_text.lower()):
# Replace selected text
self.text_area.delete(tk.SEL_FIRST, tk.SEL_LAST)
self.text_area.insert(tk.INSERT, replace_text)
self.update_status(f"Replaced: {find_text} -> {replace_text}")
else:
messagebox.showinfo("Replace", "No matching text selected")
except tk.TclError:
messagebox.showinfo("Replace", "No text selected")
def replace_all(self):
"""Method untuk replace all occurrences"""
find_text = self.replace_find_entry.get()
replace_text = self.replace_with_entry.get()
if not find_text:
return
content = self.text_area.get("1.0", tk.END)
if self.replace_case_var.get():
new_content = content.replace(find_text, replace_text)
count = content.count(find_text)
else:
# Case insensitive replace
import re
pattern = re.compile(re.escape(find_text), re.IGNORECASE)
new_content = pattern.sub(replace_text, content)
count = len(pattern.findall(content))
if count > 0:
self.text_area.delete("1.0", tk.END)
self.text_area.insert("1.0", new_content)
self.update_status(f"Replaced {count} occurrences")
messagebox.showinfo("Replace All", f"Replaced {count} occurrences of '{find_text}'")
else:
messagebox.showinfo("Replace All", f"'{find_text}' not found")
Tugas: Test find dan replace functionality. Coba find next, find all, replace current, dan replace all dengan berbagai opsi.
Praktikum 2.8: View Options dan Tools
Tambahkan method untuk view options dan tools:
def toggle_word_wrap(self):
"""Method untuk toggle word wrap"""
self.word_wrap = self.wrap_var.get()
self.text_area.config(wrap=tk.WORD if self.word_wrap else tk.NONE)
self.update_status(f"Word wrap {'enabled' if self.word_wrap else 'disabled'}")
def toggle_line_numbers(self):
"""Method untuk toggle line numbers"""
self.show_line_numbers = self.line_numbers_var.get()
# Implementation for line numbers would go here
self.update_status(f"Line numbers {'enabled' if self.show_line_numbers else 'disabled'}")
def change_font(self):
"""Method untuk mengubah font"""
# Simple font dialog
font_dialog = tk.Toplevel(self.window)
font_dialog.title("Change Font")
font_dialog.geometry("300x200")
font_dialog.transient(self.window)
font_dialog.grab_set()
# Font family
tk.Label(font_dialog, text="Font Family:").pack(pady=5)
font_families = ["Arial", "Times New Roman", "Courier New", "Consolas", "Verdana"]
font_var = tk.StringVar(value=self.current_font[0])
font_combo = ttk.Combobox(font_dialog, textvariable=font_var, values=font_families)
font_combo.pack(pady=5)
# Font size
tk.Label(font_dialog, text="Font Size:").pack(pady=5)
size_var = tk.IntVar(value=self.current_font[1])
size_spinbox = tk.Spinbox(font_dialog, from_=8, to=72, textvariable=size_var)
size_spinbox.pack(pady=5)
# Buttons
def apply_font():
new_font = (font_var.get(), size_var.get())
self.current_font = new_font
self.text_area.config(font=new_font)
self.update_status(f"Font changed to {new_font[0]} {new_font[1]}")
font_dialog.destroy()
buttons_frame = tk.Frame(font_dialog)
buttons_frame.pack(pady=20)
tk.Button(buttons_frame, text="Apply", command=apply_font).pack(side=tk.LEFT, padx=5)
tk.Button(buttons_frame, text="Cancel", command=font_dialog.destroy).pack(side=tk.LEFT, padx=5)
def show_word_count(self):
"""Method untuk menampilkan word count"""
content = self.text_area.get("1.0", tk.END)
words = len(content.split())
lines = content.count('\n')
messagebox.showinfo("Word Count", f"Words: {words}\nLines: {lines}")
def show_char_count(self):
"""Method untuk menampilkan character count"""
content = self.text_area.get("1.0", tk.END)
chars = len(content)
chars_no_spaces = len(content.replace(' ', '').replace('\n', '').replace('\t', ''))
messagebox.showinfo("Character Count", f"Characters: {chars}\nCharacters (no spaces): {chars_no_spaces}")
def insert_datetime(self):
"""Method untuk insert current date/time"""
current_datetime = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
self.text_area.insert(tk.INSERT, current_datetime)
self.update_status("Date/time inserted")
def show_about(self):
"""Method untuk menampilkan about dialog"""
about_text = """Advanced Text Editor
Version 1.0
A feature-rich text editor built with Python Tkinter.
Features:
• File operations (New, Open, Save, Save As)
• Recent files management
• Find and Replace
• Undo/Redo
• Word wrap and line numbers
• Font customization
• Word and character count
• Date/time insertion
Created for educational purposes."""
messagebox.showinfo("About", about_text)
Praktikum 2.9: Event Binding dan Helper Methods
Tambahkan method untuk event binding dan helper methods:
def bind_events(self):
"""Method untuk binding keyboard shortcuts dan events"""
# File operations
self.window.bind('<Control-n>', lambda e: self.new_file())
self.window.bind('<Control-o>', lambda e: self.open_file())
self.window.bind('<Control-s>', lambda e: self.save_file())
self.window.bind('<Control-Shift-S>', lambda e: self.save_as_file())
self.window.bind('<Control-q>', lambda e: self.on_closing())
# Edit operations
self.window.bind('<Control-z>', lambda e: self.undo_action())
self.window.bind('<Control-y>', lambda e: self.redo_action())
self.window.bind('<Control-x>', lambda e: self.cut_text())
self.window.bind('<Control-c>', lambda e: self.copy_text())
self.window.bind('<Control-v>', lambda e: self.paste_text())
self.window.bind('<Control-a>', lambda e: self.select_all())
# Find and replace
self.window.bind('<Control-f>', lambda e: self.show_find_dialog())
self.window.bind('<Control-h>', lambda e: self.show_replace_dialog())
# Text change event
self.text_area.bind('<Key>', self.on_text_change)
self.text_area.bind('<Button-1>', self.on_cursor_move)
self.text_area.bind('<KeyRelease>', self.on_cursor_move)
# Window close event
self.window.protocol("WM_DELETE_WINDOW", self.on_closing)
def on_text_change(self, event=None):
"""Method yang dipanggil saat text berubah"""
if not self.is_modified:
self.is_modified = True
self.update_title()
# Update cursor position after a short delay
self.window.after(10, self.update_cursor_position)
def on_cursor_move(self, event=None):
"""Method yang dipanggil saat cursor bergerak"""
self.window.after(10, self.update_cursor_position)
def update_cursor_position(self):
"""Method untuk update posisi cursor di status bar"""
try:
cursor_pos = self.text_area.index(tk.INSERT)
line, column = cursor_pos.split('.')
self.position_label.config(text=f"Line: {line}, Column: {int(column)+1}")
except:
pass
def update_title(self):
"""Method untuk update title window"""
title = "Advanced Text Editor - "
if self.current_file:
title += os.path.basename(self.current_file)
self.file_info_label.config(text=os.path.basename(self.current_file))
else:
title += "Untitled"
self.file_info_label.config(text="Untitled")
if self.is_modified:
title += " *"
self.window.title(title)
def update_status(self, message):
"""Method untuk update status bar"""
self.status_label.config(text=message)
# Clear status message after 3 seconds
self.window.after(3000, lambda: self.status_label.config(text="Ready"))
def confirm_save(self):
"""Method untuk konfirmasi save sebelum operasi lain"""
if not self.is_modified:
return True
response = messagebox.askyesnocancel(
"Save Changes",
"Do you want to save changes to the current document?"
)
if response: # Yes
return self.save_file()
elif response is False: # No
return True
else: # Cancel
return False
def on_closing(self):
"""Method yang dipanggil saat window akan ditutup"""
if self.confirm_save():
self.window.destroy()
def jalankan(self):
"""Method untuk menjalankan aplikasi"""
self.window.mainloop()
# Untuk menjalankan aplikasi
if __name__ == "__main__":
app = AdvancedTextEditor()
app.jalankan()
Tugas: Jalankan text editor lengkap dan test semua fitur: file operations, edit operations, find/replace, view options, tools, dan keyboard shortcuts. Perhatikan bagaimana status bar dan title terupdate secara real-time.
Bagian 3: Menangani Format File Berbeda
Dalam bagian ini, kita akan mempelajari cara menangani berbagai format file seperti CSV, JSON, dan XML. Setiap format memiliki struktur dan cara penanganan yang berbeda, sehingga aplikasi kita perlu dapat beradaptasi dengan format yang sesuai.
3.1 Memahami Format File Terstruktur
Format file terstruktur memiliki aturan dan sintaks khusus yang harus diikuti:
CSV (Comma Separated Values): Format tabular sederhana dengan pemisah koma atau karakter lain.
JSON (JavaScript Object Notation): Format pertukaran data yang ringan dan mudah dibaca manusia.
XML (eXtensible Markup Language): Format markup yang fleksibel untuk menyimpan dan transport data.
3.2 Keuntungan Menangani Multiple Format
Aplikasi yang dapat menangani multiple format file memberikan fleksibilitas lebih kepada pengguna dan dapat diintegrasikan dengan berbagai sistem lain.
3.3 Praktikum 3: Multi-Format File Manager
Mari kita buat aplikasi yang dapat menangani berbagai format file dengan interface yang intuitif.
Praktikum 3.1: Setup Multi-Format File Manager
Buat file baru bernama multi_format_manager.py:
import tkinter as tk
from tkinter import filedialog, messagebox, ttk
import csv
import json
import xml.etree.ElementTree as ET
import os
from datetime import datetime
class MultiFormatManager:
def __init__(self):
self.window = tk.Tk()
self.window.title("Multi-Format File Manager")
self.window.geometry("1000x700")
self.window.configure(bg="lightgray")
# Data storage
self.current_file = None
self.current_format = None
self.data = None
# Supported formats
self.supported_formats = {
'.csv': 'CSV',
'.json': 'JSON',
'.xml': 'XML',
'.txt': 'Text'
}
self.buat_interface()
def buat_interface(self):
# Header
header_frame = tk.Frame(self.window, bg="darkblue", height=60)
header_frame.pack(fill=tk.X)
header_frame.pack_propagate(False)
tk.Label(
header_frame,
text="MULTI-FORMAT FILE MANAGER",
font=("Arial", 16, "bold"),
fg="white",
bg="darkblue"
).pack(pady=15)
# Toolbar
toolbar = tk.Frame(self.window, bg="lightgray", relief=tk.RAISED, bd=1)
toolbar.pack(fill=tk.X, padx=2, pady=2)
# Load file button
btn_load = tk.Button(
toolbar,
text="📁 Load File",
command=self.load_file,
bg="lightblue",
font=("Arial", 10),
width=12
)
btn_load.pack(side=tk.LEFT, padx=2, pady=2)
# Save file button
btn_save = tk.Button(
toolbar,
text="💾 Save File",
command=self.save_file,
bg="lightgreen",
font=("Arial", 10),
width=12
)
btn_save.pack(side=tk.LEFT, padx=2, pady=2)
# Convert format button
btn_convert = tk.Button(
toolbar,
text="🔄 Convert",
command=self.show_convert_dialog,
bg="lightyellow",
font=("Arial", 10),
width=12
)
btn_convert.pack(side=tk.LEFT, padx=2, pady=2)
# Validate button
btn_validate = tk.Button(
toolbar,
text="✓ Validate",
command=self.validate_file,
bg="lightcoral",
font=("Arial", 10),
width=12
)
btn_validate.pack(side=tk.LEFT, padx=2, pady=2)
# File info label
self.file_info_label = tk.Label(
toolbar,
text="No file loaded",
font=("Arial", 10),
bg="lightgray"
)
self.file_info_label.pack(side=tk.RIGHT, padx=10)
Tugas: Jalankan kode ini dan lihat interface dasar dengan toolbar.
Praktikum 3.2: Membuat Notebook untuk Multiple Views
Tambahkan notebook dengan tabs untuk berbagai view:
# Main content dengan notebook
self.notebook = ttk.Notebook(self.window)
self.notebook.pack(fill=tk.BOTH, expand=True, padx=5, pady=5)
# Tab 1: Raw Data View
self.raw_frame = tk.Frame(self.notebook)
self.notebook.add(self.raw_frame, text="Raw Data")
# Text widget untuk raw data
self.raw_text = tk.Text(
self.raw_frame,
font=("Courier", 10),
wrap=tk.WORD
)
raw_scrollbar = tk.Scrollbar(self.raw_frame, orient=tk.VERTICAL, command=self.raw_text.yview)
self.raw_text.configure(yscrollcommand=raw_scrollbar.set)
self.raw_text.pack(side=tk.LEFT, fill=tk.BOTH, expand=True, padx=5, pady=5)
raw_scrollbar.pack(side=tk.RIGHT, fill=tk.Y, pady=5)
# Tab 2: Structured View
self.structured_frame = tk.Frame(self.notebook)
self.notebook.add(self.structured_frame, text="Structured View")
# Treeview untuk structured data
self.tree = ttk.Treeview(self.structured_frame)
tree_scrollbar_v = ttk.Scrollbar(self.structured_frame, orient=tk.VERTICAL, command=self.tree.yview)
tree_scrollbar_h = ttk.Scrollbar(self.structured_frame, orient=tk.HORIZONTAL, command=self.tree.xview)
self.tree.configure(yscrollcommand=tree_scrollbar_v.set, xscrollcommand=tree_scrollbar_h.set)
self.tree.pack(side=tk.LEFT, fill=tk.BOTH, expand=True, padx=5, pady=5)
tree_scrollbar_v.pack(side=tk.RIGHT, fill=tk.Y, pady=5)
tree_scrollbar_h.pack(side=tk.BOTTOM, fill=tk.X, padx=5)
# Tab 3: Statistics
self.stats_frame = tk.Frame(self.notebook)
self.notebook.add(self.stats_frame, text="Statistics")
# Text widget untuk statistics
self.stats_text = tk.Text(
self.stats_frame,
font=("Courier", 11),
wrap=tk.WORD,
state=tk.DISABLED
)
stats_scrollbar = tk.Scrollbar(self.stats_frame, orient=tk.VERTICAL, command=self.stats_text.yview)
self.stats_text.configure(yscrollcommand=stats_scrollbar.set)
self.stats_text.pack(side=tk.LEFT, fill=tk.BOTH, expand=True, padx=5, pady=5)
stats_scrollbar.pack(side=tk.RIGHT, fill=tk.Y, pady=5)
Tugas: Jalankan dan lihat notebook dengan tiga tabs untuk berbagai view data.
Praktikum 3.3: Implementasi Load File untuk Multiple Format
Tambahkan method untuk load berbagai format file:
def load_file(self):
"""Method untuk load file dengan format detection"""
file_types = [
("CSV files", "*.csv"),
("JSON files", "*.json"),
("XML files", "*.xml"),
("Text files", "*.txt"),
("All files", "*.*")
]
filename = filedialog.askopenfilename(
title="Select file to load",
filetypes=file_types
)
if filename:
try:
self.current_file = filename
self.detect_format(filename)
self.load_file_content(filename)
self.update_file_info()
messagebox.showinfo("Success", f"File loaded: {os.path.basename(filename)}")
except Exception as e:
messagebox.showerror("Error", f"Cannot load file: {str(e)}")
def detect_format(self, filename):
"""Method untuk detect format file berdasarkan ekstensi"""
_, ext = os.path.splitext(filename.lower())
self.current_format = self.supported_formats.get(ext, 'Unknown')
def load_file_content(self, filename):
"""Method untuk load content berdasarkan format"""
if self.current_format == 'CSV':
self.load_csv_file(filename)
elif self.current_format == 'JSON':
self.load_json_file(filename)
elif self.current_format == 'XML':
self.load_xml_file(filename)
else:
self.load_text_file(filename)
# Update all views
self.update_raw_view()
self.update_structured_view()
self.update_statistics()
def load_csv_file(self, filename):
"""Method untuk load CSV file"""
self.data = {
'type': 'csv',
'headers': [],
'rows': [],
'raw_content': ''
}
with open(filename, 'r', encoding='utf-8') as file:
# Baca raw content
file.seek(0)
self.data['raw_content'] = file.read()
# Parse CSV
file.seek(0)
csv_reader = csv.reader(file)
# Baca header
try:
self.data['headers'] = next(csv_reader)
except StopIteration:
self.data['headers'] = []
# Baca rows
for row in csv_reader:
self.data['rows'].append(row)
def load_json_file(self, filename):
"""Method untuk load JSON file"""
with open(filename, 'r', encoding='utf-8') as file:
raw_content = file.read()
self.data = {
'type': 'json',
'content': json.loads(raw_content),
'raw_content': raw_content
}
def load_xml_file(self, filename):
"""Method untuk load XML file"""
with open(filename, 'r', encoding='utf-8') as file:
raw_content = file.read()
tree = ET.parse(filename)
root = tree.getroot()
self.data = {
'type': 'xml',
'tree': tree,
'root': root,
'raw_content': raw_content
}
def load_text_file(self, filename):
"""Method untuk load text file"""
with open(filename, 'r', encoding='utf-8') as file:
content = file.read()
self.data = {
'type': 'text',
'content': content,
'raw_content': content
}
Tugas: Test load file dengan berbagai format dan lihat bagaimana aplikasi mendeteksi format secara otomatis.
Praktikum 3.4: Update Views untuk Setiap Format
Tambahkan method untuk update berbagai view:
def update_raw_view(self):
"""Method untuk update raw data view"""
if self.data:
self.raw_text.delete(1.0, tk.END)
self.raw_text.insert(1.0, self.data['raw_content'])
def update_structured_view(self):
"""Method untuk update structured view"""
# Clear existing items
for item in self.tree.get_children():
self.tree.delete(item)
if not self.data:
return
if self.data['type'] == 'csv':
self.update_csv_tree_view()
elif self.data['type'] == 'json':
self.update_json_tree_view()
elif self.data['type'] == 'xml':
self.update_xml_tree_view()
else:
self.update_text_tree_view()
def update_csv_tree_view(self):
"""Method untuk update CSV tree view"""
# Setup columns
if self.data['headers']:
self.tree["columns"] = self.data['headers']
self.tree["show"] = "headings"
# Configure column headers
for header in self.data['headers']:
self.tree.heading(header, text=header)
self.tree.column(header, width=100)
# Insert data rows
for i, row in enumerate(self.data['rows']):
# Pad row if it has fewer columns than headers
padded_row = row + [''] * (len(self.data['headers']) - len(row))
self.tree.insert("", tk.END, values=padded_row[:len(self.data['headers'])])
else:
# No headers, show as simple list
self.tree["columns"] = ("Data",)
self.tree["show"] = "headings"
self.tree.heading("Data", text="Data")
for i, row in enumerate(self.data['rows']):
self.tree.insert("", tk.END, values=(str(row),))
def update_json_tree_view(self):
"""Method untuk update JSON tree view"""
self.tree["show"] = "tree"
self.tree["columns"] = ("Value",)
self.tree.heading("#0", text="Key")
self.tree.heading("Value", text="Value")
def add_json_node(parent, key, value, path=""):
if isinstance(value, dict):
node = self.tree.insert(parent, tk.END, text=key, values=("Object",))
for k, v in value.items():
add_json_node(node, k, v, f"{path}.{k}" if path else k)
elif isinstance(value, list):
node = self.tree.insert(parent, tk.END, text=key, values=(f"Array ({len(value)} items)",))
for i, item in enumerate(value):
add_json_node(node, f"[{i}]", item, f"{path}[{i}]" if path else f"[{i}]")
else:
self.tree.insert(parent, tk.END, text=key, values=(str(value),))
if isinstance(self.data['content'], dict):
for key, value in self.data['content'].items():
add_json_node("", key, value)
elif isinstance(self.data['content'], list):
for i, item in enumerate(self.data['content']):
add_json_node("", f"[{i}]", item)
else:
self.tree.insert("", tk.END, text="Root", values=(str(self.data['content']),))
def update_xml_tree_view(self):
"""Method untuk update XML tree view"""
self.tree["show"] = "tree"
self.tree["columns"] = ("Attributes", "Text")
self.tree.heading("#0", text="Element")
self.tree.heading("Attributes", text="Attributes")
self.tree.heading("Text", text="Text Content")
def add_xml_node(parent, element):
# Format attributes
attrs = ", ".join([f"{k}={v}" for k, v in element.attrib.items()]) if element.attrib else ""
# Get text content (only direct text, not from children)
text_content = (element.text or "").strip()
node = self.tree.insert(
parent,
tk.END,
text=element.tag,
values=(attrs, text_content)
)
# Add child elements
for child in element:
add_xml_node(node, child)
add_xml_node("", self.data['root'])
def update_text_tree_view(self):
"""Method untuk update text tree view"""
self.tree["show"] = "tree"
self.tree["columns"] = ("Content",)
self.tree.heading("#0", text="Line")
self.tree.heading("Content", text="Content")
lines = self.data['content'].split('\n')
for i, line in enumerate(lines, 1):
self.tree.insert("", tk.END, text=f"Line {i}", values=(line,))
Tugas: Load berbagai format file dan lihat bagaimana structured view menampilkan data sesuai dengan format masing-masing.
Praktikum 3.5: Generate Statistics untuk Setiap Format
Tambahkan method untuk generate statistics:
def update_statistics(self):
"""Method untuk update statistics view"""
self.stats_text.config(state=tk.NORMAL)
self.stats_text.delete(1.0, tk.END)
if not self.data:
self.stats_text.insert(1.0, "No data loaded")
self.stats_text.config(state=tk.DISABLED)
return
stats = self.generate_statistics()
self.stats_text.insert(1.0, stats)
self.stats_text.config(state=tk.DISABLED)
def generate_statistics(self):
"""Method untuk generate statistics berdasarkan format"""
if self.data['type'] == 'csv':
return self.generate_csv_statistics()
elif self.data['type'] == 'json':
return self.generate_json_statistics()
elif self.data['type'] == 'xml':
return self.generate_xml_statistics()
else:
return self.generate_text_statistics()
def generate_csv_statistics(self):
"""Method untuk generate CSV statistics"""
stats = f"""CSV FILE STATISTICS
{'='*50}
File: {os.path.basename(self.current_file) if self.current_file else 'Unknown'}
Format: CSV (Comma Separated Values)
STRUCTURE:
- Headers: {len(self.data['headers'])}
- Data Rows: {len(self.data['rows'])}
- Total Rows: {len(self.data['rows']) + (1 if self.data['headers'] else 0)}
HEADERS:
"""
for i, header in enumerate(self.data['headers'], 1):
stats += f"{i:2d}. {header}\n"
if self.data['rows']:
stats += f"\nDATA ANALYSIS:\n"
# Analyze each column
for i, header in enumerate(self.data['headers']):
column_data = []
for row in self.data['rows']:
if i < len(row) and row[i].strip():
column_data.append(row[i].strip())
stats += f"\nColumn '{header}':\n"
stats += f" - Non-empty values: {len(column_data)}\n"
stats += f" - Empty values: {len(self.data['rows']) - len(column_data)}\n"
if column_data:
# Check if numeric
numeric_values = []
for value in column_data:
try:
numeric_values.append(float(value))
except ValueError:
pass
if numeric_values:
stats += f" - Numeric values: {len(numeric_values)}\n"
stats += f" - Min: {min(numeric_values)}\n"
stats += f" - Max: {max(numeric_values)}\n"
stats += f" - Average: {sum(numeric_values)/len(numeric_values):.2f}\n"
else:
# Text analysis
unique_values = set(column_data)
stats += f" - Unique values: {len(unique_values)}\n"
if len(unique_values) <= 10:
stats += f" - Values: {', '.join(sorted(unique_values))}\n"
return stats
def generate_json_statistics(self):
"""Method untuk generate JSON statistics"""
stats = f"""JSON FILE STATISTICS
{'='*50}
File: {os.path.basename(self.current_file) if self.current_file else 'Unknown'}
Format: JSON (JavaScript Object Notation)
"""
def analyze_json_structure(obj, path="root", depth=0):
result = ""
indent = " " * depth
if isinstance(obj, dict):
result += f"{indent}{path}: Object ({len(obj)} keys)\n"
for key, value in obj.items():
result += analyze_json_structure(value, key, depth + 1)
elif isinstance(obj, list):
result += f"{indent}{path}: Array ({len(obj)} items)\n"
if obj:
# Analyze first few items
for i, item in enumerate(obj[:3]):
result += analyze_json_structure(item, f"[{i}]", depth + 1)
if len(obj) > 3:
result += f"{indent} ... and {len(obj) - 3} more items\n"
else:
value_type = type(obj).__name__
value_str = str(obj)
if len(value_str) > 50:
value_str = value_str[:47] + "..."
result += f"{indent}{path}: {value_type} = {value_str}\n"
return result
stats += "STRUCTURE:\n"
stats += analyze_json_structure(self.data['content'])
# Count different types
def count_types(obj, counts=None):
if counts is None:
counts = {}
obj_type = type(obj).__name__
counts[obj_type] = counts.get(obj_type, 0) + 1
if isinstance(obj, dict):
for value in obj.values():
count_types(value, counts)
elif isinstance(obj, list):
for item in obj:
count_types(item, counts)
return counts
type_counts = count_types(self.data['content'])
stats += f"\nTYPE DISTRIBUTION:\n"
for obj_type, count in sorted(type_counts.items()):
stats += f"- {obj_type}: {count}\n"
return stats
def generate_xml_statistics(self):
"""Method untuk generate XML statistics"""
stats = f"""XML FILE STATISTICS
{'='*50}
File: {os.path.basename(self.current_file) if self.current_file else 'Unknown'}
Format: XML (eXtensible Markup Language)
ROOT ELEMENT: {self.data['root'].tag}
"""
# Count elements
element_counts = {}
attribute_counts = {}
total_elements = 0
elements_with_text = 0
def analyze_element(element):
nonlocal total_elements, elements_with_text
total_elements += 1
# Count element types
tag = element.tag
element_counts[tag] = element_counts.get(tag, 0) + 1
# Count attributes
if element.attrib:
for attr in element.attrib:
attribute_counts[attr] = attribute_counts.get(attr, 0) + 1
# Check for text content
if element.text and element.text.strip():
elements_with_text += 1
# Recurse through children
for child in element:
analyze_element(child)
analyze_element(self.data['root'])
stats += f"STRUCTURE ANALYSIS:\n"
stats += f"- Total elements: {total_elements}\n"
stats += f"- Elements with text: {elements_with_text}\n"
stats += f"- Unique element types: {len(element_counts)}\n"
stats += f"- Unique attributes: {len(attribute_counts)}\n"
stats += f"\nELEMENT DISTRIBUTION:\n"
for element, count in sorted(element_counts.items(), key=lambda x: x[1], reverse=True):
stats += f"- {element}: {count}\n"
if attribute_counts:
stats += f"\nATTRIBUTE DISTRIBUTION:\n"
for attr, count in sorted(attribute_counts.items(), key=lambda x: x[1], reverse=True):
stats += f"- {attr}: {count}\n"
return stats
def generate_text_statistics(self):
"""Method untuk generate text statistics"""
content = self.data['content']
stats = f"""TEXT FILE STATISTICS
{'='*50}
File: {os.path.basename(self.current_file) if self.current_file else 'Unknown'}
Format: Plain Text
BASIC COUNTS:
- Characters: {len(content)}
- Characters (no spaces): {len(content.replace(' ', '').replace('\n', '').replace('\t', ''))}
- Words: {len(content.split())}
- Lines: {content.count(chr(10)) + 1}
- Paragraphs: {len([p for p in content.split('\n\n') if p.strip()])}
CHARACTER ANALYSIS:
- Spaces: {content.count(' ')}
- Tabs: {content.count('\t')}
- Newlines: {content.count('\n')}
- Digits: {sum(1 for c in content if c.isdigit())}
- Letters: {sum(1 for c in content if c.isalpha())}
- Uppercase: {sum(1 for c in content if c.isupper())}
- Lowercase: {sum(1 for c in content if c.islower())}
"""
# Word frequency (top 10)
words = content.lower().split()
word_freq = {}
for word in words:
# Remove punctuation
clean_word = ''.join(c for c in word if c.isalnum())
if clean_word:
word_freq[clean_word] = word_freq.get(clean_word, 0) + 1
if word_freq:
stats += "MOST FREQUENT WORDS:\n"
sorted_words = sorted(word_freq.items(), key=lambda x: x[1], reverse=True)
for word, count in sorted_words[:10]:
stats += f"- {word}: {count}\n"
return stats
Tugas: Load berbagai format file dan lihat statistics yang dihasilkan untuk setiap format. Perhatikan bagaimana aplikasi menganalisis struktur dan konten file.
Praktikum 3.6: File Validation dan Conversion
Tambahkan method untuk validasi dan konversi file:
def validate_file(self):
"""Method untuk validasi file"""
if not self.data:
messagebox.showwarning("Warning", "No file loaded!")
return
try:
validation_result = self.perform_validation()
messagebox.showinfo("Validation Result", validation_result)
except Exception as e:
messagebox.showerror("Validation Error", f"Validation failed: {str(e)}")
def perform_validation(self):
"""Method untuk perform validation berdasarkan format"""
if self.data['type'] == 'csv':
return self.validate_csv()
elif self.data['type'] == 'json':
return self.validate_json()
elif self.data['type'] == 'xml':
return self.validate_xml()
else:
return self.validate_text()
def validate_csv(self):
"""Method untuk validasi CSV"""
issues = []
# Check for consistent column count
if self.data['headers']:
expected_cols = len(self.data['headers'])
for i, row in enumerate(self.data['rows'], 2): # Start from row 2 (after header)
if len(row) != expected_cols:
issues.append(f"Row {i}: Expected {expected_cols} columns, found {len(row)}")
# Check for empty cells
empty_cells = 0
for i, row in enumerate(self.data['rows'], 2):
for j, cell in enumerate(row):
if not cell.strip():
empty_cells += 1
result = f"CSV Validation Results:\n\n"
result += f"✓ File structure: Valid CSV format\n"
result += f"✓ Headers: {len(self.data['headers'])} columns\n"
result += f"✓ Data rows: {len(self.data['rows'])}\n"
if issues:
result += f"\n⚠ Issues found:\n"
for issue in issues[:10]: # Show max 10 issues
result += f" - {issue}\n"
if len(issues) > 10:
result += f" ... and {len(issues) - 10} more issues\n"
else:
result += f"✓ Column consistency: All rows have correct number of columns\n"
if empty_cells > 0:
result += f"⚠ Empty cells: {empty_cells} found\n"
else:
result += f"✓ Data completeness: No empty cells\n"
return result
def validate_json(self):
"""Method untuk validasi JSON"""
result = f"JSON Validation Results:\n\n"
result += f"✓ Syntax: Valid JSON format\n"
# Check structure
if isinstance(self.data['content'], dict):
result += f"✓ Root type: Object with {len(self.data['content'])} keys\n"
elif isinstance(self.data['content'], list):
result += f"✓ Root type: Array with {len(self.data['content'])} items\n"
else:
result += f"✓ Root type: {type(self.data['content']).__name__}\n"
# Check for common issues
def check_json_issues(obj, path=""):
issues = []
if isinstance(obj, dict):
# Check for empty keys
if "" in obj:
issues.append(f"Empty key found at {path}")
# Check for null values
for key, value in obj.items():
if value is None:
issues.append(f"Null value at {path}.{key}")
else:
issues.extend(check_json_issues(value, f"{path}.{key}" if path else key))
elif isinstance(obj, list):
for i, item in enumerate(obj):
issues.extend(check_json_issues(item, f"{path}[{i}]" if path else f"[{i}]"))
return issues
issues = check_json_issues(self.data['content'])
if issues:
result += f"\n⚠ Potential issues:\n"
for issue in issues[:10]:
result += f" - {issue}\n"
if len(issues) > 10:
result += f" ... and {len(issues) - 10} more issues\n"
else:
result += f"✓ Structure: No issues detected\n"
return result
def validate_xml(self):
"""Method untuk validasi XML"""
result = f"XML Validation Results:\n\n"
result += f"✓ Syntax: Well-formed XML\n"
result += f"✓ Root element: {self.data['root'].tag}\n"
# Count elements and check for issues
total_elements = 0
empty_elements = 0
elements_with_attrs = 0
def check_element(element):
nonlocal total_elements, empty_elements, elements_with_attrs
total_elements += 1
if not element.text or not element.text.strip():
if len(element) == 0: # No children either
empty_elements += 1
if element.attrib:
elements_with_attrs += 1
for child in element:
check_element(child)
check_element(self.data['root'])
result += f"✓ Total elements: {total_elements}\n"
result += f"✓ Elements with attributes: {elements_with_attrs}\n"
if empty_elements > 0:
result += f"⚠ Empty elements: {empty_elements} found\n"
else:
result += f"✓ Content: All elements have content or children\n"
return result
def validate_text(self):
"""Method untuk validasi text"""
content = self.data['content']
result = f"Text File Validation Results:\n\n"
result += f"✓ File readable as text\n"
result += f"✓ Character count: {len(content)}\n"
result += f"✓ Line count: {content.count(chr(10)) + 1}\n"
# Check encoding issues
try:
content.encode('utf-8')
result += f"✓ Encoding: UTF-8 compatible\n"
except UnicodeEncodeError:
result += f"⚠ Encoding: Contains non-UTF-8 characters\n"
# Check for very long lines
lines = content.split('\n')
long_lines = [i+1 for i, line in enumerate(lines) if len(line) > 200]
if long_lines:
result += f"⚠ Long lines: {len(long_lines)} lines exceed 200 characters\n"
else:
result += f"✓ Line length: All lines under 200 characters\n"
return result
def show_convert_dialog(self):
"""Method untuk menampilkan dialog konversi"""
if not self.data:
messagebox.showwarning("Warning", "No file loaded!")
return
# Create conversion dialog
convert_dialog = tk.Toplevel(self.window)
convert_dialog.title("Convert File Format")
convert_dialog.geometry("400x300")
convert_dialog.transient(self.window)
convert_dialog.grab_set()
tk.Label(
convert_dialog,
text=f"Convert from {self.current_format}",
font=("Arial", 14, "bold")
).pack(pady=10)
tk.Label(
convert_dialog,
text="Select target format:",
font=("Arial", 12)
).pack(pady=5)
# Format selection
format_var = tk.StringVar(value="JSON")
formats = ["CSV", "JSON", "XML", "Text"]
for fmt in formats:
if fmt != self.current_format:
tk.Radiobutton(
convert_dialog,
text=fmt,
variable=format_var,
value=fmt,
font=("Arial", 11)
).pack(anchor="w", padx=50)
# Buttons
button_frame = tk.Frame(convert_dialog)
button_frame.pack(pady=20)
tk.Button(
button_frame,
text="Convert",
command=lambda: self.perform_conversion(format_var.get(), convert_dialog),
bg="green",
fg="white",
width=10
).pack(side=tk.LEFT, padx=5)
tk.Button(
button_frame,
text="Cancel",
command=convert_dialog.destroy,
bg="red",
fg="white",
width=10
).pack(side=tk.LEFT, padx=5)
def perform_conversion(self, target_format, dialog):
"""Method untuk perform file conversion"""
try:
if target_format == "CSV":
converted_data = self.convert_to_csv()
extension = ".csv"
elif target_format == "JSON":
converted_data = self.convert_to_json()
extension = ".json"
elif target_format == "XML":
converted_data = self.convert_to_xml()
extension = ".xml"
else: # Text
converted_data = self.convert_to_text()
extension = ".txt"
# Save converted file
filename = filedialog.asksaveasfilename(
title=f"Save as {target_format}",
defaultextension=extension,
filetypes=[(f"{target_format} files", f"*{extension}"), ("All files", "*.*")]
)
if filename:
with open(filename, 'w', encoding='utf-8') as file:
file.write(converted_data)
dialog.destroy()
messagebox.showinfo("Success", f"File converted and saved as {target_format}")
except Exception as e:
messagebox.showerror("Conversion Error", f"Cannot convert file: {str(e)}")
def convert_to_csv(self):
"""Method untuk convert ke CSV"""
if self.data['type'] == 'csv':
return self.data['raw_content']
elif self.data['type'] == 'json':
# Convert JSON to CSV (flatten if possible)
if isinstance(self.data['content'], list) and self.data['content']:
if isinstance(self.data['content'][0], dict):
# List of objects - can convert to CSV
import io
output = io.StringIO()
# Get all possible keys
all_keys = set()
for item in self.data['content']:
if isinstance(item, dict):
all_keys.update(item.keys())
fieldnames = sorted(all_keys)
writer = csv.DictWriter(output, fieldnames=fieldnames)
writer.writeheader()
for item in self.data['content']:
if isinstance(item, dict):
writer.writerow(item)
return output.getvalue()
# Fallback: convert to simple key-value CSV
output = "Key,Value\n"
def flatten_json(obj, prefix=""):
if isinstance(obj, dict):
for key, value in obj.items():
new_key = f"{prefix}.{key}" if prefix else key
if isinstance(value, (dict, list)):
flatten_json(value, new_key)
else:
output += f'"{new_key}","{value}"\n'
elif isinstance(obj, list):
for i, item in enumerate(obj):
new_key = f"{prefix}[{i}]" if prefix else f"[{i}]"
if isinstance(item, (dict, list)):
flatten_json(item, new_key)
else:
output += f'"{new_key}","{item}"\n'
flatten_json(self.data['content'])
return output
else:
# Convert other formats to simple CSV
return f"Data,Value\n\"{self.current_format}\",\"{self.data.get('raw_content', '')[:100]}...\""
def convert_to_json(self):
"""Method untuk convert ke JSON"""
if self.data['type'] == 'json':
return self.data['raw_content']
elif self.data['type'] == 'csv':
# Convert CSV to JSON
result = []
for row in self.data['rows']:
row_dict = {}
for i, header in enumerate(self.data['headers']):
row_dict[header] = row[i] if i < len(row) else ""
result.append(row_dict)
return json.dumps(result, indent=2, ensure_ascii=False)
else:
# Convert other formats to JSON
return json.dumps({
"format": self.current_format,
"content": self.data.get('raw_content', ''),
"converted_at": datetime.now().isoformat()
}, indent=2)
def convert_to_xml(self):
"""Method untuk convert ke XML"""
if self.data['type'] == 'xml':
return self.data['raw_content']
elif self.data['type'] == 'csv':
# Convert CSV to XML
xml_content = '<?xml version="1.0" encoding="UTF-8"?>\n<data>\n'
for i, row in enumerate(self.data['rows']):
xml_content += f' <row id="{i+1}">\n'
for j, header in enumerate(self.data['headers']):
value = row[j] if j < len(row) else ""
xml_content += f' <{header}>{value}</{header}>\n'
xml_content += ' </row>\n'
xml_content += '</data>'
return xml_content
elif self.data['type'] == 'json':
# Convert JSON to XML
def json_to_xml(obj, tag="item"):
if isinstance(obj, dict):
xml = f"<{tag}>\n"
for key, value in obj.items():
xml += json_to_xml(value, key)
xml += f"</{tag}>\n"
return xml
elif isinstance(obj, list):
xml = f"<{tag}>\n"
for i, item in enumerate(obj):
xml += json_to_xml(item, f"item_{i}")
xml += f"</{tag}>\n"
return xml
else:
return f"<{tag}>{obj}</{tag}>\n"
xml_content = '<?xml version="1.0" encoding="UTF-8"?>\n'
xml_content += json_to_xml(self.data['content'], "root")
return xml_content
else:
# Convert other formats to XML
return f'''<?xml version="1.0" encoding="UTF-8"?>
<document>
<format>{self.current_format}</format>
<content>{self.data.get('raw_content', '')}</content>
</document>'''
def convert_to_text(self):
"""Method untuk convert ke text"""
if self.data['type'] == 'text':
return self.data['raw_content']
else:
return self.data.get('raw_content', '')
def save_file(self):
"""Method untuk save file"""
if not self.data:
messagebox.showwarning("Warning", "No file loaded!")
return
# Get current content from raw view (in case user edited it)
current_content = self.raw_text.get(1.0, tk.END).rstrip('\n')
if self.current_file:
# Save to current file
try:
with open(self.current_file, 'w', encoding='utf-8') as file:
file.write(current_content)
messagebox.showinfo("Success", f"File saved: {os.path.basename(self.current_file)}")
except Exception as e:
messagebox.showerror("Error", f"Cannot save file: {str(e)}")
else:
# Save as new file
self.save_as_file(current_content)
def save_as_file(self, content=None):
"""Method untuk save as new file"""
if content is None:
if not self.data:
messagebox.showwarning("Warning", "No file loaded!")
return
content = self.raw_text.get(1.0, tk.END).rstrip('\n')
# Determine file types based on current format
if self.current_format == 'CSV':
file_types = [("CSV files", "*.csv"), ("All files", "*.*")]
default_ext = ".csv"
elif self.current_format == 'JSON':
file_types = [("JSON files", "*.json"), ("All files", "*.*")]
default_ext = ".json"
elif self.current_format == 'XML':
file_types = [("XML files", "*.xml"), ("All files", "*.*")]
default_ext = ".xml"
else:
file_types = [("Text files", "*.txt"), ("All files", "*.*")]
default_ext = ".txt"
filename = filedialog.asksaveasfilename(
title="Save File As",
filetypes=file_types,
defaultextension=default_ext
)
if filename:
try:
with open(filename, 'w', encoding='utf-8') as file:
file.write(content)
self.current_file = filename
self.update_file_info()
messagebox.showinfo("Success", f"File saved as: {os.path.basename(filename)}")
except Exception as e:
messagebox.showerror("Error", f"Cannot save file: {str(e)}")
def update_file_info(self):
"""Method untuk update file info label"""
if self.current_file:
filename = os.path.basename(self.current_file)
self.file_info_label.config(text=f"{filename} ({self.current_format})")
else:
self.file_info_label.config(text="No file loaded")
def jalankan(self):
"""Method untuk menjalankan aplikasi"""
self.window.mainloop()
# Untuk menjalankan aplikasi
if __name__ == "__main__":
app = MultiFormatManager()
app.jalankan()
Tugas Akhir Pertemuan 2:
Test aplikasi Multi-Format File Manager secara lengkap:
- Load berbagai format file (CSV, JSON, XML, TXT)
- Lihat structured view untuk setiap format
- Analyze statistics yang dihasilkan
- Validate file dan lihat hasil validasi
- Convert antar format dan save hasil konversi
- Edit raw data dan save perubahan
Buat juga sample files untuk testing:
Sample CSV (students.csv):
Sample JSON (products.json):
{
"products": [
{
"id": 1,
"name": "Laptop",
"price": 15000000,
"category": "Electronics"
},
{
"id": 2,
"name": "Book",
"price": 50000,
"category": "Education"
}
]
}