Python Tkinter - 使用鼠标位置进行画布滚动

0 投票
3 回答
4442 浏览
提问于 2025-04-16 20:23

我觉得这个问题很常见,但我找不到答案。

我想做一个窗口,能够根据鼠标的位置来滚动:如果鼠标靠近屏幕顶部,它就向上滚动;如果靠近右边,它就向右滚动,依此类推。这里是我的代码:

from tkinter import *
from tkinter import ttk
root = Tk()

h = ttk.Scrollbar(root, orient = HORIZONTAL)
v = ttk.Scrollbar(root, orient = VERTICAL)
canvas = Canvas(root, scrollregion = (0, 0, 2000, 2000), width = 600, height = 600, yscrollcommand = v.set, xscrollcommand = h.set)
h['command'] = canvas.xview
v['command'] = canvas.yview
ttk.Sizegrip(root).grid(column=1, row=1, sticky=(S,E))

canvas.grid(column = 0, row = 0, sticky = (N,W,E,S))
h.grid(column = 0, row = 1, sticky = (W,E))
v.grid(column = 1, row = 0, sticky = (N,S))
root.grid_columnconfigure(0, weight = 1)
root.grid_rowconfigure(0, weight = 1)

canvas.create_rectangle((0, 0, 50, 50), fill = 'black')
canvas.create_rectangle((500, 500, 550, 550), fill = 'black')
canvas.create_rectangle((1500, 1500, 1550, 1550), fill = 'black')
canvas.create_rectangle((1000, 1000, 1050, 1050), fill = 'black')

def xy_motion(event):
    x, y = event.x, event.y

    if x < 30:        
        delta = -1
        canvas.xview('scroll', delta, 'units')

    if x > (600 - 30):
        delta = 1
        canvas.xview('scroll', delta, 'units')

    if y < 30:
        delta = -1
        canvas.yview('scroll', delta, 'units')

    if y > (600 - 30):
        delta = 1
        canvas.yview('scroll', delta, 'units')

canvas.bind('<Motion>', xy_motion)

root.mainloop()

问题是,滚动的动作是在一个只在鼠标移动时才会执行的函数里(也就是说,如果你停止移动鼠标,滚动也会停止)。我想要的是,即使鼠标不动(但仍在“滚动区域”内),窗口也能继续滚动,直到滚动到尽头。

我想的简单办法是把某个if语句(比如第30行)改成while语句,像这样:

while x < 30:

但这样一来,当鼠标到达这个位置时,程序就会卡住(我想是因为在等这个while循环结束)。

有什么建议吗?

提前谢谢大家。

更新

这里是一个有效的代码,包含了一个(或者可能的一个)答案。我不知道在问题里更新答案是否合适,但我觉得这对其他人可能有帮助。

x, y = 0, 0

def scroll():
    global x, y

    if x < 30:
        delta = - 1
        canvas.xview('scroll', delta, 'units')

    elif x > (ws - 30):
        delta = 1
        canvas.xview('scroll', delta, 'units')

    elif y < 30:
        delta = -1
        canvas.yview('scroll', delta, 'units')

    elif y > (ws - 30):
        delta = 1
        canvas.yview('scroll', delta, 'units')

    canvas.after(100, scroll)

def xy_motion(event):
    global x, y
    x, y = event.x, event.y

scroll()

canvas.bind('<Motion>', xy_motion)

请告诉我这是否正确。感谢大家的讨论和建议。 这些 链接 很有用。

3 个回答

-1

你程序卡住的明显原因是,当 x 小于 30 时,它会进入一个循环,除非它能跳出这个循环,否则你就无法控制鼠标。而只要无法控制鼠标,x 的值就会一直小于 30,这样你的循环就会一直成立,永远不会结束。

所以,你需要做的是把这个检查 while x < 30 放在一个单独的线程里。也就是说,当 x 小于 30 的时候,就可以启动这个线程,然后通过这个线程来控制滚动。当 x 大于或等于 30 的时候,就结束这个线程。

0

正如你所说,这个方法只有在鼠标移动的时候才有效,否则就不会触发<Motion>事件。你可以使用一个定时器,让它在一段时间后触发,前提是鼠标在滚动区域内。下面的内容只是一个伪代码,使用了我在ActiveState找到的一个可重置定时器

TIMEOUT = 0.5
timer = None

def _on_timeout(event):
    global timer
    scroll_xy(event)
    timer = TimerReset(TIMEOUT, _on_timeout, [event])
    timer.start()

def xy_motion(event):
    global timer
    if is_in_scrollable_area(event):
        if timer is None:
            timer = TimerReset(TIMEOUT, _on_timeout, [event])
            timer.start()
        else:
            timer.reset()
        scroll_xy(event)
    elif timer is not None:
        timer.cancel()
        timer = None

需要注意的是,这些只是我的想法,我没有检查代码,可能在timer变量上会有竞争条件,所以你应该使用锁来避免这个问题。

0

首先,设置一个方法,让窗口每次滚动一点点,然后在固定的时间间隔(比如100毫秒)后,如果鼠标还在指定区域,就再调用自己。你可以用“after”这个方法来实现。这样,只要鼠标在滚动区域,画布就会不停地滚动。

接下来,创建一个绑定,当光标第一次进入滚动区域时,就调用这个方法。

就这些了。只要确保同一时间内只运行一个这样的滚动任务就可以了。

撰写回答