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