Do the "w" and "s" key bindings work differently in Tkinter? - python

I've been working on translating a version of the game "Pong", that I built for an online Python class using CodeSkulptor, into a "desktop" python script using Tkinter, as a means to teach myself how to use Tkinter. I've managed to pretty much get the entire game working, except for the left (player 1) paddle. I think I have the key bindings correct, as the right (player 2) paddle works as expected, in that when you hold down the "Up" or "Down" arrow keys, the paddle moves until it hits the upper or lower bounds of the canvas, or stops when either key is released. I'm passing the key presses to the keydown and keyup handlers, where I check to see which key was pressed/released, and act accordingly. What's baffling to me is that if I map the left paddle movement to different keys (say "a" or "d", or the "Up" or "Down" arrows, for example), it works as expected, but it refuses to work when I have the "w" and "s" keys mapped. Does anyone have any idea why that might be, or what I may have done wrong?
The code I've provided below is a basic example I put together, that demonstrates this issue, and the paddle movement that I'm trying to achieve (it pretty much mirrors my Pong project). The right-side paddle moves correctly, where the left side paddle does not. Thanks in advance for your help!
from Tkinter import *
import random
WIDTH = 500
HEIGHT = 500
PAD_WIDTH = 10
PAD_HEIGHT = 80
HALF_PAD_WIDTH = PAD_WIDTH / 2
HALF_PAD_HEIGHT = PAD_HEIGHT / 2
class Example(Frame, object):
def __init__(self, master):
super(Example, self).__init__(master)
self._paddle1_pos = 200
self._paddle2_pos = 200
self._paddle1_vel = 0
self._paddle2_vel = 0
self.initUI()
def initUI(self):
scn_cent_height = self.master.winfo_screenheight() // 2 - HEIGHT // 2
scn_cent_width = self.master.winfo_screenwidth() // 2 - WIDTH // 2
self.master.geometry("%sx%s+%s+%s" % (WIDTH, HEIGHT, scn_cent_width, scn_cent_height))
self.master.minsize(WIDTH, HEIGHT)
self.master.title("Example Pong Paddles")
self._canvasFrame = Frame(self.master)
self._canvasFrame.pack(expand=True, fill=BOTH)
self._canvas = Canvas(self._canvasFrame, bg="black", highlightthickness=0, bd=0)
self._canvas.pack(fill=BOTH, expand=True)
self.update_idletasks()
# Key handlers
self.master.bind("<KeyPress>", self.keydown)
self.master.bind("<KeyRelease>", self.keyup)
while True:
self._canvas.after(1)
self._canvas.delete("all")
self.draw()
self._canvas.update()
def draw(self):
self._cheight = self._canvasFrame.winfo_height()
self._cwidth = self._canvasFrame.winfo_width()
# Draw mid line and gutters
self._rline = self._canvas.create_line(self._cwidth / 2, 0, self._cwidth / 2, self._cheight, width=1, fill="White")
self._mline = self._canvas.create_line(PAD_WIDTH, 0, PAD_WIDTH, self._cheight, width=1, fill="White")
self._lline = self._canvas.create_line(self._cwidth - PAD_WIDTH, 0, self._cwidth - PAD_WIDTH, self._cheight, width=1, fill="White")
# Update paddle's vertical position, keep paddle on the screen
# Paddle 1 - Check height and update position
if self._paddle1_pos + self._paddle1_vel >= HALF_PAD_HEIGHT and self._paddle1_pos + self._paddle1_vel <= HEIGHT - HALF_PAD_HEIGHT:
self._paddle1_pos += self._paddle1_vel
# Paddle 2 - Check height and update position
if self._paddle2_pos + self._paddle2_vel >= HALF_PAD_HEIGHT and self._paddle2_pos + self._paddle2_vel <= HEIGHT - HALF_PAD_HEIGHT:
self._paddle2_pos += self._paddle2_vel
# Draw paddles
self._p1paddle = self._canvas.create_line([HALF_PAD_WIDTH, self._paddle1_pos - HALF_PAD_HEIGHT],
[HALF_PAD_WIDTH, self._paddle1_pos + HALF_PAD_HEIGHT], width=PAD_WIDTH, fill="White")
self._p2paddle = self._canvas.create_line([self._cwidth - HALF_PAD_WIDTH, self._paddle2_pos - HALF_PAD_HEIGHT],
[self._cwidth - HALF_PAD_WIDTH, self._paddle2_pos + HALF_PAD_HEIGHT], width=PAD_WIDTH, fill="White")
# Draw paddles
self._p1paddle = self._canvas.create_line([HALF_PAD_WIDTH, self._paddle1_pos - HALF_PAD_HEIGHT],
[HALF_PAD_WIDTH, self._paddle1_pos + HALF_PAD_HEIGHT], width=PAD_WIDTH, fill="White")
self._p2paddle = self._canvas.create_line([self._cwidth - HALF_PAD_WIDTH, self._paddle2_pos - HALF_PAD_HEIGHT],
[self._cwidth - HALF_PAD_WIDTH, self._paddle2_pos + HALF_PAD_HEIGHT], width=PAD_WIDTH, fill="White")
def keydown(self, key):
key = key.keysym
if key == "w":
self._paddle1_vel = -10
elif key == "s":
self._paddle1_vel = 10
elif key == "Up":
self._paddle2_vel = -10
elif key == "Down":
self._paddle2_vel = 10
def keyup(self, key):
key = key.keysym
if key == "w":
self._paddle1_vel = 0
elif key == "s":
self._paddle1_vel = 0
elif key == "Up":
self._paddle2_vel = 0
elif key == "Down":
self._paddle2_vel = 0
def main():
root = Tk()
example = Example(root)
root.mainloop()
if __name__ == '__main__':
main()

