直接说结论:这是一个零基础也能照着手敲出来的 Python 小项目——用 pygame 做一个全屏播放的回忆相册,每张照片配上专属背景音乐,按一下空格键,一颗心形烟花就在屏幕中间炸开。做完之后还能打包成 exe 发给对方,不需要对方装 Python 环境。
这是“表白贺卡进阶四”这一期:前面几期我们做过静态贺卡、动态文字、粒子爱心,这一期直接升级成“会唱歌的回忆相册”——照片、音乐、全屏、动态、烟花,全塞在一个项目里。如果你完全是 Python 零基础,只需要会装软件、会复制粘贴、能耐心看完这篇文章,就能做出来。过程中涉及到的每个概念,我都会用大白话解释,不用你先啃一本语法书。
1. 从需求到方案:这个“会唱歌的回忆相册”该怎么落地
1.1 拆解需求:四个“花哨关键词”背后其实只有三件事
很多初学者看到“资产代码”的第一反应是:这么多功能,肯定要写几百行吧?其实只要把需求拆开,这个项目比你想象的简单。
标题里提到的几个核心词,翻译成人话是:
- 全屏:照片展示时铺满屏幕,类似 PPT 放映,沉浸感足够。
- 动态音乐:照片切换时音乐跟着换,每张照片有自己的 BGM。
- 心形烟花:一个视觉特效,在画面上炸出爱心形状的粒子动画。
合在一起,这个项目只做三件事:照片展示、音乐切换、粒子特效。只要把这三块各自跑通,剩下的就是拼装配对问题。
1.2 为什么选 Python + pygame,而不是网页方案
你可能想问:为什么不用 HTML + CSS + JS 做个网页相册?网页确实能做,而且视觉效果上限更高,但这里有个关键问题:目标用户是零基础学习者,不是前端工程师。网页方案涉及浏览器 API、JS 音频权限、页面自适应,任何一个环节卡住都得折腾一下午。
Python + pygame 的优势是很直白的:
- 一个
.py文件就能跑通全流程,不用理解页面标签和语义。 - pygame 把窗口、图片、键盘事件、音频播放都封装成了简单函数,学习曲线平缓。
- 最后用 PyInstaller 打包成 exe,对方双击就能跑,不需要装任何环境。
我把两种方案放在一起对比过,真实感受写在表里:
| 方案 | 环境依赖 | 全屏与音乐控制 | 打包分发 | 适合谁 |
|---|---|---|---|---|
| Python + pygame | 装 Python 并 pip 安装 | 函数调用直接搞定 | PyInstaller 打包 | 零基础 Python 学习者 |
| Web 网页 | 浏览器打开即可 | 需要 JS 处理交互和音频权限 | 部署到服务器或做桌面壳 | 有前端基础的人 |
决定用 pygame 还有一个隐藏理由:这个项目是“表白贺卡进阶”系列的一部分,系列前面的代码都是 Python 写的,延续同一个技术栈,你的学习成本不会白白浪费。
1.3 项目结构与素材规划:先把“施工图纸”画出来
写代码之前,我强烈建议先规划项目目录,不然素材一多就乱了。我的目录长这样:
heart_album/ ├── photos/ # 放照片,支持 jpg / png │ ├── photo_01.jpg │ ├── photo_02.jpg │ └── photo_03.jpg ├── music/ # 放音乐,推荐 ogg 格式 │ ├── song_01.ogg │ ├── song_02.ogg │ └── song_03.ogg └── main.py # 主程序照片和音乐怎么配对?我建议用“序号对应”这种土办法:photo_01.jpg配song_01.ogg,photo_02.jpg配song_02.ogg。程序里按索引去拿文件就行,不用维护一长串路径对应关系。
素材准备有两个经验要说:
第一,照片不要用 4K 原图。pygame 加载大图非常吃亏,内存占得高,切换时明显卡顿。实操里我都是先把照片压缩到 1920 宽度以内,微信发出去也轻松。
第二,音乐格式优先用 OGG 或 WAV,不要直接用 MP3。pygame 对 MP3 的兼容性有些随机,同一首歌在不同机器上可能一个正常一个噪音。后面第 4 章我会专门说这个问题。
2. 0基础环境搭建:Python 安装与依赖部署
2.1 安装 Python:这一步的关键是勾选 PATH
零基础读者请老实按这个流程来:
- 打开 Python 官网下载页面,找到最新的稳定版,点击下载 Windows installer 或 macOS 安装包。
- 安装时有一个很容易忽略的选项:Add Python to PATH。一定要勾上。这决定了你在命令行里能不能直接用
python命令。 - 安装完成后,打开命令行(Windows 按
Win + R输入cmd,macOS 打开终端),输入:
python --version如果输出了类似Python 3.12.x的信息,环境就装好了。
如果提示python 不是内部或外部命令,绝大多数情况是第二步没勾选 PATH。重新运行安装包,选择 Modify,把 Add Python to PATH 勾上即可。
macOS 用户如果系统自带的是 Python 2.x 或者没有 Python,建议直接用官网安装包装 3.x,不要折腾 Homebrew 版本匹配的问题——零基础阶段越省事越好。
2.2 用 pip 安装 pygame:一行命令就够
Python 装好之后,命令行里输入:
pip install pygame等它下载完,会看到类似Successfully installed pygame-2.x.x的提示。装好之后可以顺手验证一下:
pip show pygame能看到版本信息就说明安装成功。
这一步常见的报错是pip 不是内部或外部命令。解决方式和上面一样,回退一步重新装 Python 并勾选 PATH。还有同学会遇到网络慢超时,换成国内镜像源:
pip install pygame -i https://pypi.tuna.tsinghua.edu.cn/simple这里的核心建议是:不要跳过去装一大堆库。这个项目只要 pygame 一个外部依赖就够了,其他全部用 Python 标准库(sys、os、math、random)。所以你的环境搭建压力非常小。
2.3 准备照片、音乐素材:格式和命名都有讲究
素材准备阶段,我踩过的坑值得你避免。
照片方面:直接用手机相册的照片也行,但建议先过一遍压缩。Windows 用户可以用图片编辑工具的“另存为”调整宽度到 1920,macOS 用户直接预览导出即可。照片格式 jpg 或 png 都行,pygame 对这两种支持最好。不要用 webp 之类的冷门格式,加载很容易出问题。
音乐方面:前面说了,我推荐转成 OGG 格式。用什么转?搞个 FFmpeg 最省心:
ffmpeg -i input.mp3 -acodec libvorbis output.ogg不熟悉命令行的朋友,也可以下载格式工厂、Audacity 这类图形工具,导出 ogg 格式,码率选中等就行。为什么非要折腾?因为在 pygame 项目里,ogg 的兼容性比 mp3 稳得多,尤其是把 exe 发给对方电脑之后,mp3 解码出各种诡异问题的概率明显更高。
命名上严格用photo_01.jpg、song_01.ogg这种格式。顺序编号从 01 开始,程序里读取时直接用索引映射,不用做字符串解析。
3. 核心轮播:全屏照片展示与自动翻页
3.1 初始化窗口与全屏模式:这两行代码是地基
照片展示的第一步,是把 pygame 的窗口拉起来。先别急全屏,开发阶段我建议先用普通窗口调试,等所有功能都通了再切全屏。原因后面第 6 章会详细说。
先看初始化代码:
import pygame import sys import math import random pygame.init() screen = pygame.display.set_mode((0, 0), pygame.FULLSCREEN) width, height = screen.get_size() clock = pygame.time.Clock()这里set_mode((0, 0), pygame.FULLSCREEN)就是全屏模式的关键语法。第一个参数(0, 0)表示使用当前屏幕分辨率,第二个参数pygame.FULLSCREEN表示全屏。如果开发阶段想用窗口模式,把这行替换成:
screen = pygame.display.set_mode((1000, 700))然后后面涉及width、height的代码都能继续用,get_size()拿到的就是 1000×700。
有了窗口之后,还要理解 pygame 的“主循环”机制。不夸张地说,没搞懂这个循环,pygame 就学了个寂寞。
running = True while running: clock.tick(60) for event in pygame.event.get(): if event.type == pygame.QUIT: running = False pygame.display.flip()这个while True循环每秒执行约 60 次(由tick(60)控制帧率),每次循环做三件事:处理用户输入事件、更新画面内容、把画面刷新到屏幕上。想象成做动画:每一帧都重新画一张图,连续播放就动了。
3.2 图片加载与等比缩放:关键是“不把脸拉变形”
照片显示最核心的难点,不在于“加载图片”,而在于“放得下还不变形”。
回忆一下屏幕是 1920×1080,而照片可能是 4032×3024 竖图。直接screen.blit(photo, (0, 0))会只显示照片的左上角一块,不是我们想要的效果。
正确姿势是计算等比缩放比例:
def load_scaled_image(path): img = pygame.image.load(path) img_w, img_h = img.get_size() scale = min(width / img_w, height / img_h) new_w = int(img_w * scale) new_h = int(img_h * scale) return pygame.transform.scale(img, (new_w, new_h))这里的关键是min(width / img_w, height / img_h)。意思很简单:把照片同时缩放到屏幕宽和高都放得下,取两者中更保守的那个比例。
可以这样理解:你有一块 1920×1080 的画布,照片长宽比各不相同。如果按宽缩放,竖图可能超出去;如果按高缩放,横图可能超出去。min保证两个方向都不超,结果就是照片完整出现在屏幕内,剩余空间用黑色背景填充。
使用的时候:
photo = load_scaled_image("photos/photo_01.jpg") screen.blit(photo, photo.get_rect(center=(width // 2, height // 2)))get_rect(center=...)是 pygame 特别常用的一个小技巧:它返回图片的矩形,并自动把中心点放在指定位置。这样不管照片横竖,都能稳稳居中。
3.3 翻页交互与自动播放:把照片串成一部“幻灯片”
照片串起来的方式,我建议用一个列表来管理。每个元素是一个字典,存照片路径和音乐路径:
slides = [ {"photo": "photos/photo_01.jpg", "music": "music/song_01.ogg"}, {"photo": "photos/photo_02.jpg", "music": "music/song_02.ogg"}, {"photo": "photos/photo_03.jpg", "music": "music/song_03.ogg"}, ]当前显示哪张照片,用一个索引记录:
current_index = 0 current_photo = load_scaled_image(slides[current_index]["photo"])左右翻页的键盘事件处理,写法很固定:
for event in pygame.event.get(): if event.type == pygame.KEYDOWN: if event.key == pygame.K_RIGHT: current_index = (current_index + 1) % len(slides) current_photo = load_scaled_image(slides[current_index]["photo"]) elif event.key == pygame.K_LEFT: current_index = (current_index - 1) % len(slides) current_photo = load_scaled_image(slides[current_index]["photo"])这里用了% len(slides)取模。作用是:到最后一张再按右,自动回到第一张;到第一张按左,自动跳到最后一张。不用写一堆 if 去判断边界。
自动播放本质上就是一个倒计时器。用clock.tick(60)返回的毫秒数累加:
tick += dt if tick >= 8000: tick = 0 current_index = (current_index + 1) % len(slides) current_photo = load_scaled_image(slides[current_index]["photo"])dt是上一帧到这一帧的时间差,单位是毫秒。累加到 8000 就表示 8 秒到了,自动切下一张。这里强烈建议先不要加音乐,等照片轮播跑顺了再进入下一章,因为加音乐后排查音频问题会干扰照片切换的调试。
4. 让相册真正“唱歌”:音乐播放与照片联动
4.1 pygame 音乐模块:先把这几个函数记牢
pygame 对音乐播放封装得比较人性化,核心函数就几个:
| 函数 | 作用 |
|---|---|
pygame.mixer.music.load(path) | 加载音频文件,支持 ogg、wav、mp3 |
pygame.mixer.music.play(loops=0) | 开始播放;loops=-1 表示无限循环 |
pygame.mixer.music.stop() | 停止当前播放 |
pygame.mixer.music.get_busy() | 返回是否正在播放 |
需要注意一个细节:play(-1)里的-1是循环次数,不是“第几秒开始”。初学者容易把参数含义搞混。我见过不少项目,因为写了play(1)导致音乐放完一遍就没了,整个相册后半程是“哑巴”的。
4.2 照片与音乐配对:把素材绑定到同一套索引
在 3.3 节的slides列表里,我已经给每张照片预留了music字段。这就意味着,照片和音乐的配对关系已经天然建立:当前是第 0 张照片,就播slides[0]["music"]对应的音乐;切到第 1 张,就播第 1 首。
这一步的清晰逻辑很重要。早期的做法是把照片和音乐各写一个列表,再用同样的下标去取。一旦中间删了一张照片,后面的对应关系全乱了。用字典把路径放在一起,删除或调整顺序都只动一个列表,不易出错。
4.3 切换音乐与容错:不要让一首坏歌毁掉整个相册
照片切换时,音乐的切换逻辑是:
def play_slide_music(index): pygame.mixer.music.stop() pygame.mixer.music.load(slides[index]["music"]) pygame.mixer.music.play(-1)每次切照片前先stop()当前音乐,再加载新的,再无限循环播放。顺序不能反,不然音乐还没停就 load 新文件,会有奇怪的报错。
但我还要多说一句:生产环境的代码一定要加容错。
实际使用中,你不可能保证每首音乐都格式完美。如果某首坏掉了,pygame.mixer.music.load()会直接抛异常,整个程序当场崩溃。加了容错是这样:
def play_slide_music(index): pygame.mixer.music.stop() try: pygame.mixer.music.load(slides[index]["music"]) pygame.mixer.music.play(-1) except pygame.error: print("音乐文件加载失败,跳过:", slides[index]["music"])我亲测过这个场景:做好相册发给对方后,对方电脑上有一首音乐因编码问题加载报错,如果没有 try 保护,当场黑屏退出,体验非常糟糕。加三行 try 就能让程序跳过坏歌继续播放照片。
启动相册时,也要调用一次play_slide_music(0)把第一首音乐播上。别忘了这个初始化。
如果想让音乐和照片自动同步得更准,我建议在“自动播放”那段里,切换照片后同时调用play_slide_music(current_index)。这样 8 秒一到,照片和音乐一起换,观感是“各就各位”的。
5. 心形烟花:粒子系统的浪漫落地
5.1 先搞懂粒子系统:一袋彩色纸屑怎么形成爱心
烟花的本质,不是一张“爱心图片”在屏幕上移动,而是一堆小粒子从中心出发,各自飞到一个目标位置,最终拼凑成一个心形轮廓。
可以这样理解:你手里有一杯彩色纸屑,往空中使劲一撒,纸屑各自有初速度和方向,你观察到的就是一堆轨迹。如果让纸屑的终点不是随机散落,而是沿着一条爱心曲线分布,那它们在屏幕上形成的图案就是一颗心。
这个思路实现起来分三步:
- 生成心形曲线上的目标点坐标。
- 为每个目标点创建一个小粒子,粒子从中心出发向目标点移动。
- 粒子到达目标点后停留一会儿,然后逐渐变透明消失。
整个过程每帧重绘,视觉上就是“心形从中心浮现并消散”。
5.2 心形曲线方程:数学很好用,抄下来就行
心形曲线有很多种表达方式,pygame 项目里最常用的是这个参数方程:
x = 16 * sin(t)^3 y = 13 * cos(t) - 5 * cos(2t) - 2 * cos(3t) - cos(4t)当 t 从 0 变化到 2π(一个完整周期),x 和 y 的组合点就能勾勒出一个标准的心形。
写成 Python 函数:
def heart_points(cx, cy, scale=20, count=220): points = [] for i in range(count): t = i / count * 2 * math.pi x = 16 * math.sin(t) ** 3 y = 13 * math.cos(t) - 5 * math.cos(2 * t) - 2 * math.cos(3 * t) - math.cos(4 * t) points.append((cx + int(x * scale), cy - int(y * scale))) return points这里有个关键细节:屏幕坐标系的 y 轴是向下增长的,而数学坐标系的 y 轴是向上增长的。如果不做处理,生成的心形会上下颠倒。所以代码里是cy - int(y * scale),减号就是把 y 反过来,心形才能正着显示。
count=220表示生成 220 个点,点数越多心形轮廓越密,烟花效果越饱满。实战中我试过 100 个点,看起来稀疏;220 个点刚刚好;400 个点视觉提升不大,性能还下降,没必要。
5.3 让烟花“放”出来:粒子类与发射逻辑
有了目标点,接下来定义粒子类。每个粒子要有当前位置、目标位置、颜色和生命周期:
class Particle: def __init__(self, x, y, target_x, target_y, color): self.x = x self.y = y self.target_x = target_x self.target_y = target_y self.color = color self.progress = 0 self.speed = random.uniform(0.02, 0.05) self.life = 60 def update(self): if self.progress < 1: self.progress += self.speed self.x += (self.target_x - self.x) * 0.12 self.y += (self.target_y - self.y) * 0.12 else: self.life -= 1 def alive(self): return self.life > 0 def draw(self, surface): alpha = min(255, self.life * 4) temp = pygame.Surface((8, 8), pygame.SRCALPHA) pygame.draw.circle(temp, (*self.color, alpha), (4, 4), 4) surface.blit(temp, (int(self.x) - 4, int(self.y) - 4))update方法里用了一个插值公式:粒子位置每次向目标点移动 12% 的剩余距离。视觉效果是粒子先快后慢地“滑”向目标,比匀速运动自然得多。
life是粒子到达目标后的停留帧数。alive()根据生命值判断粒子是否该移除,避免粒子无限堆积导致程序越来越卡。
发射烟花时,围绕心形曲线生成粒子:
def spawn_heart_firework(cx, cy): particles = [] colors = [(255, 80, 80), (255, 150, 80), (255, 220, 120), (255, 255, 255)] for px, py in heart_points(cx, cy, scale=random.randint(15, 25)): particles.append(Particle(cx, cy, px, py, random.choice(colors))) return particles颜色我用了红、橙、浅黄、白四个色系,随机分配。红色是主基调,暖色系在黑色背景上非常亮眼。你也可以改成粉色系搭配,更有少女感。
在主循环里,按下空格键就发射烟花:
elif event.key == pygame.K_SPACE: fireworks.extend(spawn_heart_firework(width // 2, height // 2))用width // 2和height // 2把烟花中心放在屏幕正中间。如果你想让烟花跟随鼠标位置绽放,可以把这两个值替换成pygame.mouse.get_pos()的坐标。这个扩展做起来很容易。
每帧更新所有粒子,并过滤掉已经消失的:
for p in fireworks: p.update() fireworks = [p for p in fireworks if p.alive()]6. 打包分发与踩坑实录
6.1 用 PyInstaller 打包成 exe:一个命令的事
做好的相册总不能每次都让对方装 Python、装 pygame,那就直接用 PyInstaller 打包。
先安装:
pip install pyinstaller打包命令:
pyinstaller -F -w main.py参数说清楚:
-F:打包成单文件,方便传输。-w:不显示控制台窗口,让程序看起来完全像一个桌面软件。
打包完成后,会生成一个dist文件夹,里面有main.exe。把photos、music这两个文件夹复制到 exe 同目录下,双击 exe 就能跑。
这里有个大坑要提醒:PyInstaller 不会帮你打包外部资源。很多人第一次打包后运行报错,以为代码有问题,其实只是 exe 旁边没有photos和music文件夹。所以复制资源文件夹这个步骤,我会写进自己的打包清单里。
6.2 资源路径与中文字体:最容易翻车的两件事
路径问题:项目目录、照片名、音乐名都不要出现中文。中文路径在 Windows 环境下经常引发各种编码报错,别给自己找麻烦。文件夹名用photos这种英文,内部照片文件photo_01.jpg这种编号,最稳妥。
中文字体问题:如果后期你给照片加了文字标题(比如“我们的毕业旅行”),pygame 自带的默认字体不支持中文,屏幕上会显示一排方块。解决办法是手动加载一个中文字体文件,比如微软雅黑的msyh.ttc,或从网上下载开源的中文字体文件,放到项目目录里:
font = pygame.font.Font("fonts/msyh.ttc", 40)字体文件不存在时,可以先做一个判断:如果加载失败就返回默认字体,保证程序不崩。
6.3 常见问题速查表
按我自己的经验,把最常踩的坑整理成了表格,保存下来能省很多时间:
| 现象 | 可能原因 | 解决方案 |
|---|---|---|
| 运行报 ModuleNotFoundError: No module named 'pygame' | 没有安装 pygame | pip install pygame |
| 窗口一闪而过 | 代码运行时异常,程序直接退出 | 先用set_mode((1000, 700))窗口模式调试,看控制台报错信息 |
| 音乐播放是刺耳噪音 | MP3 编码兼容性问题 | 把音频转成 OGG 或 WAV |
| 图片加载失败 | 文件名拼错、路径包含中文 | 检查文件名大小写/后缀,改用纯英文路径 |
| 打包后照片音乐丢失 | exe 旁边没有资源文件夹 | 把 photos、music 和 exe 放在同一目录 |
| 中文文字全是方块 | 没有加载中文字体文件 | 手动加载 msyh.ttc 等中文字体 |
| 照片切换偶尔卡一下 | 图片太大,加载耗时 | 将图片压缩到 1920 宽度以内 |
6.4 我私藏的几点避坑技巧
最后分享几条不太会写进基础教程里的经验。
开发时先用窗口模式,不要一上来全屏。全屏模式下,print 输出的调试信息要切回桌面才能看到,排错效率极低。等所有功能稳定后再换成全屏模式,最后 30 秒的事。
照片切换时提前加载下一张图。这个项目规模不大,一张张加载无所谓。如果照片增加到几十张,切换时会有明显卡顿。进阶做法是维护两张图:当前图和下一张图,在自动播放等待阶段把下一张预先加载到内存。这个优化对零基础来说可以先不做,但知道有这回事,后面自然用得上。
素材命名规范比代码更重要。我做这个项目时,素材整理花了半个多小时,代码写完反而只要十几分钟。照片和音乐按序号对好,整个数据结构的复杂程度直线下降。
给相册加一个“惊喜彩蛋”。空格键触发烟花这个功能,可以在演示时突然按一下,对方的反应会比看照片本身更有趣。如果你想把烟花改成正能量祝福语、爱心图标之类,思路完全一样,替换目标点生成函数即可。
7. 后续可以怎么玩:从“回忆相册”到更多可能
做完这个项目,你已经掌握了 pygame 的窗口管理、图片显示、键盘事件、音乐播放、粒子系统这几块核心能力。把它们组合起来,能做的远不止相册。
比如,把照片换成“倒计时翻页”的日历,音乐换成语音提醒,就变成一个早安问候程序;把心形烟花的目标点换成文字轮廓,就变成告白弹幕;把全屏相册和 exe 打包流程结合起来,还能做一个带密码验证的私密相册。
我个人在实际操作中的体会是:零基础学 Python,最忌讳的是只看书不动手。这个项目里每一个功能都有一个直接可见的反馈:照片能全屏显示了,音乐会跟着切换了,按一下空格烟花炸开了。这些反馈会让你越写越有动力,比从变量、数据类型开始啃有趣得多。
最后再分享一个小技巧:照片、音乐都放在外部文件夹而不是打包进 exe,以后想换内容,直接替换文件就行,不用重新打包。这个细节在送人的时候尤其好用——不用改一行代码,爱情故事就能持续更新。