桌面幸运摇“钱”盒 | 摇一摇,解锁今日好运(保姆级教程)
最近折腾行空板,想着做个有意思的桌面小摆件。正好板载加速度传感器,干脆做个摇“钱”签筒——拿起来晃一晃,就随机跳出一支好运签,还有每日专属祝福、摇签次数统计,放在桌上没事摇两下,仪式感直接拉满。
项目只用板载硬件就能跑,不用外接任何元件。下面跟大家拆解下完整实现过程。

一、库导入与硬件初始化
首先导入依赖库,初始化硬件和全局对象,同时定义几个核心参数:摇签的加速度阈值、防误触冷却时间、最大记录数。
import time
import random
from unihiker import GUI, Audio
from pinpong.board import *
from pinpong.extension.unihiker import *
Board().begin()
gui = GUI()
audio = Audio()
SHAKE_THRESHOLD = 4.0
SHAKE_COOLDOWN = 1.2
MAX_RECORD = 99这里加速度阈值设为4.0g ,实测正常摇一下很容易达到,又不会轻轻碰一下就误触发;1.2秒冷却时间避免一次摇动被判定成多次。
二、内容素材:签文库与每日祝福
准备两组数据:签文和每日祝福。
SIGNS :6种运势等级,各配一句签文和专属颜色,从大吉到上签做梯度区分
DAILY_BLESSINGS :24条祝福语,按日期自动轮换,同一天内固定显示同一条
SIGNS = [
("大吉", "财源广进,好运当头!", (255, 90, 90)),
("上上签", "心想事成,万事如意!", (255, 150, 40)),
("上吉", "贵人相助,顺风顺水!", (255, 200, 60)),
("中吉", "小有进步,再接再厉!", (80, 190, 110)),
("小吉", "稳中向好,喜事可期!", (60, 170, 220)),
("上签", "桃花盛开,笑容常在!", (220, 110, 190)),
]
DAILY_BLESSINGS = [
("财源滚滚,日进斗金!", (255, 120, 60)),
("身体健康,百病不侵!", (90, 190, 120)),
("学业有成,金榜题名!", (80, 150, 240)),
("事业腾飞,步步高升!", (120, 110, 220)),
("桃花朵朵,甜蜜暴击!", (230, 100, 170)),
("心情美美,天天开心!", (250, 180, 60)),
("心想事成,万事如意!", (230, 80, 80)),
("好运连连,惊喜不断!", (255, 160, 40)),
("平安喜乐,顺遂无忧!", (90, 170, 190)),
("钱包鼓鼓,吃嘛嘛香!", (240, 140, 70)),
("睡个好觉,元气满满!", (110, 130, 220)),
("灵感爆棚,创意无限!", (200, 120, 220)),
("逢考必过,逢赛必胜!", (80, 180, 140)),
("全家安康,福气满堂!", (210, 90, 90)),
("出门遇贵人,办事处处顺!", (255, 170, 90)),
("笑口常开,福气自来!", (240, 150, 50)),
("一路绿灯,畅通无阻!", (70, 170, 110)),
("付出有回报,努力有回响!", (100, 150, 230)),
("烦恼清零,快乐加倍!", (250, 130, 160)),
("好运常伴,梦想成真!", (180, 120, 220)),
("朋友遍天下,知己长相伴!", (90, 160, 200)),
("元气满满,光芒万丈!", (255, 190, 60)),
("所求皆如愿,所行皆坦途!", (140, 180, 90)),
("今天也是元气满满的一天!", (250, 140, 80)),
]
def get_daily_blessing():
t = time.localtime()
day_index = (t.tm_year * 372 + t.tm_mon * 31 + t.tm_mday) % len(DAILY_BLESSINGS)
return DAILY_BLESSINGS[day_index]每日祝福用「年×372 + 月×31 + 日」对总数取模计算索引,不用额外存状态文件,每天自动切换下一条,就算重启板子,当天的祝福也保持不变。
三、界面搭建:用基础图形拼出签筒
行空板屏幕是 240×320,我把界面从上到下分成标题区、祝福区、签筒区、结果区、状态栏五部分,全部用 unihiker 自带的基础图形和文本函数绘制,不用额外图片资源。
shake_count = 0
last_sign = None
last_level_color = (255, 90, 90)
state = "idle"
result_time = 0
SCREEN_W, SCREEN_H = 240, 320
title = gui.draw_text(x=SCREEN_W//2, y=12, text="幸运摇钱盒",
origin="center", font_size=16, color=(120, 60, 160))
subtitle = gui.draw_text(x=SCREEN_W//2, y=34, text="摇一摇 抽好运",
origin="center", font_size=10, color=(150, 150, 150))
blessing_txt = gui.draw_text(x=SCREEN_W//2, y=52, text="", origin="center",
font_size=10, color=(200, 130, 40))
_blessing_text, _blessing_color = get_daily_blessing()
blessing_txt.config(text="今日祝福: " + _blessing_text, color=_blessing_color)
TUBE_X, TUBE_Y = SCREEN_W//2, 160
TUBE_W, TUBE_H = 84, 96
tube_body = gui.fill_round_rect(x=TUBE_X-42, y=TUBE_Y-46, w=TUBE_W, h=TUBE_H,
r=8, color=(200, 50, 50))
tube_rim = gui.draw_round_rect(x=TUBE_X-42, y=TUBE_Y-46, w=TUBE_W, h=TUBE_H,
r=8, width=4, color=(255, 200, 60))
tube_mouth = gui.draw_rect(x=TUBE_X-42, y=TUBE_Y-46, w=TUBE_W, h=12,
width=3, color=(255, 220, 110))
sign_lines = [
gui.draw_line(x0=TUBE_X-28, y0=TUBE_Y-52, x1=TUBE_X-18, y1=TUBE_Y-40,
width=5, color=(240, 200, 100)),
gui.draw_line(x0=TUBE_X-12, y0=TUBE_Y-56, x1=TUBE_X-2, y1=TUBE_Y-44,
width=5, color=(255, 255, 255)),
gui.draw_line(x0=TUBE_X+4, y0=TUBE_Y-52, x1=TUBE_X+14, y1=TUBE_Y-40,
width=5, color=(240, 200, 100)),
]
deco1 = gui.fill_circle(x=TUBE_X-30, y=TUBE_Y+60, r=6, color=(255, 200, 60))
deco2 = gui.fill_circle(x=TUBE_X, y=TUBE_Y+60, r=6, color=(255, 200, 60))
deco3 = gui.fill_circle(x=TUBE_X+30, y=TUBE_Y+60, r=6, color=(255, 200, 60))
result_txt = gui.draw_text(x=SCREEN_W//2, y=236, text="摇一摇开始",
origin="center", font_size=18, color=(90, 90, 90))
result_slip = gui.draw_text(x=SCREEN_W//2, y=264, text="", origin="center",
font_size=12, color=(160, 160, 160))
status_title = gui.draw_text(x=12, y=292, text="累计", font_size=11,
color=(120, 120, 120))
status_count = gui.draw_digit(x=52, y=288, text="0", font_size=18,
color=(0, 150, 120), origin="left")
hint_a = gui.draw_text(x=190, y=292, text="A:统计", font_size=11,
color=(160, 160, 160))
hint_b = gui.draw_text(x=190, y=306, text="B:换签", font_size=11,
color=(160, 160, 160))签筒用圆角矩形做红色筒身,加金色边框和筒口,里面画三条斜线当露出的签头,视觉上很有那味儿。所有控件都提前创建好,后面用 config() 更新属性,避免反复新建对象导致界面卡顿。
四、核心逻辑:摇动检测与抽签动画
这部分是项目的灵魂,分成三块:摇动检测、摇签动画、界面自动复位。
1. 计数更新与摇动动画
先写两个工具函数:更新计数显示、播放签筒左右晃动的动画。
def update_count_display():
status_count.config(text=str(shake_count))
def play_shake_animation(iterations=6):
for i in range(iterations):
offset = 10 if i % 2 == 0 else -10
tube_body.config(x=TUBE_X-42+offset, y=TUBE_Y-46)
tube_rim.config(x=TUBE_X-42+offset, y=TUBE_Y-46)
tube_mouth.config(x=TUBE_X-42+offset, y=TUBE_Y-46)
sign_lines[0].config(x0=TUBE_X-28+offset)
sign_lines[0].config(x1=TUBE_X-18+offset)
sign_lines[1].config(x0=TUBE_X-12+offset)
sign_lines[1].config(x1=TUBE_X-2+offset)
sign_lines[2].config(x0=TUBE_X+4+offset)
sign_lines[2].config(x1=TUBE_X+14+offset)
time.sleep(0.06)
tube_body.config(x=TUBE_X-42, y=TUBE_Y-46)
tube_rim.config(x=TUBE_X-42, y=TUBE_Y-46)
tube_mouth.config(x=TUBE_X-42, y=TUBE_Y-46)
sign_lines[0].config(x0=TUBE_X-28, x1=TUBE_X-18)
sign_lines[1].config(x0=TUBE_X-12, x1=TUBE_X-2)
sign_lines[2].config(x0=TUBE_X+4, x1=TUBE_X+14)
2. 飞签动画与抽签执行
摇完之后播放一支签从筒口飞出的动画,然后随机抽取签文、更新结果和计数。
def draw_flying_slip(level, color):
slip = gui.draw_line(x0=TUBE_X-3, y0=TUBE_Y-58, x1=TUBE_X+3, y1=TUBE_Y-82,
width=5, color=color)
y_start = TUBE_Y - 58
y_end = 205
for step in range(5 + 1):
yy = y_start + (y_end - y_start) * step // 5
slip.config(y0=yy, y1=yy-24)
time.sleep(0.05)
result_txt.config(text=level, color=color)
result_slip.config(text=last_sign)
def do_shake_draw():
global shake_count, last_sign, state, result_time, last_level_color
state = "shaking"
play_shake_animation()
level, text, color = random.choice(SIGNS)
last_sign = text
last_level_color = color
shake_count += 1
state = "result"
result_time = time.time()
draw_flying_slip(level, color)
update_count_display()
print("抽到[{}]: {}".format(level, text))
3. 摇动检测线程
开一个独立线程持续读取加速度传感器,满足条件就触发摇签,不阻塞主界面
def shake_detect_loop():
global state, result_time
last_trigger = 0.0
while True:
strength = accelerometer.get_strength()
now = time.time()
if (strength > SHAKE_THRESHOLD
and now - last_trigger > SHAKE_COOLDOWN
and state != "shaking"):
last_trigger = now
do_shake_draw()
time.sleep(0.02)
4. 界面自动复位
再开一个UI线程,结果展示3秒后自动回到待机界面,恢复显示每日祝福。
def sign_stats_loop():
global state
while True:
if state == "result" and time.time() - result_time > 3:
result_txt.config(text="摇一摇开始", color=(90, 90, 90))
result_slip.config(text="")
blessing_txt.config(text="今日祝福: " + _blessing_text, color=_blessing_color)
state = "idle"
time.sleep(0.1)
五、按键功能:统计与洗牌
把行空板的A、B两个按键利用起来:
A键:查看累计摇签次数 + 当前环境音量
B键:打乱签文库顺序,相当于「洗牌换手气」
def show_stats():
vol = audio.sound_level()
result_txt.config(text="已摇 {} 次".format(shake_count), color=(0, 150, 120))
result_slip.config(text="环境音量 {}%".format(vol), color=(160, 160, 160))
def reshuffle_pool():
random.shuffle(SIGNS)
result_txt.config(text="签库已洗牌!", color=(120, 60, 160))
result_slip.config(text="再摇一摇试试手气", color=(160, 160, 160))
gui.on_a_click(show_stats)
gui.on_b_click(reshuffle_pool)
六、启动程序
最后启动两个子线程,主循环保持程序运行即可,行空板的GUI不需要手动 update()。
shake_thread = gui.start_thread(shake_detect_loop)
ui_thread = gui.start_thread(sign_stats_loop)
print("幸运摇钱盒已启动:摇动行空板(加速度强度>4)抽好运签!")
print("今日祝福[{}]: {}".format(time.strftime("%Y-%m-%d"), _blessing_text))
while True:
time.sleep(0.1)
七、完整代码
import time
import random
from unihiker import GUI, Audio
from pinpong.board import *
from pinpong.extension.unihiker import *
Board().begin()
gui = GUI()
audio = Audio()
SHAKE_THRESHOLD = 4.0
SHAKE_COOLDOWN = 1.2
MAX_RECORD = 99
SIGNS = [
("大吉", "财源广进,好运当头!", (255, 90, 90)),
("上上签", "心想事成,万事如意!", (255, 150, 40)),
("上吉", "贵人相助,顺风顺水!", (255, 200, 60)),
("中吉", "小有进步,再接再厉!", (80, 190, 110)),
("小吉", "稳中向好,喜事可期!", (60, 170, 220)),
("上签", "桃花盛开,笑容常在!", (220, 110, 190)),
]
DAILY_BLESSINGS = [
("财源滚滚,日进斗金!", (255, 120, 60)),
("身体健康,百病不侵!", (90, 190, 120)),
("学业有成,金榜题名!", (80, 150, 240)),
("事业腾飞,步步高升!", (120, 110, 220)),
("桃花朵朵,甜蜜暴击!", (230, 100, 170)),
("心情美美,天天开心!", (250, 180, 60)),
("心想事成,万事如意!", (230, 80, 80)),
("好运连连,惊喜不断!", (255, 160, 40)),
("平安喜乐,顺遂无忧!", (90, 170, 190)),
("钱包鼓鼓,吃嘛嘛香!", (240, 140, 70)),
("睡个好觉,元气满满!", (110, 130, 220)),
("灵感爆棚,创意无限!", (200, 120, 220)),
("逢考必过,逢赛必胜!", (80, 180, 140)),
("全家安康,福气满堂!", (210, 90, 90)),
("出门遇贵人,办事处处顺!", (255, 170, 90)),
("笑口常开,福气自来!", (240, 150, 50)),
("一路绿灯,畅通无阻!", (70, 170, 110)),
("付出有回报,努力有回响!", (100, 150, 230)),
("烦恼清零,快乐加倍!", (250, 130, 160)),
("好运常伴,梦想成真!", (180, 120, 220)),
("朋友遍天下,知己长相伴!", (90, 160, 200)),
("元气满满,光芒万丈!", (255, 190, 60)),
("所求皆如愿,所行皆坦途!", (140, 180, 90)),
("今天也是元气满满的一天!", (250, 140, 80)),
]
def get_daily_blessing():
t = time.localtime()
day_index = (t.tm_year * 372 + t.tm_mon * 31 + t.tm_mday) % len(DAILY_BLESSINGS)
return DAILY_BLESSINGS[day_index]
shake_count = 0
last_sign = None
last_level_color = (255, 90, 90)
state = "idle"
result_time = 0
SCREEN_W, SCREEN_H = 240, 320
title = gui.draw_text(x=SCREEN_W//2, y=12, text="幸运摇钱盒",
origin="center", font_size=16, color=(120, 60, 160))
subtitle = gui.draw_text(x=SCREEN_W//2, y=34, text="摇一摇 抽好运",
origin="center", font_size=10, color=(150, 150, 150))
blessing_txt = gui.draw_text(x=SCREEN_W//2, y=52, text="", origin="center",
font_size=10, color=(200, 130, 40))
_blessing_text, _blessing_color = get_daily_blessing()
blessing_txt.config(text="今日祝福: " + _blessing_text, color=_blessing_color)
TUBE_X, TUBE_Y = SCREEN_W//2, 160
TUBE_W, TUBE_H = 84, 96
tube_body = gui.fill_round_rect(x=TUBE_X-42, y=TUBE_Y-46, w=TUBE_W, h=TUBE_H,
r=8, color=(200, 50, 50))
tube_rim = gui.draw_round_rect(x=TUBE_X-42, y=TUBE_Y-46, w=TUBE_W, h=TUBE_H,
r=8, width=4, color=(255, 200, 60))
tube_mouth = gui.draw_rect(x=TUBE_X-42, y=TUBE_Y-46, w=TUBE_W, h=12,
width=3, color=(255, 220, 110))
sign_lines = [
gui.draw_line(x0=TUBE_X-28, y0=TUBE_Y-52, x1=TUBE_X-18, y1=TUBE_Y-40,
width=5, color=(240, 200, 100)),
gui.draw_line(x0=TUBE_X-12, y0=TUBE_Y-56, x1=TUBE_X-2, y1=TUBE_Y-44,
width=5, color=(255, 255, 255)),
gui.draw_line(x0=TUBE_X+4, y0=TUBE_Y-52, x1=TUBE_X+14, y1=TUBE_Y-40,
width=5, color=(240, 200, 100)),
]
deco1 = gui.fill_circle(x=TUBE_X-30, y=TUBE_Y+60, r=6, color=(255, 200, 60))
deco2 = gui.fill_circle(x=TUBE_X, y=TUBE_Y+60, r=6, color=(255, 200, 60))
deco3 = gui.fill_circle(x=TUBE_X+30, y=TUBE_Y+60, r=6, color=(255, 200, 60))
result_txt = gui.draw_text(x=SCREEN_W//2, y=236, text="摇一摇开始",
origin="center", font_size=18, color=(90, 90, 90))
result_slip = gui.draw_text(x=SCREEN_W//2, y=264, text="", origin="center",
font_size=12, color=(160, 160, 160))
status_title = gui.draw_text(x=12, y=292, text="累计", font_size=11,
color=(120, 120, 120))
status_count = gui.draw_digit(x=52, y=288, text="0", font_size=18,
color=(0, 150, 120), origin="left")
hint_a = gui.draw_text(x=190, y=292, text="A:统计", font_size=11,
color=(160, 160, 160))
hint_b = gui.draw_text(x=190, y=306, text="B:换签", font_size=11,
color=(160, 160, 160))
def update_count_display():
status_count.config(text=str(shake_count))
def play_shake_animation(iterations=6):
for i in range(iterations):
offset = 10 if i % 2 == 0 else -10
tube_body.config(x=TUBE_X-42+offset, y=TUBE_Y-46)
tube_rim.config(x=TUBE_X-42+offset, y=TUBE_Y-46)
tube_mouth.config(x=TUBE_X-42+offset, y=TUBE_Y-46)
sign_lines[0].config(x0=TUBE_X-28+offset)
sign_lines[0].config(x1=TUBE_X-18+offset)
sign_lines[1].config(x0=TUBE_X-12+offset)
sign_lines[1].config(x1=TUBE_X-2+offset)
sign_lines[2].config(x0=TUBE_X+4+offset)
sign_lines[2].config(x1=TUBE_X+14+offset)
time.sleep(0.06)
tube_body.config(x=TUBE_X-42, y=TUBE_Y-46)
tube_rim.config(x=TUBE_X-42, y=TUBE_Y-46)
tube_mouth.config(x=TUBE_X-42, y=TUBE_Y-46)
sign_lines[0].config(x0=TUBE_X-28, x1=TUBE_X-18)
sign_lines[1].config(x0=TUBE_X-12, x1=TUBE_X-2)
sign_lines[2].config(x0=TUBE_X+4, x1=TUBE_X+14)
def draw_flying_slip(level, color):
slip = gui.draw_line(x0=TUBE_X-3, y0=TUBE_Y-58, x1=TUBE_X+3, y1=TUBE_Y-82,
width=5, color=color)
y_start = TUBE_Y - 58
y_end = 205
for step in range(5 + 1):
yy = y_start + (y_end - y_start) * step // 5
slip.config(y0=yy, y1=yy-24)
time.sleep(0.05)
result_txt.config(text=level, color=color)
result_slip.config(text=last_sign)
def do_shake_draw():
global shake_count, last_sign, state, result_time, last_level_color
state = "shaking"
play_shake_animation()
level, text, color = random.choice(SIGNS)
last_sign = text
last_level_color = color
shake_count += 1
state = "result"
result_time = time.time()
draw_flying_slip(level, color)
update_count_display()
print("抽到[{}]: {}".format(level, text))
def shake_detect_loop():
global state, result_time
last_trigger = 0.0
while True:
strength = accelerometer.get_strength()
now = time.time()
if (strength > SHAKE_THRESHOLD
and now - last_trigger > SHAKE_COOLDOWN
and state != "shaking"):
last_trigger = now
do_shake_draw()
time.sleep(0.02)
def sign_stats_loop():
global state
while True:
if state == "result" and time.time() - result_time > 3:
result_txt.config(text="摇一摇开始", color=(90, 90, 90))
result_slip.config(text="")
blessing_txt.config(text="今日祝福: " + _blessing_text, color=_blessing_color)
state = "idle"
time.sleep(0.1)
def show_stats():
vol = audio.sound_level()
result_txt.config(text="已摇 {} 次".format(shake_count), color=(0, 150, 120))
result_slip.config(text="环境音量 {}%".format(vol), color=(160, 160, 160))
def reshuffle_pool():
random.shuffle(SIGNS)
result_txt.config(text="签库已洗牌!", color=(120, 60, 160))
result_slip.config(text="再摇一摇试试手气", color=(160, 160, 160))
gui.on_a_click(show_stats)
gui.on_b_click(reshuffle_pool)
shake_thread = gui.start_thread(shake_detect_loop)
ui_thread = gui.start_thread(sign_stats_loop)
print("幸运摇钱盒已启动:摇动行空板(加速度强度>4)抽好运签!")
print("今日祝福[{}]: {}".format(time.strftime("%Y-%m-%d"), _blessing_text))
while True:
time.sleep(0.1)
效果展示


项目总结
这个小项目虽然代码不多,但互动感很强,摇一摇的操作很自然,放在桌面当个解压小玩具特别合适。
核心用到的行空板特性:
- 板载加速度传感器作为交互输入
- 原生GUI函数纯代码绘制界面,零资源依赖
- 多线程分离硬件检测和UI逻辑,运行流畅
- 板载按键做扩展功能,玩法更丰富
大家有兴趣的话还可以继续拓展:加上摇签音效、把摇签记录存在本地、增加更多签文和运势等级,甚至加上联网功能同步每日黄历~
最后如果你不熟悉代码可以仿照我的思路使用我的“万年历”扩展库,在mind+V2 Python积木模式扩展搜索万年历,找到作者lumens的那个,里面有较为全面的万年历功能