I just wanted to add, for the benefit of anyone else who runs into this problem, that I was able to trace the source of it back to my Anaconda virtual environments (I have one each for Python 2 and Python 3). In testing, I was able to reproduce this issue when specifically using those environments, but was unable to reproduce it when using Python 2 (mac framework build) or Python 3 (installed via Brew) outside of Anaconda. I wiped out both Anaconda virtual environments in case something was messed up there, and rebuilt them both (i.e. clean install of both Python 2 and 3), and was still able to reproduce this issue (without installing any additional modules). I can only surmise that there is something different between the python(s) that is/are installed by Anaconda, and those that either are part of the system framework, or installed separately.
Also, just to note, it turns out when I successfully tested on Python 3 in #Novel's above comment, I inadvertently tested on the system Python 3, rather than in the Anaconda Python 3 virtual environment.
EDIT:
It turns out this problem is actually due to the version of Tkinter (Tcl/Tk) that my python installs were using. The python framework on my laptop was using Tk 8.5.9, whereas the Anaconda installs are using 8.6.8. In testing, I ran into this issue when running my Pong script in python 2.7.16 and 3.7.3 with Tk 8.6.8, but it ran perfectly fine on the same python versions with Tk 8.5.9. I'm not sure if this is an issue with Tk, or some incompatibility between Tk 8.6.8 and the MacOS framework (since the framework natively uses 8.5.9).

