14

How would I add zooming in and out to the following script, i'd like to bind it to the mousewheel. If you're testing this script on linux don't forget to change the MouseWheel event to Button-4 and Button-5.

from Tkinter import * 
import Image, ImageTk

class GUI:
    def __init__(self,root):
        frame = Frame(root, bd=2, relief=SUNKEN)

        frame.grid_rowconfigure(0, weight=1)
        frame.grid_columnconfigure(0, weight=1)
        xscrollbar = Scrollbar(frame, orient=HORIZONTAL)
        xscrollbar.grid(row=1, column=0, sticky=E+W)
        yscrollbar = Scrollbar(frame)
        yscrollbar.grid(row=0, column=1, sticky=N+S)
        self.canvas = Canvas(frame, bd=0, xscrollcommand=xscrollbar.set, yscrollcommand=yscrollbar.set, xscrollincrement = 10, yscrollincrement = 10)
        self.canvas.grid(row=0, column=0, sticky=N+S+E+W)

        File = "PATH TO JPG PICTURE HERE"

        self.img = ImageTk.PhotoImage(Image.open(File))
        self.canvas.create_image(0,0,image=self.img, anchor="nw")
        self.canvas.config(scrollregion=self.canvas.bbox(ALL))
        xscrollbar.config(command=self.canvas.xview)
        yscrollbar.config(command=self.canvas.yview)

        frame.pack()

        self.canvas.bind("<Button 3>",self.grab)
        self.canvas.bind("<B3-Motion>",self.drag)
        root.bind("<MouseWheel>",self.zoom)


    def grab(self,event):
        self._y = event.y
        self._x = event.x

    def drag(self,event):
        if (self._y-event.y < 0): self.canvas.yview("scroll",-1,"units")
        elif (self._y-event.y > 0): self.canvas.yview("scroll",1,"units")
        if (self._x-event.x < 0): self.canvas.xview("scroll",-1,"units")
        elif (self._x-event.x > 0): self.canvas.xview("scroll",1,"units")
        self._x = event.x
        self._y = event.y

    def zoom(self,event):
        if event.delta>0: print "ZOOM IN!"
        elif event.delta<0: print "ZOOM OUT!"


root = Tk()   
GUI(root)
root.mainloop()
martineau
  • 119,623
  • 25
  • 170
  • 301
Symon
  • 2,170
  • 4
  • 26
  • 34
  • Are you actually looking to scale the canvas or just the image? – John Giotta Apr 11 '11 at 14:04
  • 2
    everything on the canvas, the image and an assortment of lines and circles will be on the canvas eventually. And once placed it's incredibly important that everything keeps its x,y coordinates. – Symon Apr 11 '11 at 14:06

4 Answers4

15

To my knowledge the built-in Tkinter Canvas class scale will not auto-scale images. If you are unable to use a custom widget, you can scale the raw image and replace it on the canvas when the scale function is invoked.

