Skip to main content
added 119 characters in body
Source Link
import tkinter as tk

class InfiniteCanvas(tk.Canvas):
    '''
    Initial idea by Nordine Lofti
    https://stackoverflow.com/users/12349101/nordine-lotfi
    written by Thingamabobs
    https://stackoverflow.com/users/13629335/thingamabobs
    with additional ideas by patrik-gustavsson
    https://stackoverflow.com/users/4332183/patrik-gustavsson

    The infinite canvas allows you to have infinite space to draw.

    ALL BINDINGS ARE JUST AVAILABLE WHEN CANVAS HAS FOCUS!
    FOCUS IS GIVEN WHEN YOU LEFT CLICK ONTO THE CANVAS!
    
    You can move around the world as follows:
    - MouseWheel for Y movement.
    - Shift-MouseWheel will perform X movement.
    - Alt-Button-1-Motion will perform X and Y movement.
    (pressing ctrl while moving will invoke a multiplier)
    
    You can zoom in and out with:
    - Alt-MouseWheel
    (pressing ctrl will invoke a multiplier)

    Additional features to the standard tk.Canvas:
    - Keeps track of the viewable area
    --> Acess via InfiniteCanvas().viewing_box()
    - Keeps track of the visibile items
    --> Acess via InfiniteCanvas().inview()
    - Keeps track of the NOT visibile items
    --> Acess via InfiniteCanvas().outofview()

    Also a new standard tag is introduced to the Canvas.
    All visible items will have the tag "inview"

    Notification bindings:
    "<<ItemsDropped>>" = dropped items stored in self.dropped
    "<<ItemsEntered>>" = entered items stored in self.entered
    "<<VerticalScroll>>"
    "<<HorizontalScroll>>"
    "<<Zoom>>"
    "<<DragView>>"
    '''

    def __init__(self, master, **kwargs):
        super().__init__(master, **kwargs)
        self._xshifted  = 0             #view moved in x direction
        self._yshifted  = 0             #view moved in y direction
        self._use_multi = False         #Multiplier for View-manipulation
        self.configure(confine=False)   #confine=False ignores scrollregion
        self.dropped    = set()         #storage
        self.entered    = set()         #storage
        #NotificationBindings
        self.event_add('<<VerticalScroll>>',    '<MouseWheel>')
        self.event_add('<<HorizontalScroll>>',  '<Shift-MouseWheel>')
        self.event_add('<<Zoom>>',              '<Alt-MouseWheel>')
        self.event_add('<<DragView>>',          '<Alt-B1-Motion>')
        self.bind(#MouseWheel
            '<<VerticalScroll>>',   lambda e:self._wheel_scroll(e,'y'))
        self.bind(#Shift+MouseWheel
            '<<HorizontalScroll>>', lambda e:self._wheel_scroll(e,'x'))
        self.bind(#Alt+MouseWheel
            '<<Zoom>>',             self._zoom)
        self.bind(#Alt+LeftClick+MouseMovement
            '<<DragView>>',         self._drag_scroll)
        self.event_generate('<<ItemsDropped>>') #invoked in _update_tags
        self.event_generate('<<ItemsEntered>>') #invoked in _update_tags
