Python Tkinter: зависание окна при щелчке пользователя во время update_idletasks()

Я пытаюсь создать приложение в tkinter для визуализации структур данных и алгоритмов. Однако у меня возникла проблема с тем, как программа обновляет холст во время процесса визуализации. Когда процесс визуализации занимает более ~2 секунд, программа зависает, если пользователь нажимает на окно (не отвечает), и возобновляет работу после завершения визуализации. Я уверен, что вызывает эту проблему, и она не возникнет, если пользователь никогда не нажимает кнопку мыши. Есть идеи относительно реальной проблемы и/или проблемы? Приложение все еще находится на стадии тестирования, поэтому оно не очень чистое, извините за это.

"""Module for visualizing data structures and algorithms"""
import random
import tkinter as tk
from tkinter import HORIZONTAL


def bubble(data, draw_data, speed):
    data_length = len(data)

    for i in range(data_length):
        for j in range(0, data_length - i - 1):

            if data[j] > data[j + 1]:
                data[j], data[j + 1] = data[j + 1], data[j]

                # if swapped then color becomes Green else stays Red
                draw_data(data, ['Green' if x == j + 1 else 'Red' for x in range(len(data))])
                window.after(int(speed * 1000), bubble, data, draw_data, int(speed * 1000))

    # sorted elements generated with Green color
    draw_data(data, ['Green' for _ in range(len(data))])


window = tk.Tk()
window.minsize(700, 580)

app_width, app_height = 700, 580
screen_width, screen_height = window.winfo_screenwidth(), window.winfo_screenheight()

mid_x = (screen_width - app_width) // 2
mid_y = (screen_height - app_height) // 2

window.title("StructViz")
window.iconbitmap("./assets/favicon.ico")
window.geometry(f'{app_width}x{app_height}+{mid_x}+{mid_y}')

select_alg = tk.StringVar()
data = []


def regenerate():
    global data

    minval = int(minEntry.get())

    maxval = int(maxEntry.get())

    sizeval = int(amountEntry.get())

    data = []
    for _ in range(sizeval):
        data.append(random.randint(minval, maxval + 1))

    draw_data(data, ['Red' for _ in range(len(data))])


def draw_data(data, colorlist):
    structVizC.delete("all")

    canvas_height = 380
    canvas_width = 500
    x_width = canvas_width / (len(data) + 1)
    offset = 30
    spacing = 10

    normalized_data = [i / max(data) for i in data]

    for i, height in enumerate(normalized_data):
        x0 = i * x_width + offset + spacing
        y0 = canvas_height - height * 340

        x1 = ((i + 1) * x_width + offset)
        y1 = canvas_height

        structVizC.create_rectangle(x0, y0, x1, y1, fill=colorlist[i])
        structVizC.create_text(x0 + 2, y0, anchor='se', text=str(data[i]))
    window.update_idletasks()


def start_algorithm():
    global data
    bubble(data, draw_data, speedbar.get())


window.columnconfigure(0, weight=0)
window.columnconfigure(1, weight=45)
window.rowconfigure((0, 1), weight=1)
window.rowconfigure(2, weight=45)

navbarLB = tk.Listbox(window, selectmode=tk.SINGLE)
for item in ["Option 1", "Option 2", "Option 3"]:
    navbarLB.insert(tk.END, item)
navbarLB.grid(row=0, column=0, rowspan=3, sticky='nsew')

userSettingsF = (tk.Frame(window, background='bisque2')
                 .grid(row=0, column=1, columnspan=2, rowspan=2, sticky='news', padx=7, pady=5))

# Change navbarLB.get(0) when user generates a selected option ( from func selected_item() )
AlgorithmL = tk.Label(userSettingsF, text=navbarLB.get(0), background='bisque2')
AlgorithmL.grid(row=0, column=1, sticky='nw', padx=10, pady=10)

amountEntry = tk.Scale(userSettingsF, from_=5, to=40, label='Amount', background='bisque2',
                       orient=HORIZONTAL, resolution=1, cursor='arrow')
amountEntry.grid(row=0, column=1, sticky='n', padx=10, pady=10)

minEntry = tk.Scale(userSettingsF, from_=0, to=10, resolution=1, background='bisque2',
                    orient=HORIZONTAL, label = "Minimum Value")
minEntry.grid(row=1, column=1, sticky='s', padx=10, pady=10)

maxEntry = tk.Scale(userSettingsF, from_=10, to=100, resolution=1, background='bisque2',
                    orient=HORIZONTAL, label = "Maximum Value")
