You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Tkinter滚动Canvas多次更新后UI崩溃问题及按需绘制优化求助

文本聚类应用UI崩溃问题排查与优化咨询

我正在开发一款文本聚类应用,界面用于展示所有聚类集群。参考其他方案实现了支持滚动的组件列表,每个集群自身可滚动,整体网格也支持滚动。应用正常运行时界面如下:
应用正常运行截图

当用户调整滑块时,集群会更新,所有滚动列表会被销毁并重新绘制新集群。但调整滑块3-4次后,UI会崩溃,元素被绘制到整个桌面。请帮我排查是否有遗漏的处理步骤?
UI崩溃截图


滚动功能基类ScrollFrame

from typing import List
import platform
import tkinter as tk
from tkinter import ttk
from tksheet._tksheet_vars import *


class ScrollFrame(tk.Frame):
    def __init__(self, parent, bg="white", **kwargs):
        super().__init__(parent, bg=bg, borderwidth=0,  **kwargs) 

        self.canvas = tk.Canvas(self, bg=bg, borderwidth=0)
        self.viewPort = tk.Frame(self.canvas, bg=bg, borderwidth=0)

        
        self.canvas.pack(side="left", fill="both", expand=True)                     
        self.canvas_window = self.canvas.create_window(
                (0,0),
                window=self.viewPort, anchor="nw",tags="self.viewPort")

        self.viewPort.bind("<Configure>", self.onFrameConfigure)                      
        self.canvas.bind("<Configure>", self.onCanvasConfigure)                       
            
        self.viewPort.bind('<Enter>', self.onEnter)                                   
        self.viewPort.bind('<Leave>', self.onLeave)                                  

        self.onFrameConfigure(None)

    def destroy(self):
        self.canvas.delete("alll")
        super().destroy()
        # self.destroy()

    def onFrameConfigure(self, event):
        self.canvas.configure(scrollregion=self.canvas.bbox("all"))                

    def onCanvasConfigure(self, event):
        canvas_width = event.width
        self.canvas.itemconfig(self.canvas_window, width = canvas_width)           

    def onMouseWheel(self, event):
        if platform.system() == 'Windows':
            self.canvas.yview_scroll(int(-1* (event.delta/120)), "units")
        elif platform.system() == 'Darwin':
            self.canvas.yview_scroll(int(-1 * event.delta), "units")
        else:
            if event.num == 4:
                self.canvas.yview_scroll( -1, "units" )
            elif event.num == 5:
                self.canvas.yview_scroll( 1, "units" )
    
    def onEnter(self, event):
        if platform.system() == 'Linux':
            self.canvas.bind_all("<Button-4>", self.onMouseWheel)
            self.canvas.bind_all("<Button-5>", self.onMouseWheel)
        else:
            self.canvas.bind_all("<MouseWheel>", self.onMouseWheel)

    def onLeave(self, event):
        if platform.system() == 'Linux':
            self.canvas.unbind_all("<Button-4>")
            self.canvas.unbind_all("<Button-5>")
        else:
            self.canvas.unbind_all("<MouseWheel>")

    def scroll_to(self, fraction=1):
        self.canvas.yview_moveto(fraction)

文本项与集群相关类

class TextItem(tk.Frame):
    def __init__(
            self,
            master,
            idx:int,
            text:str,
            selectable = False,
            selected=True,
            bg="white",
            on_item_selected=None,
            **kwds
        ):
            tk.Frame.__init__(self, master, bg=bg, borderwidth=0, **kwds)
            self.__text = text
            self.__select_var = tk.BooleanVar()
            self.__select_var.set(selected)
            self.grid_rowconfigure(0, weight=1)
            self.grid_columnconfigure(1, weight=1)

            if selectable:
                self.check = tk.Checkbutton(
                        self,
                        anchor='e',
                        text=self.__text,
                        variable=self.__select_var,
                        bg=bg,
                        fg=theme_light_blue["header_fg"],
                        command=on_item_selected
                    )
                self.check.grid(row=0, column=1, sticky='e')
            else:
                self.idx = tk.Label(
                    self,
                    text=str(idx),
                    bg=bg,
                    fg="#728494"
                ).grid(row=0, column=0, sticky='e')
                self.check = tk.Label(
                        self,
                        anchor='e',
                        text=self.__text,
                        bg=bg,
                        fg=theme_light_blue["header_fg"]
                    )
                self.check.grid(row=0, column=1, sticky='e')
    
    @property
    def is_selected(self):
        return self.__select_var.get()