##        self.bind('<<ItemsDropped>>', lambda e:print('d',self.dropped))
##        self.bind('<<ItemsEntered>>', lambda e:print('e',self.entered))
        #Normal bindings
        self.bind(#left click
            '<ButtonPress-1>',          lambda e:e.widget.focus_set())
        self.bind(
            '<KeyPress-Alt_L>',         self._prepend_drag_scroll, add='+')
        self.bind(
            '<KeyRelease-Alt_L>',       self._prepend_drag_scroll, add='+')
        self.bind(
            '<KeyPress-Control_L>',     self._configure_multi)
        self.bind(
            '<KeyRelease-Control_L>',   self._configure_multi)
        return None

    def viewing_box(self) -> tuple:
        'Returns a tuple of the form x1,y1,x2,y2 represents visible area'
        off = (int(self.cget('highlightthickness'))
               +int(self.cget('borderwidth')))
        x1 = 0 - self._xshifted+off
        y1 = 0 - self._yshifted+off
        x2 = self.winfo_width()-self._xshifted-off-1
        y2 = self.winfo_height()-self._yshifted-off-1
        return x1,y1,x2,y2

    def inview(self) -> set:
        'Returns a set of identifiers that are currently viewed'
        return set(self.find_overlapping(*self.viewing_box()))

    def outofview(self) -> set:
        'Returns a set of identifiers that are currently NOT viewed'
        all_ = set(self.find_all())
        return all_ - self.inview()

    def _configure_multi(self, event):
        if (et:=event.type.name) == 'KeyPress':
            self._use_multi = True
        elif et == 'KeyRelease':
            self._use_multi = False
        
    def _zoom(self,event):
        if str(self.focus_get()) == str(self):
            x = canvas.canvasx(event.x)
            y = canvas.canvasy(event.y)
            multiplier = 1.005 if self._use_multi else 1.001
            factor = multiplier ** event.delta
            canvas.scale('all', x, y, factor, factor)
            self._update_tags()

    def _prepend_drag_scroll(self, event):
        if (et:=event.type.name) == 'KeyPress':
            self._recent_drag_point_x = event.x
            self._recent_drag_point_y = event.y
            self.scan_mark(event.x,event.y)
            self.configure(cursor='fleur')
        elif et == 'KeyRelease':
            self.configure(cursor='')
            self._recent_drag_point_x = None
            self._recent_drag_point_y = None

    def _update_tags(self):
        vbox = self.viewing_box()
        old = set(self.find_withtag('inview'))
        self.addtag_overlapping('inview',*vbox)
        inbox = set(self.find_overlapping(*vbox))
        witag = set(self.find_withtag('inview'))
        self.dropped = witag-inbox
        if self.dropped:
            [self.dtag(i, 'inview') for i in self.dropped]
            self.event_generate('<<ItemsDropped>>')
        new = set(self.find_withtag('inview'))
        self.entered = new-old
        if self.entered:
            self.event_generate('<<ItemsEntered>>')
        
    def _create(self, *args):
        ident = super()._create(*args)
        self._update_tags()
        return ident

    def _wheel_scroll(self, event, xy):
        if str(self.focus_get()) == str(self):
            parsed = int(event.delta/120)
            amount = parsed*10 if self._use_multi else parsed
            cx,cy = self.winfo_rootx(), self.winfo_rooty()
            self.scan_mark(cx, cy)
            if xy == 'x': x,y = cx+amount, cy
            elif xy == 'y': x,y = cx, cy+amount
            name = f'_{xy}shifted'
            setattr(self,name, getattr(self,name)+amount)
            self.scan_dragto(x,y, gain=1)
            self._update_tags()

    def _drag_scroll(self,event):
        if str(self.focus_get()) == str(self):
            self._xshifted += event.x-self._recent_drag_point_x
            self._yshifted += event.y-self._recent_drag_point_y
            gain = 2 if self._use_multi else 1
            self.scan_dragto(event.x, event.y, gain=gain)
            self._recent_drag_point_x = event.x
            self._recent_drag_point_y = event.y
            self.scan_mark(event.x,event.y)
            self._update_tags()


if __name__ == '__main__':
    root = tk.Tk()
    canvas = InfiniteCanvas(root)
    canvas.pack(fill=tk.BOTH, expand=True)

    size, offset, start = 100, 10, 0
    canvas.create_rectangle(start,start, size,size, fill='green')
    canvas.create_rectangle(
        start+offset,start+offset, size+offset,size+offset, fill='darkgreen')
    root.after(100, lambda:print(canvas.viewing_box()))

    root.mainloop()
import tkinter as tk

