Python实战:用Tkinter和Pillow制作动态贺卡生成器
2026/9/16 15:13:52 网站建设 项目流程

简介:面向Python初学者与编程爱好者的实例源码包,围绕“电子贺卡”主题,演示如何用Tkinter库搭建图形界面、结合Pillow处理图片、加载字体与背景素材,并融入事件处理、文件读写和动态效果实现,帮助读者综合运用基础语法完成一个趣味小项目。资源采用7z压缩格式,共9个文件,包含1个核心Python脚本、5张背景图、1个图标、1个字体文件及依赖清单,整体大小5.41MB,目录简洁清晰,便于对照学习。目前已有67人学习下载。通过该实例,不仅可以学习GUI控件布局、图像整合、动画触发等实用技巧,还能理解项目文件的命名与组织方式;同时,从需求分析到代码实现再到素材打包,提供了完整的参考路径。非常适合作为Python入门后的完整练习项目,在实践中巩固变量、循环、条件判断等知识点,体会编程与日常创意结合的魅力。

1. 为什么用Python写贺卡,而不是PPT

逢年过节送贺卡,用Word或PPT也能做,但那是“排版思维”,不是“程序思维”。这个编号为17的贺卡实例,是一套用Python源码实现的小型桌面程序:启动后弹出一个Tkinter窗口,背景图先展示出来,你在输入框里写下祝福语,点按钮就能把文字合成到图片上,甚至还能加动画和背景音乐。对刚学完Python基础语法、想找Python实战项目练手的人来说,它的信息量恰到好处——覆盖了GUI编程、事件处理、Pillow图像处理、文件路径管理这些日常工作里高频出现的东西,但你不需要先啃完一本Qt教程才能跑起来。一个值得注意的点是,它把“程序”和“素材”拆得很清楚:图片、字体、文案都放在独立目录里,主脚本只管逻辑。这种结构在Python入门教程里很少被强调,但却是真实项目里最先决定维护成本的部分。

2. 拆解实例-17贺卡的资源结构与加载方式

拿到压缩包,解压后第一眼会有点懵:generateCard(1).pycontentsrequirements.txtbgimagesiconfonts,看起来不像教程里那种单文件脚本。其实这正是这个实例值得看的原因——它教的是“怎么组织一个会长期改需求的Python小程序”,而不是一次性跑完就丢的玩具。

2.1 文件分工:代码、素材、依赖各归各位

先看一个实际的目录展开,这是我根据自己的项目习惯整理后的等效结构:

实例-17 贺卡/ ├── generateCard(1).py # 主程序 ├── requirements.txt # 第三方依赖说明 ├── contents/ # 祝福语文案,txt 或配置文件 ├── bgimages/ # 背景图,1.png ~ 5.png ├── icon/ # 窗口图标 └── fonts/ # 中文字体,如 font.TTF

各个资源的用途可以列成一张表,方便对照源码时快速定位:

资源作用常见问题
generateCard(1).py主逻辑:界面、合成、保存文件名里带着(1),是浏览器重复下载自动加的,正式归档建议改成generate_card.py
contents预先准备好祝福语列表,供用户选择或随机抽取读取时注意编码,Windows 下常用utf-8,偶尔遇到gbk,要按异常情况切换
bgimages不同风格的背景底图,jpg/png 都行png 带透明通道,合成时要注意模式转换
fonts字体文件,解决中文字体缺失问题不指定字体直接画中文,Pillow 会画成方框
requirements.txt记录依赖包用 pip freeze 生成的往往版本过严,建议手动整理

这种拆分不是花架子。如果所有图片、字体都和脚本放在同一个目录里,改一张背景图就要翻代码;把bgimages单独拿出来后,用户可以直接替换图片,程序一行都不用改。

2.2 动态定位资源路径,别再写死绝对路径

在源码里,最容易被初学者抄错的地方是图片加载路径。很多人初学时会写成:

bg = Image.open("D:/my_project/bgimages/1.png")

这在你自己电脑上能跑,但只要目录移动,或者发给别人,立刻报FileNotFoundError。这个实例既然把素材和代码分开了,就应该用相对基准路径来加载。常见做法是用pathlib定位脚本所在目录:

from pathlib import Path BASE_DIR = Path(__file__).resolve().parent BG_DIR = BASE_DIR / "bgimages" FONT_DIR = BASE_DIR / "fonts" def load_background(image_name: str): """ 根据文件名拼接完整路径返回图片对象。 使用 resolve() 处理符号链接,避免在打包运行时路径跑偏。 """ img_path = BG_DIR / image_name if not img_path.exists(): raise FileNotFoundError(f"背景图不存在: {img_path}") from PIL import Image return Image.open(img_path) bg = load_background("1.png")

这里有两个关键点。Path(__file__).resolve().parent拿到的是当前脚本的绝对路径,不依赖“运行目录”是哪里,这一点在命令行、IDE、双击运行时行为都一致。另一个是exists()检查,虽然不如 try-except 细,但至少让错误信息一眼能看到是哪个图片缺失,而不是让你去猜。

2.3 requirements.txt:环境复现的第一道关卡

通常项目描述里会给一个requirements.txt,这个文件在源码分发时非常重要。你不需要让别人手动pip install pillow一个个装,直接让他执行:

pip install -r requirements.txt

我一般会在文件里写清楚核心依赖和最小版本,例如:

Pillow>=8.0.0 pygame>=2.0.0

如果你只需要静态图片合成,pygame可以不装;但贺卡实例原文里提到声音和动画,所以pygame是一个合理选择。注意这里没有列tkinter,因为它是Python标准库的一部分,不需要单独安装,只要有Python环境就有。这一点经常被问“为什么requirements里没有Tkinter”,需要在博客里点一下。

另外要强调:不要把freeze出来的所有包都丢进这个文件。比如numpy如果是 Pillow 的间接依赖,就不必写;写进来反而会让安装过程变慢,还会因为版本冲突导致别人装不上。

3. 用Tkinter把界面搭出来:画布、输入框和按钮事件

这个项目选择 Tkinter 是有道理的:不需要额外装包,Python 自带,写个窗口只要几十行;而且它的事件模型简单,对“点按钮生成贺卡”这种单线交互来说足够用。选型时不用纠结“为什么不用 PyQt”,在这个体量下 Tkinter 的维护成本最低,也最容易让初学者看懂。

3.1 核心控件拆解

贺卡界面至少要包含四个部分:背景图展示区、祝福语输入区、生成按钮、状态提示。对应到 Tkinter 控件,可以这样选:

需求推荐控件替换方案
展示背景图CanvasLabel+image,但缩放和重绘麻烦
输入祝福语TextEntry只能单行,适合写短句
触发操作Button绑定<Return>事件到输入框
反馈提示LabelmessageboxLabel更轻量,不弹窗

这里我倾向于把背景图放在Canvas上,因为后面做动画效果时需要canvas.move()移动元素,如果一开始用Label后面还得重构。

3.2 完整主界面代码骨架

下面的代码是从实例思路里提炼出来的简化版,保留了界面搭建和事件处理的完整链路。注意,这段代码只负责界面,图片合成逻辑会在下一章接入:

import tkinter as tk from tkinter import ttk from PIL import Image, ImageTk from pathlib import Path BASE_DIR = Path(__file__).resolve().parent BG_DIR = BASE_DIR / "bgimages" def create_card(): """按钮回调:从输入框拿文本,交给合成函数(下一章实现)。""" message = text_editor.get("1.0", tk.END).strip() if not message: status_label.config(text="请先输入祝福语") return # 将 message 传给生成函数,这里先占位 status_label.config(text=f"已收到祝福语:{message[:20]}...") # 创建主窗口 root = tk.Tk() root.title("Python 贺卡生成器") root.geometry("800x600") root.resizable(False, False) # 画布:用于铺背景图和后续做动画 canvas = tk.Canvas(root, width=800, height=480, bg="white") canvas.pack() # 将 PIL 图片转成 Tkinter PhotoImage bg_img = Image.open(BG_DIR / "1.png") bg_img = bg_img.resize((800, 480)) bg_photo = ImageTk.PhotoImage(bg_img) canvas.create_image(0, 0, image=bg_photo, anchor="nw") # 输入区域 text_editor = tk.Text(root, height=4, font=("Microsoft YaHei", 12)) text_editor.pack(padx=10, pady=10, fill=tk.X) # 按钮和状态栏 btn_generate = ttk.Button(root, text="生成贺卡", command=create_card) btn_generate.pack(pady=5) status_label = tk.Label(root, text="等待输入", fg="gray") status_label.pack() root.mainloop()

