If you're not adverse to some out-of-the-box thinking, you can solve this with a little bit of custom Tcl code. I write this not because it's the best solution per se, but because it's an interesting one.
The solution works like this: when you scroll, ultimately what gets called is a subcommand of the underlying tk widget to actually perform the scroll. For example, self.canvas.xview_moveto(...)
results in a tcl command that looks something like .123455.234123 xview moveto ...
. The strange looking series of numbers and dots is the internal name of the widget. It is also the name of a command which implements the scrolling behavior. "xview" is called a subcommand in tcl nomenclature, though it can be thought of like a method on the widget object.
Now, the cool thing with Tcl is, you can rename any command and replace it with something different. Since everything that happens to this widget calls this command, we can create a proxy through which all commands are sent.
In your case, you would like an event to fire whenever the canvas is scrolled. We know it scrolls whenever the widget command is called with an "xview" or "yview" subcommand. So, by replacing the widget command with a proxy, and having the proxy look for these subcommands, we can accomplish that very thing.
Here's a working example using python 2.7:
# use 'tkinter' instead of 'Tkinter' if using python 3.x
import Tkinter as tk
class CustomCanvas(tk.Canvas):
def __init__(self, *args, **kwargs):
'''A custom canvas that generates <<ScrollEvent>> events whenever
the canvas scrolls by any means (scrollbar, key bindings, etc)
'''
tk.Canvas.__init__(self, *args, **kwargs)
# replace the underlying tcl object with our own function
# so we can generate virtual events when the object scrolls
tcl='''
proc widget_proxy {actual_widget args} {
set result [$actual_widget {*}$args]
set command [lindex $args 0]
set subcommand [lindex $args 1]
if {$command in {xview yview} && $subcommand in {scroll moveto}} {
# widget has been scrolled; generate an event
event generate {widget} <<ScrollEvent>>
}
return $result
}
rename {widget} _{widget}
interp alias {} ::{widget} {} widget_proxy _{widget}
'''.replace("{widget}", str(self))
self.tk.eval(tcl)
class Example(tk.Frame):
def __init__(self, *args, **kwargs):
tk.Frame.__init__(self, *args, **kwargs)
# create an instance of the custom canvas. Make sure it
# has a largeish scroll region, for demonstration purposes
self.canvas = CustomCanvas(self, width=400, height=400,
borderwidth=0, scrollregion=(0,0,1000,1000))
self.vsb = tk.Scrollbar(self, orient="vertical", command=self.canvas.yview)
self.hsb = tk.Scrollbar(self, orient="horizontal", command=self.canvas.xview)
self.canvas.configure(xscrollcommand=self.hsb.set, yscrollcommand=self.vsb.set)
self.canvas.grid(row=0, column=0, sticky="nsew")
self.vsb.grid(row=0, column=1, sticky="ns")
self.hsb.grid(row=1, column=0, sticky="ew")
self.grid_rowconfigure(0, weight=1)
self.grid_columnconfigure(0, weight=1)
# this binds to the virtual event that is sent by the proxy
self.canvas.bind("<<ScrollEvent>>", self.on_scroll)
# some data, just so that we can see that the canvas
# really is scrolling
for y in range(0, 1000, 100):
for x in range(0, 1000, 100):
self.canvas.create_text(x, y, text="%s/%s" % (x,y), anchor="nw")
def on_scroll(self, event):
print "widget scrolled..."
if __name__ == "__main__":
root = tk.Tk()
view = Example(root)
view.pack(side="top", fill="both", expand=True)
root.mainloop()
Caveats: this will only work for when you scroll the area, though it should work even if you scroll with the keyboard (eg: page up, page down, etc). If you resize the window the event won't fire. You can handle that case by binding to <Configure>
. Also, I left out error checking for the sake of brevity, though it should be fairly robust. Finally, you can only use this specific implementation once in a program since I hard-coded "widget_proxy" rather than make it something more unique. That's an exercise left for the reader.