Home > Software design >  Tkinter - Canvas scrolling/scrollregion explained - (un-)restricted area
Tkinter - Canvas scrolling/scrollregion explained - (un-)restricted area

Time:12-07

It was brought to my attention that you can't restrict the view with e.g. scrollregion = (0,0,0,0) and the question was why is that ? Since the official documentation would indicate that this should work and does not throw and badRegion error.

scrollregion:

Specifies a list with four coordinates describing the left, top, right, and bottom coordinates of a rectangular region. This region is used for scrolling purposes and is considered to be the boundary of the information in the canvas. Each of the coordinates may be specified in any of the forms given in the COORDINATES section below.

scan dragto:

This command computes the difference between its x and y arguments (which are typically mouse coordinates) and the x and y arguments to the last scan mark command for the widget. It then adjusts the view by gain times the difference in coordinates, where gain defaults to 10. This command is typically associated with mouse motion events in the widget, to produce the effect of dragging the canvas at high speed through its window. The return value is an empty string.

Example:

import tkinter
master = tkinter.Tk()
c = tkinter.Canvas(
    master, width=100, height=100, bg="yellow", scrollregion=(0,0,0,0))
c.pack()
c.create_rectangle(0,0,20,20, outline="black", fill='green')

def scroll_start(event=None):
    c.scan_mark(event.x, event.y)

def scroll_move(event=None):
    c.scan_dragto(event.x, event.y, gain=1)

master.bind("<ButtonPress-1>", scroll_start)
master.bind("<B1-Motion>", scroll_move)
print('try to adjust the view by dragging')

master.mainloop()

CodePudding user response:

TL;DR:

Make sure the scroll region is bigger than the canvas, otherwise your canvas will be anchored around the scrollregion. For a infinite world use confine=False


To be clear:

  • The scrollregion, is guaranteed to be in the View, not your items!
  • The scrollregion should define a rectangle of viewable space

Try the examples below and decide for yourself if this is a bug or a feature and act accordingly.


The misunderstanding is based on the false assumption that the scrollregion could be smaller than the canvas visible area, while this isn't directly noted in the documentation you will find a hint in the C-implementation of the canvas. The note for CavnasSetOringin that is called from CANV_SCAN:

Adjust the origin if necessary to keep as much as possible of the canvas in the view. The variables left, right, etc. keep track of how much extra space there is on each side of the view before it will stick out past the scroll region. If one side sticks out past the edge of the scroll region, adjust the view to bring that side back to the edge of the scrollregion (but don't move it so much that the other side sticks out now).

So the basic idea of the scroll-able area and the implicit condition is that the scroll region is (should always be) bigger than the visible area and should look like this:

sx1           vx1    ix1    ix2    vx2            sx2
|             |      |      |      |              | 
 ------------------------------------------------- 
|                                                 |
|              --------------------               |
|             |                    |              |
|             |       ------       |              |
|             |      | item |      |              |
|             |       ------       |              |
|             |                    |              |
|             |    visible area    |              |
|              --------------------               |
|                                                 |
|                  scroll region                  |
 ------------------------------------------------- 

However, the core developers decided, for what ever reason it is a valid region when the scrollregion is smaller than the visible area. I would guess it was convenient to have the scrollregion bound to a '<Configure>' command without messing around with an error. See the comment in the source code:

The call below is needed in order to recenter the canvas if it's confined and its scroll region is smaller than the window.

You should also be aware that all of this only applies to a canvas where confine=True:

Specifies a boolean value that indicates whether or not it should be allowable to set the canvas's view outside the region defined by the scrollRegion argument. Defaults to true, which means that the view will be constrained within the scroll region.

Examples:

Example of scrollregion matches size of canvas

'''
This is an example where the scrollregion matches the dimensions of the Canvas.
- No dragging of the canvas-"world" is performed
- Scrollregion works with scan_dragto as expected
'''
import tkinter
master = tkinter.Tk()
c = tkinter.Canvas(
    master, width=100, height=100, bg="yellow", scrollregion=(0,0,100,100))
c.pack()
c.create_rectangle(0,0,20,20, outline="black", fill='green')

def scroll_start(event=None):
    c.scan_mark(event.x, event.y)

def scroll_move(event=None):
    c.scan_dragto(event.x, event.y, gain=1)

master.bind("<ButtonPress-1>", scroll_start)
master.bind("<B1-Motion>", scroll_move)
print('try to adjust the view by dragging')

master.mainloop()

Scrollregion is smaller than the Canvas and confine=True

'''
This is an example where the scrollregion is smaller than the Canvas.
- Dragging of the canvas-"world" is performed
- You can drag the world until the point of scrollregion is on the edge

Additional notes:

The Canvas is 100 pixels wide and 100 pixels heigh.
You can drag the canvas-"world" until the "scrollregion",
in this case more of an anchor point, is about to leave
the visible area of the canvas.

Therefore when dragging with the mouse to south east:
The canvas will stop move the canvas-"world" behind.
To make this visible I have drawn a red rectangle at -100,-100.
'''
import tkinter
master = tkinter.Tk()
c = tkinter.Canvas(
    master, width=100, height=100, bg="yellow", scrollregion=(0,0,0,0))
c.pack()
c.create_rectangle(0,0,20,20, outline="black", fill='green')
c.create_rectangle(-100,-100,-80,-80, outline="black", fill='red')

def scroll_start(event=None):
    c.scan_mark(event.x, event.y)

def scroll_move(event=None):
    c.scan_dragto(event.x, event.y, gain=1)

master.bind("<ButtonPress-1>", scroll_start)
master.bind("<B1-Motion>", scroll_move)
print('drag south east as far as possible')

master.mainloop()

Example with confine=False

'''
This is an example where confine is set to False.
- Dragging of the canvas-"world" is performed
- You can drag the world to "infinity" (at least no known bounderies.

Additional notes:
When the items leave the visible space they are unmapped!
https://github.com/tcltk/tk/blob/main/generic/tkCanvas.c#L3142
'''

import tkinter
master = tkinter.Tk()
c = tkinter.Canvas(
    master, width=100, height=100, bg="yellow", scrollregion=(0,0,0,0),
    confine=False)
c.pack()
c.create_rectangle(0,0,20,20, outline="black", fill='green')
c.create_rectangle(-100,-100,-80,-80, outline="black", fill='red')

def scroll_start(event=None):
    c.scan_mark(event.x, event.y)

def scroll_move(event=None):
    c.scan_dragto(event.x, event.y, gain=1)

master.bind("<ButtonPress-1>", scroll_start)
master.bind("<B1-Motion>", scroll_move)

master.mainloop()

Example of scrollregion is bigger than the canvas and confine=True

'''
confine=True by default, scrollregion restricts area that can be displayed.
For demonstration a green rectangle is in the middle and red ones in the corners
'''

import tkinter
master = tkinter.Tk()
c = tkinter.Canvas(
    master, width=100, height=100, bg="yellow", scrollregion=(-100,-100,200,200))
c.pack()
#rectangle arround the middle
c.create_rectangle(-10,-10,10,10, outline="black", fill='green')
#rectangle in upper left corner of the View
c.create_rectangle(-100,-100,-80,-80, outline="black", fill='red')
#rectangle in upper right corner of the View
c.create_rectangle(180,-100,200,-80, outline="black", fill='red')
#rectangle in bottom left corner of the View
c.create_rectangle(-100,180,-80,200, outline="black", fill='red')
#rectangle in bottom right corner of the View
c.create_rectangle(180,180,200,200, outline="black", fill='red')


def scroll_start(event=None):
    c.scan_mark(event.x, event.y)

def scroll_move(event=None):
    c.scan_dragto(event.x, event.y, gain=1)

master.bind("<ButtonPress-1>", scroll_start)
master.bind("<B1-Motion>", scroll_move)

master.mainloop()
  • Related