尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

Python实战:用Tkinter和Pillow制作动态贺卡生成器

Python实战:用Tkinter和Pillow制作动态贺卡生成器 简介面向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).py、contents、requirements.txt、bgimages、icon、fonts看起来不像教程里那种单文件脚本。其实这正是这个实例值得看的原因——它教的是“怎么组织一个会长期改需求的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.pycontents预先准备好祝福语列表供用户选择或随机抽取读取时注意编码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我一般会在文件里写清楚核心依赖和最小版本例如Pillow8.0.0 pygame2.0.0如果你只需要静态图片合成pygame可以不装但贺卡实例原文里提到声音和动画所以pygame是一个合理选择。注意这里没有列tkinter因为它是Python标准库的一部分不需要单独安装只要有Python环境就有。这一点经常被问“为什么requirements里没有Tkinter”需要在博客里点一下。另外要强调不要把freeze出来的所有包都丢进这个文件。比如numpy如果是 Pillow 的间接依赖就不必写写进来反而会让安装过程变慢还会因为版本冲突导致别人装不上。3. 用Tkinter把界面搭出来画布、输入框和按钮事件这个项目选择 Tkinter 是有道理的不需要额外装包Python 自带写个窗口只要几十行而且它的事件模型简单对“点按钮生成贺卡”这种单线交互来说足够用。选型时不用纠结“为什么不用 PyQt”在这个体量下 Tkinter 的维护成本最低也最容易让初学者看懂。3.1 核心控件拆解贺卡界面至少要包含四个部分背景图展示区、祝福语输入区、生成按钮、状态提示。对应到 Tkinter 控件可以这样选需求推荐控件替换方案展示背景图CanvasLabelimage但缩放和重绘麻烦输入祝福语TextEntry只能单行适合写短句触发操作Button绑定Return事件到输入框反馈提示Label或messagebox用Label更轻量不弹窗这里我倾向于把背景图放在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(textf已收到祝福语{message[:20]}...) # 创建主窗口 root tk.Tk() root.title(Python 贺卡生成器) root.geometry(800x600) root.resizable(False, False) # 画布用于铺背景图和后续做动画 canvas tk.Canvas(root, width800, height480, bgwhite) 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, imagebg_photo, anchornw) # 输入区域 text_editor tk.Text(root, height4, font(Microsoft YaHei, 12)) text_editor.pack(padx10, pady10, filltk.X) # 按钮和状态栏 btn_generate ttk.Button(root, text生成贺卡, commandcreate_card) btn_generate.pack(pady5) status_label tk.Label(root, text等待输入, fggray) status_label.pack() root.mainloop()这段代码要说明几个容易踩坑的点。create_image里anchornw表示图片左上角对齐画布左上角不设这个值默认以坐标点为中心背景图会偏移。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), 一份祝福, fonttitle_font, fill(255, 255, 255, 255)) # 祝福语区域 lines wrap_text(message, content_font, max_widthbg.width - 120) y 160 for line in lines: draw.text((60, y), line, fontcontent_font, fill(255, 255, 255, 255)) y 40 # 用 alpha_composite 合成透明度边界更平滑 result Image.alpha_composite(bg, overlay) result.convert(RGB).save(output_path, quality95) return output_path这里解释几个设计选择。convert(RGBA)是必须的如果背景图本身是 JPEG 没有 alpha 通道直接创建 RGBA 遮罩粘贴会报错。font.getlength()返回的是像素宽度用它来判断是否换行比用字符数相加准确得多因为中文标点和英文字母宽度不一致。alpha_composite比paste更适合半透明效果前者会正确计算 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(textf贺卡已保存到 {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, fillpink, 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 的mixerimport pygame def play_music(music_path: str): 初始化混音器并循环播放背景音乐。 pygame.mixer.init(frequency44100, size-16, channels2) 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 位有符号采样channels2表示立体声。如果你播放 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(statetk.DISABLED) root.update_idletasks() # 执行 draw_card 保存图片 btn_generate.config(statetk.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这段代码只影响 WindowsmacOS 和 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。打包后资源目录不会自动打进去。常见做法是把bgimages、fonts、contents放在可执行文件旁边代码里用上一章讲到的Path(__file__).parent去定位。如果希望全部打进单文件需要用 PyInstaller 的--add-data参数但那样读取路径会变成临时目录反而更绕。错误现象原因排查方法打开程序闪退缺依赖或-w隐藏了报错用-F不带-w运行一次看控制台输出点击按钮提示 no module named PILPyInstaller 没收集到 Pillow 相关包检查 hidden imports 或手动在源码里from PIL import ImageTk生成图片没有文字字体路径不对在字体加载前打印font_path确认路径存在完成打包后把.exe和素材目录一起发给别人这就是一份可以脱离 Python 环境运行的贺卡程序。整个过程中你会发现真正的工作量不在写代码而在处理不同环境的差异。这个实例提供的是一个很好的起点后面改造成动态背景、输入个性化署名、甚至接入图像识别都可以在这个骨架上继续长出来。本文还有配套的精品资源点击获取
返回列表