class InfiniteCanvas(tk.Canvas):
    '''
    Initial idea by Nordine Lofti
    https://stackoverflow.com/users/12349101/nordine-lotfi
    written by Thingamabobs
    https://stackoverflow.com/users/13629335/thingamabobs

    The infinite canvas allows you to have infinite space to draw.

    ALL BINDINGS ARE JUST AVAILABLE WHEN CANVAS HAS FOCUS!
    FOCUS IS GIVEN WHEN YOU LEFT CLICK ONTO THE CANVAS!
    
    You can move around the world as follows:
    - MouseWheel for Y movement.
    - Shift-MouseWheel will perform X movement.
    - Alt-Button-1-Motion will perform X and Y movement.
    (pressing ctrl while moving will invoke a multiplier)
    
    You can zoom in and out with:
    - Alt-MouseWheel
    (pressing ctrl will invoke a multiplier)

    Additional features to the standard tk.Canvas:
    - Keeps track of the viewable area
    --> Acess via InfiniteCanvas().viewing_box()
    - Keeps track of the visibile items
    --> Acess via InfiniteCanvas().inview()
    - Keeps track of the NOT visibile items
    --> Acess via InfiniteCanvas().outofview()

    Also a new standard tag is introduced to the Canvas.
    All visible items will have the tag "inview"

    Notification bindings:
    "<<ItemsDropped>>" = dropped items stored in self.dropped
    "<<ItemsEntered>>" = entered items stored in self.entered
    "<<VerticalScroll>>"
    "<<HorizontalScroll>>"
    "<<Zoom>>"
    "<<DragView>>"
    '''

    def __init__(self, master, **kwargs):
        super().__init__(master, **kwargs)
        self._xshifted  = 0             #view moved in x direction
        self._yshifted  = 0             #view moved in y direction
        self._use_multi = False         #Multiplier for View-manipulation
        self.configure(confine=False)   #confine=False ignores scrollregion
        self.dropped    = set()         #storage
        self.entered    = set()         #storage
        #NotificationBindings
        self.event_add('<<VerticalScroll>>',    '<MouseWheel>')
        self.event_add('<<HorizontalScroll>>',  '<Shift-MouseWheel>')
        self.event_add('<<Zoom>>',              '<Alt-MouseWheel>')
        self.event_add('<<DragView>>',          '<Alt-B1-Motion>')
        self.bind(#MouseWheel
            '<<VerticalScroll>>',   lambda e:self._wheel_scroll(e,'y'))
        self.bind(#Shift+MouseWheel
            '<<HorizontalScroll>>', lambda e:self._wheel_scroll(e,'x'))
        self.bind(#Alt+MouseWheel
            '<<Zoom>>',             self._zoom)
        self.bind(#Alt+LeftClick+MouseMovement
            '<<DragView>>',         self._drag_scroll)
        self.event_generate('<<ItemsDropped>>') #invoked in _update_tags
        self.event_generate('<<ItemsEntered>>') #invoked in _update_tags
##        self.bind('<<ItemsDropped>>', lambda e:print('d',self.dropped))
##        self.bind('<<ItemsEntered>>', lambda e:print('e',self.entered))
        #Normal bindings
        self.bind(#left click
            '<ButtonPress-1>',          lambda e:e.widget.focus_set())
        self.bind(
            '<KeyPress-Alt_L>',         self._prepend_drag_scroll, add='+')
        self.bind(
            '<KeyRelease-Alt_L>',       self._prepend_drag_scroll, add='+')
        self.bind(
            '<KeyPress-Control_L>',     self._configure_multi)
        self.bind(
            '<KeyRelease-Control_L>',   self._configure_multi)
        return None

    def viewing_box(self) -> tuple:
        'Returns a tuple of the form x1,y1,x2,y2 represents visible area'
        off = (int(self.cget('highlightthickness'))
               +int(self.cget('borderwidth')))
        x1 = 0 - self._xshifted+off
        y1 = 0 - self._yshifted+off
        x2 = self.winfo_width()-self._xshifted-off-1
        y2 = self.winfo_height()-self._yshifted-off-1
        return x1,y1,x2,y2

    def inview(self) -> set:
        'Returns a set of identifiers that are currently viewed'
        return set(self.find_overlapping(*self.viewing_box()))

    def outofview(self) -> set:
        'Returns a set of identifiers that are currently NOT viewed'
        all_ = set(self.find_all())
        return all_ - self.inview()

    def _configure_multi(self, event):
        if (et:=event.type.name) == 'KeyPress':
            self._use_multi = True
        elif et == 'KeyRelease':
            self._use_multi = False
        
    def _zoom(self,event):
        if str(self.focus_get()) == str(self):
            x = canvas.canvasx(event.x)
            y = canvas.canvasy(event.y)
            multiplier = 1.005 if self._use_multi else 1.001
            factor = multiplier ** event.delta
            canvas.scale('all', x, y, factor, factor)
            self._update_tags()

    def _prepend_drag_scroll(self, event):
        if (et:=event.type.name) == 'KeyPress':
            self._recent_drag_point_x = event.x
            self._recent_drag_point_y = event.y
            self.scan_mark(event.x,event.y)
            self.configure(cursor='fleur')
        elif et == 'KeyRelease':
            self.configure(cursor='')
            self._recent_drag_point_x = None
            self._recent_drag_point_y = None

    def _update_tags(self):
        vbox = self.viewing_box()
        old = set(self.find_withtag('inview'))
        self.addtag_overlapping('inview',*vbox)
        inbox = set(self.find_overlapping(*vbox))
        witag = set(self.find_withtag('inview'))
        self.dropped = witag-inbox
        if self.dropped:
            [self.dtag(i, 'inview') for i in self.dropped]
            self.event_generate('<<ItemsDropped>>')
        new = set(self.find_withtag('inview'))
        self.entered = new-old
        if self.entered:
            self.event_generate('<<ItemsEntered>>')
        
    def _create(self, *args):
        ident = super()._create(*args)
        self._update_tags()
        return ident

    def _wheel_scroll(self, event, xy):
        if str(self.focus_get()) == str(self):
            parsed = int(event.delta/120)
            amount = parsed*10 if self._use_multi else parsed
            cx,cy = self.winfo_rootx(), self.winfo_rooty()
            self.scan_mark(cx, cy)
            if xy == 'x': x,y = cx+amount, cy
            elif xy == 'y': x,y = cx, cy+amount
            name = f'_{xy}shifted'
            setattr(self,name, getattr(self,name)+amount)
            self.scan_dragto(x,y, gain=1)
            self._update_tags()

    def _drag_scroll(self,event):
        if str(self.focus_get()) == str(self):
            self._xshifted += event.x-self._recent_drag_point_x
            self._yshifted += event.y-self._recent_drag_point_y
            gain = 2 if self._use_multi else 1
            self.scan_dragto(event.x, event.y, gain=gain)
            self._recent_drag_point_x = event.x
            self._recent_drag_point_y = event.y
            self.scan_mark(event.x,event.y)
            self._update_tags()


