i'm working on a Python application using customtkinter and CTkTable to display a paginated table with dynamic resizing and various widgets (frames, labels, buttons, comboboxes, etc.). The code below creates a dashboard-like interface with a table, pagination controls, and metrics cards. However, when the window is resized or the application is initialized, the widgets render slowly, often appearing one at a time, which creates a noticeable lag and a suboptimal user experience.
I've implemented debouncing for the resize event to limit excessive updates, but the rendering of widgets (especially during initialization or pagination) still feels sluggish. I'm looking for ways to optimize the rendering speed so that all widgets appear smoothly and simultaneously, rather than rendering incrementally.
Here's the relevant code
from customtkinter import *
from CTkTable import CTkTable
from PIL import Image
import time
import threading
import queue
# Initialize the main application window
app = CTk()
app.geometry("1280x720") # Set initial window size
app.resizable(True, True) # Allow resizing
set_appearance_mode("light")
# Get initial screen dimensions
screen_width = app.winfo_screenwidth()
screen_height = app.winfo_screenheight()
# Variables for debouncing and pagination
last_resize_time = 0
resize_delay = 0.5 # Increased to 500ms for better performance
last_width = screen_width
last_height = screen_height
rows_per_page = 5 # Limit to 5 rows per page
current_page = 0
resize_enabled = True # Flag to control resize handling
# Thread-safe queue for initial table load
update_queue = queue.Queue()
# Table data
table_data = [
["Order ID", "Item Name", "Customer", "Address", "Status", "Quantity"],
['3833', 'Smartphone', 'Alice', '123 Main St', 'Confirmed', '8'],
['6432', 'Laptop', 'Bob', '456 Elm St', 'Packing', '5'],
['2180', 'Tablet', 'Crystal', '789 Oak St', 'Delivered', '1'],
['5438', 'Headphones', 'John', '101 Pine St', 'Confirmed', '9'],
['9144', 'Camera', 'David', '202 Cedar St', 'Processing', '2'],
['7689', 'Printer', 'Alice', '303 Maple St', 'Cancelled', '2'],
['1323', 'Smartwatch', 'Crystal', '404 Birch St', 'Shipping', '6'],
['7391', 'Keyboard', 'John', '505 Redwood St', 'Cancelled', '10'],
['4915', 'Monitor', 'Alice', '606 Fir St', 'Shipping', '6'],
['5548', 'External Hard Drive', 'David', '707 Oak St', 'Delivered', '10'],
['5485', 'Table Lamp', 'Crystal', '808 Pine St', 'Confirmed', '4'],
['7764', 'Desk Chair', 'Bob', '909 Cedar St', 'Processing', '9'],
['8252', 'Coffee Maker', 'John', '1010 Elm St', 'Confirmed', '6'],
['2377', 'Blender', 'David', '1111 Redwood St', 'Shipping', '2'],
['5287', 'Toaster', 'Alice', '1212 Maple St', 'Processing', '1'],
['7739', 'Microwave', 'Crystal', '1313 Cedar St', 'Confirmed', '8'],
['3129', 'Refrigerator', 'John', '1414 Oak St', 'Processing', '5'],
['4789', 'Vacuum Cleaner', 'Bob', '1515 Pine St', 'Cancelled', '10']
]
# Precompute paginated data
paginated_data_cache = []
total_pages = max(1, (len(table_data) - 1 + rows_per_page - 1) // rows_per_page)
for page in range(total_pages):
start_idx = page * rows_per_page + 1
end_idx = min(start_idx + rows_per_page, len(table_data))
paginated_data_cache.append([table_data[0]] + table_data[start_idx:end_idx])
# Main view with dynamic sizing
main_view = CTkFrame(master=app, fg_color="#F7F7F7", width=screen_width, height=screen_height, corner_radius=0)
main_view.pack_propagate(0)
main_view.pack(fill="both", expand=True)
# --- Title Frame ---
title_frame = CTkFrame(master=main_view, fg_color="transparent")
title_frame.pack(anchor="n", fill="x", padx=20, pady=(20, 0))
# --- Title Label (Top Row) ---
title_label = CTkLabel(title_frame, text="Orders", font=("Arial Black", 20), text_color="#FFFFFF")
title_label.pack(anchor="w", padx=10, pady=(10, 0))
# --- Breadcrumbs + Buttons Row ---
top_bar_frame = CTkFrame(master=title_frame, fg_color="transparent")
top_bar_frame.pack(fill="x", padx=10, pady=(5, 10))
# --- Breadcrumbs (Left Side) ---
breadcrumb_frame = CTkFrame(master=top_bar_frame, fg_color="transparent")
breadcrumb_frame.pack(side="left")
CTkLabel(breadcrumb_frame, text="Home", text_color="#AAAAAA").pack(side="left")
CTkLabel(breadcrumb_frame, text=" > ", text_color="#AAAAAA").pack(side="left")
CTkLabel(breadcrumb_frame, text="Orders", text_color="#AAAAAA").pack(side="left")
CTkLabel(breadcrumb_frame, text=" > ", text_color="#AAAAAA").pack(side="left")
CTkLabel(breadcrumb_frame, text="New Order", text_color="#FFFFFF", font=("Arial", 13, "bold")).pack(side="left")
# --- Button Frame (Right Side) ---
button_frame = CTkFrame(master=top_bar_frame, fg_color="transparent")
button_frame.pack(side="right")
# --- Refresh Button ---
refresh_button = CTkButton(
master=button_frame,
text="�是一件 Refresh",
font=("Arial Black", 15),
text_color="#FFFFFF",
fg_color="#2A8C55",
hover_color="#207244",
corner_radius=8,
)
refresh_button.pack(side="right", padx=(5, 0))
# --- Print Button ---
print_button = CTkButton(
master=button_frame,
text="🖨 Print",
font=("Arial Black", 15),
text_color="#FFFFFF",
fg_color="#2A8C55",
hover_color="#207244",
corner_radius=8,
)
print_button.pack(side="right", padx=(5, 0))
# --- Export Button ---
export_button = CTkButton(
master=button_frame,
text="Export ▼",
font=("Arial Black", 15),
text_color="#FFFFFF",
fg_color="#2A8C55",
hover_color="#207244",
corner_radius=8,
)
export_button.pack(side="right", padx=(5, 0))
# --- New Order Button ---
new_order_button = CTkButton(
master=button_frame,
text="+ New Order",
font=("Arial Black", 15),
text_color="#FFFFFF",
fg_color="#2A8C55",
hover_color="#207244",
corner_radius=8,
)
new_order_button.pack(side="right", padx=(5, 0))
# --- Export Menu ---
from tkinter import Menu
export_menu = Menu(app, tearoff=0,
background="#2A8C55",
foreground="#FFFFFF",
activebackground="#207244",
activeforeground="#FFFFFF",
font=("Arial", 13),
borderwidth=0,
relief="flat")
export_menu.add_command(label="Export as PDF", command=lambda: print("Exporting to PDF..."))
export_menu.add_command(label="Export as Excel", command=lambda: print("Exporting to Excel..."))
def show_export_menu_below(event):
x = export_button.winfo_rootx()
y = export_button.winfo_rooty() + export_button.winfo_height()
export_menu.tk_popup(x, y)
export_button.bind("<Button-1>", show_export_menu_below)
# Metrics frame
metrics_frame = CTkFrame(master=main_view, fg_color="transparent")
metrics_frame.pack(anchor="n", fill="x", padx=20, pady=(30, 0))
# Metric widths
metric_width = int((screen_width - 60) / 3.5)
orders_metric = CTkFrame(master=metrics_frame, fg_color="#2A8C55", width=metric_width, height=60)
orders_metric.grid_propagate(0)
orders_metric.pack(side="left", padx=(0, 10))
logistics_img_data = Image.open("logistics_icon.png")
logistics_img = CTkImage(light_image=logistics_img_data, dark_image=logistics_img_data, size=(43, 43))
CTkLabel(master=orders_metric, image=logistics_img, text="").grid(row=0, column=0, rowspan=2, padx=(12, 5), pady=10)
CTkLabel(master=orders_metric, text="Orders", text_color="#FFFFFF", font=("Arial Black", 15)).grid(row=0, column=1, sticky="sw")
CTkLabel(master=orders_metric, text="123", text_color="#FFFFFF", font=("Arial Black", 15), justify="left").grid(row=1, column=1, sticky="nw", pady=(0, 10))
shipped_metric = CTkFrame(master=metrics_frame, fg_color="#2A8C55", width=metric_width, height=60)
shipped_metric.grid_propagate(0)
shipped_metric.pack(side="left", expand=True, anchor="center", padx=10)
shipping_img_data = Image.open("shipping_icon.png")
shipping_img = CTkImage(light_image=shipping_img_data, dark_image=shipping_img_data, size=(43, 43))
CTkLabel(master=shipped_metric, image=shipping_img, text="").grid(row=0, column=0, rowspan=2, padx=(12, 5), pady=10)
CTkLabel(master=shipped_metric, text="Shipping", text_color="#FFFFFF", font=("Arial Black", 15)).grid(row=0, column=1, sticky="sw")
CTkLabel(master=shipped_metric, text="91", text_color="#FFFFFF", font=("Arial Black", 15), justify="left").grid(row=1, column=1, sticky="nw", pady=(0, 10))
delivered_metric = CTkFrame(master=metrics_frame, fg_color="#2A8C55", width=metric_width, height=60)
delivered_metric.grid_propagate(0)
delivered_metric.pack(side="right", padx=(10, 0))
delivered_img_data = Image.open("delivered_icon.png")
delivered_img = CTkImage(light_image=delivered_img_data, dark_image=delivered_img_data, size=(43, 43))
CTkLabel(master=delivered_metric, image=delivered_img, text="").grid(row=0, column=0, rowspan=2, padx=(12, 5), pady=10)
CTkLabel(master=delivered_metric, text="Delivered", text_color="#FFFFFF", font=("Arial Black", 15)).grid(row=0, column=1, sticky="sw")
CTkLabel(master=delivered_metric, text="23", text_color="#FFFFFF", font=("Arial Black", 15), justify="left").grid(row=1, column=1, sticky="nw", pady=(0, 10))
# Search container
search_container = CTkFrame(master=main_view, height=50, fg_color="#F0F0F0")
search_container.pack(fill="x", pady=(30, 0), padx=20)
search_width = int((screen_width - 60) * 0.5)
combo_width = int((screen_width - 60) * 0.2)
search_entry = CTkEntry(master=search_container, width=search_width, placeholder_text="Search Order", border_color="#2A8C55", border_width=2, font=("Arial", 14))
search_entry.pack(side="left", padx=(10, 5), pady=10)
date_combo = CTkComboBox(master=search_container, width=combo_width, values=["Date", "Most Recent Order", "Least Recent Order"], button_color="#2A8C55", border_color="#2A8C55", border_width=2, button_hover_color="#207244", dropdown_hover_color="#207244", dropdown_fg_color="#2A8C55", dropdown_text_color="#FFFFFF", font=("Arial", 14))
date_combo.pack(side="left", padx=5, pady=10)
status_combo = CTkComboBox(master=search_container, width=combo_width, values=["Status", "Processing", "Confirmed", "Packing", "Shipping", "Delivered", "Cancelled"], button_color="#2A8C55", border_color="#2A8C55", border_width=2, button_hover_color="#207244", dropdown_hover_color="#207244", dropdown_fg_color="#2A8C55", dropdown_text_color="#FFFFFF", font=("Arial", 14))
status_combo.pack(side="left", padx=(5, 0), pady=10)
# Table frame with pagination
table_frame = CTkScrollableFrame(master=main_view, fg_color="transparent")
table_frame.pack(expand=True, fill="both", padx=20, pady=20)
# Function to update table with cached paginated data
def update_table():
global resize_enabled
resize_enabled = False # Disable resize during table update
try:
if 0 <= current_page < len(paginated_data_cache):
table.configure(values=paginated_data_cache[current_page])
page_label.configure(text=f"Page {current_page + 1} of {total_pages}")
finally:
resize_enabled = True # Re-enable resize after update
# Initialize table with first page of data
table = CTkTable(master=table_frame, values=paginated_data_cache[0], colors=["#E6E6E6", "#EEEEEE"], header_color="#2A8C55", hover_color="#B4B4B4")
table.edit_row(0, text_color="#FFFFFF", hover_color="#2A8C55")
table.pack(expand=True, fill="both")
# Pagination controls
pagination_frame = CTkFrame(master=main_view, fg_color="transparent")
pagination_frame.pack(fill="x", padx=20, pady=10)
page_label = CTkLabel(master=pagination_frame, text=f"Page {current_page + 1} of {total_pages}", font=("Arial", 14))
page_label.pack(side="left")
def prev_page():
global current_page
if current_page > 0:
current_page -= 1
update_table()
def next_page():
global current_page
if current_page < len(paginated_data_cache) - 1:
current_page += 1
update_table()
CTkButton(master=pagination_frame, text="Previous", font=("Arial", 14), fg_color="#2A8C55", hover_color="#207244", command=prev_page).pack(side="left", padx=5)
CTkButton(master=pagination_frame, text="Next", font=("Arial", 14), fg_color="#2A8C55", hover_color="#207244", command=next_page).pack(side="left", padx=5)
# Bind resize event with debouncing
def on_resize(event):
global last_resize_time, last_width, last_height, screen_width, screen_height, metric_width, search_width, combo_width
if not resize_enabled:
return
current_time = time.time()
if current_time - last_resize_time < resize_delay:
return
new_width = app.winfo_width()
new_height = app.winfo_height()
if abs(new_width - last_width) < 20 and abs(new_height - last_height) < 20: # Increased threshold
return
last_resize_time = current_time
last_width = new_width
last_height = new_height
screen_width = new_width
screen_height = new_height
# Update main view
main_view.configure(width=screen_width, height=screen_height)
# Update metrics
metric_width = int((screen_width - 60) / 3.5)
for frame in [orders_metric, shipped_metric, delivered_metric]:
frame.configure(width=metric_width)
# Update search and comboboxes
search_width = int((screen_width - 60) * 0.5)
combo_width = int((screen_width - 60) * 0.2)
search_entry.configure(width=search_width)
date_combo.configure(width=combo_width)
status_combo.configure(width=combo_width)
app.bind("<Configure>", on_resize)
# Allow exiting full-screen with Escape key
def exit_fullscreen(event):
if app.attributes('-fullscreen'):
app.attributes('-fullscreen', False)
try:
fullscreen_button.configure(text="Maximize")
except NameError:
pass # fullscreen_button may not exist yet
app.bind("<Escape>", exit_fullscreen)
app.mainloop()
How can I optimize the rendering performance of this customtkinter application to ensure all widgets render smoothly and simultaneously, rather than appearing one at a time?