CustomTkinter icon indicating copy to clipboard operation
CustomTkinter copied to clipboard

CTkEntry.destroy() does not remove trace from self._textvariable

Open the-sabin opened this issue 4 months ago • 0 comments

Description
In CTkEntry.__init__, a trace_add callback is attached to self._textvariable:

if not (self._textvariable is None or self._textvariable == ""):
    self._textvariable_callback_name = self._textvariable.trace_add("write", self._textvariable_callback)

However, in CTkEntry.destroy(), this trace is never removed. If the tkinter.Variable outlives the widget, the callback can still fire after the widget is destroyed, causing TclError: invalid command name when _textvariable_callback tries to access self._entry.

Example with a top-level dialog:

import customtkinter as ctk
import tkinter as tk

def open_dialog():
    dialog = ctk.CTkToplevel(root)
    dialog.title("Dialog with CTkEntry")

    # Reuse the same StringVar every time
    entry = ctk.CTkEntry(dialog, textvariable=shared_var, placeholder_text="Type here...")
    entry.pack(padx=20, pady=20)

    ctk.CTkButton(dialog, text="Close", command=dialog.destroy).pack(pady=10)

root = ctk.CTk()
root.geometry("300x200")

# This variable persists beyond the lifetime of any CTkEntry
shared_var = tk.StringVar(value="Default text")

ctk.CTkButton(root, text="Open Dialog", command=open_dialog).pack(pady=50)

root.mainloop()

Steps to reproduce issue:

  1. Open dialog, type in entry: this works fine.
  2. Close dialog.
  3. Open dialog again.
  4. Typing at the end of existing text works fine (no errors).
  5. Select all text in the entry and type something new. Now observing this error inside the terminal:
_tkinter.TclError: invalid command name ".!ctktoplevel.!ctkentry.!entry"

Note: The issue is more visible when reusing the same StringVar across dialogs. Appending text works fine, but replacing all text (select all + type) causes the error because it triggers the placeholder logic on a destroyed widget.

Proposed fix: destroy() should remove the trace from self._textvariable before destroying the widget.

def destroy(self):
    if self._textvariable and self._textvariable_callback_name:
        try:
            self._textvariable.trace_remove("write", self._textvariable_callback_name)
        except Exception:
            pass
        self._textvariable_callback_name = ""

    if isinstance(self._font, CTkFont):
        self._font.remove_size_configure_callback(self._update_font)

    super().destroy()

the-sabin avatar Aug 12 '25 22:08 the-sabin