Tkinter滚动Canvas多次更新后UI崩溃问题及按需绘制优化求助
文本聚类应用UI崩溃问题排查与优化咨询
我正在开发一款文本聚类应用,界面用于展示所有聚类集群。参考其他方案实现了支持滚动的组件列表,每个集群自身可滚动,整体网格也支持滚动。应用正常运行时界面如下:
当用户调整滑块时,集群会更新,所有滚动列表会被销毁并重新绘制新集群。但调整滑块3-4次后,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
相关产品推荐
相关产品推荐