maxEntry.grid(row=1, column=1, sticky='se', padx=10, pady=10)

speedbar = tk.Scale(userSettingsF, from_=0.10, to=2.0, length=100, digits=2, background='bisque2',
                    resolution=0.1, orient=HORIZONTAL, label = "Speed")
speedbar.grid(row=0, column=1, sticky='ne', padx=10, pady=10)

tk.Button(userSettingsF, text = "Start", bg = "Blue", command=start_algorithm, background='bisque2').grid(
    row=1, column=1, sticky='nw', padx=10, pady=10)

tk.Button(userSettingsF, text = "Regenerate", bg = "Red", command=regenerate, background='bisque2').grid(
    row=1, column=1, sticky='sw', padx=10, pady=10)

structVizC = tk.Canvas(window, background='bisque2')
structVizC.grid(row=2, column=1, sticky='news', padx=5, pady=5)

window.mainloop()

Во время визуализации приложение должно отображать процесс. При щелчке пользователя он ничего не должен делать. Вместо этого он зависает (не отвечает) до тех пор, пока процесс не завершится.

Лучше используйте window.after() вместо time.sleep()

toyota Supra 07.06.2024 14:29

Использование window.after, похоже, не дает результата, отличного от time.sleep. Нажатие на окно по-прежнему приводит к его зависанию до завершения процесса.

Harbourheading 07.06.2024 14:45

@Harbourheading, использующий after только с 1 аргументом, аналогичен time.sleep. Используйте его с обратным вызовом, например this. Если вы уже используете это, отредактируйте свой вопрос, указав новый код.

TheLizzard 07.06.2024 15:05

@TheLizzard Я обновил код до того решения, которое, по моему мнению, вы просили? Однако теперь код, похоже, не работает с той скоростью, которую я установил. Я увеличил масштаб/размер алгоритма, чтобы проверить, зависает ли он при нажатии примерно через 2 секунды, и так оно и было.

Harbourheading 07.06.2024 15:32

