1

I want to redraw the canvas whenever the view changes (custom scrolling of a large image).

Currently I hook into a couple of functions to achieve this: xview_moveto, yview_moveto, scan_dragto

Is there an event that can be used to do this cleaner? Or another way to do it?

current code looks something like:

class CustomCanvas(Tkinter.Canvas):
    def xview(self, *args):
        r = Tkinter.Canvas.xview(self, *args)
        if args:
            self.event_generate("<<ScrollEvent>>")
        return r
    def yview(self, *args):
        r = Tkinter.Canvas.yview(self, *args)
        if args:
            self.event_generate("<<ScrollEvent>>")
        return r    
    def xview_moveto(self, *args):
        Tkinter.Canvas.xview_moveto(self, *args)
        self.event_generate("<<ScrollEvent>>")
    def yview_moveto(self, *args):
        Tkinter.Canvas.yview_moveto(self, *args)
        self.event_generate("<<ScrollEvent>>")
    def scan_dragto(self, *args):
        Tkinter.Canvas.scan_dragto(self, *args)
        self.event_generate("<<ScrollEvent>>")
Bryan Oakley
  • 370,779
  • 53
  • 539
  • 685
Gonzo
  • 2,023
  • 3
  • 21
  • 30
  • Something like `canvas.bind("", redraw)`? – A. Rodas Mar 04 '13 at 18:20
  • I'm not familiar with the TKinter event system, but this is how you redraw canvas to simulate "scrolling" over a larger image: http://stackoverflow.com/questions/15074974/canvas-not-showing-scrollbars-although-the-content-does-not-fit-the-page/15080689#15080689 – markE Mar 04 '13 at 19:51
  • 1
    @markE Tkinter is a Python module - not JavaScript ;) I'll retag this question to avoid confusions. – A. Rodas Mar 04 '13 at 20:35

1 Answers1

1

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.

Bryan Oakley
  • 370,779
  • 53
  • 539
  • 685
  • Does this work with scan_dragto ? Otherwise it is the same in TCL as I do now in Python - hooking. – Gonzo Mar 05 '13 at 07:37
  • @Phelix: The advantage to doing it at the Tcl level is that _everything_ must eventually call the built-in xview and yview commands: default bindings, internal commands, etc. In the case of hooks at the python level, there's no guarantee that other bindings and functions know to call your hooks. – Bryan Oakley Mar 05 '13 at 11:40
  • From the Tkinter.py code I thought scan_dragto would call something else but I did not really try it. def scan_dragto [...] self.tk.call(self._w, 'scan', 'dragto', x, y, gain) – Gonzo Mar 05 '13 at 12:44
  • I had hoped there was a built-in event that I missed but it looks like there is none if you don't know of any :) – Gonzo Mar 05 '13 at 12:46
  • @BryanOakley : do you have a similar example (you probably did one but I could not find with search in SO or Google) but with scrolling thanks to MOUSE CLICK+HOLD+MOTION, instead of Scrollbars ? – Basj Dec 17 '13 at 21:29
  • @Basj: That should be asked as a separate question. There's no way I can answer it in a comment box. – Bryan Oakley Dec 17 '13 at 21:57
  • I posted here @BryanOakley : http://stackoverflow.com/questions/20645532/move-a-tkinter-canvas-with-mouse – Basj Dec 17 '13 at 22:43