Title: Building a Modern CRUD Menu Application with Python and ttkbootstrap
Introduction
Welcome to my latest tech adventure! In this blog post, I鈥檒l walk you through the development of a Modern CRUD Menu Application built with Python, leveraging the power of ttkbootstrap
鈥攁 Bootstrap-inspired extension for Tkinter. This project combines data management functionality with a sleek, user-friendly interface, making it a perfect example of blending backend logic with frontend design. Whether you’re a developer looking to enhance your skills or just curious about Python GUI applications, this post is for you!
What is a CRUD Application?
CRUD stands for Create, Read, Update, and Delete鈥攖he four basic operations for managing data in any application. My CRUD Menu Application allows users to manage a menu database, complete with item details, prices, categories, and images. It鈥檚 a practical tool that could be used in a restaurant or caf茅 setting, but it鈥檚 also a great learning project for mastering GUI development.
Project Overview
Here鈥檚 what makes this application stand out:
- CRUD Operations: Add new menu items, update existing ones, delete items, and refresh the list with intuitive buttons.
- Image Preview: Double-click an item to view its associated image, supporting both local files and web URLs.
- Data Persistence: Data is stored in a JSON file, ensuring it persists across sessions.
- Modern UI: Powered by
ttkbootstrap
, the app features a professional, responsive design with Bootstrap-inspired themes like ‘flatly’.
The application is built using:
- Python: The core programming language.
- Tkinter: The standard Python GUI library.
- ttkbootstrap: Adds modern styling and themes to Tkinter widgets.
- PIL (Pillow): For image processing and display.
Development Journey
Setting Up the Environment
To get started, I installed ttkbootstrap
via pip:
pip install ttkbootstrap
This library extends Tkinter with Bootstrap themes, providing a polished look without the need for external CSS frameworks.
Building the Interface
I structured the app with a top bar for CRUD buttons and a main area for a treeview to display the menu data. The ttk.Treeview
widget, styled with ttkbootstrap
, lists items with columns for ID, Item Name, Image URL, Price, and Category. Here鈥檚 a snippet of the treeview setup:
self.tree = ttk.Treeview(self.tree_frame, columns=columns, show="headings", height=25, style="info.Treeview")
for col in columns:
self.tree.heading(col, text=col)
self.tree.column(col, anchor=CENTER, width=220)
Adding Functionality
- Create: A form dialog (built with
tk.Toplevel
) allows users to input item details. - Read: The treeview displays all items from the JSON file.
- Update: Selecting an item opens the form pre-filled with existing data for editing.
- Delete: A confirmation prompt ensures safe deletion.
Image Handling
One challenge was loading images from web URLs (e.g., https://rest.diciu.org/menus/
). I implemented threaded image loading with urllib.request
and added a User-Agent
header to mimic browser requests. However, some URLs failed due to server restrictions (e.g., HTTP 403 errors). The solution? Enhanced error handling to display specific messages like “Failed to load image: HTTP Error 403: Forbidden.”
Challenges and Solutions
- Button Visibility: Initial layouts hid the CRUD buttons. I resolved this by using a clear frame hierarchy with
pack
andgrid
. - Image Loading: Inaccessible URLs were a hurdle. I added fallback support for local files and plan to update the dataset with public URLs (e.g., from
https://picsum.photos/
).
Screenshots
[Insert Screenshot Here]
Caption: The CRUD Menu Application showing the treeview with sample data.
Future Improvements
- Integrate a file picker for local image uploads.
- Add validation for price inputs beyond numeric checks.
- Explore database integration (e.g., SQLite) instead of JSON.
Conclusion
This project was a fantastic learning experience, blending Python鈥檚 versatility with a modern UI. The ttkbootstrap
library made styling effortless, and tackling real-world issues like image loading deepened my problem-solving skills. I鈥檝e shared the code on [GitHub Link if available], and I鈥檇 love your feedback or collaboration ideas!
Have you worked with ttkbootstrap
or built a CRUD app? Share your thoughts in the comments below!
Tags
Python #Tkinter #ttkbootstrap #CRUD #Programming #SoftwareDevelopment #UIUX #TechProjects #LearnToCode
Instructions for WordPress:
- Add to Editor:
- Copy the text into the WordPress editor. Use the “Text” tab to preserve code blocks and formatting, or switch to “Visual” and manually format headings, lists, and code snippets.
- Add the screenshot you shared earlier by uploading it to the media library and inserting it where indicated.
- Categories and Tags:
- Assign categories like “Programming,” “Tutorials,” or “Projects.”
- Use the suggested tags for better discoverability.
- Featured Image:
- Set the screenshot as the featured image for the post.
- Call to Action:
- Encourage reader engagement with the question at the end. You can also add a “Subscribe” or “Contact Me” link if desired.
import json
import tkinter as tk
from tkinter import messagebox
import ttkbootstrap as ttk
from ttkbootstrap.constants import *
from PIL import Image, ImageTk
import os
import urllib.request
import urllib.error
import threading
DATA_FILE = ‘data.json’
class ItemForm(tk.Toplevel):
def init(self, parent, title, item=None):
super().init(parent)
self.parent = parent
self.title(title)
self.geometry(“400×500”)
self.transient(parent)
self.grab_set()
self.item = item
self.result = None
form_frame = ttk.Frame(self, padding=20)
form_frame.pack(fill=BOTH, expand=YES)
self.fields = {}
labels = [“Item Name”, “Price”, “Category”, “Image Path/URL”]
defaults = [item.get(field.lower().replace(” “, “”), “”) if item else “”
for field in labels]
for i, (label, default) in enumerate(zip(labels, defaults)):
ttk.Label(form_frame, text=label + “:”, style=”info.TLabel”).grid(row=i, column=0, pady=10, padx=10, sticky=E)
entry = ttk.Entry(form_frame, width=30)
entry.insert(0, default)
entry.grid(row=i, column=1, pady=10, padx=10)
self.fields[label.lower().replace(” “, “”)] = entry
# Button frame
btn_frame = ttk.Frame(form_frame)
btn_frame.grid(row=len(labels), column=0, columnspan=2, pady=20)
ttk.Button(btn_frame, text=”Submit”, command=self.submit, style=”success.TButton”).pack(side=LEFT, padx=5)
ttk.Button(btn_frame, text=”Cancel”, command=self.destroy, style=”danger.TButton”).pack(side=LEFT, padx=5)
def submit(self):
self.result = {
“itemname”: self.fields[“itemname”].get(),
“price”: self.fields[“price”].get(),
“category”: self.fields[“category”].get(),
“image”: self.fields[“imagepath/url”].get()
}
self.destroy()
class ModernCRUDApp:
def init(self, root):
self.root = root
self.root.title(“Modern CRUD Menu”)
self.root.geometry(“1200×700”)
# Use a Bootstrap theme
style = ttk.Style()
style.theme_use('flatly') # Options: 'flatly', 'cosmo', 'minty', etc.
# Button frame at the top
self.button_frame = ttk.Frame(self.root, padding=10)
self.button_frame.pack(fill=X, pady=(20, 10))
# Main frame for treeview below buttons
self.main_frame = ttk.Frame(self.root)
self.main_frame.pack(fill=BOTH, expand=YES, pady=(0, 20))
# Treeview frame inside main frame
self.tree_frame = ttk.Frame(self.main_frame, padding=10)
self.tree_frame.pack(fill=BOTH, expand=YES)
self.setup_treeview()
self.setup_buttons()
self.refresh_data()
def setup_treeview(self):
columns = ("ID", "Item Name", "Image", "Price", "Category")
self.tree = ttk.Treeview(self.tree_frame, columns=columns, show="headings", height=25, style="info.Treeview")
style = ttk.Style()
style.configure("info.Treeview", rowheight=30, font=('Helvetica', 10))
style.configure("info.Treeview.Heading", font=('Helvetica', 11, 'bold'))
for col in columns:
self.tree.heading(col, text=col)
self.tree.column(col, anchor=CENTER, width=220)
scrollbar = ttk.Scrollbar(self.tree_frame, orient=VERTICAL, command=self.tree.yview)
self.tree.configure(yscrollcommand=scrollbar.set)
self.tree.pack(side=LEFT, fill=BOTH, expand=YES, padx=(0, 10))
scrollbar.pack(side=RIGHT, fill=Y)
self.tree.bind("<Double-1>", self.show_image_preview)
def setup_buttons(self):
buttons = [
("Add Item", self.add_item, "success"),
("Update Item", self.update_item, "info"),
("Delete Item", self.delete_item, "danger"),
("Refresh", self.refresh_data, "secondary")
]
for text, command, style_type in buttons:
btn = ttk.Button(self.button_frame, text=text, command=command, style=f"{style_type}.TButton", width=15)
btn.pack(side=LEFT, padx=5, pady=5)
def load_data(self):
try:
with open(DATA_FILE, 'r') as file:
return json.load(file)
except (FileNotFoundError, json.JSONDecodeError):
return []
def save_data(self, data):
with open(DATA_FILE, 'w') as file:
json.dump(data, file, indent=4)
def refresh_data(self):
data = self.load_data()
self.tree.delete(*self.tree.get_children())
for item in data:
self.tree.insert('', END, values=(
item["id"],
item["itemName"],
item["itemImage"],
f"${item['price']}",
item["category"]
))
def add_item(self):
form = ItemForm(self.root, "Add New Item")
self.root.wait_window(form)
if not form.result:
return
result = form.result
if not all(result.values()):
messagebox.showwarning("Warning", "All fields are required.")
return
try:
float(result["price"])
except ValueError:
messagebox.showerror("Error", "Price must be a number.")
return
data = self.load_data()
new_id = max((item["id"] for item in data), default=0) + 1
new_item = {
"id": new_id,
"itemName": result["itemname"],
"itemImage": result["image"],
"price": result["price"],
"category": result["category"],
"ingredients": []
}
data.append(new_item)
self.save_data(data)
self.refresh_data()
def delete_item(self):
selected = self.tree.focus()
if not selected:
messagebox.showwarning("Warning", "Please select an item to delete.")
return
if messagebox.askyesno("Confirm", "Are you sure you want to delete this item?"):
item_id = int(self.tree.item(selected)["values"][0])
data = self.load_data()
data = [item for item in data if item["id"] != item_id]
self.save_data(data)
self.refresh_data()
def update_item(self):
selected = self.tree.focus()
if not selected:
messagebox.showwarning("Warning", "Please select an item to update.")
return
values = self.tree.item(selected)["values"]
item_id = int(values[0])
data = self.load_data()
item = next((x for x in data if x["id"] == item_id), None)
if not item:
return
form = ItemForm(self.root, "Update Item", item)
self.root.wait_window(form)
if not form.result:
return
result = form.result
if not all(result.values()):
messagebox.showwarning("Warning", "All fields are required.")
return
try:
float(result["price"])
except ValueError:
messagebox.showerror("Error", "Price must be a number.")
return
item.update({
"itemName": result["itemname"],
"itemImage": result["image"],
"price": result["price"],
"category": result["category"]
})
self.save_data(data)
self.refresh_data()
def show_image_preview(self, event):
selected = self.tree.focus()
if not selected:
return
values = self.tree.item(selected)["values"]
image_path = values[2]
preview_window = tk.Toplevel(self.root)
preview_window.title("Image Preview")
preview_window.geometry("450x450")
preview_window.transient(self.root)
preview_window.grab_set()
def load_image():
try:
if image_path.startswith(('http://', 'https://')):
temp_file = "temp_img.jpg"
req = urllib.request.Request(
image_path,
headers={'User-Agent': 'Mozilla/5.0'}
)
with urllib.request.urlopen(req, timeout=10) as response:
with open(temp_file, 'wb') as out_file:
out_file.write(response.read())
img = Image.open(temp_file)
else:
img = Image.open(image_path)
img = img.resize((400, 400), Image.Resampling.LANCZOS)
photo = ImageTk.PhotoImage(img)
label = ttk.Label(preview_window, image=photo, text="")
label.image = photo
label.pack(pady=20)
except (urllib.error.URLError, urllib.error.HTTPError, ValueError, FileNotFoundError, OSError) as e:
error_label = ttk.Label(preview_window, text=f"Failed to load image: {str(e)}",
style="danger.TLabel")
error_label.pack(pady=20)
print(f"Image loading error: {e}")
finally:
if os.path.exists("temp_img.jpg"):
os.remove("temp_img.jpg")
threading.Thread(target=load_image, daemon=True).start()
def main():
root = tk.Tk()
app = ModernCRUDApp(root)
root.mainloop()
if name == “main“:
main()