class Cluster():
    def __init__(self, id:int, samples: List[str],) -> None:
        self.__samples = samples
        self.__id = id

    @property
    def samples(self):
        return self.__samples
    
    @property
    def id(self):
        return self.__id


class ClusterFrame(tk.Frame):
    def __init__(
        self,
        root,
        cluster: Cluster,
        bg="white",
        selectable=False,
        selecteds=None,
        title=None,
        **kwds
        ):
            tk.Frame.__init__(self, root, bg=bg, **kwds)
            self.__wrapper = tk.Frame(
                self,
                highlightbackground = theme_light_blue["index_border_fg"],
                highlightthickness=0.5, bg=bg
            )
            self.__selectable = selectable
            self.__cluster = cluster
            if not title:
                title = f"Cluster {cluster.id}: { len(cluster.samples)} samples"
            self.__title_label = tk.Label(self.__wrapper, text = title)
            self.__title_label.pack(side="top", fill="both")
            if self.__selectable:
                self.__selection_status = tk.Label(self.__wrapper, text = "Selected: ")
                self.__selection_status.pack(side="top", fill="both")


            self.scrollFrame = ScrollFrame(self.__wrapper)
            if not selecteds:
                selecteds = [True] * len(cluster.samples)
            # self.__selected_count = selecteds.sum()
            self.__view_items = []
            for idx, sample in enumerate(cluster.samples):
                item = TextItem(
                    self.scrollFrame.viewPort,
                    idx + 1,
                    sample,
                    selected=selecteds[idx],
                    selectable=self.__selectable,
                    on_item_selected=self.update_selected_status
                )
                item.grid(row=idx, column=0, sticky='w', padx=1, pady=1)
                self.__view_items.append(item)

            self.scrollFrame.pack(side="top", fill="both", expand=True)
            self.__wrapper.pack( fill="both", expand=True)
            if self.__selectable:
                self.update_selected_status()
    
    def get_selected_samples(self):
        return [self.__cluster.samples[idx] for idx, item in enumerate(self.__view_items) if item.is_selected]
    
    def update_selected_status(self):
        selected_count = len(self.get_selected_samples())
        self.__selection_status.config(text=f"Selected: {selected_count}")
    
    def scroll_to(self, fraction=1):
        self.scrollFrame.scroll_to(fraction)

网格视图ClusterGrid代码