if __name__ == '__main__':
    root = tk.Tk()
    canvas = InfiniteCanvas(root)
    canvas.pack(fill=tk.BOTH, expand=True)

    size, offset, start = 100, 10, 0
    canvas.create_rectangle(start,start, size,size, fill='green')
    canvas.create_rectangle(
        start+offset,start+offset, size+offset,size+offset, fill='darkgreen')
    root.after(100, lambda:print(canvas.viewing_box()))

    root.mainloop()
import tkinter as tk

class InfiniteCanvas(tk.Canvas):
    '''
    Initial idea by Nordine Lofti
    https://stackoverflow.com/users/12349101/nordine-lotfi
    written by Thingamabobs
    https://stackoverflow.com/users/13629335/thingamabobs
    with additional ideas by patrik-gustavsson
    https://stackoverflow.com/users/4332183/patrik-gustavsson

    The infinite canvas allows you to have infinite space to draw.

    ALL BINDINGS ARE JUST AVAILABLE WHEN CANVAS HAS FOCUS!
    FOCUS IS GIVEN WHEN YOU LEFT CLICK ONTO THE CANVAS!
    
    You can move around the world as follows:
    - MouseWheel for Y movement.
    - Shift-MouseWheel will perform X movement.
    - Alt-Button-1-Motion will perform X and Y movement.
    (pressing ctrl while moving will invoke a multiplier)
    
    You can zoom in and out with:
    - Alt-MouseWheel
    (pressing ctrl will invoke a multiplier)

    Additional features to the standard tk.Canvas:
    - Keeps track of the viewable area
    --> Acess via InfiniteCanvas().viewing_box()
    - Keeps track of the visibile items
    --> Acess via InfiniteCanvas().inview()
    - Keeps track of the NOT visibile items
    --> Acess via InfiniteCanvas().outofview()

    Also a new standard tag is introduced to the Canvas.
    All visible items will have the tag "inview"

    Notification bindings:
    "<<ItemsDropped>>" = dropped items stored in self.dropped
    "<<ItemsEntered>>" = entered items stored in self.entered
    "<<VerticalScroll>>"
    "<<HorizontalScroll>>"
    "<<Zoom>>"
    "<<DragView>>"
    '''

    def __init__(self, master, **kwargs):
        super().__init__(master, **kwargs)
        self._xshifted  = 0             #view moved in x direction
        self._yshifted  = 0             #view moved in y direction
        self._use_multi = False         #Multiplier for View-manipulation
        self.configure(confine=False)   #confine=False ignores scrollregion
        self.dropped    = set()         #storage
        self.entered    = set()         #storage
        #NotificationBindings
        self.event_add('<<VerticalScroll>>',    '<MouseWheel>')
        self.event_add('<<HorizontalScroll>>',  '<Shift-MouseWheel>')
        self.event_add('<<Zoom>>',              '<Alt-MouseWheel>')
        self.event_add('<<DragView>>',          '<Alt-B1-Motion>')
        self.bind(#MouseWheel
            '<<VerticalScroll>>',   lambda e:self._wheel_scroll(e,'y'))
        self.bind(#Shift+MouseWheel
            '<<HorizontalScroll>>', lambda e:self._wheel_scroll(e,'x'))
        self.bind(#Alt+MouseWheel
            '<<Zoom>>',             self._zoom)
        self.bind(#Alt+LeftClick+MouseMovement
            '<<DragView>>',         self._drag_scroll)
        self.event_generate('<<ItemsDropped>>') #invoked in _update_tags
        self.event_generate('<<ItemsEntered>>') #invoked in _update_tags
