如何在Tkinter画布小部件中添加缩放功能?
我想在下面的脚本中添加缩放功能,也就是让它可以通过鼠标滚轮来放大和缩小。如果你在Linux系统上测试这个脚本,记得把鼠标滚轮的事件改成Button-4和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()
4 个回答
2
你可以考虑使用 TkZinc 这个小工具,而不是简单的画布。因为它支持通过OpenGL来进行缩放,这样会更方便。
10
为了帮助其他人找到这个问题,我附上了我的接近最终版本的测试代码,它使用了画中画/放大镜的缩放效果。这基本上只是对samplebias已经发布的内容进行了一些修改。看到这个效果真的很酷 :).
之前我提到过,如果你在Linux上使用这个脚本,别忘了把鼠标滚轮事件改成Button-4和Button-5。而且你还需要在“INSERT JPG FILE PATH”那里插入一个.JPG文件的路径。
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()
15
据我所知,内置的Tkinter Canvas类的缩放功能并不会自动缩放图像。如果你不能使用自定义的控件,可以在调用缩放函数时,手动缩放原始图像,并将其替换到画布上。
下面的代码片段可以合并到你的原始类中。它的功能如下:
- 缓存
Image.open()
的结果。 - 添加一个
redraw()
函数,用来计算缩放后的图像,并将其添加到画布上,同时移除之前绘制的图像(如果有的话)。 - 使用鼠标坐标来放置图像。我只是将
x和y
传递给create_image
函数,以展示当鼠标移动时图像位置的变化。你可以用自己的中心/偏移计算来替代这个。 - 这个代码使用的是Linux的鼠标滚轮按钮4和5(你需要调整它以便在Windows等系统上也能工作)。
(更新) 代码:
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)
更新 我对不同缩放比例进行了测试,发现resize / create_image使用了相当多的内存。我在一台配备32GB内存的Mac Pro上测试了一个540x375的JPEG图像。以下是不同缩放因子下的内存使用情况:
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
基于以上情况,更有效的解决方案可能是确定图像将要显示的视口大小,计算一个围绕鼠标坐标中心的裁剪矩形,使用这个矩形裁剪图像,然后只对裁剪后的部分进行缩放。这样应该可以使用固定的内存来存储临时图像。否则,你可能需要使用第三方的Tkinter控件来为你执行这个裁剪/窗口缩放。
更新2 这是一个工作但过于简化的裁剪逻辑,仅供你入门:
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()