【自由生长的Python】为了能有资格放弃,增加菜单栏,增加新功能,修复bug【03】

目标

桌面应用增加一个菜单栏,菜单栏中增加一个新功能,修改上次发现的一个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。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值