目标
桌面应用增加一个菜单栏,菜单栏中增加一个新功能,修改上次发现的一个bug。
- 增加菜单栏,使用menu组件;
- 增加一个将图片分格的功能;
- 修复上次发现的,缩放图片时,显示图片不完整的bug;
- 整体代码打个标签:V1.2.1,代表另一个小的可以发布的里程碑。(可以用于克隆这个代码,复现这些功能)
总结
最终你可以通过可以使用下面四行命令,体验一下程序运行的效果
git clone https://github.com/kamigo2018/myTkinter.git
cd myTkinter
git checkout V1.2.1
python HelloTkinter.py
简单说一下要做啥
1、设计一下菜单栏
菜单栏放置三个菜单:
- 文件:
有两项菜单:打开,结束:
打开菜单,调用一个对话框,选择一幅图片,显示在画布区域;
结束菜单,提供退出这个应用的功能。 - 功能:
提供一项新的功能“分格”,用白色的横线和竖线将图片分隔成几块,调用系统应用展示分格结果,并保存临时文件
分格结果:
- 其他:
就提供一个说明功能,通过对话框显示一个“Hello”
2、设计一下分格功能
最近看抖音时,发现很多小视频的封面是一幅完整的图切开拼在一起的。受这个事情的启发,我想将一幅图分割成多个小块,中间用线分隔开:
3、修改上次发现的bug
V1.2.0版本的程序中,在对图像进行放大后,图像上边一部分的内容就不能显示了:
这个应该是图像在画布(canvas)显示的位置有问题,或者是滚动条(scrollbar)设置的范围有问题,在这个版本中进行修改:
再说一下都是咋做的
1、菜单栏
在 HelloTkinter.py 中,创建 createMenu()函数:
def createMenu(self):
self.menuBar = tk.Menu()
self.root.config(menu=self.menuBar)
# 文件菜单
# tearoff,默认为True,这时菜单栏中有一条虚线,点击虚线,可以将菜单弹出。
self.fileMenu = tk.Menu(self.menuBar, tearoff=False)
self.menuBar.add_cascade(label="文件", menu = self.fileMenu)
self.fileMenu.add_command(label="打开",command=self.openImageFile)
self.fileMenu.add_separator()
self.fileMenu.add_command(label="结束",command = self.root.quit)
# 小功能菜单
self.functionMenu = tk.Menu(self.menuBar,tearoff=False)
self.menuBar.add_cascade(label="功能", menu = self.functionMenu)
self.functionMenu.add_command(label="分格",command=self.divideImage)
# 帮助菜单
self.helpMenu = tk.Menu(self.menuBar,tearoff=False)
self.menuBar.add_cascade(label="其他", menu = self.helpMenu)
self.helpMenu.add_command(label="说明",command=self.showInfo)
先创建一个menuBar,然后创建三个菜单,每个菜单中添加对应的功能,指明对应的函数。
例如文件菜单,其中有的“打开”功能,指定调用函数self.openImageFile,“结束”功能,指定调用self.root.quit。这两个功能之间有个横线,作为分隔线,是用add_spearator()函数添加的。另外的“功能”和“其他”菜单基本也是一样的。
# tearoff,默认为True,这时菜单栏中有一条虚线,点击虚线,可以将菜单弹出。
self.fileMenu = tk.Menu(self.menuBar, tearoff=False)
self.menuBar.add_cascade(label="文件", menu = self.fileMenu)
self.fileMenu.add_command(label="打开",command=self.openImageFile)
self.fileMenu.add_separator()
## 这里的self.root.quit,是这个应用根窗体的退出函数。
self.fileMenu.add_command(label="结束",command = self.root.quit)
这里的 openImageFile() 函数,基本上和之前 inputSend() 函数一样;不同点在于,这次是用 tkinter 提供的文件选择对话框选择文件,获取图片路径。
def openImageFile(self):
filePath = tkFileDialog.askopenfilename(title="打开文件")
if len(filePath)==0:
#print("没有输入")
return
temp = filePath.split('.')
if len(temp)>1 and (temp[-1].lower() in ['jpg','png','jpeg']) :
self.showImage(filePath)
askopenfilename,对应的对话框如下图:
“其他”菜单中的“说明”功能,也是调用tkinter提供的对话框:
def showInfo(self):
# 调用tkinter.messagebox模块中的显示信息对话框
tkMessageBox.showinfo(title="说明",message="hello")
而“功能”菜单中,“分格”功能也弹出了一个对话框。由于这个对话框要采集用户输入的两个参数,tkinter没有对应的对话框,所以这个对话框是自定义的: 定义在 myUtil.py 文件中,SimpleDialog类。一会再说。
# 参考 http://effbot.org/tkinterbook/tkinter-dialog-windows.htm
# 参考 https://www.cnblogs.com/hhh5460/p/6664021.html?utm_source=itdadao&utm_medium=referral
class SimpleDialog():
def __init__(self, master):
self.master = master
self.top = tk.Toplevel(master)
... ...
2、图片分格功能
这个分格功能的思路也比较简单,就是将原图按照要求,裁剪成一块一块的小图,然后粘贴到一张新的白色背景上面。粘贴时,在横纵两个方向留下一些空隙,让空隙形成分格图片的横线和竖线。
在“功能”菜单中,指明“分格”功能调用 self.divideImage() 函数:
# 小功能菜单
self.functionMenu = tk.Menu(self.menuBar,tearoff=False)
self.menuBar.add_cascade(label="功能", menu = self.functionMenu)
self.functionMenu.add_command(label="分格",command=self.divideImage)
divideImage 函数,首先获取用户输出的分格参数;然后根据参数,调用图片分格类实例的分格函数,取得分格后的图片;展示图片,并将图片存储在tmp目录中。
def divideImage(self):
if self.imageFlag == 0:
tkMessageBox.showinfo(title="提示", \
message="先打开一张图片,再进行分格")
return
## 自定义对话框,用来获取分格参数。不过这还有个一个问题,
## 就是调出这个对话框后,root窗体还是可以被选中,还可以
## 再次选择这个功能。应该显示这个对话框后,冻结root窗体,
## 就像前面提到的showinfo这个对话框的功能一样。
dialog = SimpleDialog(self.root)
self.root.wait_window(dialog.top)
rowNumber,columnNubmer = dialog.getInput()
rowNumber = int(rowNumber)
columnNubmer = int(columnNubmer)
## 实例化一个分格工具,返回一张新的分割后的图片。
imageDivider = ImageDivider()
self.newImage = imageDivider.divide(self.image,rowNumber,columnNubmer)
# 调用系统工具,查看图片
self.newImage.show()
if not os.path.exists("tmp"):
os.mkdir("tmp")
# 这儿其实可以随机生成一个文件名字,为了简单就弄个1.jgp
self.newImage.save('tmp\\1.jpg','JPEG')
自定义的获取参数对话框SimpleDialog类和分格图片的 ImageDivider 类,都定义在myUtil.py中。
先看 SimpleDialog
import tkinter as tk
import tkinter.font as tkFont
from PIL import Image
# 参考 http://effbot.org/tkinterbook/tkinter-dialog-windows.htm
# 参考 https://www.cnblogs.com/hhh5460/p/6664021.html?utm_source=itdadao&utm_medium=referral
class SimpleDialog():
def __init__(self, master):
self.master = master
self.top = tk.Toplevel(master)
self.top.title("参数输入")
#self.top.geometry('300x200')
self.top.resizable(width=False,height=False)
self.input = {}
self.input['row'] = 0
self.input['column'] = 0
self.create()
def getInput(self):
return (self.input['row'],self.input['column'])
def create(self):
self.inputFont = tkFont.Font(family='微软雅黑',size = '16')
self.inputFont2 = tkFont.Font(family='微软雅黑',size = '12')
self.label = tk.Label(self.top,text = "输入分成的行列数",font = self.inputFont)
self.labelRow = tk.Label(self.top,text = "行数",font = self.inputFont)
self.labelColumn = tk.Label(self.top,text = "列数",font = self.inputFont)
self.entryRow = tk.Entry(self.top,font = self.inputFont)
self.entryColumn = tk.Entry(self.top,font = self.inputFont)
self.buttonOk = tk.Button(self.top,text=' OK ',command = self.inputOK,font = self.inputFont2)
self.buttonCancle = tk.Button(self.top,text='Cancle',command = self.inputCancle,font = self.inputFont2)
self.label.grid( row=0,column=0,columnspan=4 )
self.labelRow.grid( row=1,column=0,columnspan=2 )
self.entryRow.grid( row=1,column=2,columnspan=2 )
self.labelColumn.grid( row=2,column=0,columnspan=2 )
self.entryColumn.grid( row=2,column=2,columnspan=2 )
self.buttonCancle.grid( row=3,column=2 ,sticky = 'NWS')
self.buttonOk.grid( row=3,column=3 ,sticky = 'NES')
def inputOK(self):
self.input['row'],self.input['column']=(self.entryRow.get(),self.entryColumn.get())
self.top.destroy()
def inputCancle(self):
self.input['row'],self.input['column']=(0,0)
self.top.destroy()
这个对话框实际上利用 toplevel 生成了一个新的窗体。窗体中还是使用了Label,Entry,Button这些组件。并向外不提供一个 self.input 字典,返回用户输入的行数和列数。
在主窗体中调用这个对话框时:
... ...
# 在HelloTkinter.py的 divideImage() 函数中:
dialog = SimpleDialog(self.root)
self.root.wait_window(dialog.top)
rowNumber,columnNubmer = dialog.getInput()
rowNumber = int(rowNumber)
columnNubmer = int(columnNubmer)
... ...
使用了 self.root.wait_window(dialog.top) 这一句,这就是调用toplevel产生窗体的方法。
再看ImageDivider
# 参考:https://www.cnblogs.com/xiaohai2003ly/p/8778618.html
class ImageDivider():
def __init__(self):
pass
def addMargin(self,image):
localImage = Image.new('RGB',image.size,(255,255,255))
MARGIN = 16
newWidth = image.size[0]+2*MARGIN
newHeight = image.size[1]+2*MARGIN
localImage = Image.new('RGB',(newWidth,newHeight),(255,255,255))
localImage.paste(image,(MARGIN,MARGIN,newWidth-MARGIN,newHeight-MARGIN))
return localImage
def divide(self,image,rowNumber,columnNumber):
row = rowNumber
column = columnNumber
# Padding 表示用2像素单位对图像进行分格。
PADDING = 8
# row,表示要将图片分成多少行,值不能小于1,不能大于图像的纵向像素数
row = int(row) if row>1 else 1
row = row if row<image.size[1] else image.size[1]
# column,图片分成多少列。
column = int(column) if column > 1 else 1
column = column if column < image.size[0] else image.size[0]
# 如果1行1列,直接给图像价格边框,返回新的图像。
if column*row == 1:
return self.addMargin(image)
# 计算新图像的宽和高
newX = image.size[0]+(column-1)*PADDING
newY = image.size[1]+(row-1)*PADDING
# 计算给一个小格子图像的宽和高
Xunit = int(round(image.size[0]/column))
Yunit = int(round(image.size[1]/row))
# 新建一张白纸,用来粘贴小格子图像
newImage = Image.new('RGB',(newX,newY),(255,255,255))
for i in range(row):
for j in range(column):
# 将一张图划分成row行,column列,
# 计算第i+1行,第j+1列小格子的左上角、右下角坐标
tempX1 = (j)*Xunit if image.size[0] > (j)*Xunit else image.size[0]
tempY1 = (i)*Yunit if image.size[1] > (i)*Yunit else image.size[1]
tempX2 = (j+1)*Xunit if image.size[0] > (j+1)*Xunit else image.size[0]
tempY2 = (i+1)*Yunit if image.size[1] > (i+1)*Yunit else image.size[1]
box=(tempX1,tempY1,tempX2,tempY2)
# 根据box确定的坐标,取出image对应区域的图像,生成一份临时图像。
tempImage = image.crop(box)
# 将临时图像贴到对应的白纸图像上,每贴一张,留下一些空白。
newImage.paste(tempImage,(j*PADDING+box[0],i*PADDING+box[1],j*PADDING+box[2],i*PADDING+box[3]))
# 给图像加个白边,返回新生成的图像
return self.addMargin(newImage)
主要可以看一下Image的这几个函数:
... ...
# 新生成一幅图
localImage = Image.new('RGB',image.size,(255,255,255))
... ...
# 粘贴一张图
localImage.paste(image,(图片的两个顶点))
... ...
# 从image上裁剪出box这两个顶点划分出来的一张图
tempImage = image.crop(box)
... ...
# 调用系统默认的图片工具,显示一张图
self.newImage.show()
... ...
# 保存一张图
self.newImage.save('tmp\\1.jpg','JPEG')
3、修复图片显示不全的bug
之前没有仔细考虑放大和缩小后图片和画布的相对位置,所以导致显示图片不全。这次重新计划一下。图片放大或者缩小后,可能超过画布的尺寸,所以需要滚动条。但是为了方便,这里我决定:
- 无论放大还是缩小操作后,都将图片中心放在画布中心;
- 放大缩小后,重新调整混动条的显示范围。
修改后的 zoomIn() 如下:
def zoomIn(self):
self.imageResizeRatio += 3
#print("此处应该重新设置图像大小,重新绘制图像")
winX = self.picCanvas.winfo_width()
winY = self.picCanvas.winfo_height()
(x,y) = self.image.size
newY = int(y*self.imageResizeRatio/100)
newX = int(x*self.imageResizeRatio/100)
self.newImage = self.image.resize((newX,newY), Image.ANTIALIAS)
self.picCanvasImage = ImageTk.PhotoImage(self.newImage)
#self.picCanvas ['scrollregion']=(0, 0, newX, newY)
# 将新生成的图像的中心,放在画布的中心,从而确定滚动条的位置:
# 需要画个图,用newX,winX表示一下,就能得到下面的关系
self.picCanvas ['scrollregion']=( int((winX-newX)/2), int((winY-newY)/2), winX-int((winX-newX)/2) , winY-int((winY-newY)/2))
self.picCanvas.create_image(winX/2,winY/2,image = self.picCanvasImage)
self.imageFlag = 1
修改内容就是:
## 注释掉原来的语句。
#self.picCanvas ['scrollregion']=(0, 0, newX, newY)
# 将新生成的图像的中心,放在画布的中心,从而确定滚动条的位置:
# 需要画个图,用newX,winX表示一下,就能得到下面的关系
self.picCanvas ['scrollregion']=( int((winX-newX)/2), int((winY-newY)/2), winX-int((winX-newX)/2) , winY-int((winY-newY)/2))
## 将图片的中心点定位在cavas中心
self.picCanvas.create_image(winX/2,winY/2,image = self.picCanvasImage)
打上标签
由于V1.2.0是一个可以用的应用程序,这里是对V1.2.0增加了新功能,修复了bug,不算大的改动,所以新标签确定为:V1.2.1。