##        self.bind('<<ItemsDropped>>', lambda e:print('d',self.dropped))
##        self.bind('<<ItemsEntered>>', lambda e:print('e',self.entered))
        #Normal bindings
        self.bind(#left click
            '<ButtonPress-1>',          lambda e:e.widget.focus_set())
        self.bind(
            '<KeyPress-Alt_L>',         self._prepend_drag_scroll, add='+')
        self.bind(
            '<KeyRelease-Alt_L>',       self._prepend_drag_scroll, add='+')
        self.bind(
            '<KeyPress-Control_L>',     self._configure_multi)
        self.bind(
            '<KeyRelease-Control_L>',   self._configure_multi)
        return None

    def viewing_box(self) -> tuple:
        'Returns a tuple of the form x1,y1,x2,y2 represents visible area'
        off = (int(self.cget('highlightthickness'))
               +int(self.cget('borderwidth')))
        x1 = 0 - self._xshifted+off
        y1 = 0 - self._yshifted+off
        x2 = self.winfo_width()-self._xshifted-off-1
        y2 = self.winfo_height()-self._yshifted-off-1
        return x1,y1,x2,y2

    def inview(self) -> set:
        'Returns a set of identifiers that are currently viewed'
        return set(self.find_overlapping(*self.viewing_box()))

    def outofview(self) -> set:
        'Returns a set of identifiers that are currently NOT viewed'
        all_ = set(self.find_all())
        return all_ - self.inview()

    def _configure_multi(self, event):
        if (et:=event.type.name) == 'KeyPress':
            self._use_multi = True
        elif et == 'KeyRelease':
            self._use_multi = False
        
    def _zoom(self,event):
        if str(self.focus_get()) == str(self):
            x = canvas.canvasx(event.x)
            y = canvas.canvasy(event.y)
            multiplier = 1.005 if self._use_multi else 1.001
            factor = multiplier ** event.delta
            canvas.scale('all', x, y, factor, factor)
            self._update_tags()

    def _prepend_drag_scroll(self, event):
        if (et:=event.type.name) == 'KeyPress':
            self._recent_drag_point_x = event.x
            self._recent_drag_point_y = event.y
            self.scan_mark(event.x,event.y)
            self.configure(cursor='fleur')
        elif et == 'KeyRelease':
            self.configure(cursor='')
            self._recent_drag_point_x = None
            self._recent_drag_point_y = None

    def _update_tags(self):
        vbox = self.viewing_box()
        old = set(self.find_withtag('inview'))
        self.addtag_overlapping('inview',*vbox)
        inbox = set(self.find_overlapping(*vbox))
        witag = set(self.find_withtag('inview'))
        self.dropped = witag-inbox
        if self.dropped:
            [self.dtag(i, 'inview') for i in self.dropped]
            self.event_generate('<<ItemsDropped>>')
        new = set(self.find_withtag('inview'))
        self.entered = new-old
        if self.entered:
            self.event_generate('<<ItemsEntered>>')
        
    def _create(self, *args):
        ident = super()._create(*args)
        self._update_tags()
        return ident

    def _wheel_scroll(self, event, xy):
        if str(self.focus_get()) == str(self):
            parsed = int(event.delta/120)
            amount = parsed*10 if self._use_multi else parsed
            cx,cy = self.winfo_rootx(), self.winfo_rooty()
            self.scan_mark(cx, cy)
            if xy == 'x': x,y = cx+amount, cy
            elif xy == 'y': x,y = cx, cy+amount
            name = f'_{xy}shifted'
            setattr(self,name, getattr(self,name)+amount)
            self.scan_dragto(x,y, gain=1)
            self._update_tags()

    def _drag_scroll(self,event):
        if str(self.focus_get()) == str(self):
            self._xshifted += event.x-self._recent_drag_point_x
            self._yshifted += event.y-self._recent_drag_point_y
            gain = 2 if self._use_multi else 1
            self.scan_dragto(event.x, event.y, gain=gain)
            self._recent_drag_point_x = event.x
            self._recent_drag_point_y = event.y
            self.scan_mark(event.x,event.y)
            self._update_tags()


