Я пытаюсь создать приложение в 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. Нажатие на окно по-прежнему приводит к его зависанию до завершения процесса.
@Harbourheading, использующий after только с 1 аргументом, аналогичен time.sleep. Используйте его с обратным вызовом, например this. Если вы уже используете это, отредактируйте свой вопрос, указав новый код.
@TheLizzard Я обновил код до того решения, которое, по моему мнению, вы просили? Однако теперь код, похоже, не работает с той скоростью, которую я установил. Я увеличил масштаб/размер алгоритма, чтобы проверить, зависает ли он при нажатии примерно через 2 секунды, и так оно и было.
если вы запускаете долго выполняющийся код, каждый графический интерфейс (не только tkinter) зависнет. Внутри bubble у вас есть вложенные for-циклы с window.after(... bubble) - поэтому в каждом цикле он начинается заново bubble - поэтому он может выполнять много bubble одновременно - и это может убить вашу программу. В конце концов, вам лучше использовать ` window.after(... bubble)for-цикл - для запуска только одного bubble
Другая проблема может заключаться в том, что во вложенных for-циклах вы запускаете draw_data - поэтому ему приходится перерисовывать весь график после каждого пикселя. И это также может убить вашу программу. Лучше рисуйте его после каждой линии. или всё-таки for-петли
у меня код работает правильно, когда я использую window.update() вместо window.update_idletasks() или когда я использую оба
для управления скоростью нужен window.after(int(speed * 1000)) без других параметров. Но для больших значений код замораживается. Возникла необходимость организовать весь код по-другому. Придется выходить из функции после каждого внутреннего цикла, но это затрудняет сохранение информации о значениях из for-циклов.
@furas Огромное спасибо за ценную информацию! Кажется, отключение window.update_idletasks() на window.update() сработало. Однако я подумаю о реорганизации кода, чтобы сделать его более эффективным при рисовании. Еще раз спасибо : )
У меня уже есть рабочий код — он использует yield для выхода из цикла for (после запуска window.after()), а позже может продолжить его в том же месте. И наконец мне это не нужно window.update_idletasks() with window.update()






Графический интерфейс не зависает, когда я использую 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 Я думал о классе, позволяющем скрыть генератор, и о нескольких других изменениях, но в этом коде было бы слишком много изменений. Итератор может стать большим изменением для OP, если OP не использовал его раньше :) Поэтому я сохраняю только более важные изменения.
Лучше используйте
window.after()вместоtime.sleep()