Your code is very messy and needs to be refactored, so I did that. Some notes:
The biggest note is that instead of clearing the screen and redrawing everything, it's much neater and performs much better if you draw objects once and then simply move them around.
Use tkinter's mainloop; don't make your own and update it manually. The update() or update_idletasks() methods are last resort; normal code does not have them. Use tkinter's mainloop via after() instead. This will make your window much more reactive.
Python2 Tkinter classes are old-style, do not force them to be new-style by adding an object inheritance.
the canvas frame is useless, so I removed that.
keeping the keys in a dictionary frees up a lot of repeated code.
Prefixing all your variable names with _ does nothing and makes it hard to read and type; leave that off.
-
try:
import tkinter as tk # python3 detected
except ImportError:
import Tkinter as tk # python2 detected
WIDTH = 500
HEIGHT = 500
PAD_WIDTH = 10
PAD_HEIGHT = 80
VELOCITY = 10
HALF_PAD_WIDTH = PAD_WIDTH // 2
HALF_PAD_HEIGHT = PAD_HEIGHT // 2
P1_UP = 111 # Up arrow key
P1_DOWN = 116 # Down arrrow key
P2_UP = 25 # 'w' key
P2_DOWN = 39 # 's' key
class Example(tk.Frame):
def __init__(self, master):
tk.Frame.__init__(self, master)
self.keys = {}
self.initUI()
# Key handlers
self.master.bind("<KeyPress>", self.keydown)
self.master.bind("<KeyRelease>", self.keyup)
self.draw() # add the game loop to the mainloop
def initUI(self):
scn_cent_height = self.master.winfo_screenheight() // 2 - HEIGHT // 2
scn_cent_width = self.master.winfo_screenwidth() // 2 - WIDTH // 2
self.master.geometry("%sx%s+%s+%s" % (WIDTH, HEIGHT, scn_cent_width, scn_cent_height))
self.master.minsize(WIDTH, HEIGHT)
self.master.title("Example Pong Paddles")
self.canvas = tk.Canvas(self, bg="black", highlightthickness=0, bd=0, width=WIDTH, height=HEIGHT)
self.canvas.pack(fill=tk.BOTH, expand=True)
# Draw mid line and gutters
self.rline = self.canvas.create_line(WIDTH//2, 0, WIDTH//2, HEIGHT, width=1, fill="White")
self.mline = self.canvas.create_line(PAD_WIDTH, 0, PAD_WIDTH, HEIGHT, width=1, fill="White")
self.lline = self.canvas.create_line(WIDTH - PAD_WIDTH, 0, WIDTH - PAD_WIDTH, HEIGHT, width=1, fill="White")
# Draw paddles
self.p1paddle = self.canvas.create_line([HALF_PAD_WIDTH, HEIGHT//2 - HALF_PAD_HEIGHT],
[HALF_PAD_WIDTH, HEIGHT//2 + HALF_PAD_HEIGHT], width=PAD_WIDTH, fill="White")
self.p2paddle = self.canvas.create_line([WIDTH - HALF_PAD_WIDTH, HEIGHT//2 - HALF_PAD_HEIGHT],
[WIDTH - HALF_PAD_WIDTH, HEIGHT//2 + HALF_PAD_HEIGHT], width=PAD_WIDTH, fill="White")
def draw(self):
if self.keys.get(P2_UP) and self.canvas.coords(self.p1paddle)[1] > 0:
self.canvas.move(self.p1paddle, 0, -VELOCITY)
if self.keys.get(P2_DOWN) and self.canvas.coords(self.p1paddle)[3] < HEIGHT:
self.canvas.move(self.p1paddle, 0, VELOCITY)
if self.keys.get(P1_UP) and self.canvas.coords(self.p2paddle)[1] > 0:
self.canvas.move(self.p2paddle, 0, -VELOCITY)
if self.keys.get(P1_DOWN) and self.canvas.coords(self.p2paddle)[3] < HEIGHT:
self.canvas.move(self.p2paddle, 0, VELOCITY)
self.after(10, self.draw)
def keydown(self, key):
self.keys[key.keycode] = True
def keyup(self, key):
self.keys[key.keycode] = False
def main():
root = tk.Tk()
example = Example(root)
example.pack()
root.mainloop()
if __name__ == '__main__':
main()
Perhaps, by chance, some improvement in there will fix your original problem too.

Related

Dynamically updating polygon shape in tkinter window

I'm writing a program for my raspberry pi4 for my Master's project which sounded so simple at first but for some reason Python is causing me grief.
In short - I have created a tkinter canvas with a polygon centred on the canvas. The shape of this polygon will depend upon the value of a counter.
The count is controlled by a blink event from a neurosky mindwave headset - this count is working (mostly).
What I want to then do is update the canvas to put the new points for the polygon into the pack but nothing I have tried seems to work. The closest I got was trying a .redraw() command which drew an infinite number of windows before I pulled the plug.
I am not a complete novice to coding having taught many languages in my time but have never used python before and am clearly missing a very simple step which will cause everything to fall out.
I will try to modify the code to use a keyboard press rather than a headset and add it below later if folk think it will help.
import keyboard
import time
from tkinter import *
count = 0
points = [250,250,350,250,350,350,250,350]
root = Tk()
while True:
# set window to middle of screen
screen_width = root.winfo_screenwidth()
screen_height = root.winfo_screenheight()
xcoord = screen_width/2-300
ycoord = screen_height/2 - 300
root.geometry("%dx%d+%d+%d" % (600,600,xcoord,ycoord))
#set up canvas size and background colour
canvas1 = Canvas(root, relief = FLAT,width = 600, height = 600, background = "blue")
#set up buttons shape and colour
button = canvas1.create_polygon(points, fill="darkgreen", outline="yellow")
canvas1.pack()
if keyboard.is_pressed("f"):
if count < 4:
count += 1
elif count == 4:
count = 0
time.sleep(0.1)
if count == 0:
points = [250,250,350,250,350,350,250,350]
elif count == 1:
points = [300,100,500,500,100,500]
elif count == 2:
points = [200,100,400,100,300,500]
elif count == 3:
points = [100,300,500,100,500,500]
elif count == 4:
points = [100,100,100,500,500,300]
print(count)
root.update()
You need to delete the old polygon and create new one. Also don't use while loop in tkinter application. For your case, you can bind a callback on <Key> event and update the polygon in the callback:
import tkinter as tk
count = 0
points = [
[250,250,350,250,350,350,250,350],
[300,100,500,500,100,500],
[200,100,400,100,300,500],
[100,300,500,100,500,500],
[100,100,100,500,500,300],
]
root = tk.Tk()
# set window to middle of screen
screen_width = root.winfo_screenwidth()
screen_height = root.winfo_screenheight()
xcoord = screen_width//2 - 300
ycoord = screen_height//2 - 300
root.geometry("%dx%d+%d+%d" % (600,600,xcoord,ycoord))
#set up canvas
canvas1 = tk.Canvas(root, relief=tk.FLAT, background="blue")
canvas1.pack(fill=tk.BOTH, expand=1)
# create the polygon with tag "button"
canvas1.create_polygon(points[count], fill="darkgreen", outline="yellow", tag="button")
def on_key(event):
global count
if event.char == 'f':
count = (count + 1) % len(points)
print(count)
canvas1.delete("button") # delete the old polygon
canvas1.create_polygon(points[count], fill="darkgreen", outline="yellow", tag="button")
root.bind("<Key>", on_key)
root.mainloop()
You can update any parameters of a shape.
canvas.itemconfigure(shape1_id_or_tag, fill="green")
canvas.itemconfigure(shape2_id_or_tag, fill="#900", outline="red", width=3)
But for your situation try '.coords()':
canvas1.coords("Button", points[count])
Source: https://anzeljg.github.io/rin2/book2/2405/docs/tkinter/canvas-methods.html

Tkinter .after() not responding to keypress fast enough

I am trying to animate a ball, that is be moving forward. In addition, if the user presses up or down button, it should move respectively.
try:
import tkinter as tk
except ImportError:
import Tkinter as tk
class Movement:
def __init__(self, _frame, _canvas):
self.frame = _frame
self.canvas = _canvas
self.x = 10
self.y = 150
self.count = 0
self.update()
self.frame.bind_all("<Up>", lambda event: self.move_up())
self.frame.bind_all("<Down>", lambda event: self.move_down())
pass
def move_up(self):
self.y -= 10
pass
def move_down(self):
self.y += 10
pass
def update(self):
canvas.delete("obj")
self.count += 10
x2 = self.x + 50
y2 = self.y + 50
self.canvas.create_oval(self.x + self.count, self.y, x2 + self.count, y2, fill="red", tag="obj")
root.after(1000, self.update)
pass
root = tk.Tk()
width = 400
height = 400
canvas = tk.Canvas(root, width=width, height=height)
frame = tk.Frame(root, width=width, height=height)
if __name__ == '__main__':
Movement(frame, canvas)
canvas.grid()
root.mainloop()
There's an issue, though. Given the fact that the ball is moving every 1 second, it doesn't respond to the key press fast enough. As a result, if you click the up kick, the screen is updating after one second.
I know there's a .move() function inside Tkinter. However, due to many reason I cannot use that.
My main concern is the interval it take for the screen to be updated.
From what I understand you want to be able to keep the timer moving forward consistent but also be able to move up and down at any point even outside of the timer.
to accomplish this I moved the timer to its own function and allowed the up down functions to call the object destroy/create function.
I have updated you code to reflect that.
Take a look at this code and let me know if it helps.
try:
import tkinter as tk
except ImportError:
import Tkinter as tk
class Movement:
def __init__(self, _frame, _canvas):
self.frame = _frame
self.canvas = _canvas
self.x = 10
self.y = 150
self.count = 0
self.object_timer()
self.frame.bind_all("<Up>",lambda event: self.move_up())
self.frame.bind_all("<Down>",lambda event: self.move_down())
def move_up(self):
self.y -= 10
self.update()
def move_down(self):
self.y += 10
self.update()
def update(self):
canvas.delete("obj")
x2 = self.x + 50
y2 = self.y + 50
self.canvas.create_oval(self.x + self.count, self.y, x2 + self.count, y2, fill="red", tag="obj")
def object_timer(self):
self.update()
self.count += 10
root.after(1000, self.object_timer)
root = tk.Tk()
width = 400
height = 400
canvas = tk.Canvas(root, width=width, height=height)
frame = tk.Frame(root, width=width, height=height)
if __name__ == '__main__':
Movement(frame, canvas)
canvas.grid()
root.mainloop()
It's all just math. You can call update twice as much, and move the ball half as much if you want to keep the ball moving at the same speed while reacting to keypresses faster.
For example, instead of moving 10 pixels every second, have it move 1 pixel every tenth of a second. The end result is still 10 pixels every second. The animation will be smoother, and it will react to keypresses much faster.
By the way, there's no need to delete and recreate the oval every iteration. It's possible to move items on the canvas. Deleting and recreating will eventually cause performance problems do to how the canvas is implemented internally. Each object you create gets a new unique id, and the canvas will slow down when it has a large number of ids. The canvas does not re-use an id.
I would also recommend against using lambda. Unless you need it, it adds complexity without adding any value. Just add an extra parameter to move_up and move_down:
self.frame.bind_all("<Up>",self.move_up)
self.frame.bind_all("<Down>", self.move_down)
...
def move_up(self, event=None):
...
def move_down(self, event=None):
...

Remove canvas widgets in Tkinter

from Tkinter import *
import random
root = Tk()
width = 700
height = 600
canvas = Canvas(root, width = width, height = height, bg = "light blue")
canvas.pack()
pipes = []
class NewPipe:
def __init__(self, pipe_pos, pipe_hole):
self.pipe_pos = list(pipe_pos)
def update(self):
self.pipe_pos[0] -= 3
self.pipe_pos[2] -= 3
def draw(self):
canvas.create_rectangle(self.pipe_pos, fill = "green")
def get_pos(self):
return self.pipe_pos
def generate_pipe():
pipe_hole = random.randrange(0, height)
pipe_pos = [width - 100, 0, width, pipe_hole]
pipes.append(NewPipe(pipe_pos, pipe_hole))
draw_items()
canvas.after(2000, generate_pipe)
def draw_items():
for pipe in pipes:
if pipe.get_pos()[2] <= 0 - 5:
pipes.remove(pipe)
else:
pipe.draw()
pipe.update()
canvas.after(100, draw_items)
def jump(press):
pass
canvas.bind("<Button-1>", jump)
canvas.after(2000, generate_pipe)
draw_items()
mainloop()
Right now I am trying to make a game where you have to dodge rectangles, which are pipes. It is basically Flappy Bird, but on Tkinter. In this code I am trying to generate pipes and move them, but the pipes I have drawn before do not leave and they just stay there. This means that when the pipe moves, the position it was just in doesnt change and that shape stays there. Is there any way to delete past shapes, or another way to move them?
canvas.create_rectangle(self.pipe_pos, fill = "green") returns an ID.
You can use this ID to put it into methods like
canvas.coords
canvas.delete
canvas.itemconfigure
canvas.scale
canvas.type
...
Have a look at help(canvas).
The canvas is not a framebuffer on which you paint stuff for one frame. The painted stuff does not go away and you can move it and change all the parameters you can use when creating.

Tkinter events outside of the mainloop?

The program I am writing has a tkinter window that is constantly being fed with data manually rather than being part of a mainloop. It also needs to track mouse location. I havn't found a workaround for tracking the mouse outside of mainloop yet, but if you have one please do tell.
from Tkinter import *
import random
import time
def getCoords(event):
xm, ym = event.x, event.y
str1 = "mouse at x=%d y=%d" % (xm, ym)
print str1
class iciclePhysics(object):
def __init__(self, fallrange, speed=5):
self.speed = speed
self.xpos = random.choice(range(0,fallrange))
self.ypos = 0
def draw(self,canvas):
try:
self.id = canvas.create_polygon(self.xpos-10, self.ypos, self.xpos+10, self.ypos, self.xpos, self.ypos+25, fill = 'lightblue')
except:
pass
def fall(self,canvas):
self.ypos+=self.speed
canvas.move(self.id, 0, self.ypos)
root = Tk()
mainFrame = Frame(root, bg= 'yellow', width=300, height=200)
mainFrame.pack()
mainCanvas = Canvas(mainFrame, bg = 'black', height = 500, width = 500, cursor = 'circle')
mainCanvas.bind("<Motion>", getCoords)
mainCanvas.pack()
root.resizable(0, 0)
difficulty = 1500
#root.mainloop()
currentIcicles = [iciclePhysics(difficulty)]
root.update()
currentIcicles[0].draw(mainCanvas)
root.update_idletasks()
time.sleep(0.1)
currentIcicles[0].fall(mainCanvas)
root.update_idletasks()
tracker = 0
sleeptime = 0.04
while True:
tracker+=1
time.sleep(sleeptime)
if tracker % 3 == 0 and difficulty > 500:
difficulty -= 1
elif difficulty <= 500:
sleeptime-=.00002
currentIcicles.append(iciclePhysics(difficulty))
currentIcicles[len(currentIcicles)-1].draw(mainCanvas)
for i in range(len(currentIcicles)):
currentIcicles[i].fall(mainCanvas)
root.update_idletasks()
for i in currentIcicles:
if i.ypos >= 90:
currentIcicles.remove(i)
root.update_idletasks()
There is no way. Mouse movement is presented to the GUI as a series of events. In order to process events, the event loop must be running.
Also, you should pretty much never do a sleep inside a GUI application. All that does is freeze the GUI during the sleep.
Another hint: you only need to create an icicle once; to make it fall you can use the move method of the canvas.
If you are having problems understanding event based programming, the solution isn't to avoid the event loop, the solution is to learn how event loops work. You pretty much can't create a GUI without it.

Adding Zooming in and out with a Tkinter Canvas Widget?

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()
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:
Caches the result of Image.open().
Adds a redraw() function to calculate the scaled image and adds that to the canvas, and also removes the previously-drawn image if any.
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.
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()
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()
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.
Advanced zoom example, based on tiles. Like in Google Maps.
Simplified zoom example, based on resizing the whole image.

Categories