The code snippet below can be merged into your original class. It does the following:

  1. Caches the result of Image.open().
  2. Adds a redraw() function to calculate the scaled image and adds that to the canvas, and also removes the previously-drawn image if any.
  3. Uses the mouse coordinates as part of the image placement. I just pass x and y to the create_image function to show how the image placement shifts around as the mouse moves. You can replace this with your own center/offset calculation.
  4. This uses the Linux mousewheel buttons 4 and 5 (you'll need to generalize it to work on Windows, etc).

(Updated) Code:

class GUI:
    def __init__(self, root):

        # ... omitted rest of initialization code

        self.canvas.config(scrollregion=self.canvas.bbox(ALL))
        self.scale = 1.0
        self.orig_img = Image.open(File)
        self.img = None
        self.img_id = None
        # draw the initial image at 1x scale
        self.redraw()

        # ... rest of init, bind buttons, pack frame

    def zoom(self,event):
        if event.num == 4:
            self.scale *= 2
        elif event.num == 5:
            self.scale *= 0.5
        self.redraw(event.x, event.y)

    def redraw(self, x=0, y=0):
        if self.img_id:
            self.canvas.delete(self.img_id)
        iw, ih = self.orig_img.size
        size = int(iw * self.scale), int(ih * self.scale)
        self.img = ImageTk.PhotoImage(self.orig_img.resize(size))
        self.img_id = self.canvas.create_image(x, y, image=self.img)

        # tell the canvas to scale up/down the vector objects as well
        self.canvas.scale(ALL, x, y, self.scale, self.scale)

Update I did a bit of testing for varying scales and found that quite a bit of memory is being used by resize / create_image. I ran the test using a 540x375 JPEG on a Mac Pro with 32GB RAM. Here is the memory used for different scale factors:

 1x  (500,     375)      14 M
 2x  (1000,    750)      19 M
 4x  (2000,   1500)      42 M
 8x  (4000,   3000)     181 M
16x  (8000,   6000)     640 M
32x  (16000, 12000)    1606 M
64x  (32000, 24000)  ...  
reached around ~7400 M and ran out of memory, EXC_BAD_ACCESS in _memcpy

Given the above, a more efficient solution might be to determine the size of the viewport where the image will be displayed, calculate a cropping rectangle around the center of the mouse coordinates, crop the image using the rect, then scale just the cropped portion. This should use constant memory for storing the temporary image. Otherwise you may need to use a 3rd party Tkinter control which performs this cropping / windowed scaling for you.

Update 2 Working but oversimplified cropping logic, just to get you started:

    def redraw(self, x=0, y=0):
        if self.img_id: self.canvas.delete(self.img_id)
        iw, ih = self.orig_img.size
        # calculate crop rect
        cw, ch = iw / self.scale, ih / self.scale
        if cw > iw or ch > ih:
            cw = iw
            ch = ih
        # crop it
        _x = int(iw/2 - cw/2)
        _y = int(ih/2 - ch/2)
        tmp = self.orig_img.crop((_x, _y, _x + int(cw), _y + int(ch)))
        size = int(cw * self.scale), int(ch * self.scale)
        # draw
        self.img = ImageTk.PhotoImage(tmp.resize(size))
        self.img_id = self.canvas.create_image(x, y, image=self.img)
        gc.collect()
samplebias
  • 37,113
  • 6
  • 107
  • 103
  • I can't zoom in past 2 else i get: Runtime Error! Program: C:\Python27\pythonw.exe This application has requested the Runtime terminate it in an unusual way, Please contact the application's support team for more information. – Symon Apr 12 '11 at 14:42
  • 2
    @Symon you're welcome, glad it's helping you make progress. Given that the canvas cannot automatically resize images, the `redraw()` method does this manually -- rescaling and re-adding the image to the canvas. To simultaneously scale any vectors drawn on the canvas (lines, ovals, polygons) you can add a call to `self.canvas.scale(ALL, x, y, self.scale, self.scale)` to the `redraw()` method above. – samplebias Apr 12 '11 at 14:51
  • 1
    @Symon hmm, I'm using Linux and haven't seen that error. Is the image you're scaling up really large? – samplebias Apr 12 '11 at 14:54
  • It's a 2058x2165 211KB .jpg image, i'll see if i can find anything on the interwebs. Scaling the items on the canvas is working great thanks for that as well! – Symon Apr 12 '11 at 15:31
  • Meanwhile i edited my question and posted the code that's still giving me an error, maybe i'm placing something somewhere wrong. – Symon Apr 12 '11 at 15:42
  • Nice, Thanks for looking into the problem :), that memory use is pretty intense. I am determined to get this working without external modules, so i'll try the crop workaround and edit my question with the solution when i've got it finalized. Thanks a lot for your troubleshooting help! – Symon Apr 12 '11 at 17:06
  • 1
    @Symon I updated the code with a simple crop/scale example which is very fast and uses constant memory. Hopefully that is useful. Best of luck with your efforts! – samplebias Apr 12 '11 at 17:15
  • excellent answer and follow through, thanks a lot samplebias! – Symon Apr 12 '11 at 20:17
  • @samplebias : just to be sure to understand, do you mean `self.canvas.scale(...)` only works for vector objects (rectangles, etc.) but **doesn't** for images created with `create_image`? Isn't there an updated version of tkinter for which it would work? Because even if your solution works, it won't be easy to implement zoom + drag (with click + move move)... – Basj Jan 06 '17 at 14:11
  • I posted a relation question here @samplebias : http://stackoverflow.com/q/41656176/1422096. – Basj Jan 14 '17 at 23:37
