原创声明:本文为个人从零打造 Linux 桌面番茄时钟应用的完整开发记录。转载请注明出处。
从零打造一个 Linux 桌面番茄时钟应用
作为一个效率工具爱好者,我一直想要一个简洁的番茄时钟应用。市面上的番茄钟要么功能过于复杂,要么不支持 Linux。于是决定自己动手,用 Python 从零打造一个。
本文记录了完整的开发过程,包括遇到的各种坑和解决方案。
项目背景
番茄工作法是一种时间管理方法:每工作 25 分钟,休息 5 分钟,循环进行。核心需求很简单:
- 25 分钟倒计时,可视化显示
- 完成后声音 + 语音提醒
- 记录每天的番茄数
- 系统托盘运行,不打扰工作
技术选型
| 需求 | 技术方案 |
|---|---|
| GUI 界面 | tkinter(Python 内置,轻量) |
| 系统托盘 | pystray + Pillow |
| 声音播放 | pygame.mixer |
| 语音合成 | 科大讯飞 TTS WebSocket API |
| 单实例控制 | Socket 端口监听 |
整个应用仅依赖 pystray、Pillow、pygame 三个外部库,其余全部使用 Python 标准库。
开发过程中的坑
坑一:Linux 托盘菜单不弹出
使用 pystray 创建系统托盘图标后,发现点击图标时菜单完全不弹出。
原因分析:Linux 桌面环境(如 Cinnamon、GNOME)使用 AppIndicator 后端,右键菜单行为与 Windows/macOS 不同,点击事件可能无法触发。
解决方案:放弃依赖托盘菜单,改为创建独立的控制窗口:
# 创建一个小的控制窗口,提供所有操作按钮
self._control_window = tk.Tk()
self._control_window.title("番茄时钟")
self._control_window.geometry("300x220")
# Start、Stop、Stats、Logs、Exit 按钮
tk.Button(btn_frame, text="开始", command=self._start_tomato)
tk.Button(btn_frame, text="停止", command=self._stop_tomato)托盘图标仅用于显示倒计时状态,控制窗口提供完整功能。
坑二:中文语音播报变成英文
最初使用 espeak 播报中文:
subprocess.run(["espeak", "-v", "zh", "休息时间到了"])结果听到的是一串英文乱读,中文支持极差。
解决方案:调用已有的科大讯飞 TTS 配置:
def play_tts(self, text: str):
xunfei_tts = "/home/lei/.local/bin/xunfei-tts"
if os.path.exists(xunfei_tts):
subprocess.run([xunfei_tts, text])科大讯飞的中文语音自然流畅,效果完全不同。
坑三:窗口按钮显示不全
控制窗口最初设置为 220x200,退出按钮只显示一半。
解决方案:调整为 300x220,三个按钮完整显示:
tk.Button(bottom_frame, text="统计", width=8)
tk.Button(bottom_frame, text="日志", width=8)
tk.Button(bottom_frame, text="退出", width=8)坑四:托盘图标方形不够美观
默认生成的图标是方形,视觉上比较生硬。
解决方案:用 Pillow 绘制圆角图标:
def _create_icon_image(self, text: str, color: str):
size = 64
radius = 12 # 圆角半径
# 创建圆角蒙版
mask = Image.new('L', (size, size), 0)
mask_draw = ImageDraw.Draw(mask)
mask_draw.rounded_rectangle([(0, 0), (size-1, size-1)],
radius=radius, fill=255)
# 应用蒙版
image = Image.new('RGB', (size, size), color=color)
result = Image.new('RGB', (size, size), (0, 0, 0))
result.paste(image, mask=mask)
return result坑五:应用可以多开
双击桌面图标可以启动多个实例,导致托盘出现多个番茄图标。
错误方案:使用文件锁。问题是窗口关闭后锁文件仍存在,无法重新启动。
正确方案:Socket 端口监听,实现「单实例 + 窗口唤醒」:
SOCKET_PORT = 19527
def check_single_instance():
# 尝试连接已有实例
try:
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.connect(('127.0.0.1', SOCKET_PORT))
sock.sendall(b'SHOW_WINDOW') # 发送唤醒命令
return False # 已有实例,退出
except socket.error:
return True # 没有已有实例,正常启动
def start_instance_listener(tray):
# 监听其他实例的唤醒请求
def listener():
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.bind(('127.0.0.1', SOCKET_PORT))
sock.listen(1)
while True:
conn, addr = sock.accept()
data = conn.recv(1024)
if data == b'SHOW_WINDOW':
tray.show_window() # 弹出窗口
conn.close()
threading.Thread(target=listener, daemon=True).start()效果:关闭窗口后,再次双击图标会唤醒已运行实例的窗口,而不是报错或启动新实例。
坑六:pystray 标题不支持中文
self.icon = pystray.Icon("tomato", image, "番茄时钟") # 报错!报错:UnicodeEncodeError: 'latin-1' codec can't encode...
解决方案:托盘标题用英文,窗口界面用中文:
self.icon = pystray.Icon("tomato", image, "Tomato Timer") # OK
self._control_window.title("番茄时钟") # OK最终效果
- 控制窗口:显示倒计时、开始/停止/统计/日志/退出按钮
- 系统托盘:圆角图标,动态显示剩余分钟数
- 语音提醒:完成时播报「番茄钟完成,休息时间到了」
- 单实例:无法多开,窗口关闭后可唤醒
桌面快捷方式:红色圆角番茄图标,双击启动。
项目结构
tomato-timer/
├── main.py # 主入口、单实例管理
├── timer.py # 计时逻辑(25分钟倒计时)
├── logger.py # 日志记录(JSON格式)
├── alert.py # 提醒(声音 + 科大讯飞语音)
├── tray.py # 托盘图标 + 控制窗口
├── sounds/
│ └── alarm.mp3 # 提醒音效
├── tomato.png # 桌面图标
└── venv/ # Python 虚拟环境安装与运行
# 创建虚拟环境
python3 -m venv venv
source venv/bin/activate
# 安装依赖
pip install pystray pillow pygame
# 运行
python main.py总结
这次开发让我深刻体会到 Linux 桌面应用开发的一些特殊挑战:
- AppIndicator 与传统托盘行为不同,不能简单移植 Windows 的经验
- 中文支持是Linux弱项,espeak 中文效果很差,科大讯飞是更好选择
- 单实例需要考虑唤醒场景,文件锁不够,Socket 更灵活
- 虚拟环境必不可少,避免与系统包管理冲突
最终成品虽然简单,但功能完整、体验流畅。有时候,自己动手打造工具,比使用现成的复杂软件更高效。
源码地址:~/tomato-timer/