2

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?

1 Answer 1

4

Why it stutters

  • Tk has to paint every single widget; with dozens of frames/labels that happens row by row.

  • You re-configure many widgets on each <Configure> event; that easily fires 30–40 times while the user drags the border.

  • CTkTable creates a CTkLabel per cell, so repopulating the table is expensive.

Make Tk draw once

def build_ui():
    ...                         # all your widget creation here
    app.update_idletasks()      # one geometry pass, off-screen

app.withdraw()                  # keep the window hidden
build_ui()
app.after_idle(app.deiconify)   # show everything in one paint

Hiding the toplevel while you build it prevents the incremental “pop-in” you see at start-up.

Throttle expensive work

# one global timer id
_resize_job = None
def on_resize(event):
    global _resize_job
    if _resize_job:            # remove previous timer
        app.after_cancel(_resize_job)
    _resize_job = app.after(200, do_resize)   # run once after 200 ms

200 ms is enough for smooth dragging and you don’t touch 20+ widgets on every tiny size change.

Avoid repacking the table

def update_table():
    table.freeze()                                 # CTkTable ≥0.5
    table.values = paginated_data_cache[current_page]
    page_label.configure(text=f'Page {current_page+1} of {total_pages}')
    table.thaw()

freeze/thaw suspends internal redraws so the cells appear together.
If you use an older CTkTable, hide the widget:

table.pack_forget()
table.configure(values=paginated_data_cache[current_page])
table.pack(expand=True, fill='both')

Let geometry managers work for you

Use relative sizes instead of configure(width=…) in every resize:

search_entry.place(relwidth=.50, relx=.01, rely=.5, anchor='w')
date_combo.place(relwidth=.20, relx=.52, rely=.5, anchor='w')
status_combo.place(relwidth=.20, relx=.74, rely=.5, anchor='w')

Now nothing in on_resize has to touch those widgets at all.

Reuse widgets

Create the metric cards once and only change their text/image; do not destroy/re-create them per page or per refresh.

With the window hidden during construction, debounced resize handling, table-level freeze, and relative geometry you get a single, smooth paint instead of the current “one-widget-at-a-time” effect.

Sign up to request clarification or add additional context in comments.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.