首页
学习
活动
专区
圈层
工具
发布
社区首页 >问答首页 >_tkinter.TclError:无法删除Tcl命令- customtkinter -定制提示

_tkinter.TclError:无法删除Tcl命令- customtkinter -定制提示
EN

Stack Overflow用户
提问于 2022-11-18 11:09:26
回答 1查看 68关注 0票数 2

我需要什么

我试图在tkinter的帮助下实现一个自定义的“是/否”提示框。但是,我不想使用默认的messagebox,因为我需要以下两个功能:

  • 默认值
  • 小部件自毁并以默认值作为回答的倒计时。

什么是不可预测的错误?

我已经成功地用下面的代码实现了这些需求,但是当我在以下意义上使用这些小部件时,我会发现一些非常不可预测的行为:

  • 有时一切都如预期的那样运作。当我按下按钮时,正确的答案会被存储,或者如果我让倒计时超时,默认答案就会被存储,或者如果我点击关闭窗口,它将正确地应用默认值作为答案。
  • 但是,当我单击按钮时,我会得到一些_tkinter.TclError: invalid command name ".!ctkframe2.!ctkcanvas"错误(请参阅下面的执行日志中的整个堆栈跟踪)。

我怀疑这与计时器有关,因为当按下按钮时,错误并不总是会出现。真的快把我逼疯了..。

示例代码

代码语言:javascript
复制
# util_gui_classes.py
# -*- coding: utf-8 -*-

"""
Classes which serve for gui applications.
"""

from typing import Any

import tkinter
import tkinter.messagebox
import customtkinter


# ____________________________________________________________________________________________


customtkinter.set_appearance_mode('System')  # Modes: 'System' (standard), 'Dark', 'Light'
customtkinter.set_default_color_theme('blue')  # Themes: 'blue' (standard), 'green', 'dark-blue'


# ____________________________________________________________________________________________


