0

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.

Community
  • 1
  • 1
Marcos Saito
  • 530
  • 2
  • 10
  • 17

3 Answers3

0

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.

mg.
  • 7,822
  • 1
  • 26
  • 30
0

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.

Bryan Oakley
  • 370,779
  • 53
  • 539
  • 685
  • I think I understood your suggestion, but after playing with this concept (and others) I still can't figure out how to do it. I've made a function that calls itself, but it keeps calling itself after the mouse leaves the scroll region. I'm still trying it, but, could you show me an example or some pseudo code? Thanks in advance. – Marcos Saito Jun 27 '11 at 22:01
  • I didn't know the `after` method, which I think better suits in this case because the `Timer` class involves threads. – mg. Jun 28 '11 at 08:36
  • I think I've figured out, at least, it's working. I've updated the question with the working code. Please let me know if I've done it right (if the code is good). – Marcos Saito Jun 28 '11 at 12:55
-1

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.

Pushpak Dagade
  • 6,280
  • 7
  • 28
  • 41
  • why the downvote? it might be heavyweight solution but it is not incorrect. Atleast I made him figure out why his program is getting stuck. – Pushpak Dagade Jun 27 '11 at 17:33
  • @Guanidene, actually I didn't understand your suggestion, because I can control the mouse, it's just that the program doesn't respond anymore (still, I think you were right about the while condition being satisfied forever). And because of the suggestion I made a search about "threads", that I've never heard about before. – Marcos Saito Jun 27 '11 at 21:32
  • @Marcos: if you've never heard of threads before, this solution is _definitely_ not the right solution for you. Threads add a considerable amount of complexity, some of which is very subtle. – Bryan Oakley Jun 27 '11 at 21:49
  • @Guanidene: I dowvoted for the same reason I would downvote a suggestion to cut your hair with a lawn mower. I feel the answer is less than useful becaus it sends one down the wrong path. Threads are good for what threads are good for, and terrible for everything else. This problem falls into the "everything else" category IMHO. – Bryan Oakley Jun 27 '11 at 21:57
  • @Macros - what I mean by you not being able to control the mouse is, your mouse move events won't be accepted by your program unless it exits the while loop... – Pushpak Dagade Jun 28 '11 at 05:23