Python Tkinter - canvas scrolling with mouse posit

2019-06-14 13:27发布

I think this is a very common question, but I couldn't find the answer.

I'm trying to make a window that scrolls depending on the mouse position: if the mouse is close to top of the screen, it scrolls to the top, if it is close to the right border, it scrolls to the right and so on. Here is the code:

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()

The problem is that the scrolling movement is in the motion function that only works if there is mouse movement (if you stop moving the mouse, the scrolling stops too). I would like to make it a way that even if the mouse is not moving (but still in the "scrolling area") the window would keep scrolling until it reaches the end.

I thought the obvious way would be changing the if statement (from line 30 for example) to a while statement, like this:

while x < 30:

But then when the mouse reaches this position the program freezes (waiting for the while loop to finish I think).

Any suggestion?

Thanks in advance.

UPDATE

Here is the working code with an (or one of the possible) answer. I don't know if it is right to update the question itself with the answer, but I think it can be useful to others.

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)

Please let me know if it is correct. Thanks to everyone for the discussion and suggestions. These links were useful too.

3条回答
放我归山
2楼-- · 2019-06-14 13:58

First, set up a method that scrolls the window by a tiny amount, then calls itself again after some fixed period of time (eg 100ms) if the mouse is in the region. You can use the method "after" todo this. With this, the canvas will continuously scroll as long as the mouse is in the scroll region.

Next, create a binding that calls this function when the cursor enters the scroll zone for the first time.

And that's all you need. Just make sure you only have one of tbese scroll jobs running at any one time.

查看更多
女痞
3楼-- · 2019-06-14 14:03

The obvious reason for your program getting stuck is the moment x is less than 30 it goes into the loop and unless it gets out of the loop, you are not going to be able to control the mouse and unless you are able to control the mouse, the position will always be < 30 so your loop will be forever satisfied and will never end.

So, what you need to do is run this check while x < 30 in a separate thread. You can initialize the thread the moment x becomes less than 30 and from that thread control the scrolling. The moment x becomes more than or equal to 30, you kill the thread.

查看更多
成全新的幸福
4楼-- · 2019-06-14 14:11

As you said this works only if mouse is in movement, otherwise the <Motion> event is not triggered. You can use a timer that is triggered after a timeout and only if the mouse is in the scrolling area. The following is just a pseudo-code which uses a resettable timer I found in 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

Beware that these are just thoughts, I didn't check the code and probably there is a race condition on the timer variable and you should use a lock.

查看更多
登录 后发表回答