class GuiPromptYesNo(customtkinter.CTk):
    """
    Creates a yes / no gui based prompt with default value and countdown functionality.
    The user input will be stored in:
    > instance.answer
    """
    WIDTH = 500
    HEIGHT = 200

    def __init__(self, question: str, default_value: str = 'no', countdown_seconds: int = 0):
        super().__init__()

        self.title('input required')
        self.geometry(f'{self.__class__.WIDTH}x{self.__class__.HEIGHT}')
        self.protocol('WM_DELETE_WINDOW', self.on_closing)  # call .on_closing() when app gets closed
        self.resizable(False, False)

        self.question = question
        self.answer = None
        self.default_value = default_value
        self.countdown_seconds = countdown_seconds
        self.remaining_seconds = countdown_seconds

        # ============ create top-level-frames ============

        # configure grid layout (4x1)
        self.equal_weighted_grid(self, 4, 1)
        self.grid_rowconfigure(0, minsize=10)
        self.grid_rowconfigure(3, minsize=10)

        self.frame_label = customtkinter.CTkFrame(master=self, corner_radius=10)
        self.frame_label.grid(row=1, column=0)

        self.frame_buttons = customtkinter.CTkFrame(master=self, corner_radius=0, fg_color=None)
        self.frame_buttons.grid(row=2, column=0)

        # ============ design frame_label ============

        # configure grid layout (5x4)
        self.equal_weighted_grid(self.frame_label, 5, 4)
        self.frame_label.grid_rowconfigure(0, minsize=10)
        self.frame_label.grid_rowconfigure(2, minsize=10)
        self.frame_label.grid_rowconfigure(5, minsize=10)

        self.label_question = customtkinter.CTkLabel(
            master=self.frame_label,
            text=self.question,
            text_font=('Consolas',),
        )
        self.label_question.grid(row=1, column=0, columnspan=4, pady=5, padx=10)

        self.label_default_value = customtkinter.CTkLabel(
            master=self.frame_label,
            text='default value: ',
            text_font=('Consolas',),
        )
        self.label_default_value.grid(row=3, column=0, pady=5, padx=10)

        self.entry_default_value = customtkinter.CTkEntry(
            master=self.frame_label,
            width=40,
            justify='center',
            placeholder_text=self.default_value,
            state='disabled',
            textvariable=tkinter.StringVar(value=self.default_value),
            text_font=('Consolas',),
        )
        self.entry_default_value.grid(row=3, column=1, pady=5, padx=10)

        if countdown_seconds > 0:
            self.label_timer = customtkinter.CTkLabel(
                master=self.frame_label,
                text='timer [s]: ',
                text_font=('Consolas',),
            )
            self.label_timer.grid(row=3, column=2, pady=5, padx=10)

            self.entry_timer = customtkinter.CTkEntry(
                master=self.frame_label,
                width=40,
                justify='center',
                state='disabled',
                textvariable=tkinter.StringVar(value=str(self.remaining_seconds)),
                placeholder_text=str(self.remaining_seconds),
                text_font=('Consolas',),
            )
            self.entry_timer.grid(row=3, column=3, pady=5, padx=10)

        # ============ design frame_buttons ============

        # configure grid layout (3x2)
        self.equal_weighted_grid(self.frame_buttons, 3, 2)
        self.frame_buttons.grid_rowconfigure(0, minsize=10)
        self.frame_buttons.grid_rowconfigure(2, minsize=10)

        self.button_yes = customtkinter.CTkButton(
            master=self.frame_buttons,
            text='yes',
            text_font=('Consolas',),
            command=lambda: self.button_event('yes'),
        )
        self.button_yes.grid(row=1, column=0, pady=5, padx=20)

        self.button_no = customtkinter.CTkButton(
            master=self.frame_buttons,
            text='no',
            text_font=('Consolas',),
            command=lambda: self.button_event('no'),
        )
        self.button_no.grid(row=1, column=1, pady=5, padx=20)

        if self.countdown_seconds > 0:
            self.countdown()

        self.attributes('-topmost', True)
        self.mainloop()

    # __________________________________________________________
    # methods

    @staticmethod
    def equal_weighted_grid(obj: Any, rows: int, cols: int):
        """configures the grid to be of equal cell sizes for rows and columns."""
        for row in range(rows):
            obj.grid_rowconfigure(row, weight=1)
        for col in range(cols):
            obj.grid_columnconfigure(col, weight=1)

    def button_event(self, answer):
        """Stores the user input as instance attribute `answer`."""
        self.answer = answer
        self.terminate()

    def countdown(self):
        """Sets the timer for the question."""
        if self.answer is not None:
            self.terminate()
        elif self.remaining_seconds < 0:
            self.answer = self.default_value
            self.terminate()
        else:
            self.entry_timer.configure(textvariable=tkinter.StringVar(value=str(self.remaining_seconds)))
            self.remaining_seconds -= 1
            self.after(1000, self.countdown)

    def stop_after_callbacks(self):
        """Stops all after callbacks on the root."""
        for after_id in self.tk.eval('after info').split():
            self.after_cancel(after_id)

    def on_closing(self, event=0):
        """If the user presses the window x button without providing input"""
        if self.answer is None and self.default_value is not None:
            self.answer = self.default_value
        self.terminate()

    def terminate(self):
        """Properly terminates the gui."""
        # stop all .after callbacks to avoid error message "Invalid command ..." after destruction
        self.stop_after_callbacks()

        self.destroy()


# ____________________________________________________________________________________________


if __name__ == '__main__':
    print('\n', 'do some python stuff before', '\n', sep='')

    q1 = GuiPromptYesNo(question='1. do you want to proceed?', countdown_seconds=5)
    print(f'>>>{q1.answer=}')

    print('\n', 'do some python stuff in between', '\n', sep='')

    q2 = GuiPromptYesNo(question='2. do you want to proceed?', countdown_seconds=5)
    print(f'>>>{q2.answer=}')

    print('\n', 'do some python stuff at the end', '\n', sep='')


# ____________________________________________________________________________________________

带有错误的执行日志

前三个测试成功的地方(包括点击按钮),然后出现错误。

代码语言:javascript
复制
(py311) C:\Users\user\PycharmProjects\Sandbox\gui_tools>python util_guis.py

 do some python stuff before 

