Python Tkinter:在 update_idletasks() 期间用户单击时窗口冻结

问题描述 投票:0回答:1

我正在尝试在 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()

在可视化过程中,应用程序应显示该过程。用户单击时,它不应该执行任何操作。相反,它会冻结(不响应),直到该过程完成。

python tkinter canvas task tkinter-canvas
1个回答
0
投票

当我使用

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()
© www.soinside.com 2019 - 2024. All rights reserved.