если вы запускаете долго выполняющийся код, каждый графический интерфейс (не только tkinter) зависнет. Внутри bubble у вас есть вложенные for-циклы с window.after(... bubble) - поэтому в каждом цикле он начинается заново bubble - поэтому он может выполнять много bubble одновременно - и это может убить вашу программу. В конце концов, вам лучше использовать ` window.after(... bubble)for-цикл - для запуска только одного bubble

furas 07.06.2024 19:13

Другая проблема может заключаться в том, что во вложенных for-циклах вы запускаете draw_data - поэтому ему приходится перерисовывать весь график после каждого пикселя. И это также может убить вашу программу. Лучше рисуйте его после каждой линии. или всё-таки for-петли

furas 07.06.2024 19:17

у меня код работает правильно, когда я использую window.update() вместо window.update_idletasks() или когда я использую оба

furas 07.06.2024 19:31

для управления скоростью нужен window.after(int(speed * 1000)) без других параметров. Но для больших значений код замораживается. Возникла необходимость организовать весь код по-другому. Придется выходить из функции после каждого внутреннего цикла, но это затрудняет сохранение информации о значениях из for-циклов.

furas 07.06.2024 19:44

@furas Огромное спасибо за ценную информацию! Кажется, отключение window.update_idletasks() на window.update() сработало. Однако я подумаю о реорганизации кода, чтобы сделать его более эффективным при рисовании. Еще раз спасибо : )

Harbourheading 07.06.2024 20:19

У меня уже есть рабочий код — он использует yield для выхода из цикла for (после запуска window.after()), а позже может продолжить его в том же месте. И наконец мне это не нужно window.update_idletasks() with window.update()

furas 07.06.2024 20:24
Почему в Python есть оператор "pass"?
Почему в Python есть оператор "pass"?
Оператор pass в Python - это простая концепция, которую могут быстро освоить даже новички без опыта программирования.
Некоторые методы, о которых вы не знали, что они существуют в Python
Некоторые методы, о которых вы не знали, что они существуют в Python
Python - самый известный и самый простой в изучении язык в наши дни. Имея широкий спектр применения в области машинного обучения, Data Science,...
Основы Python Часть I
Основы Python Часть I
Вы когда-нибудь задумывались, почему в программах на Python вы видите приведенный ниже код?
LeetCode - 1579. Удаление максимального числа ребер для сохранения полной проходимости графа
LeetCode - 1579. Удаление максимального числа ребер для сохранения полной проходимости графа
Алиса и Боб имеют неориентированный граф из n узлов и трех типов ребер:
Оптимизация кода с помощью тернарного оператора Python
Оптимизация кода с помощью тернарного оператора Python
И последнее, что мы хотели бы показать вам, прежде чем двигаться дальше, это
Советы по эффективной веб-разработке с помощью Python
Советы по эффективной веб-разработке с помощью Python
Как веб-разработчик, Python может стать мощным инструментом для создания эффективных и масштабируемых веб-приложений.
0
10
57
1
Перейти к ответу Данный вопрос помечен как решенный

Ответы 1

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

Графический интерфейс не зависает, когда я использую window.update() вместо window.update_idletasks() или когда я использую оба вместе.

Но настоящая проблема в том, что bubble() работает window.after(...), но никогда не выходит bubble() после window.after() и не может сделать задержку перед следующим рисованием - поэтому он не может контролировать скорость. И это тоже создает проблемы с update_idletasks().

Ему необходимо выйти из вложенных циклов, но возникает проблема с возвратом во вложенный цикл. И для этого, возможно, потребуется использовать yield, который может выйти из функции, а затем запустить функцию после yield вместо того, чтобы начинать с начала.

Вот bubble() с yield но без window.after()

def bubble(data, draw_data):
    data_length = len(data)

    for i in range(data_length):
        for j in range(0, data_length - i - 1):

            if data[j] > data[j + 1]:
                data[j], data[j + 1] = data[j + 1], data[j]
                draw_data(data, ['Green' if x == j + 1 else 'Red' for x in range(data_length)])
                yield
    
    # sorted elements generated with Green color
    draw_data(data, ['Green' for _ in range(len(data))])

    # here python runs as default `return None`

А вот функция, которая запускается bubble() и использует window.after()

generator = None

def repeater(data, draw_data, speed):
    global generator
    
    # run it only once - at start
    if not generator:
        generator = bubble(data, draw_data)

    try:
        # run function - it will raise `StopIteration` when it use `return` instead of `yield`
        next(generator)  
        
        # set next execution after some time
        window.after(speed, repeater, data, draw_data, speed)     
        
        # exit this function
        return
    except StopIteration:  # 
        # reset value after last execution 
        generator = None

И теперь кнопка начинается repeater вместо bubble

def start_algorithm():
    repeater(data, draw_data, int(speedbar.get()*1000))

И теперь он может работать с разной скоростью.
И это также работает правильно без window.update() и window.update_idletasks()


Полный рабочий код.

Я увеличил Amount до 100 и уменьшил Speed до 0.01 — чтобы увидеть более быструю анимацию с большим количеством значений.

Я также добавил логическую переменную running_animation, чтобы остановить анимацию при повторном нажатии кнопки Start во время анимации. Но это работает скорее как pause, потому что при повторном нажатии анимация продолжается. Я также использую его, чтобы остановить анимацию при нажатии Regenerate, но для корректной работы нужны некоторые изменения.

import random
import tkinter as tk

# --- classes ---

# --- functions ---

def bubble(data, draw_data):
    data_length = len(data)

    for i in range(data_length):
        #print('i:', i)
        for j in range(0, data_length - i - 1):
            #print('j:', j)

            if data[j] > data[j + 1]:
                data[j], data[j + 1] = data[j + 1], data[j]
                draw_data(data, ['Green' if x == j + 1 else 'Red' for x in range(data_length)])
                #print('before yield')
                yield
                #print('after yield') 

    #print('finish')
    # sorted elements generated with Green color
    draw_data(data, ['Green' for _ in range(len(data))])

generator = None

def repeater(data, draw_data, speed):
    global generator
    
    if not generator:
        generator = bubble(data, draw_data)

    try:
        #print('next')
        next(generator)
        
        if running_animation:
            #print('after:', )
            window.after(speed, repeater, data, draw_data, speed)     
        else:
            generator = None
        
        #print('return')
        return
    except StopIteration:
        #print('StopIteration')
        generator = None

def regenerate():
    global data
    global running_animation
    
    print('regenerate')

    if running_animation:
        running_animation = False
        
    minval = int(minEntry.get())
    maxval = int(maxEntry.get())
    sizeval = int(amountEntry.get())

    data = []
    for _ in range(sizeval):
        data.append(random.randint(minval, maxval + 1))

    draw_data(data, ['Red' for _ in range(len(data))])


def draw_data(data, colorlist):
    print('draw')
    
    structVizC.delete("all")

    canvas_height = 380
    canvas_width = 500
    x_width = canvas_width / (len(data) + 1)
    offset = 30
    spacing = 10

    normalized_data = [i / max(data) for i in data]

    for i, height in enumerate(normalized_data):
        x0 = i * x_width + offset + spacing
        y0 = canvas_height - height * 340

        x1 = ((i + 1) * x_width + offset)
        y1 = canvas_height

        structVizC.create_rectangle(x0, y0, x1, y1, fill=colorlist[i])
        structVizC.create_text(x0 + 2, y0, anchor='se', text=str(data[i]))
    
    #window.update_idletasks()
    #window.update()

def start_algorithm():
    global running_animation 
    
    if not running_animation:
        running_animation = True
        repeater(data, draw_data, int(speedbar.get()*1000))
        start_button['text'] = 'Stop'
    else:
        running_animation = False
        start_button['text'] = 'Start'

# --- main ---

running_animation = False

window = tk.Tk()
window.minsize(700, 580)

app_width, app_height = 700, 580
screen_width, screen_height = window.winfo_screenwidth(), window.winfo_screenheight()

mid_x = (screen_width - app_width) // 2
mid_y = (screen_height - app_height) // 2

window.title("StructViz")
#window.iconbitmap("./assets/favicon.ico")
window.geometry(f'{app_width}x{app_height}+{mid_x}+{mid_y}')

select_alg = tk.StringVar()
data = []


window.columnconfigure(0, weight=0)
window.columnconfigure(1, weight=45)
window.rowconfigure((0, 1), weight=1)
window.rowconfigure(2, weight=45)

navbarLB = tk.Listbox(window, selectmode=tk.SINGLE)
for item in ["Option 1", "Option 2", "Option 3"]:
    navbarLB.insert(tk.END, item)
navbarLB.grid(row=0, column=0, rowspan=3, sticky='nsew')

userSettingsF = (tk.Frame(window, background='bisque2')
                 .grid(row=0, column=1, columnspan=2, rowspan=2, sticky='news', padx=7, pady=5))

# Change navbarLB.get(0) when user generates a selected option ( from func selected_item() )
AlgorithmL = tk.Label(userSettingsF, text=navbarLB.get(0), background='bisque2')
AlgorithmL.grid(row=0, column=1, sticky='nw', padx=10, pady=10)

amountEntry = tk.Scale(userSettingsF, from_=5, to=100, label='Amount', background='bisque2',
                       orient = "horizontal", resolution=1, cursor='arrow')
amountEntry.grid(row=0, column=1, sticky='n', padx=10, pady=10)

minEntry = tk.Scale(userSettingsF, from_=0, to=10, resolution=1, background='bisque2',
                    orient = "horizontal", label = "Minimum Value")
minEntry.grid(row=1, column=1, sticky='s', padx=10, pady=10)

maxEntry = tk.Scale(userSettingsF, from_=10, to=100, resolution=1, background='bisque2',
                    orient = "horizontal", label = "Maximum Value")
maxEntry.grid(row=1, column=1, sticky='se', padx=10, pady=10)

speedbar = tk.Scale(userSettingsF, from_=0.01, to=1.0, length=100, digits=3, background='bisque2',
                    resolution=0.01, orient = "horizontal", label = "Speed")
speedbar.grid(row=0, column=1, sticky='ne', padx=10, pady=10)

start_button = tk.Button(userSettingsF, text = "Start", bg = "Blue", command=start_algorithm, background='bisque2')
start_button.grid(row=1, column=1, sticky='nw', padx=10, pady=10)

tk.Button(userSettingsF, text = "Regenerate", bg = "Red", command=regenerate, background='bisque2').grid(
    row=1, column=1, sticky='sw', padx=10, pady=10)

structVizC = tk.Canvas(window, background='bisque2')
structVizC.grid(row=2, column=1, sticky='news', padx=5, pady=5)

window.mainloop()

+1 Хороший метод использования итератора для преобразования вложенных циклов for в цикл tkinterafter. Также вы можете избежать использования global generator, если вместо этого передадите его в качестве необязательного аргумента. Более того, вы даже можете выделить из него отдельный класс, который просто берет любой итератор и использует .after().

TheLizzard 07.06.2024 22:52

@TheLizzard Я думал о классе, позволяющем скрыть генератор, и о нескольких других изменениях, но в этом коде было бы слишком много изменений. Итератор может стать большим изменением для OP, если OP не использовал его раньше :) Поэтому я сохраняю только более важные изменения.

furas 07.06.2024 23:08

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