q1.answer='yes'

 do some python stuff in between 

q2.answer='no'

 do some python stuff at the end 


(py311) C:\Users\user\PycharmProjects\Sandbox\gui_tools>python util_guis.py

 do some python stuff before

q1.answer='yes'

 do some python stuff in between

q2.answer='yes'

 do some python stuff at the end


(py311) C:\Users\user\PycharmProjects\Sandbox\gui_tools>python util_guis.py

 do some python stuff before

q1.answer='no'

 do some python stuff in between

q2.answer='no'

 do some python stuff at the end


(py311) C:\Users\user\PycharmProjects\Sandbox\gui_tools>python util_guis.py

do some python stuff before

>>>q1.answer='yes'

do some python stuff in between

Exception in Tkinter callback
Traceback (most recent call last):
  File "C:\Program Files\Python311\Lib\tkinter\__init__.py", line 1948, in __call__
    return self.func(*args)
           ^^^^^^^^^^^^^^^^
  File "C:\Program Files\Python311\Lib\tkinter\__init__.py", line 861, in callit
    func(*args)
  File "C:\Users\user\PycharmProjects\Sandbox\gui_tools\util_guis.py", line 197, in countdown
    self.terminate()
  File "C:\Users\user\PycharmProjects\Sandbox\gui_tools\util_guis.py", line 224, in terminate
    child.destroy()
  File "C:\Users\user\python\shared_venvs\py311\Lib\site-packages\customtkinter\widgets\widget_base_class.py", line 85, in destroy
    super().destroy()
  File "C:\Program Files\Python311\Lib\tkinter\__init__.py", line 2635, in destroy
    for c in list(self.children.values()): c.destroy()
                                           ^^^^^^^^^^^
  File "C:\Users\user\python\shared_venvs\py311\Lib\site-packages\customtkinter\widgets\widget_base_class.py", line 85, in destroy
    super().destroy()
  File "C:\Program Files\Python311\Lib\tkinter\__init__.py", line 2639, in destroy
    Misc.destroy(self)
  File "C:\Program Files\Python311\Lib\tkinter\__init__.py", line 687, in destroy
    self.tk.deletecommand(name)
_tkinter.TclError: can't delete Tcl command
>>>q2.answer='no'

do some python stuff at the end


(py311) C:\Users\user\PycharmProjects\Sandbox\gui_tools>python util_guis.py

do some python stuff before

Exception in Tkinter callback
Traceback (most recent call last):
  File "C:\Program Files\Python311\Lib\tkinter\__init__.py", line 1948, in __call__
    return self.func(*args)
           ^^^^^^^^^^^^^^^^
  File "C:\Users\user\python\shared_venvs\py311\Lib\site-packages\customtkinter\widgets\ctk_button.py", line 377, in clicked
    self.command()
  File "C:\Users\user\PycharmProjects\Sandbox\gui_tools\util_guis.py", line 124, in <lambda>
    command=lambda: self.button_event('yes'),
                    ^^^^^^^^^^^^^^^^^^^^^^^^
  File "C:\Users\user\PycharmProjects\Sandbox\gui_tools\util_guis.py", line 156, in button_event
    self.terminate()
  File "C:\Users\user\PycharmProjects\Sandbox\gui_tools\util_guis.py", line 186, in terminate
    self.destroy()
  File "C:\Users\user\python\shared_venvs\py311\Lib\site-packages\customtkinter\windows\ctk_tk.py", line 108, in destroy
    super().destroy()
  File "C:\Program Files\Python311\Lib\tkinter\__init__.py", line 2367, in destroy
    for c in list(self.children.values()): c.destroy()
                                           ^^^^^^^^^^^
  File "C:\Users\user\python\shared_venvs\py311\Lib\site-packages\customtkinter\widgets\widget_base_class.py", line 85, in destroy
    super().destroy()
  File "C:\Program Files\Python311\Lib\tkinter\__init__.py", line 2635, in destroy
    for c in list(self.children.values()): c.destroy()
                                           ^^^^^^^^^^^
  File "C:\Users\user\python\shared_venvs\py311\Lib\site-packages\customtkinter\widgets\widget_base_class.py", line 85, in destroy
    super().destroy()
  File "C:\Program Files\Python311\Lib\tkinter\__init__.py", line 2639, in destroy
    Misc.destroy(self)
  File "C:\Program Files\Python311\Lib\tkinter\__init__.py", line 687, in destroy
    self.tk.deletecommand(name)
