_tkinter.TclError: невозможно удалить команду Tcl — customtkinter — пользовательское приглашение

Что мне нужно

Я пытаюсь реализовать пользовательское окно подсказки «Да/Нет» с помощью tkinter. Однако я не хочу использовать окно сообщений по умолчанию, потому что мне нужны следующие две функции:

  • значение по умолчанию
  • обратный отсчет, после которого виджет уничтожает себя и принимает значение по умолчанию в качестве ответа

Какие непредсказуемые ошибки

Мне удалось реализовать эти требования с помощью приведенного ниже кода, однако я получаю действительно непредсказуемое поведение при использовании виджетов в следующем смысле:

  • Иногда все работает так, как ожидалось. Когда я нажимаю кнопки, правильный ответ сохраняется, или если я пропускаю время обратного отсчета, сохраняется ответ по умолчанию, или если я нажимаю закрыть окно, он правильно применяет значение по умолчанию в качестве ответа.
  • Но затем, время от времени, когда я нажимаю кнопки, я получаю странные ошибки _tkinter.TclError: invalid command name ".!ctkframe2.!ctkcanvas" (см. журнал выполнения ниже для всей трассировки стека)

Я подозреваю, что это как-то связано с таймером, так как ошибки не всегда появляются при нажатии кнопок. Это действительно сводит меня с ума...

Пример кода

# 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='')


# ____________________________________________________________________________________________

Журнал выполнения с ошибками

Первые три теста прошли успешно (включая нажатие кнопок), после этого появились ошибки.

(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 в качестве ОС и имею виртуальную среду Python 3.11 с установленным customtkinter.

Обновлено:

С помощью ответа @Thingamabobs мне удалось добиться ожидаемого поведения без ошибок. Вот окончательный код:

# 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.

Можете ли вы воспроизвести ошибки с чистым tkinter вместо customtkinter? Также self.tk.eval("after info") может ломаться customtkinter в зависимости от того, как запрограммировано customtkinter

TheLizzard 18.11.2022 12:33
Мутабельность и переработка объектов в Python
Мутабельность и переработка объектов в Python
Объекты являются основной конструкцией любого языка ООП, и каждый язык определяет свой собственный синтаксис для их создания, обновления и...
Другой маршрут в Flask Python
Другой маршрут в Flask Python
Flask - это фреймворк, который поддерживает веб-приложения. В этой статье я покажу, как мы можем использовать @app .route в flask, чтобы иметь другую...
14 Задание: Типы данных и структуры данных Python для DevOps
14 Задание: Типы данных и структуры данных Python для DevOps
Проверить тип данных используемой переменной, мы можем просто написать: your_variable=100
Python PyPDF2 - запись метаданных PDF
Python PyPDF2 - запись метаданных PDF
Python скрипт, который будет записывать метаданные в PDF файл, для этого мы будем использовать PDF ридер из библиотеки PyPDF2 . PyPDF2 - это...
Переменные, типы данных и операторы в Python
Переменные, типы данных и операторы в Python
В Python переменные используются как место для хранения значений. Пример переменной формы:
Почему Python - идеальный выбор для проекта AI и ML
Почему Python - идеальный выбор для проекта AI и ML
Блог, которым поделился Harikrishna Kundariya в нашем сообществе Developer Nation Community.
2
1
133
1
Перейти к ответу Данный вопрос помечен как решенный

Ответы 1

Ответ принят как подходящий

Пока у меня нет Ctk, чтобы дать вам точный код. Я могу точно сказать вам, что не так и как вам нужно это решить.

У вас есть самоповторяющаяся функция через после здесь:

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 (первым пришел, первым вышел). Таким образом, самое старое событие обрабатывается. Это означает, что вы можете попытаться отменить будильник, но включите будильник до того, как отмените его.

Как решить?

Доступны разные подходы. На мой взгляд, самое простое и чистое решение — установить флаг и не пытаться уничтожить уже уничтоженное окно. Что-то вроде:

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

И установите его как:

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 и установить флаг, так как это, вероятно, также происходит при уничтожении окна через оконный менеджер.


Другой подход — блок try и except в terminate, где вы ловите except _tkinter.TclError:. Но обратите внимание на подчеркивание, модуль не предназначен для использования, и он может измениться в будущем и может снова сломать ваше приложение, даже если это кажется маловероятным.

Вау, чувак, молниеносный ответ!! Большое спасибо за этот качественный ответ. Кажется, теперь я понимаю, откуда взялись эти ошибки. В итоге я использовал комбинацию двух подходов. Я внедрил флаг self.terminated и добавил его в свой метод terminate (который, кстати, также вызывается обработчиком протокола для WM_DELETE_WINDOW, см. метод self.on_closing. Однако я не смог удалить свой метод self.stop_after_callbacks. Без него я получаю другие уродливые сообщения об ошибках. распечатаны. Но скрипт не ломали. С try: ... except TclError: я и от них избавился.

N. Maks 18.11.2022 14:41

@N.Maks Когда вы знаете ответ, вам просто нужно его напечатать, но спасибо за добрые слова. Я много играл с tkinter, так что я стал экспертом в python-tkinter, и я ценю хорошие вопросы. Рад, что смог тебе помочь. :)

Thingamabobs 18.11.2022 19:55

Другие вопросы по теме