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

Tkinter拖拽应用:拖动文本时绑定鼠标滚轮至对应滚动条

解决方案

问题核心是拖拽过程中鼠标悬浮在自定义的Toplevel拖拽窗口上,原有的canvas进入/离开事件无法触发,导致滚轮绑定不会自动切换。需要在拖拽过程中实时检测鼠标位置,动态切换滚轮的滚动目标。

以下是修改后的完整代码:

import tkinter as tk
from tkinter import ttk

class MainApplication(tk.Frame):
    def __init__(self, parent, *args, **kwargs):
        tk.Frame.__init__(self, parent, *args, **kwargs) 

        self.first_frame = tk.LabelFrame(root, text="")
        self.first_frame.pack(padx=15, pady=15, fill="both", expand="true", side="left", anchor="w")

        self.first_canvas = tk.Canvas(self.first_frame, highlightthickness=0)
        self.first_canvas.pack(side="left", fill="both", expand="true")
        # 移除原有的Enter/Leave绑定
        # self.first_canvas.bind("<Enter>", lambda _: self.first_canvas.bind_all('<MouseWheel>', self.on_mousewheel_left))
        # self.first_canvas.bind("<Leave>", lambda _: self.first_canvas.unbind_all('<MouseWheel>'))

        self.second_frame = tk.LabelFrame(root, text="")
        self.second_frame.pack(padx=15, pady=15, fill="both", expand="true", side="left")

        self.second_canvas = tk.Canvas(self.second_frame, highlightthickness=0)
        self.second_canvas.pack(side="left", fill="both", expand="true")
        # 移除原有的Enter/Leave绑定
        # self.second_canvas.bind("<Enter>", lambda _: self.second_canvas.bind_all('<MouseWheel>', self.on_mousewheel_right))
        # self.second_canvas.bind("<Leave>", lambda _: self.second_canvas.unbind_all('<MouseWheel>'))

        self.upload_one()
        self.upload_two()

    def get_canvas_under_mouse(self, x_root, y_root):
        # 递归查找鼠标下方的目标canvas
        widget = root.winfo_containing(x_root, y_root)
        while widget:
            if widget in (self.first_canvas, self.second_canvas):
                return widget
            widget = widget.winfo_parent()
        return None

    def upload_one(self):
        self.first_list = ["11:00", "03:43", "22:04", "17:21", "22:35", "07:01", "16:11", "09:00"]

        self.first_scrollbar = ttk.Scrollbar(self.first_frame, orient="vertical", command=self.first_canvas.yview)
        self.first_scrollbar.pack(side="right", fill="y")

        self.first_canvas.configure(yscrollcommand=self.first_scrollbar.set)
        self.first_canvas.bind('<Configure>', lambda e: self.first_canvas.configure(scrollregion = self.first_canvas.bbox("all")))

        self.first_list_frame = tk.Frame(self.first_canvas)

        self.first_canvas.create_window((0,0), window=self.first_list_frame, anchor="nw")

        for index, cit in enumerate(sorted(self.first_list)):
            self.one_frame = tk.LabelFrame(self.first_list_frame)
            self.one_frame.grid(row=index, column=0, padx=10, pady=(20,0))

            self.one_text = tk.Text(self.one_frame, width=77, height=4)
            self.one_text.grid(row=1, column=1, rowspan=2, sticky="nw")        

            self.dragged_text = tk.Text(self.one_frame, wrap="word", width=68, height=3, highlightthickness=1, highlightcolor= "DodgerBlue3",)
            self.dragged_text.grid(row=3, column=1, rowspan=2, columnspan=2, pady=(10,20), ipady=7, sticky="nw")

    def upload_two(self):
        self.second_list = ["morning\nmorning\nmorning", "afternoon\nafternoon\nafternoon", "evening\nevening\nevening", "night\nnight\nnight"]
        self.second_scrollbar = ttk.Scrollbar(self.second_frame, orient="vertical", command=self.second_canvas.yview)
        self.second_scrollbar.pack(side="right", fill="y")

        self.second_canvas.configure(yscrollcommand=self.second_scrollbar.set)
        self.second_canvas.bind('<Configure>', lambda e: self.second_canvas.configure(scrollregion = self.second_canvas.bbox("all")))

        self.two_list_frame = tk.Frame(self.second_canvas)

        self.second_canvas.create_window((0,0), window=self.two_list_frame, anchor="nw")

        for index, two in enumerate(sorted(self.second_list)):   
            self.two_frame = tk.Frame(self.two_list_frame)
            self.two_frame.pack(pady=(15,0), fill="x", expand="yes", side="top")

            self.two_text = tk.Text(self.two_frame, font=("Times New Roman", 11), wrap="word", highlightthickness=1, highlightcolor= "DodgerBlue3", cursor="hand2", width=81, height=4)
            self.two_text.grid(row=0, column=0, padx=15)
            self.two_text.insert("end", str(two.strip()), "normal")
            self.two_text.config(state="disabled")

            self.two_text.bind('<Button-1>', self.click)            
            self.two_text.bind('<B1-Motion>',self.drag)
            self.two_text.bind('<ButtonRelease-1>',self.release)

    def on_mousewheel_left(self, event):
        # 直接操作左侧canvas,无需额外获取widget
        self.first_canvas.yview_scroll(int(-1*(event.delta//120)), 'units')            

    def on_mousewheel_right(self, event):
        # 直接操作右侧canvas
        self.second_canvas.yview_scroll(int(-1*(event.delta//120)), 'units')   

    def click(self, event):
        self.text = event.widget.get("1.0",'end-1c')

        root.clipboard_clear()
        root.clipboard_append(self.text)

        self.top = tk.Toplevel(root) 
        self.top.attributes('-alpha', 0.7)
        self.top.overrideredirect(True) 

        self.top._offsetx = event.x
        self.top._offsety = event.y 

        x = self.top.winfo_pointerx() - self.top._offsetx - 30
        y = self.top.winfo_pointery() - self.top._offsety - 30 

        self.top.geometry('+{x}+{y}'.format(x=x,y=y))               

        two_toplevel_label = tk.Label(self.top, text=self.text, wraplength=571, justify="left", bg="SystemButtonFace")
        two_toplevel_label.grid(row=0, column=0)

    def drag(self, event):
        x = self.top.winfo_pointerx() - self.top._offsetx
        y = self.top.winfo_pointery() - self.top._offsety

        self.two_text.tag_remove("sel", "1.0", "end")
        self.top.geometry(f"+{x}+{y}")

        # 实时检测鼠标下方的canvas
        current_canvas = self.get_canvas_under_mouse(event.x_root, event.y_root)
        if current_canvas == self.first_canvas:
            # 绑定左侧滚轮事件,解绑右侧
            root.bind_all('<MouseWheel>', self.on_mousewheel_left)
        elif current_canvas == self.second_canvas:
            # 绑定右侧滚轮事件,解绑左侧
            root.bind_all('<MouseWheel>', self.on_mousewheel_right)
        else:
            # 不在任何canvas上,解绑所有滚轮事件
            root.unbind_all('<MouseWheel>')

    def release(self, event):        
        widget = event.widget.winfo_containing(event.x_root, event.y_root)
        self.top.destroy()
        # 释放鼠标后解绑所有滚轮绑定,恢复默认
        root.unbind_all('<MouseWheel>')


if __name__ == "__main__":
    root = tk.Tk()   
    MainApplication(root).pack(side="top", fill="both", expand=True)
    root.mainloop()

关键修改说明

  1. 移除原有绑定:删除了canvas的<Enter>和<Leave>事件绑定,因为拖拽时鼠标不在canvas上,这些事件不会触发。
  2. 新增位置检测函数:get_canvas_under_mouse函数递归查找鼠标当前位置下方的目标canvas,解决Toplevel窗口遮挡导致直接获取不到canvas的问题。
  3. 拖拽时动态切换绑定:在drag方法中,实时检测鼠标所在的canvas,动态绑定对应的滚轮滚动方法,实现拖拽时滚轮跟随鼠标位置切换滚动目标。
  4. 释放鼠标清理绑定:在release方法中解绑所有滚轮绑定,避免影响后续正常操作。
  5. 简化滚轮方法:直接操作对应的canvas,去掉了不必要的widget父级查找逻辑。

内容的提问来源于stack exchange,提问作者Diana Mele

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.21 22:45:53