表白墙:用 Python 做一个能表白、能点赞、还能看统计图的桌面应用

这篇文章带你用 Python 从零做一个校园表白墙桌面应用,跑起来之后,你能发布表白、点赞、留言、搜索,还能打开一个带统计图表的数据面板,拿去做课程设计或者毕业设计的演示都挺合适。
这有一些练手的项目,完整项目:https://pan.quark.cn/s/1e54aa2ae950
一、先说说这个项目是干什么的
表白墙在高校里其实很常见,就是一个大家匿名或实名发帖、说想说的话、别人可以点赞留言的地方。我们这次用 Python 把它做成一个本地桌面程序,功能和真实场景尽量靠齐:
- 发布表白:填昵称(可以匿名)、表白对象、想说的话
- 列表展示:左侧一条条列出来,点哪条看哪条
- 点赞:点一下赞数加一
- 留言:每条表白下面都可以聊两句
- 搜索:按昵称、对象、内容关键词找
- 随机一条:随机抽一条出来看
- 数据统计:总量、点赞排行、对象热度、近 14 天趋势,全部画成图
技术选型上我特意没引入任何第三方库,只用 Python 自带的 tkinter 做界面、sqlite3 存数据,图表是用 Canvas 一笔一笔画出来的。这样做的好处很明显:答辩现场换一台电脑,只要装了 Python 就能直接跑,不用 pip install 一堆东西,也少了很多环境报错的风险。
界面字体我统一用了 SimHei(黑体),中文显示更清晰、更像正式产品;如果某台电脑上没装黑体,代码里做了回退,会自动换到微软雅黑,不会出现字体缺失的方框。
二、整体结构:先把架子搭出来
整个项目就两个核心文件,职责分得很清楚:
27_表白墙/
├─ database.py 数据层:建表、增删改查、统计、示例数据
├─ main.py 界面层:主窗口、发布弹窗、统计窗口、图表绘制
├─ requirements.txt 依赖说明(其实什么都不用装)
└─ 表白墙数据.db 第一次运行自动生成
我的习惯是「数据归数据、界面归界面」:database.py 里只写和数据有关的操作,main.py 里只关心界面和用户交互。以后想改成 MySQL、想换网页版,只需要动数据层,界面一行都不用改。这也是答辩时老师比较认可的一个点——分层清晰。
三、先写数据层 database.py
数据层要做四件事:建两张表(表白表、留言表)、写示例数据、提供增删改查、提供统计查询。
3.1 建表
我们建两张表:posts 存表白,comments 存留言。两张表用 post_id 关联起来,这就是最简单的「一对多」关系——一条表白下面可以有很多条留言。
def _init_db(self):
with self._connect() as conn:
conn.executescript(
"""
CREATE TABLE IF NOT EXISTS posts (
id INTEGER PRIMARY KEY AUTOINCREMENT,
sender TEXT NOT NULL,
target TEXT NOT NULL DEFAULT '',
content TEXT NOT NULL,
anonymous INTEGER NOT NULL DEFAULT 0,
likes INTEGER NOT NULL DEFAULT 0,
created_at TEXT NOT NULL
);
CREATE TABLE IF NOT EXISTS comments (
id INTEGER PRIMARY KEY AUTOINCREMENT,
post_id INTEGER NOT NULL,
author TEXT NOT NULL DEFAULT '匿名',
content TEXT NOT NULL,
created_at TEXT NOT NULL,
FOREIGN KEY (post_id) REFERENCES posts (id)
);
"""
)
几个字段解释一下:anonymous 用 0/1 表示是否匿名,展示的时候直接决定昵称显示「匿名」还是真名;likes 是点赞数,点赞就是把这里加一;created_at 存发布时间,后面做趋势图要用它。
3.2 示例数据
第一次运行时如果表是空的,程序会自动塞一批示例数据进去,这样打开界面就有内容可以玩,答辩演示也不用现场一条条打。我用了一个比较省事的办法:数据写在一个列表里,每条记录标上「距今天多少天」,插入时换算成真实日期。这样无论哪天运行,示例数据都像最近发布的,趋势图也不会是空的。
SAMPLE_POSTS = [
("匿名", "图书馆三楼靠窗的女生",
"上周四下午在图书馆三楼靠窗位置复习高数的女生,你抬头思考的样子很好看,想认识你。",
1, 36, 12),
("奶茶不加糖", "广播站的阿泽",
"每天中午都能听到你的声音,能在电波里遇见你,是我一天里最期待的事。",
0, 52, 1),
("一只柠檬精", "篮球场12号",
"校赛最后那个三分球太帅了,虽然我们输了比赛,但你赢得了全场的掌声。",
0, 28, 30),
# …… 共 16 条,字段含义:昵称、对象、内容、是否匿名、点赞数、几天前
]
这里的每一行依次对应:昵称、表白对象、内容、是否匿名(1 匿名 / 0 实名)、初始点赞数、几天前发布。播种的判断很简单:get_post_count() 大于 0 就跳过,等于 0 才写入,所以不会被重复灌数据。
3.3 常用查询方法
发布、搜索、点赞、留言这几个方法都很直白:
def add_post(self, sender, target, content, anonymous=False, likes=0,
created_at=None):
"""新增一条表白,返回新记录 id。"""
if created_at is None:
created_at = _now_str(0)
sender = (sender or "").strip() or "匿名"
with self._connect() as conn:
cur = conn.execute(
"INSERT INTO posts(sender, target, content, anonymous, likes, created_at)"
" VALUES(?,?,?,?,?,?)",
(sender, target.strip(), content.strip(), 1 if anonymous else 0,
likes, created_at),
)
return cur.lastrowid
def search_posts(self, keyword):
"""按关键词在昵称、对象、内容里模糊搜索。"""
kw = f"%{keyword.strip()}%"
with self._connect() as conn:
return conn.execute(
"SELECT * FROM posts WHERE sender LIKE ? OR target LIKE ?"
" OR content LIKE ? ORDER BY id DESC",
(kw, kw, kw),
).fetchall()
def like_post(self, post_id):
"""点赞:点赞数 +1。"""
with self._connect() as conn:
conn.execute("UPDATE posts SET likes=likes+1 WHERE id=?", (post_id,))
这里一定要提的一点是:SQL 里所有用户输入都用 ? 占位符传参,而不是拼字符串。这样既能避免 SQL 注入,代码也更清爽,答辩如果被问到「安全性」就有得说。
3.4 统计查询
统计是项目里比较出彩的部分,我写了几个专门的查询方法:
def get_average_likes(self):
n = self.get_post_count()
if n == 0:
return 0.0
return round(self.get_total_likes() / n, 1)
def get_top_liked_posts(self, limit=5):
with self._connect() as conn:
return conn.execute(
"SELECT * FROM posts ORDER BY likes DESC, id DESC LIMIT ?", (limit,)
).fetchall()
def get_trend(self, days=14):
"""近 N 天每天的发布条数,返回 [(日期, 条数), ...]。"""
start = datetime.now() - timedelta(days=days - 1)
start_str = start.strftime("%Y-%m-%d")
with self._connect() as conn:
rows = conn.execute(
"SELECT substr(created_at,1,10) AS d, COUNT(*) AS c FROM posts"
" WHERE created_at >= ? GROUP BY d",
(start_str + " 00:00:00",),
).fetchall()
count_map = {r["d"]: r["c"] for r in rows}
trend = []
for i in range(days):
day = (start + timedelta(days=i)).strftime("%Y-%m-%d")
trend.append((day[5:], count_map.get(day, 0)))
return trend
get_trend 这一段要稍微解释一下:先算出近 14 天每天对应的日期字符串,再查数据库里每天有多少条,最后把 14 天的数据按顺序拼成一个列表。这里用了一个小技巧——substr(created_at,1,10) 把 “2026-08-17 14:30:00” 这种完整时间截成 “2026-08-17” 这样的纯日期,再按日期分组计数,非常方便。
四、再写界面 main.py
界面部分我用 tkinter 来实现,整体是一个主窗口加两个弹窗。
4.1 界面布局
主窗口从上到下分四块:
- 顶部标题栏:应用名 + 一句简介
- 工具栏:搜索框 + 「搜索 / 发布表白 / 数据统计 / 随机一条」四个按钮
- 中间主体:左边是表白列表,右边是详情(内容、点赞按钮、留言区)
- 底部状态栏:实时显示「共多少条表白 · 累计多少点赞 · 多少条留言」
def _build_toolbar(self):
bar = tk.Frame(self.root, bg=CARD_BG)
bar.pack(fill="x", padx=18, pady=8)
self.search_var = tk.StringVar()
search_entry = tk.Entry(bar, textvariable=self.search_var,
font=get_font(11), width=30, relief="solid")
search_entry.pack(side="left", padx=(12, 8), ipady=4)
search_entry.bind("<Return>", lambda e: self.do_search())
self._make_button(bar, "搜索", self.do_search).pack(side="left", padx=(0, 8))
self._make_button(bar, "发布表白", self.open_post_dialog).pack(side="left", padx=8)
self._make_button(bar, "数据统计", self.open_stats).pack(side="left", padx=8)
self._make_button(bar, "随机一条", self.show_random).pack(side="left", padx=8)
有个细节值得一说:我在搜索框上绑定了回车键事件,输入完直接按回车就能搜索,不用非得点按钮,这个小交互在答辩演示时很加分。
左侧列表我用 Listbox 展示,每条显示成一行摘要,把「赞数、昵称、对象、内容前几个字」都放进去:
for p in self.current_posts:
sender = "匿名" if p["anonymous"] else p["sender"]
target = p["target"] or "大家"
preview = p["content"] if len(p["content"]) <= 26 \
else p["content"][:26] + "…"
self.listbox.insert("end",
f"[赞{p['likes']}] {sender} → {target}:{preview}")
点击某一条时,on_select 拿到选中项的索引,再去数据库查完整内容,刷新右侧详情区,包括重新加载这条表白下的所有留言。
4.2 发布表白弹窗
发布用的是一个 Toplevel 弹窗,里面有几个输入框和一个「匿名发布」的勾选框:
def submit(self):
content = self.txt_content.get("1.0", "end").strip()
if not content:
messagebox.showwarning("提示", "想说的话不能为空", parent=self)
return
sender = self.entry_sender.get().strip()
target = self.entry_target.get().strip()
self.app.db.add_post(sender, target, content, self.anon_var.get())
self.app.refresh_wall()
self.destroy()
发布前会先校验「想说的话」不能为空,弹窗里也能直接用 Ctrl + 回车 提交,用户体验会更顺。发布成功后就刷新主界面,新表白出现在列表最上面(因为查询是按 id 倒序的)。
4.3 图表怎么画出来的
很多同学一提到画图表就想到 matplotlib,其实 tkinter 的 Canvas 画简单的柱状图、折线图、饼图完全够用,而且零依赖。我的做法是把绘图逻辑封装成几个函数,统计窗口里直接调用:
def draw_hbar(canvas, items, title, color=ACCENT):
"""横向条形图,items = [(名称, 数值)]。"""
canvas.delete("all")
w = int(canvas["width"])
h = int(canvas["height"])
canvas.create_text(w / 2, 22, text=title, font=get_font(12, True),
fill=TEXT_COLOR)
left, right, top, bottom = 150, w - 24, 42, h - 16
n = max(len(items), 1)
max_v = max([v for _, v in items] or [0], default=0)
max_v = max(max_v, 1) # 防止全 0 时除零
row_h = (bottom - top) / n
for i, (label, value) in enumerate(items):
y = top + i * row_h + row_h / 2
canvas.create_text(left - 10, y, text=str(label), anchor="e",
font=get_font(10))
bar_w = (right - left) * value / max_v
canvas.create_rectangle(left, y - row_h * 0.3, left + bar_w,
y + row_h * 0.3, fill=color, outline="")
canvas.create_text(left + bar_w + 8, y, text=str(value),
anchor="w", font=get_font(10, True), fill=ACCENT_DARK)
原理一句话就能讲清楚:把数值映射成像素长度,然后用 create_rectangle 画矩形。这里有一个我踩过的坑——如果所有值都是 0,max_v 会是 0,一除就崩了,所以加了一行 max_v = max(max_v, 1) 兜底。这个边界情况建议你在代码里留着,答辩时能体现出你考虑过空数据的情况。
折线图的原理类似:每个点算好 (x, y) 坐标,用 create_oval 画点、create_line 连线,再把数值标在点上方。饼图用的是 create_arc 画扇形,按占比把 360 度切给每一块,右侧配上图例。
4.4 统计窗口
统计窗口用 ttk.Notebook 做了三个标签页,数据全是实时从数据库读的:
- 概览:6 张指标卡(表白总数、累计点赞、留言总数、平均每条点赞、人气最高对象、最热门表白点赞数)+ 一张「匿名/实名」饼图
- 热门榜:点赞数 TOP 5 横向条形图 + 表白对象热度 TOP 5
- 发布趋势:近 14 天发布量折线图
核心代码就是把数据库的查询结果喂给绘图函数:
top = self.db.get_top_liked_posts(5)
items1 = []
for p in top:
sender = "匿名" if p["anonymous"] else p["sender"]
items1.append((f"{sender} 的告白", p["likes"]))
canvas1 = tk.Canvas(page, width=820, height=280, bg=CARD_BG, highlightthickness=0)
canvas1.pack(fill="x", padx=20, pady=(18, 6))
draw_hbar(canvas1, items1, "点赞数 TOP 5 表白")
五、程序里能看到哪些数据
为了让演示有说服力,示例数据是我精心编排过的:16 条表白、9 条留言,点赞数从 19 到 95 不等,发布时间分布在最近一个多月里,这样统计面板每一项都有内容。
运行后概览页会显示这样的指标:
| 指标 | 数值 |
|---|---|
| 表白总数 | 16 条 |
| 累计点赞 | 785 个 |
| 留言总数 | 9 条 |
| 平均每条点赞 | 49.1 个 |
| 人气最高的表白对象 | 校门口的橘猫 |
| 最热门表白的点赞数 | 95 个 |
点赞数 TOP 5 是这个效果:
| 排名 | 表白 | 点赞数 |
|---|---|---|
| 1 | 风声 的告白(对象:校门口的橘猫) | 95 |
| 2 | 平平无奇干饭人 的告白(对象:食堂糖醋排骨窗口) | 81 |
| 3 | 橘子汽水 的告白(对象:楼下的宿管阿姨) | 72 |
| 4 | 小鹿乱撞 的告白(对象:学生会宣传部的学姐) | 67 |
| 5 | 咕咕 的告白(对象:大三考研的学长) | 64 |
近 14 天发布趋势大概是这个样子的(日期和条数):
08-04:0 08-05:1 08-06:0 08-07:1 08-08:1
08-09:1 08-10:0 08-11:1 08-12:1 08-13:1
08-14:1 08-15:1 08-16:1 08-17:0
这些都只是「一次运行」的结果,你在程序里发布新的表白、点赞、留言之后,这些数字会实时变化,图表也会跟着变,用来展示「数据驱动界面」再合适不过了。
六、怎么运行
环境要求很简单:Python 3.8 以上,自带的 tkinter 组件。先确保把 database.py 和 main.py 放在同一个文件夹,然后在那个目录下运行:
python main.py
或者在资源管理器里直接双击 main.py 也行。第一次运行会自动生成 表白墙数据.db 并写入示例数据。如果哪天把数据库文件删了,重新运行又会自动生成一套全新的示例数据,不用担心把项目「玩坏」。
七、答辩可能会被问到的问题
提前想好这几个问题的答案,答辩会稳很多:
- 为什么选 tkinter 而不是 PyQt? 答:tkinter 是 Python 自带的标准库,零安装、跨平台一致,对课程设计这种规模完全够用;PyQt 功能更强但要额外安装,演示环境不友好。
- 数据是怎么存的?为什么用 SQLite? 答:SQLite 是单文件数据库,轻量、不需要服务器,非常适合单机应用;两张表用外键关联,满足最基本的表设计规范。
- 怎么防止 SQL 注入? 答:所有 SQL 参数都用占位符
?绑定,不拼接字符串,这是最直接有效的做法。 - 有没有考虑异常和边界情况? 答:点赞、留言都要先判断有没有选中数据;内容为空会提示;图表数据全为 0 时做了兜底,不会崩溃;数据库为空时界面也会正常显示提示文案。
- 以后想扩展怎么办? 答:数据层和界面层分离,可以平滑换成 MySQL,或把界面换成网页端复用同一套数据逻辑。
八、最后总结一下
这个项目麻雀虽小五脏俱全:分层设计、关系型数据存储、界面交互、数据可视化、安全性、异常处理全都有。代码量不大(两个文件,几百行),但每一部分都踩得住提问,很适合用来完成课程设计或作为毕业设计的基础框架。你只需要在示例数据上再加点自己的想法——比如加个「表白标签」、做个「每日一言」,就是你的创新点了。
更多推荐



所有评论(0)