if __name__ == '__main__':
    root = tk.Tk()
    canvas = InfiniteCanvas(root)
    canvas.pack(fill=tk.BOTH, expand=True)

    size, offset, start = 100, 10, 0
    canvas.create_rectangle(start,start, size,size, fill='green')
    canvas.create_rectangle(
        start+offset,start+offset, size+offset,size+offset, fill='darkgreen')
    root.after(100, lambda:print(canvas.viewing_box()))

    root.mainloop()
Source Link

Reworked code:

import tkinter as tk

class InfiniteCanvas(tk.Canvas):
    '''
    Initial idea by Nordine Lofti
    https://stackoverflow.com/users/12349101/nordine-lotfi
    written by Thingamabobs
    https://stackoverflow.com/users/13629335/thingamabobs

    The infinite canvas allows you to have infinite space to draw.

    ALL BINDINGS ARE JUST AVAILABLE WHEN CANVAS HAS FOCUS!
    FOCUS IS GIVEN WHEN YOU LEFT CLICK ONTO THE CANVAS!
    
    You can move around the world as follows:
    - MouseWheel for Y movement.
    - Shift-MouseWheel will perform X movement.
    - Alt-Button-1-Motion will perform X and Y movement.
    (pressing ctrl while moving will invoke a multiplier)
    
    You can zoom in and out with:
    - Alt-MouseWheel
    (pressing ctrl will invoke a multiplier)

    Additional features to the standard tk.Canvas:
    - Keeps track of the viewable area
    --> Acess via InfiniteCanvas().viewing_box()
    - Keeps track of the visibile items
    --> Acess via InfiniteCanvas().inview()
    - Keeps track of the NOT visibile items
    --> Acess via InfiniteCanvas().outofview()

    Also a new standard tag is introduced to the Canvas.
    All visible items will have the tag "inview"

    Notification bindings:
    "<<ItemsDropped>>" = dropped items stored in self.dropped
    "<<ItemsEntered>>" = entered items stored in self.entered
    "<<VerticalScroll>>"
    "<<HorizontalScroll>>"
    "<<Zoom>>"
    "<<DragView>>"
    '''

    def __init__(self, master, **kwargs):
        super().__init__(master, **kwargs)
        self._xshifted  = 0             #view moved in x direction
        self._yshifted  = 0             #view moved in y direction
        self._use_multi = False         #Multiplier for View-manipulation
        self.configure(confine=False)   #confine=False ignores scrollregion
        self.dropped    = set()         #storage
        self.entered    = set()         #storage
        #NotificationBindings
        self.event_add('<<VerticalScroll>>',    '<MouseWheel>')
        self.event_add('<<HorizontalScroll>>',  '<Shift-MouseWheel>')
        self.event_add('<<Zoom>>',              '<Alt-MouseWheel>')
        self.event_add('<<DragView>>',          '<Alt-B1-Motion>')
        self.bind(#MouseWheel
            '<<VerticalScroll>>',   lambda e:self._wheel_scroll(e,'y'))
        self.bind(#Shift+MouseWheel
            '<<HorizontalScroll>>', lambda e:self._wheel_scroll(e,'x'))
        self.bind(#Alt+MouseWheel
            '<<Zoom>>',             self._zoom)
        self.bind(#Alt+LeftClick+MouseMovement
            '<<DragView>>',         self._drag_scroll)
        self.event_generate('<<ItemsDropped>>') #invoked in _update_tags
        self.event_generate('<<ItemsEntered>>') #invoked in _update_tags
