微信公众号封面一键生成器-续

上一篇文章介绍了我对小工具的需求,实现过程中用到的 python 库的信息以及 demo 的展示,这篇文章主要就是对其中一些实现细节的总结

代码细节

这个工程是在 python 3.6 下实现的小工具,主要使用到 tkinter 中的若干控件,以及 PIL 的图片生成和图片放缩,以及截图的功能

源码地址:https://github.com/catcheroftime/CreateWxCover

界面布局

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-AOTTDE9u-1614868578876)(/images/blog/python/wxcover/frame.png)]

代码很简单,一步一步按照预设的布局添加控件即可

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)

哦,对了,引入 PILImageTk 对象主要是因为 self.canvas 自带的 PhotoImageBitmapImage 支持的图片样式太少, 并且刚好不支持 pngjpg 类型

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)

StringVartkinter 中获取控件值的一个好方法,它很单纯的将控件和控件的值信息隔离开,并且 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()

下拉菜单控件

下拉菜单有,字体样式的下拉菜单、字体大小的下拉菜单以及移动对象选择的下拉菜单,我这里以字体样式的下拉菜单为例,介绍一下 tkintercombobox 的使用方法

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 下自带的 画图 中对文本的编辑功能的样式,最后功能实现了,但是在样式上还是有点不太如意

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-MvLh10jm-1614868578880)(/images/blog/python/wxcover/font_demo.png)]

先看一下我的实现,我也还是摘录出的部分代码

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 显示的就是不太明显,当我在设置其他风格的时候,就比较明显了,但是不好看

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-EfLq4fJ3-1614868578881)(/images/blog/python/wxcover/altstyle.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-OIJZxIvI-1614868578882)(/images/blog/python/wxcover/clam.png)]

感觉还是我对 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

在这里插入图片描述

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

会偷懒的程序猿

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值