这段代码要说明几个容易踩坑的点。create_imageanchor="nw"表示图片左上角对齐画布左上角,不设这个值,默认以坐标点为中心,背景图会偏移。ImageTk.PhotoImage赋值给局部变量bg_photo后,会被 Python 垃圾回收掉,导致显示空白——所以这里保存在模块级变量里,避免被回收。这是 Tkinter 图片显示最常见的坑,比逻辑写错的概率高得多。

按钮回调里用text_editor.get("1.0", tk.END)而不是get(),因为Text多行文本框需要指定起始索引和结束索引。"1.0"表示第 1 行第 0 个字符,tk.END表示末尾。如果这里误用Entry控件的写法,会直接抛出AttributeError

3.3 用 bind 支持回车键生成

除了点击按钮,还应该允许用户按 Ctrl+回车直接生成。这在做演示时很实用,不用一直挪鼠标。做法是在输入框上绑事件:

def on_ctrl_return(event): create_card() text_editor.bind("<Control-Return>", on_ctrl_return)

<Control-Return>是 Tkinter 里的事件模式,表示同时按下 Ctrl 和回车。回调函数接收 event 对象,这里用不到可以忽略。如果你希望单按回车就生成,把模式改成<Return>即可,但单行输入可能在回车时换行,需要先用bind_class或换用Entry。实战里我建议用 Ctrl+回车,避免误触。

4. 用Pillow合成祝福语:中文字体、换行与半透明遮罩

Tkinter 界面负责“交互”,但最终给别人的贺卡应该是直接可查看的图片或 PDF。这时候 Pillow 才是主角。它可以读取背景图、在图上写字、调整透明度,最终保存成一张新的 PNG。如果你的需求只是“打印或发微信”,Pillow 输出的图片已经够了。

4.1 关键对象与流程

Pillow 图像合成的核心流程是:打开背景 -> 创建临时图层 -> 画文字 -> 合成 -> 保存。这里最容易被忽略的是颜色空间和字体问题。

方法作用注意点
Image.open()打开图片文件不会立即加载全部数据,需要.load()或使用前确保复用
ImageDraw.Draw()创建绘图对象同时支持text()line()等基础图形
ImageDraw.text()在图片上写文字必须指定font,否则默认字体大多不支持中文
ImageTk.PhotoImage()转为 Tkinter 可显示格式需要与Image.open区分
paste()将图层粘贴到底图上通过mask参数控制透明度

4.2 支持中文换行的合成函数

下面这段代码可以放到generateCard(1).py里,作为第3章create_card的真实后端。它做了三件事:加载指定字体、按最大宽度换行、把祝福语绘制到半透明遮罩上:

from PIL import Image, ImageDraw, ImageFont def wrap_text(text: str, font: ImageFont.FreeTypeFont, max_width: int) -> list: """按像素宽度自动换行,避免最后保存的贺卡文字挤出图片外。""" lines = [] for paragraph in text.split("\n"): if not paragraph: lines.append("") continue current = "" for char in paragraph: test_line = current + char w = font.getlength(test_line) if w <= max_width: current = test_line else: lines.append(current) current = char lines.append(current) return lines def draw_card(background_path: str, message: str, output_path: str): bg = Image.open(background_path).convert("RGBA") # 半透明遮罩,保证深色背景上文字也清晰 overlay = Image.new("RGBA", bg.size, (0, 0, 0, 0)) draw = ImageDraw.Draw(overlay) # 加载实例 fonts 目录下的字体,字号可以按图尺寸动态调整 font_path = str(BASE_DIR / "fonts" / "font.TTF") title_font = ImageFont.truetype(font_path, 36) content_font = ImageFont.truetype(font_path, 28) # 标题 draw.text((60, 80), "一份祝福", font=title_font, fill=(255, 255, 255, 255)) # 祝福语区域 lines = wrap_text(message, content_font, max_width=bg.width - 120) y = 160 for line in lines: draw.text((60, y), line, font=content_font, fill=(255, 255, 255, 255)) y += 40 # 用 alpha_composite 合成,透明度边界更平滑 result = Image.alpha_composite(bg, overlay) result.convert("RGB").save(output_path, quality=95) return output_path

这里解释几个设计选择。convert("RGBA")是必须的,如果背景图本身是 JPEG 没有 alpha 通道,直接创建 RGBA 遮罩粘贴会报错。font.getlength()返回的是像素宽度,用它来判断是否换行,比用字符数相加准确得多,因为中文标点和英文字母宽度不一致。alpha_compositepaste更适合半透明效果,前者会正确计算 RGB 与 alpha 的混合,后者在叠加半透明图层时容易出现边缘色块。

