我正在尝试在 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.update()
而不是 window.update_idletasks()
或同时使用两者时,GUI 不会冻结。
但真正的问题是
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))
完整的工作代码
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)
#print('after:', )
window.after(speed, repeater, data, draw_data, speed)
#print('return')
return
except StopIteration:
generator = None
def regenerate():
global data
print('regenerate')
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():
repeater(data, draw_data, int(speedbar.get()*1000))
# --- main ---
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)
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()