##        self.bind('<<ItemsDropped>>', lambda e:print('d',self.dropped))
##        self.bind('<<ItemsEntered>>', lambda e:print('e',self.entered))
        #Normal bindings
        self.bind(#left click
            '<ButtonPress-1>',          lambda e:e.widget.focus_set())
        self.bind(
            '<KeyPress-Alt_L>',         self._prepend_drag_scroll, add='+')
        self.bind(
            '<KeyRelease-Alt_L>',       self._prepend_drag_scroll, add='+')
        self.bind(
            '<KeyPress-Control_L>',     self._configure_multi)
        self.bind(
            '<KeyRelease-Control_L>',   self._configure_multi)
        return None

    def viewing_box(self) -> tuple:
        'Returns a tuple of the form x1,y1,x2,y2 represents visible area'
        off = (int(self.cget('highlightthickness'))
               +int(self.cget('borderwidth')))
        x1 = 0 - self._xshifted+off
        y1 = 0 - self._yshifted+off
        x2 = self.winfo_width()-self._xshifted-off-1
        y2 = self.winfo_height()-self._yshifted-off-1
        return x1,y1,x2,y2

    def inview(self) -> set:
        'Returns a set of identifiers that are currently viewed'
        return set(self.find_overlapping(*self.viewing_box()))

    def outofview(self) -> set:
        'Returns a set of identifiers that are currently NOT viewed'
        all_ = set(self.find_all())
        return all_ - self.inview()

    def _configure_multi(self, event):
        if (et:=event.type.name) == 'KeyPress':
            self._use_multi = True
        elif et == 'KeyRelease':
            self._use_multi = False
        
    def _zoom(self,event):
        if str(self.focus_get()) == str(self):
            x = canvas.canvasx(event.x)
            y = canvas.canvasy(event.y)
            multiplier = 1.005 if self._use_multi else 1.001
            factor = multiplier ** event.delta
            canvas.scale('all', x, y, factor, factor)
            self._update_tags()

    def _prepend_drag_scroll(self, event):
        if (et:=event.type.name) == 'KeyPress':
            self._recent_drag_point_x = event.x
            self._recent_drag_point_y = event.y
            self.scan_mark(event.x,event.y)
            self.configure(cursor='fleur')
        elif et == 'KeyRelease':
            self.configure(cursor='')
            self._recent_drag_point_x = None
            self._recent_drag_point_y = None

    def _update_tags(self):
        vbox = self.viewing_box()
        old = set(self.find_withtag('inview'))
        self.addtag_overlapping('inview',*vbox)
        inbox = set(self.find_overlapping(*vbox))
        witag = set(self.find_withtag('inview'))
        self.dropped = witag-inbox
        if self.dropped:
            [self.dtag(i, 'inview') for i in self.dropped]
            self.event_generate('<<ItemsDropped>>')
        new = set(self.find_withtag('inview'))
        self.entered = new-old
        if self.entered:
            self.event_generate('<<ItemsEntered>>')
        
    def _create(self, *args):
        ident = super()._create(*args)
        self._update_tags()
        return ident

    def _wheel_scroll(self, event, xy):
        if str(self.focus_get()) == str(self):
            parsed = int(event.delta/120)
            amount = parsed*10 if self._use_multi else parsed
            cx,cy = self.winfo_rootx(), self.winfo_rooty()
            self.scan_mark(cx, cy)
            if xy == 'x': x,y = cx+amount, cy
            elif xy == 'y': x,y = cx, cy+amount
            name = f'_{xy}shifted'
            setattr(self,name, getattr(self,name)+amount)
            self.scan_dragto(x,y, gain=1)
            self._update_tags()

    def _drag_scroll(self,event):
        if str(self.focus_get()) == str(self):
            self._xshifted += event.x-self._recent_drag_point_x
            self._yshifted += event.y-self._recent_drag_point_y
            gain = 2 if self._use_multi else 1
            self.scan_dragto(event.x, event.y, gain=gain)
            self._recent_drag_point_x = event.x
            self._recent_drag_point_y = event.y
            self.scan_mark(event.x,event.y)
            self._update_tags()


if __name__ == '__main__':
    root = tk.Tk()
    canvas = InfiniteCanvas(root)
    canvas.pack(fill=tk.BOTH, expand=True)

    size, offset, start = 100, 10, 0
    canvas.create_rectangle(start,start, size,size, fill='green')
    canvas.create_rectangle(
        start+offset,start+offset, size+offset,size+offset, fill='darkgreen')
    root.after(100, lambda:print(canvas.viewing_box()))

    root.mainloop()