字体的fill参数是 RGBA 元组,最后一维是透明度。如果只写(255, 255, 255),Pillow 会忽略 alpha,导致文字没有质感。另外,ImageFont.truetype需要字体文件的绝对路径,如果你不提供fonts/font.TTF,要先从系统目录拷贝一份中文字体,比如 Windows 下可从C:/Windows/Fonts/msyh.ttc复制过来,否则合成图片上全是乱码方框。

4.3 调用合成函数的界面回调

把前面第3章的create_card改成真实行为:

def create_card(): message = text_editor.get("1.0", tk.END).strip() if not message: status_label.config(text="请先输入祝福语") return out_path = BASE_DIR / "output.png" draw_card( str(BG_DIR / "2.png"), message, str(out_path) ) status_label.config(text=f"贺卡已保存到 {out_path}")

这里选取2.png硬编码在函数里,实际开发时可以做一个下拉框,让用户从bgimages目录里动态选择。注意output_path一定要写str(),因为 Pillow 虽然支持Path对象,但某些扩展插件只认字符串路径,传Path会报 TypeError。

5. 用after与pygame让贺卡动起来:动画循环和声音播放

如果只生成一张静态图片,那还只是个“Python脚本作品”,算不上“贺卡程序”。实例描述里明确提到动态效果和声音处理,这一章讲怎么样在控制复杂度的情况下加上这两个功能。核心逻辑并不复杂:动画依赖 Tkinter 的after()定时回调,背景音乐依赖 pygame 混音器。

5.1 用 after 实现飘落花瓣动画

Tkinter 的mainloop是一个无限循环,你不能在上面再写while True,否则界面卡死。正确的做法是注册一个定时任务,让它每隔多少毫秒执行一次更新函数。

def create_snowflake(): """在画布上随机生成一个花瓣对象,并让它往下飘落。""" import random x = random.randint(20, 780) y = -10 item = canvas.create_oval(x, y, x + 15, y + 15, fill="pink", outline="") particles.append(item) def animate(): """每 50ms 移动一次所有花瓣,越界后移除。""" for item in particles[:]: canvas.move(item, 0, 3) # 检查是否超出画布下界 x1, y1, x2, y2 = canvas.coords(item) if y1 > 480: canvas.delete(item) particles.remove(item) root.after(50, animate) particles = [] # 在主窗口创建后启动动画 root.after(100, create_snowflake) root.after(200, create_snowflake) root.after(50, animate)

root.after(50, animate)表示 50 毫秒后再次调用animate,这就形成了一个循环。注意不要在animate里无限递归,实际上每一次回调结束就释放了栈帧,所以不会栈溢出。canvas.coords(item)返回当前所有坐标点,对于圆来说就是 (x1,y1,x2,y2)。用y1判断是否越过画布高度,比较稳妥。

如果希望花瓣有不同颜色和大小,可以加上随机数生成参数。但要注意,canvas.create_oval绘制的是椭圆形,数量控制在几十个以内性能没问题,超过一百个就要考虑删除不可见的元素,否则越跑越卡。

5.2 pygame播放背景音乐的正确打开方式

声音处理可以选playsound库,但它在某些 Linux 无桌面环境会直接报错,且不能循环播放。更通用的做法是 pygame 的mixer

import pygame def play_music(music_path: str): """初始化混音器并循环播放背景音乐。""" pygame.mixer.init(frequency=44100, size=-16, channels=2) try: pygame.mixer.music.load(music_path) pygame.mixer.music.play(-1) # -1 表示无限循环 except pygame.error as e: print(f"音乐加载失败: {e}") # 在进入 mainloop 之前调用 # play_music(str(BASE_DIR / "contents" / "bgm.mp3"))

pygame.mixer.init的参数size=-16表示 16 位有符号采样,channels=2表示立体声。如果你播放 mp3 卡顿,可以先转成 wav 再试。重点在于,pygame.mixer.music是独立于 Tkinter 主循环的音频通道,播放不会阻塞界面。

方案优点缺点
playsound调用简单不支持循环,异常处理不透明
pygame.mixer循环、音量控制、多格式安装包较大,初始化参数需要配好
tkinter+ winsound(仅windows)不需要额外依赖只有 Windows 可用,格式受限