class ClusterGrid(tk.Frame):
    def __init__(self, root,  clusters, num_cols=3, **kwargs):
        super().__init__(root, bg="white", **kwargs)

    # main_frame = tk.Frame(root)
        self.__num_cols = num_cols
        self.__clusters = clusters
        
        self.__main_canvas = tk.Canvas(self, borderwidth=0)
        self.__main_canvas.pack(side=tk.LEFT, fill=tk.BOTH, expand=1)

        main_scrbar = ttk.Scrollbar(
            self, 
            orient=tk.VERTICAL, 
            command=self.__main_canvas.yview
        )
        main_scrbar.pack(side=tk.RIGHT, fill=tk.Y)

        self.__main_canvas.configure(yscrollcommand=main_scrbar.set)
        # self.main_canvas.bind('<Configure>', lambda e: self.main_canvas.configure(scrollregion=self.main_canvas.bbox("all")))


        self.__scroll_frame = tk.Frame(self.__main_canvas, bg="white")
        self.__canvas_window = self.__main_canvas.create_window((0,0), window=self.__scroll_frame, anchor="nw")

        self.__scroll_frame.bind("<Configure>", self.onFrameConfigure)                      
        self.__main_canvas.bind("<Configure>", self.onCanvasConfigure)                       
            
        self.__scroll_frame.bind('<Enter>', self.onEnter)                                   
        self.__scroll_frame.bind('<Leave>', self.onLeave)

        for i, intent in enumerate(self.__clusters[:30]):
            col = i % self.__num_cols
            row = int(i/self.__num_cols)
            sc = ClusterFrame(self.__scroll_frame, intent, selectable=True)
            sc.grid(row=row, column=col, sticky='nsew', padx=5, pady=5)

        self.onFrameConfigure(None)

    def destroy(self):
        self.__main_canvas.delete("all")
        super().destroy()


    def onFrameConfigure(self, event):    
        '''Reset the scroll region to encompass the inner frame'''
        # print("ScrollFrame: onFrameConfigure")
        self.__main_canvas.configure(scrollregion=self.__main_canvas.bbox("all"))   

    def onCanvasConfigure(self, event):
        '''Reset the canvas window to encompass inner frame when required'''
        # print("ScrollFrame: onCanvasConfigure")
        canvas_width = event.width
        self.__main_canvas.itemconfig(self.__canvas_window, width = canvas_width)   


    def onEnter(self, event):
        print("ScrollFrame: onEnter")                                
        if platform.system() == 'Linux':
            self.__main_canvas.bind_all("<Button-4>", self.onMouseWheel)
            self.__main_canvas.bind_all("<Button-5>", self.onMouseWheel)
        else:
            self.__main_canvas.bind_all("<MouseWheel>", self.onMouseWheel)


    def onMouseWheel(self, event):
        # print("ScrollFrame: onMouseWheel")                            
        if platform.system() == 'Windows':
            self.__main_canvas.yview_scroll(int(-1* (event.delta/120)), "units")
        elif platform.system() == 'Darwin':
            self.__main_canvas.yview_scroll(int(-1 * event.delta), "units")
        else:
            if event.num == 4:
                self.__main_canvas.yview_scroll( -1, "units" )
            elif event.num == 5:
                self.__main_canvas.yview_scroll( 1, "units" )

    def onLeave(self, event):
        print("ScrollFrame: onLeave")                                                  
        if platform.system() == 'Linux':
            self.__main_canvas.unbind_all("<Button-4>")
            self.__main_canvas.unbind_all("<Button-5>")
        else:
            self.__main_canvas.unbind_all("<MouseWheel>") 

    def destroy(self) -> None:
        # self.__main_canvas.delete("all")
        return super().destroy()

网格视图初始化代码

import random
from threading import Timer
from tkinter import *
from scrollable_frames import ClusterGrid, Cluster


def clear_grid(view):
    print("clear_grid: ", view)
    if not view:
        return
    for widget in view.winfo_children():
        print("widget: ", widget)
        widget.destroy()
    


def on_slider_change(new_val, root):
    global slider_timer
    print("new_val: ", new_val)
    if slider_timer:
        slider_timer.cancel()
        print("cancelled")
    slider_timer = Timer(2, process, args=(new_val, root,))
    slider_timer.start()

def process(val, root):
    global intent_grid
    print("process...", val, intent_grid)
    
    print(val)
    clear_grid(intent_grid)
    clusters = []
    for i in range(100):
        samples = [ f"This is a sample text {i}" for i in range(100)]
        cl = Cluster(i, samples=samples)
        clusters.append(cl)
    intent_grid = ClusterGrid(root, clusters=clusters)
    intent_grid.grid(column=0, row=1, sticky="nsew")


root=Tk()
root.geometry("900x600")
intent_grid = None

root.columnconfigure(0, weight=1)
root.grid_rowconfigure(1, weight=1)

slider_timer = None
threshold_slider_val = DoubleVar(value=0.6)
threshold_slider = Scale(
        root,
        from_= 0.3,
        to=1,
        resolution=0.05,
        orient=HORIZONTAL,
        variable=threshold_slider_val,
        command=lambda x: on_slider_change(new_val=x, root=root)
    )
threshold_slider.grid(column=0, row=0, pady=20)
root.mainloop()

我了解在Canvas上绘制所有元素并非最优方案,请问是否有方法仅绘制可见区域的元素?感谢您的帮助!


内容的提问来源于stack exchange,提问作者Hoa Vu

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.09 11:10:41