多页面(类)场景下Scrollbar的MouseWheel事件失效问题排查
问题解决:Tkinter多页面Canvas鼠标滚轮滚动冲突
问题描述
用Tkinter实现带滚动内容的Canvas,单页面时鼠标滚轮功能正常,但添加第二个页面后,只有最后创建的页面能正常滚动,首个页面的滚轮事件失效。当前每个页面类内部都定义了MouseWheel处理函数,推测是事件绑定冲突导致。
原因分析
问题出在使用bind_all("<MouseWheel>", ...)——这个方法会给所有组件绑定全局事件,后面页面的绑定会直接覆盖前面的,所以只有最后一次绑定的函数生效。
解决方案
方案1:页面切换时动态绑定/解绑滚轮事件
修改主应用的show_frame方法,在切换页面时,先解绑全局的滚轮事件,再绑定当前激活页面的滚轮处理函数。同时让每个滚动页面类暴露自己的滚轮处理方法:
import tkinter as tk from tkinter import ttk import numpy as np from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg from matplotlib.figure import Figure import matplotlib.pyplot as plt class scrolltest(tk.Tk): def __init__(self, *args, **kwargs): tk.Tk.__init__(self, *args, **kwargs) container = tk.Frame(self) container.pack(side="top", fill="both", expand=True) container.grid_rowconfigure(0, weight=1) container.grid_columnconfigure(0, weight=1) self.frames = {} self.current_scroll_handler = None for F in (StartPage, Example, Example2): frame = F(container, self) self.frames[F] = frame frame.grid(row=0, column=0, sticky="nsew") self.show_frame(StartPage) def show_frame(self, cont): frame = self.frames[cont] frame.tkraise() # 先解绑之前的全局滚轮事件 if self.current_scroll_handler: self.unbind_all("<MouseWheel>") # 如果当前页面有滚轮处理方法,绑定它 if hasattr(frame, "on_mouse_wheel"): self.current_scroll_handler = frame.on_mouse_wheel self.bind_all("<MouseWheel>", self.current_scroll_handler) class StartPage(tk.Frame): def __init__(self, parent, controller): tk.Frame.__init__(self, parent) button1 = tk.Button(self, text="Example", command=lambda: controller.show_frame(Example)) button1.pack() button2 = tk.Button(self, text="Example2", command=lambda: controller.show_frame(Example2)) button2.pack() class Example(tk.Frame): def __init__(self, parent, controller): tk.Frame.__init__(self, parent) self.columnconfigure(0, weight=1) self.rowconfigure(3, weight=3) tk.Button(self, bg='red', text="Back", command=lambda: controller.show_frame(StartPage)).grid(row=0, column=0, sticky='we', padx=5, pady=10) tk.Label(self, bg='green', text="label").grid(row=2, column=0, sticky='we', padx=5, pady=5) first_frame = tk.Frame(self, height=480, width=400, pady=5, padx=5) first_frame.grid(row=3, column=0) self.canvas = tk.Canvas(first_frame) self.canvas.pack(side="left", fill="both", expand=True) second_frame = tk.Canvas(self.canvas) self.canvas.create_window((0, 0), window=second_frame, anchor="nw") scrollbar = ttk.Scrollbar(first_frame, orient="vertical", command=self.canvas.yview) self.canvas.configure(yscrollcommand=scrollbar.set) # 修正滚动区域绑定:绑定到canvas的Configure事件 self.canvas.bind("<Configure>", lambda e: self.canvas.configure(scrollregion=self.canvas.bbox("all"))) scrollbar.pack(side="right", fill="y") # 生成图表 np.random.seed(3) x = 0.5 + np.arange(8) y = np.random.uniform(2, 7, len(x)) fig, ax1 = plt.subplots(2, figsize=(5.5, 10)) ax1[0].step(x, y, linewidth=2.5) ax1[0].set(xlim=(0, 8), xticks=np.arange(1, 8), ylim=(0, 8), yticks=np.arange(1, 8)) ax1[1].step(x, y, linewidth=2.5) ax1[1].set(xlim=(0, 8), xticks=np.arange(1, 8), ylim=(0, 8), yticks=np.arange(1, 8)) canvas_fig1 = FigureCanvasTkAgg(fig, second_frame) canvas_fig1.get_tk_widget().pack(side="bottom", fill="y") fig2, ax1 = plt.subplots(2, figsize=(5.5, 10)) ax1[0].step(x, y, linewidth=2.5) ax1[0].set(xlim=(0, 8), xticks=np.arange(1, 8), ylim=(0, 8), yticks=np.arange(1, 8)) ax1[1].step(x, y, linewidth=2.5) ax1[1].set(xlim=(0, 8), xticks=np.arange(1, 8), ylim=(0, 8), yticks=np.arange(1, 8)) canvas_fig2 = FigureCanvasTkAgg(fig2, second_frame) canvas_fig2.get_tk_widget().pack(side="bottom", fill="y") def on_mouse_wheel(self, event): self.canvas.yview_scroll(-1 * int((event.delta / 120)), "units") class Example2(tk.Frame): def __init__(self, parent, controller): tk.Frame.__init__(self, parent) self.columnconfigure(0, weight=1) self.rowconfigure(3, weight=3) tk.Button(self, bg='red', text="Back", command=lambda: controller.show_frame(StartPage)).grid(row=0, column=0, sticky='we', padx=5, pady=10) tk.Label(self, bg='green', text="label").grid(row=2, column=0, sticky='we', padx=5, pady=5) first_frame2 = tk.Frame(self, height=480, width=400, pady=5, padx=5) first_frame2.grid(row=3, column=0) self.canvas = tk.Canvas(first_frame2) self.canvas.pack(side="left", fill="both", expand=True) second_frame2 = tk.Canvas(self.canvas) self.canvas.create_window((0, 0), window=second_frame2, anchor="nw") scrollbar2 = ttk.Scrollbar(first_frame2, orient="vertical", command=self.canvas.yview) self.canvas.configure(yscrollcommand=scrollbar2.set) self.canvas.bind("<Configure>", lambda e: self.canvas.configure(scrollregion=self.canvas.bbox("all"))) scrollbar2.pack(side="right", fill="y") # 生成图表 np.random.seed(3) x = 0.5 + np.arange(8) y = np.random.uniform(2, 7, len(x)) fig, ax1 = plt.subplots(2, figsize=(5.5, 10)) ax1[0].step(x, y, linewidth=2.5) ax1[0].set(xlim=(0, 8), xticks=np.arange(1, 8), ylim=(0, 8), yticks=np.arange(1, 8)) ax1[1].step(x, y, linewidth=2.5) ax1[1].set(xlim=(0, 8), xticks=np.arange(1, 8), ylim=(0, 8), yticks=np.arange(1, 8)) canvas_fig1 = FigureCanvasTkAgg(fig, second_frame2) canvas_fig1.get_tk_widget().pack(side="bottom", fill="y") fig2, ax1 = plt.subplots(2, figsize=(5.5, 10)) ax1[0].step(x, y, linewidth=2.5) ax1[0].set(xlim=(0, 8), xticks=np.arange(1, 8), ylim=(0, 8), yticks=np.arange(1, 8)) ax1[1].step(x, y, linewidth=2.5) ax1[1].set(xlim=(0, 8), xticks=np.arange(1, 8), ylim=(0, 8), yticks=np.arange(1, 8)) canvas_fig2 = FigureCanvasTkAgg(fig2, second_frame2) canvas_fig2.get_tk_widget().pack(side="bottom", fill="y") def on_mouse_wheel(self, event): self.canvas.yview_scroll(-1 * int((event.delta / 120)), "units") app = scrolltest() app.title("Scrollfigure") app.mainloop()
方案2:封装滚动Canvas为基类(减少重复代码)
创建一个带滚动功能的基类,所有需要滚动的页面继承该类,同时让基类判断页面是否激活再响应滚动:
import tkinter as tk from tkinter import ttk import numpy as np from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg from matplotlib.figure import Figure import matplotlib.pyplot as plt class ScrollableFrame(tk.Frame): def __init__(self, parent, controller, **kwargs): super().__init__(parent, **kwargs) self.controller = controller self.columnconfigure(0, weight=1) self.rowconfigure(3, weight=3) # 通用返回按钮 tk.Button(self, bg='red', text="Back", command=lambda: controller.show_frame(StartPage)).grid(row=0, column=0, sticky='we', padx=5, pady=10) # 通用标签 tk.Label(self, bg='green', text="label").grid(row=2, column=0, sticky='we', padx=5, pady=5) # 滚动区域布局 first_frame = tk.Frame(self, height=480, width=400, pady=5, padx=5) first_frame.grid(row=3, column=0) self.canvas = tk.Canvas(first_frame) self.canvas.pack(side="left", fill="both", expand=True) self.inner_frame = tk.Canvas(self.canvas) self.canvas.create_window((0, 0), window=self.inner_frame, anchor="nw") scrollbar = ttk.Scrollbar(first_frame, orient="vertical", command=self.canvas.yview) self.canvas.configure(yscrollcommand=scrollbar.set) self.canvas.bind("<Configure>", lambda e: self.canvas.configure(scrollregion=self.canvas.bbox("all"))) scrollbar.pack(side="right", fill="y") def on_mouse_wheel(self, event): # 只在当前页面激活时处理滚动 if self == self.controller.winfo_children()[0].winfo_children()[0]: self.canvas.yview_scroll(-1 * int((event.delta / 120)), "units") class scrolltest(tk.Tk): def __init__(self, *args, **kwargs): tk.Tk.__init__(self, *args, **kwargs) container = tk.Frame(self) container.pack(side="top", fill="both", expand=True) container.grid_rowconfigure(0, weight=1) container.grid_columnconfigure(0, weight=1) self.frames = {} for F in (StartPage, Example, Example2): frame = F(container, self) self.frames[F] = frame frame.grid(row=0, column=0, sticky="nsew") # 给所有滚动页面绑定滚轮事件,通过add="+"避免覆盖 for frame in self.frames.values(): if isinstance(frame, ScrollableFrame): self.bind_all("<MouseWheel>", frame.on_mouse_wheel, add="+") self.show_frame(StartPage) def show_frame(self, cont): frame = self.frames[cont] frame.tkraise() class StartPage(tk.Frame): def __init__(self, parent, controller): tk.Frame.__init__(self, parent) button1 = tk.Button(self, text="Example", command=lambda: controller.show_frame(Example)) button1.pack() button2 = tk.Button(self, text="Example2", command=lambda: controller.show_frame(Example2)) button2.pack() class Example(ScrollableFrame): def __init__(self, parent, controller): super().__init__(parent, controller) # 生成图表 np.random.seed(3) x = 0.5 + np.arange(8) y = np.random.uniform(2, 7, len(x)) fig, ax1 = plt.subplots(2, figsize=(5.5, 10)) ax1[0].step(x, y, linewidth=2.5) ax1[0].set(xlim=(0, 8), xticks=np.arange(1, 8), ylim=(0, 8), yticks=np.arange(1, 8)) ax1[1].step(x, y, linewidth=2.5) ax1[1].set(xlim=(0, 8), xticks=np.arange(1, 8), ylim=(0, 8), yticks=np.arange(1, 8)) canvas_fig1 = FigureCanvasTkAgg(fig, self.inner_frame) canvas_fig1.get_tk_widget().pack(side="bottom", fill="y") fig2, ax1 = plt.subplots(2, figsize=(5.5, 10)) ax1[0].step(x, y, linewidth=2.5) ax1[0].set(xlim=(0, 8), xticks=np.arange(1, 8), ylim=(0, 8), yticks=np.arange(1, 8)) ax1[1].step(x, y, linewidth=2.5) ax1[1].set(xlim=(0, 8), xticks=np.arange(1, 8), ylim=(0, 8), yticks=np.arange(1, 8)) canvas_fig2 = FigureCanvasTkAgg(fig2, self.inner_frame) canvas_fig2.get_tk_widget().pack(side="bottom", fill="y") class Example2(ScrollableFrame): def __init__(self, parent, controller): super().__init__(parent, controller) # 生成图表 np.random.seed(3) x = 0.5 + np.arange(8) y = np.random.uniform(2, 7, len(x)) fig, ax1 = plt.subplots(2, figsize=(5.5, 10)) ax1[0].step(x, y, linewidth=2.5) ax1[0].set(xlim=(0, 8), xticks=np.arange(1, 8), ylim=(0, 8), yticks=np.arange(1, 8)) ax1[1].step(x, y, linewidth=2.5) ax1[1].set(xlim=(0, 8), xticks=np.arange(1, 8), ylim=(0, 8), yticks=np.arange(1, 8)) canvas_fig1 = FigureCanvasTkAgg(fig, self.inner_frame) canvas_fig1.get_tk_widget().pack(side="bottom", fill="y") fig2, ax1 = plt.subplots(2, figsize=(5.5, 10)) ax1[0].step(x, y, linewidth=2.5) ax1[0].set(xlim=(0, 8), xticks=np.arange(1, 8), ylim=(0, 8), yticks=np.arange(1, 8)) ax1[1].step(x, y, linewidth=2.5) ax1[1].set(xlim=(0, 8), xticks=np.arange(1, 8), ylim=(0, 8), yticks=np.arange(1, 8)) canvas_fig2 = FigureCanvasTkAgg(fig2, self.inner_frame) canvas_fig2.get_tk_widget().pack(side="bottom", fill="y") app = scrolltest() app.title("Scrollfigure") app.mainloop()
关键说明
- 方案1逻辑简单直接,适合页面较少的场景,通过切换页面时替换全局绑定实现。
- 方案2通过基类封装滚动逻辑,大幅减少重复代码,适合多页面场景,通过判断页面激活状态来响应
相关产品推荐
相关产品推荐