_tkinter.TclError: can't delete Tcl command
Exception in Tkinter callback
Traceback (most recent call last):
  File "C:\Program Files\Python311\Lib\tkinter\__init__.py", line 1948, in __call__
    return self.func(*args)
           ^^^^^^^^^^^^^^^^
  File "C:\Users\user\python\shared_venvs\py311\Lib\site-packages\customtkinter\widgets\widget_base_class.py", line 142, in update_dimensions_event
    self.draw(no_color_updates=True)  # faster drawing without color changes
    ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "C:\Users\user\python\shared_venvs\py311\Lib\site-packages\customtkinter\widgets\ctk_frame.py", line 80, in draw
    requires_recoloring = self.draw_engine.draw_rounded_rect_with_border(self.apply_widget_scaling(self._current_width),
                          ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "C:\Users\user\python\shared_venvs\py311\Lib\site-packages\customtkinter\draw_engine.py", line 88, in draw_rounded_rect_with_border
    return self.__draw_rounded_rect_with_border_font_shapes(width, height, corner_radius, border_width, inner_corner_radius, ())
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "C:\Users\user\python\shared_venvs\py311\Lib\site-packages\customtkinter\draw_engine.py", line 207, in __draw_rounded_rect_with_border_font_shapes
    self._canvas.delete("border_parts")
  File "C:\Program Files\Python311\Lib\tkinter\__init__.py", line 2879, in delete
    self.tk.call((self._w, 'delete') + args)
_tkinter.TclError: invalid command name ".!ctkframe2.!ctkcanvas"
>>>q1.answer='yes'

do some python stuff in between

Exception in Tkinter callback
Traceback (most recent call last):
  File "C:\Program Files\Python311\Lib\tkinter\__init__.py", line 1948, in __call__
    return self.func(*args)
           ^^^^^^^^^^^^^^^^
  File "C:\Users\user\python\shared_venvs\py311\Lib\site-packages\customtkinter\widgets\ctk_button.py", line 377, in clicked
    self.command()
    super().destroy()
  File "C:\Program Files\Python311\Lib\tkinter\__init__.py", line 2639, in destroy
    Misc.destroy(self)
  File "C:\Program Files\Python311\Lib\tkinter\__init__.py", line 687, in destroy
    self.tk.deletecommand(name)
_tkinter.TclError: can't delete Tcl command
>>>q2.answer='no'

do some python stuff at the end


(py311) C:\Users\user\PycharmProjects\Sandbox\gui_tools>

要求

我使用Windows 11作为操作系统,并安装了一个带有定做机的虚拟python3.11环境。

编辑:

在@Thingamabobs回答的帮助下,我成功地实现了预期的行为而没有错误。以下是最终代码:

代码语言:javascript
复制
# util_gui_classes.py
# -*- coding: utf-8 -*-

"""
Classes which serve for gui applications.
"""

from typing import Any

import tkinter
import tkinter.messagebox
import customtkinter
from _tkinter import TclError


# _______________________________________________________________________


customtkinter.set_appearance_mode('System')  # Modes: 'System' (standard), 'Dark', 'Light'
customtkinter.set_default_color_theme('blue')  # Themes: 'blue' (standard), 'green', 'dark-blue'


# _______________________________________________________________________


class GuiPromptYesNo(customtkinter.CTk):
    """
    Creates a yes / no gui based prompt with default value and countdown functionality.
    The user input will be stored in:
    >>> instance.answer
    """
    WIDTH = 500
    HEIGHT = 200

    def __init__(self, question: str, default_value: str = 'no', countdown_seconds: int = 0):
        super().__init__()
        self.terminated = False

        self.title('input required')
        self.geometry(f'{self.__class__.WIDTH}x{self.__class__.HEIGHT}')
        self.protocol('WM_DELETE_WINDOW', self.on_closing)  # call .on_closing() when app gets closed
        self.resizable(False, False)

        self.question = question
        self.answer = None
        self.default_value = default_value
        self.countdown_seconds = countdown_seconds
        self.remaining_seconds = countdown_seconds

        # ============ create top-level-frames ============

        # configure grid layout (4x1)
        self.equal_weighted_grid(self, 4, 1)
        self.grid_rowconfigure(0, minsize=10)
        self.grid_rowconfigure(3, minsize=10)

        self.frame_label = customtkinter.CTkFrame(master=self, corner_radius=10)
        self.frame_label.grid(row=1, column=0)

        self.frame_buttons = customtkinter.CTkFrame(master=self, corner_radius=0, fg_color=None)
        self.frame_buttons.grid(row=2, column=0)

        # ============ design frame_label ============

        # configure grid layout (5x4)
        self.equal_weighted_grid(self.frame_label, 5, 4)
        self.frame_label.grid_rowconfigure(0, minsize=10)
        self.frame_label.grid_rowconfigure(2, minsize=10)
        self.frame_label.grid_rowconfigure(5, minsize=10)

        self.label_question = customtkinter.CTkLabel(
            master=self.frame_label,
            text=self.question,
            text_font=('Consolas',),
        )
        self.label_question.grid(row=1, column=0, columnspan=4, pady=5, padx=10)

        self.label_default_value = customtkinter.CTkLabel(
            master=self.frame_label,
            text='default value: ',
            text_font=('Consolas',),
        )
        self.label_default_value.grid(row=3, column=0, pady=5, padx=10)

        self.entry_default_value = customtkinter.CTkEntry(
            master=self.frame_label,
            width=40,
            justify='center',
            placeholder_text=self.default_value,
            state='disabled',
            textvariable=tkinter.StringVar(value=self.default_value),
            text_font=('Consolas',),
        )
        self.entry_default_value.grid(row=3, column=1, pady=5, padx=10)

        if countdown_seconds > 0:
            self.label_timer = customtkinter.CTkLabel(
                master=self.frame_label,
                text='timer [s]: ',
                text_font=('Consolas',),
            )
            self.label_timer.grid(row=3, column=2, pady=5, padx=10)

            self.entry_timer = customtkinter.CTkEntry(
                master=self.frame_label,
                width=40,
                justify='center',
                state='disabled',
                textvariable=tkinter.StringVar(value=str(self.remaining_seconds)),
                placeholder_text=str(self.remaining_seconds),
                text_font=('Consolas',),
            )
            self.entry_timer.grid(row=3, column=3, pady=5, padx=10)

        # ============ design frame_buttons ============

        # configure grid layout (3x2)
        self.equal_weighted_grid(self.frame_buttons, 3, 2)
        self.frame_buttons.grid_rowconfigure(0, minsize=10)
        self.frame_buttons.grid_rowconfigure(2, minsize=10)

        self.button_yes = customtkinter.CTkButton(
            master=self.frame_buttons,
            text='yes',
            text_font=('Consolas',),
            command=lambda: self.button_event('yes'),
        )
        self.button_yes.grid(row=1, column=0, pady=5, padx=20)

        self.button_no = customtkinter.CTkButton(
            master=self.frame_buttons,
            text='no',
            text_font=('Consolas',),
            command=lambda: self.button_event('no'),
        )
        self.button_no.grid(row=1, column=1, pady=5, padx=20)

        if self.countdown_seconds > 0:
            self.countdown()

        self.attributes('-topmost', True)
        self.mainloop()

    # __________________________________________________________
    # methods

    @staticmethod
    def equal_weighted_grid(obj: Any, rows: int, cols: int):
        """configures the grid to be of equal cell sizes for rows and columns."""
        for row in range(rows):
            obj.grid_rowconfigure(row, weight=1)
        for col in range(cols):
            obj.grid_columnconfigure(col, weight=1)

    def button_event(self, answer):
        """Stores the user input as instance attribute `answer`."""
        self.answer = answer
        self.terminate()

    def countdown(self):
        """Sets the timer for the question."""
        if self.answer is not None:
            self.terminate()
        elif self.remaining_seconds < 0:
            self.answer = self.default_value
            self.terminate()
        else:
            self.entry_timer.configure(textvariable=tkinter.StringVar(value=str(self.remaining_seconds)))
            self.remaining_seconds -= 1
            self.after(1000, self.countdown)

    def stop_after_callbacks(self):
        """Stops all after callbacks on the root."""
        for after_id in self.tk.eval('after info').split():
            self.after_cancel(after_id)

    def on_closing(self, event=0):
        """If the user presses the window x button without providing input"""
        if self.answer is None and self.default_value is not None:
            self.answer = self.default_value
        self.terminate()

    def terminate(self):
        """Properly terminates the gui."""
        # stop all .after callbacks to avoid error message "Invalid command ..." after destruction
        self.stop_after_callbacks()

        if not self.terminated:
            self.terminated = True
            try:
                self.destroy()
            except TclError:
                self.destroy()


# _______________________________________________________________________


if __name__ == '__main__':
    print('before')

    q1 = GuiPromptYesNo(question='1. do you want to proceed?', countdown_seconds=5)
    print(f'>>>{q1.answer=}')

    print('between')

    q2 = GuiPromptYesNo(question='2. do you want to proceed?', countdown_seconds=5)
    print(f'>>>{q2.answer=}')

    print('after')


# _______________________________________________________________________

顺便说一句:这个类也可以在我的包utils_nm中找到,它位于模块util_gui_classes中。

EN

回答 1

Stack Overflow用户

回答已采纳

发布于 2022-11-18 12:03:10

而我没有Ctk给你确切的密码。我可以告诉你到底出了什么问题,你需要怎么解决。

在这里之后,您有自我重复的功能:

代码语言:javascript
复制
def countdown(self):
        """Sets the timer for the question."""
        if self.answer is not None:
            self.terminate()
        elif self.remaining_seconds < 0:
            self.answer = self.default_value
            self.terminate()
        else:
            self.entry_timer.configure(textvariable=tkinter.StringVar(value=str(self.remaining_seconds)))
            self.remaining_seconds -= 1
            self.after(1000, self.countdown)

您所面临的问题是,您试图delete已在一个事后调用中已被破坏的窗口。因此,取决于哪个事件运行得更快,您是否遇到了错误。

为什么会发生这种事?

当您给出一个指令时,不管它是什么(除了一些例外,例如更新),它都被放置在一个事件队列中,并以某种形式的FIFO (先进先出)调度。所以最古老的事件就会被处理。这意味着您可以尝试取消警报,但在实际取消警报之前运行警报。

如何解决?

有不同的方法可供选择。在我看来,最简单和最干净的解决方案是设置一个标志,避免试图破坏一个已经被破坏的窗口。类似于:

代码语言:javascript
复制
    def __init__(self, question: str, default_value: str = 'no', countdown_seconds: int = 0):
        super().__init__()
        self.terminated = False

并将其设置为:

代码语言:javascript
复制
def terminate(self):
    """Properly terminates the gui."""
    # stop all .after callbacks to avoid error message "Invalid command ..." after destruction
    #self.stop_after_callbacks() shouldn't be needed
    if not self.terminated:
        self.terminated = True
        self.destroy()

除了上述建议的之外,我建议您为WM_DELETE_WINDOW设置一个协议处理程序并设置标志,因为它可能也是通过窗口管理器销毁窗口来实现的。

不同的方法是在terminate中捕获except _tkinter.TclError:tryexcept块。但请注意下划线,该模块不打算使用,它可能会在未来改变,并可能再次破坏您的应用程序,即使这似乎不太可能。

票数 2
EN
页面原文内容由Stack Overflow提供。腾讯云小微IT领域专用引擎提供翻译支持
原文链接:

https://stackoverflow.com/questions/74488759

复制
相关文章

相似问题

领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档