上一篇文章介绍了我对小工具的需求,实现过程中用到的 python
库的信息以及 demo
的展示,这篇文章主要就是对其中一些实现细节的总结
代码细节
这个工程是在 python 3.6
下实现的小工具,主要使用到 tkinter
中的若干控件,以及 PIL
的图片生成和图片放缩,以及截图的功能
源码地址:https://github.com/catcheroftime/CreateWxCover
界面布局
代码很简单,一步一步按照预设的布局添加控件即可
def __createMainwinow(self):
self.root = tk.Tk()
self.root.iconphoto(True, ImageTk.PhotoImage(data=base64.b64decode(logo_ico)))
self.root.title("一键生成微信公众号封面")
self.root.geometry('960x520')
self.root.resizable(0,0)
self.control_frame = ttk.Frame(self.root)
self.control_frame.pack(anchor='w', padx=10, pady=5)
self.__createElementframe()
self.__createMoveFrame()
# 创建一个Canvas,设置其背景色为白色
self.canvas = tk.Canvas(self.root, height=self.cv_height, width=self.cv_width,highlightthickness=0, bg = 'white')
self.canvas.pack()
self.__createRuler()
选择本地图片功能
点击 打开图片
按键,弹出选择文件选择对话框,而这一部分 tkinter
有现成的 filedialog
from tkinter import filedialog
def __openLocalImage(self):
ftypes = [('png files', '*.png'), ('jpg files', '*.jpg') ]
file_path = filedialog.askopenfilename(title="选择背景图片", filetypes=ftypes )
if file_path:
self.picpath.set(file_path)
self.__clearImageInfo()
self.originimage = Image.open(file_path)
self.__canvasShowImage()
ftypes
是一个简单的文件筛选的过滤器- 当用户选择图片之后,将地址显示在
图片地址
之后的文本框中 - 删除旧的图片
- 将原始图片的信息保存到
self.originimage
, 因为之后可能需要对图片放大缩小,保存一份原始图片信息 - 最后按照我们之前定义的规则绘制图片即可
简单看一下绘制图片 __canvasShowImage()
的函数
def __canvasShowImage(self):
ratio = self.originimage.width/self.originimage.height
if ratio > 1:
self.resizeimage = self.originimage.resize((self.curImageSize, int(self.curImageSize/ratio)),Image.NEAREST)
else :
self.resizeimage = self.originimage.resize((int(self.curImageSize*ratio), self.curImageSize),Image.NEAREST)
self.tk_image = ImageTk.PhotoImage(self.resizeimage)
self.canvas_imgIndex = self.canvas.create_image(self.curImageCenterPos[0]-self.resizeimage.width/2,
self.curImageCenterPos[1]-self.resizeimage.height/2,
anchor ='nw',
image=self.tk_image)
self.__FontChange()
self.__createRuler()
上一篇文章提过我希望文字和图片尽量居中,对一张图片我需要在保证原比例的情况下,进行一定放缩,让其居中,并且考虑到还有移动功能,使用 self.curImageCenterPos
来保存图片当前的位置,初始值为 self.curImageCenterPos = (self.cv_width/2,self.cv_height/2)
哦,对了,引入 PIL
下 ImageTk
对象主要是因为 self.canvas
自带的 PhotoImage
和 BitmapImage
支持的图片样式太少, 并且刚好不支持 png
和 jpg
类型
class PhotoImage(Image):
"""Widget which can display colored images in GIF, PPM/PGM format."""
...
class BitmapImage(Image):
"""Widget which can display a bitmap."""
...
最后我还调用了 __FontChange()
和 __createRuler()
, 是因为我画布上只有3个元素 图片
,文字
,标尺
并且我定义的 图片
,文字
,标尺
的关系是 : 标尺
> 文字
> 图片
, 也就是说,标尺
在 文字
之上,文字
在 图片
之上
- 当
文字
改动的时候,为了防止文字
遮盖了标尺
, 需要在绘制完文字
之后,再绘制标尺
- 当
图片
改动的时候,为了防止图片
遮盖了文字
和标尺
,需要在绘制完图片
之后,再绘制文字
和标尺
输入框控件
这里的输入框主要有2个,一个是 文本输入框
,另一个是 图片地址展示输入框
,以 文本输入
为例 :
self.var_text = tk.StringVar()
self.var_text.trace_add("write", self.__FontChange)
ttk.Entry(self.element_frame, textvariable=self.var_text, width = 30).grid(row=1,column=1)
StringVar
是 tkinter
中获取控件值的一个好方法,它很单纯的将控件和控件的值信息隔离开,并且 StringVar
也提供了绑定回调函数等功能
作为文本输入框,当文字改变的时候,需要实时更新画面中的文字信息,所以需要给 self.var_text
绑定一个回调函数 self.var_text.trace_add("write", self.__FontChange)
简单点解释就是 指当文本输入框的值被被写入write
的时候,也就是有改动时候会调用我们定义的 self.__FontChange
函数,而关于 __FontChange
函数可以简单看一下
def __FontChange(self, *args):
# 删除旧的字体
if self.canvas_textIndex :
self.canvas.delete(self.canvas_textIndex)
if self.var_text.get():
f = font.Font( family = self.var_fontfamily.get(),
size = self.var_fontsize.get(),
weight = "bold" if self.fontweight_status else "normal",
slant = "italic" if self.fontslant_status else "roman",
underline = 1 if self.fontunderline_status else 0,
overstrike = 1 if self.fontoverstrike_status else 0 )
self.canvas_textIndex = self.canvas.create_text(self.curTextPos[0],
self.curTextPos[1],
anchor="center",
text=self.var_text.get(),
font=f,
fill = self.color_button["background"])
self.__createRuler()
在 self.canvas
写文字的时候
- 需要先将原本的文字删除掉,
self.canvas_textIndex
是之前文字在画布上的索引 - 然后获取文本输入框中的文字信息,结合其他文字属性的信息,生成新的
font.Font()
对象,其中包括文字的字体
,大小
,粗细
,斜体
,下划线
,删除线
这些设置 - 最后在画布上绘制
self.canvas.create_text()
- 为了防止
文字
将标尺
遮盖了,重新绘制一遍标尺self.__createRuler()
下拉菜单控件
下拉菜单有,字体样式的下拉菜单、字体大小的下拉菜单以及移动对象选择的下拉菜单,我这里以字体样式的下拉菜单为例,介绍一下 tkinter
下 combobox
的使用方法
def __createFontFamilyCombobox(self):
self.var_fontfamily = tk.StringVar()
self.var_fontfamily.trace_add("write", self.__FontChange)
# 创建字体的下拉菜单
fontfamily_combobox = ttk.Combobox(self.element_frame, textvariable=self.var_fontfamily)
fontfamily_combobox['state'] = "readonly"
# 获取当前系统中所有的字体,并筛选出首字母是中文的字体
list_families = []
for i in font.families():
if '\u4e00' <= i[0] <= '\u9fff':
list_families.append(i)
self.families = tuple(list_families)
fontfamily_combobox['value'] = self.families
# 默认选用 微软雅黑字体
try:
currentindex = self.families.index("微软雅黑")
except:
currentindex = 0
fontfamily_combobox.current(currentindex)
return fontfamily_combobox
字体样式下拉菜单 fontfamily_combobox
:
- 绑定一个
self.var_fontfamily
对象,并且当值改变的时候调用self.__FontChange
fontfamily_combobox
设置为只读状态,防止用户输入导致样式被改动- 通过
font.families()
获取当前系统所有字体样式
系统的样式太多是一方面原因,另一方面原因是我大部分都是中文,而且恰好字体名称此时一般也是中文,所以我筛选出中文的字体名称 - 最后下拉菜单默认值设置为
微软雅黑
颜色选择器
颜色选择器,tkinter
也提供了超简单的对象 colorchooser
,仅仅需要一行代码 colorchooser.askcolor()
, 简单看一下我摘录出的部分代码
from tkinter import colorchooser
...
self.color_button = tk.Button(self.font_frame, text ="", width = 3, background="#3C70C6",command = self.__ColorChange)
...
def __ColorChange(self):
result = colorchooser.askcolor()
if result[1]:
self.color_button["background"] = result[1]
self.__FontChange()
我定义了一个按键 self.color_button
, 这个按键的背景色就是当前文字的颜色,并且点击的时候调用 self.__ColorChange()
函数
- 打开颜色选择器,判断用户选择结果
- 结果不为空的时候,将得到的颜色结果(
result
的结构((128.5, 0.0, 255.99609375), '#8000ff')
),也就是result[1]
赋给self.color_button
的背景色background
属性 - 最后重新绘制一下画布上的文字即可
按键样式
我最初的本意是实现 windows
下自带的 画图
中对文本的编辑功能的样式,最后功能实现了,但是在样式上还是有点不太如意
先看一下我的实现,我也还是摘录出的部分代码
def __changeFontweightStyle(self):
if not self.fontweight_status:
self.fontweight_checkbutton['style'] = 'check_bold.TButton'
else:
self.fontweight_checkbutton['style'] = 'bold.TButton'
self.fontweight_status = bool(1-self.fontweight_status)
self.__FontChange()
# 粗体
def __createFontBoldCheckButton(self):
self.fontweight_status = False
ttkstyle = ttk.Style()
ttkstyle.configure('bold.TButton', font=('宋体', 8, 'bold'))
ttkstyle.configure('check_bold.TButton', font=('宋体', 8, 'bold'),background ="#0078D7")
self.fontweight_checkbutton = ttk.Button(self.font_frame,
text ="B",
style='bold.TButton',
width=2,
takefocus=False,
command = self.__changeFontweightStyle)
return self.fontweight_checkbutton
我就是单纯使用 ttk.Button
按键来实现的,通过点击改变不同的样式,来实现选中和未选中的样式
takefocus
设置成False
是不想按键在焦点的情况下出现虚线的外框style
就是设置按键的样式
最后效果不太理想的原因主要就是 ttk.Style()
是可以设置整体控件的风格,主要是以下几种
('winnative', 'clam', 'alt', 'default', 'classic', 'vista', 'xpnative')
而我使用风格 vista
虽然按键更好看,但是背景色这个属性 background
显示的就是不太明显,当我在设置其他风格的时候,就比较明显了,但是不好看
感觉还是我对 ttk.Style
的用法存在问题
保存图片
保存图片我使用的是比较原始的办法,直接截图,截取画布区域保存成图片
def __saveImage(self):
self.__clearRuler()
self.canvas.update()
# 获取初始位置
x0 = self.root.winfo_rootx() + self.canvas.winfo_x()
y0 = self.root.winfo_rooty() + self.canvas.winfo_y()
# 获取结束位置
x1 = x0 + self.canvas.winfo_width()
y1 = y0 + self.canvas.winfo_height()
size = (x0, y0, x1, y1)
pic = ImageGrab.grab(size)
pic.save("./cover.png")
self.__createRuler()
因为保存图片的时候不能将 标尺
也保存下来,所有先清空 标尺
,然后找出画布 左上角,右下角 的2个点,截图,保存图片
其实使用截图会存在一个问题,因为是通过截取桌面指定区域来实现的保存图片,所以当 canvas
不能完整在桌面上显示的时候,截图的结果其实就错误了,如下图
这也是我在上一篇提到的选择 940:400
大小画布的原因
现在电脑的屏幕分辨率最少都是
1366*768
, 在2.35:1
的前提下我们将画布大小固定在940:400
,这样封面大小感觉在电脑上呈现时应该还不错
我需要保证最后画布可以完整的展示在桌面上
当然最后保存图片也有其它的实现方式
- 比如通过
PIL
生成底片,然后对应将导入的图片和写入的文字直接写到底片中,用户对应的操作,也对应在该底片上操作,最后保存图片也可以,但是我觉得还是比较麻烦; - 或许
canvas
提供类似于获取画布上所有信息的简单接口,直接保存图片也可以,但是在看canvas
接口的时候没有太仔细去找
所有选择 截图
这种比较容易讨巧,但是可能会有风险的方案了
打包细节
因为最后我只想打包出一个 exe
的文件,那么我使用到图片等资源文件也需要想办法打包到 exe
中,所有第一时间想到的办法是,将图片通过 base64
编码成二进制信息,并且以文件名作为二进制字符串的对象名,保存到一个 python 文件中
所以先简单实现一个转换的脚本 pictobase64.py
import base64
import glob
import os
# 使用 pyinstaller 时将资源文件直接导入到 exe 时执行的脚本
# 执行此文件,生成 `resource.py` 文件
# 需要导入到 EXE 的图片地址
import_pictures = glob.glob(r'./resource/*')
# 导出途径
target_path = r'./resource.py'
# 删除旧的文件
if os.path.exists(target_path):
os.remove(target_path)
# 以追加的形式将通过 base64 转换后图片信息存储导出的文件中
with open(target_path,"a+") as f:
for file_path in import_pictures:
# 以 文件名称_文件类型 作为二进制信息的对象名
file_name = (os.path.split(file_path)[-1]).replace('.', '_')
with open(file_path,"rb") as pic:
b64str = base64.b64encode(pic.read())
write_data = f'{file_name} = {b64str}'
f.write(write_data)
f.write('\n')
我们将 ./resource/
路径下的所有的文件通过 base64
编码成二进制信息后存入 resource.py
使用起来也还算简单,通过执行 pictobase64.py
生成 resource.py
文件,导入 from resource import *
写一段简单的测试代码
import tkinter as tk
from PIL import ImageGrab, ImageTk, Image
from resource import *
import base64
root = tk.Tk()
root.iconphoto(True, ImageTk.PhotoImage(data=base64.b64decode(logo_ico)))
root.title("一键生成微信公众号封面")
root.geometry('960x520')
root.resizable(0,0)
root.mainloop()
总结
主要总结了一些我觉得还挺值得记录的细节,可能介绍的有点乱,感兴趣的可以自己看一下源码
完整的源码地址:https://github.com/catcheroftime/CreateWxCover