10

Just for other's benefit who find this question I'm attaching my neer final test code which uses picture in picture/magnifying glass zooming. Its basically just an alteration to what samplebias already posted. It's also very cool to see as well :).

As I said before, if you're using this script on linux don't forget to change the MouseWheel event to Button-4 and Button-5. And you obviously need to insert a .JPG path where it says "INSERT JPG FILE PATH".

from Tkinter import *
import Image, ImageTk

class LoadImage:
    def __init__(self,root):
        frame = Frame(root)
        self.canvas = Canvas(frame,width=900,height=900)
        self.canvas.pack()
        frame.pack()
        File = "INSERT JPG FILE PATH"
        self.orig_img = Image.open(File)
        self.img = ImageTk.PhotoImage(self.orig_img)
        self.canvas.create_image(0,0,image=self.img, anchor="nw")

        self.zoomcycle = 0
        self.zimg_id = None

        root.bind("<MouseWheel>",self.zoomer)
        self.canvas.bind("<Motion>",self.crop)

    def zoomer(self,event):
        if (event.delta > 0):
            if self.zoomcycle != 4: self.zoomcycle += 1
        elif (event.delta < 0):
            if self.zoomcycle != 0: self.zoomcycle -= 1
        self.crop(event)

    def crop(self,event):
        if self.zimg_id: self.canvas.delete(self.zimg_id)
        if (self.zoomcycle) != 0:
            x,y = event.x, event.y
            if self.zoomcycle == 1:
                tmp = self.orig_img.crop((x-45,y-30,x+45,y+30))
            elif self.zoomcycle == 2:
                tmp = self.orig_img.crop((x-30,y-20,x+30,y+20))
            elif self.zoomcycle == 3:
                tmp = self.orig_img.crop((x-15,y-10,x+15,y+10))
            elif self.zoomcycle == 4:
                tmp = self.orig_img.crop((x-6,y-4,x+6,y+4))
            size = 300,200
            self.zimg = ImageTk.PhotoImage(tmp.resize(size))
            self.zimg_id = self.canvas.create_image(event.x,event.y,image=self.zimg)

if __name__ == '__main__':
    root = Tk()
    root.title("Crop Test")
    App = LoadImage(root)
    root.mainloop()
Symon
  • 2,170
  • 4
  • 26
  • 34
  • 1
    Managed to launch this script on Python 3.8 in 2020. Just changed first two lines: import tkinter (mind the case) and from PIL import Image, ImageTk. Thank you for your work. – Eugene Chabanov Aug 21 '20 at 19:58
2
  1. Advanced zoom example, based on tiles. Like in Google Maps.
  2. Simplified zoom example, based on resizing the whole image.
FooBar167
  • 2,721
  • 1
  • 26
  • 37
2

Might be a good idea to look at the TkZinc widget instead of the simple canvas for what you are doing, it supports scaling via OpenGL.

schlenk
  • 7,002
  • 1
  • 25
  • 29
  • I did look into TkZinc, however if possible I'd like to develop the GUI with only the standard modules that come with Python 2.7.1 – Symon Apr 11 '11 at 21:35
  • seems the link moved to [Github](https://github.com/asb-capfan/TkZinc) – mo FEAR Mar 12 '22 at 16:39