实际项目里,如果不需要复杂音效,我通常不准备音频文件,因为外部素材动不动几 MB,而且会拖慢启动。可以做一个开关:有 mp3 就播放,没有就静音,避免强制依赖。

5.3 动画与合成模块的时序协调

一个易犯的错误是:在动画运行时去调用create_card保存图片,会导致 Tkinter 界面短暂无响应。因为 Pillow 的save是同步操作,图片尺寸大时可能要几百毫秒。对这个实例来说,可以先在回调里禁用生成按钮,等合成完成后再恢复,同时用root.update_idletasks()强制刷新界面:

btn_generate.config(state=tk.DISABLED) root.update_idletasks() # 执行 draw_card 保存图片 btn_generate.config(state=tk.NORMAL)

这样用户在生成图片时不会反复点击,也不会看到“未响应”的白屏。如果你要处理更耗时的任务,应该放到多线程或异步协程里,但那个复杂度对这个项目就超纲了。

6. 字体缺失、DPI缩放与PyInstaller打包:贺卡上线的最后一公里

写到这里,大部分功能已经跑通,但真正把这份源码变成“能发给别人”的程序,还有三个问题要处理:中文字体路径、系统缩放对窗口的影响、打包成可执行文件。这几个问题不解决,贺卡很容易在自己电脑上正常、在同事电脑上乱码或错位。

6.1 字体缺失和中文方框

如果你的fonts目录里没有字体,或者别人把字体删了,Pillow 绘制中文就会变成方框。一个检测方式是运行时检查字体文件是否存在:

if not (FONT_DIR / "font.TTF").exists(): raise RuntimeError("缺少字体文件,请将字体放入 fonts 目录")

在 Windows 上,也可以临时回退到系统字体;Linux 上则需要确保安装了中文字体,比如fonts-wqy-zenhei。判断字体是否支持中文,最直接的方法是打开字体后用它的getbbox("中"),如果宽度异常,说明这个字体没有覆盖 CJK 字符:

font = ImageFont.truetype(str(font_path), 28) bbox = font.getbbox("中") if bbox[2] - bbox[0] == 0: print("警告:当前字体不支持中文")

6.2 DPI 缩放导致窗口变形

现在很多笔记本电脑默认缩放是 125% 或 150%。Tkinter 窗口在高 DPI 下会变得模糊或尺寸不对。在generateCard(1).py开头加入一段:

import ctypes try: ctypes.windll.shcore.SetProcessDpiAwareness(1) except Exception: pass

这段代码只影响 Windows,macOS 和 Linux 下用不到。设置之后,Tkinter 会按照物理像素绘制,窗口不会因为缩放而被拉伸。如果你希望界面元素自适应不同分辨率,可以把画布尺寸写成一个配置常量,而不是散落在代码里:

WIN_WIDTH = 800 WIN_HEIGHT = 600

后续改尺寸时,只需要替换这一处。

6.3 用 PyInstaller 打包成单文件

打包是分享的最后一环,命令很简单:

pip install pyinstaller pyinstaller -F -w generateCard.py --hidden-import PIL._tkinter_finder

-F表示打包成单文件,-w表示运行时不弹出黑框控制台。--hidden-import是 Pillow 在 Tkinter 模式下会动态导入的一个模块,不加上去,打包后在部分系统上点击按钮会报ModuleNotFoundError

打包后,资源目录不会自动打进去。常见做法是把bgimagesfontscontents放在可执行文件旁边,代码里用上一章讲到的Path(__file__).parent去定位。如果希望全部打进单文件,需要用 PyInstaller 的--add-data参数,但那样读取路径会变成临时目录,反而更绕。

错误现象原因排查方法
打开程序闪退缺依赖或-w隐藏了报错-F不带-w运行一次,看控制台输出
点击按钮提示 no module named 'PIL'PyInstaller 没收集到 Pillow 相关包检查 hidden imports 或手动在源码里from PIL import ImageTk
生成图片没有文字字体路径不对在字体加载前打印font_path,确认路径存在

完成打包后,把.exe和素材目录一起发给别人,这就是一份可以脱离 Python 环境运行的贺卡程序。整个过程中,你会发现真正的工作量不在写代码,而在处理不同环境的差异。这个实例提供的是一个很好的起点,后面改造成动态背景、输入个性化署名、甚至接入图像识别,都可以在这个骨架上继续长出来。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询