1. 项目概述:打造一个始终置顶的动态时钟圆盘
最近在开发一个需要长时间运行监控脚本的项目时,我遇到了一个实际需求:希望在不影响其他窗口操作的前提下,始终能在屏幕某个角落看到一个简洁的时间显示。市面上虽然有不少桌面时钟工具,但要么功能过于复杂,要么无法满足自定义需求。于是决定用Python自己实现一个极简的动态时钟圆盘,并且让它始终保持在所有窗口的最前端。
这个项目看似简单,实则涉及几个关键技术点:首先是GUI框架的选择,需要支持窗口置顶特性;其次是时钟的动态绘制,要保证流畅不卡顿;最后是性能优化,避免这个小工具占用过多系统资源。经过对比测试,最终选择Tkinter作为基础框架,配合Canvas实现动态绘制,整体代码控制在100行以内,CPU占用率保持在1%以下。
提示:如果你需要的是一个轻量级、可自定义且不干扰其他工作的桌面时钟,这个方案比安装第三方软件更灵活可控。所有代码都可以根据你的屏幕尺寸和审美偏好进行调整。
2. 环境准备与基础配置
2.1 Python环境与必要库
本项目需要Python 3.6及以上版本,主要依赖标准库中的tkinter模块,无需额外安装第三方包。建议使用虚拟环境隔离项目依赖:
bash复制python -m venv clock_env
source clock_env/bin/activate # Linux/Mac
clock_env\Scripts\activate # Windows
验证tkinter是否可用:
python复制import tkinter as tk
print(tk.TkVersion) # 应该输出8.6或更高版本
如果遇到"ModuleNotFoundError: No module named 'tkinter'"错误,说明系统缺少Tkinter支持。在Ubuntu/Debian上可以通过以下命令安装:
bash复制sudo apt-get install python3-tk
2.2 基础窗口设置
我们先创建一个最简窗口框架,设置窗口属性和基本样式:
python复制import tkinter as tk
from time import strftime
class FloatingClock:
def __init__(self):
self.root = tk.Tk()
self.root.title("浮动时钟")
self.root.attributes('-topmost', True) # 关键:窗口置顶
self.root.overrideredirect(True) # 隐藏标题栏
self.root.config(bg='black')
self.root.geometry("200x200+100+100") # 初始位置
# 后续添加其他组件
def run(self):
self.root.mainloop()
if __name__ == "__main__":
clock = FloatingClock()
clock.run()
这段代码已经实现了窗口置顶的关键功能:
attributes('-topmost', True):强制窗口保持在最上层overrideredirect(True):移除窗口边框和标题栏geometry("宽x高+X+Y"):设置窗口大小和屏幕位置
3. 动态时钟圆盘的实现
3.1 Canvas绘制时钟表盘
使用Tkinter的Canvas组件绘制时钟的静态部分:
python复制def __init__(self):
# ...前面的初始化代码...
self.canvas = tk.Canvas(self.root, bg='black',
highlightthickness=0)
self.canvas.pack(fill=tk.BOTH, expand=True)
# 表盘半径
self.radius = 80
self.center_x = 100
self.center_y = 100
# 绘制表盘外圆
self.canvas.create_oval(
self.center_x - self.radius,
self.center_y - self.radius,
self.center_x + self.radius,
self.center_y + self.radius,
outline='white', width=2
)
# 绘制刻度
for i in range(12):
angle = i * 30 # 每小时30度
rad = math.radians(angle)
inner_x = self.center_x + (self.radius-10) * math.sin(rad)
inner_y = self.center_y - (self.radius-10) * math.cos(rad)
outer_x = self.center_x + self.radius * math.sin(rad)
outer_y = self.center_y - self.radius * math.cos(rad)
self.canvas.create_line(
inner_x, inner_y, outer_x, outer_y,
fill='white', width=2
)
3.2 动态指针的实现
时钟的核心是动态更新的指针。我们需要创建三个指针(时、分、秒)并定期更新它们的位置:
python复制def __init__(self):
# ...前面的初始化代码...
# 创建指针
self.hour_hand = self.canvas.create_line(
0, 0, 0, 0, fill='white', width=4)
self.minute_hand = self.canvas.create_line(
0, 0, 0, 0, fill='white', width=2)
self.second_hand = self.canvas.create_line(
0, 0, 0, 0, fill='red', width=1)
# 启动时钟更新
self.update_clock()
def update_clock(self):
# 获取当前时间
now = time.localtime()
hour = now.tm_hour % 12
minute = now.tm_min
second = now.tm_sec
# 计算指针角度(转换为弧度)
hour_angle = math.radians((hour + minute/60) * 30 - 90)
minute_angle = math.radians(minute * 6 - 90)
second_angle = math.radians(second * 6 - 90)
# 更新时针
hour_length = self.radius * 0.5
hour_x = self.center_x + hour_length * math.cos(hour_angle)
hour_y = self.center_y + hour_length * math.sin(hour_angle)
self.canvas.coords(self.hour_hand,
self.center_x, self.center_y, hour_x, hour_y)
# 更新分针
minute_length = self.radius * 0.7
minute_x = self.center_x + minute_length * math.cos(minute_angle)
minute_y = self.center_y + minute_length * math.sin(minute_angle)
self.canvas.coords(self.minute_hand,
self.center_x, self.center_y, minute_x, minute_y)
# 更新秒针
second_length = self.radius * 0.9
second_x = self.center_x + second_length * math.cos(second_angle)
second_y = self.center_y + second_length * math.sin(second_angle)
self.canvas.coords(self.second_hand,
self.center_x, self.center_y, second_x, second_y)
# 1000ms后再次更新
self.root.after(1000, self.update_clock)
注意:角度计算中减去90度是因为Canvas坐标系中0度指向右侧,而时钟12点方向需要指向正上方。
4. 高级功能与优化
4.1 添加数字时间显示
为了更直观地读取时间,可以在表盘下方添加数字显示:
python复制def __init__(self):
# ...前面的初始化代码...
# 数字时间显示
self.time_label = tk.Label(self.root,
font=('Arial', 12),
fg='white', bg='black')
self.time_label.pack(pady=10)
# 更新数字时间
self.update_digital()
def update_digital(self):
current_time = strftime('%H:%M:%S')
self.time_label.config(text=current_time)
self.root.after(1000, self.update_digital)
4.2 窗口拖动功能
由于移除了标题栏,我们需要自己实现窗口拖动功能:
python复制def __init__(self):
# ...前面的初始化代码...
# 绑定鼠标事件
self.canvas.bind("<ButtonPress-1>", self.start_move)
self.canvas.bind("<ButtonRelease-1>", self.stop_move)
self.canvas.bind("<B1-Motion>", self.on_move)
self._drag_data = {"x": 0, "y": 0}
def start_move(self, event):
self._drag_data["x"] = event.x
self._drag_data["y"] = event.y
def stop_move(self, event):
self._drag_data["x"] = 0
self._drag_data["y"] = 0
def on_move(self, event):
# 计算移动距离
delta_x = event.x - self._drag_data["x"]
delta_y = event.y - self._drag_data["y"]
# 获取当前窗口位置
x = self.root.winfo_x() + delta_x
y = self.root.winfo_y() + delta_y
# 移动窗口
self.root.geometry(f"+{x}+{y}")
4.3 性能优化与注意事项
-
减少重绘开销:
- 使用
canvas.coords()更新现有图形而非重新创建 - 避免在
update_clock中创建新对象
- 使用
-
内存泄漏预防:
- 确保
after回调不会无限累积 - 在窗口关闭时取消所有待处理的
after调用
- 确保
-
多显示器适配:
python复制# 获取屏幕尺寸 screen_width = self.root.winfo_screenwidth() screen_height = self.root.winfo_screenheight() # 将窗口定位到右下角 self.root.geometry(f"200x200+{screen_width-220}+{screen_height-220}") -
DPI缩放适配:
python复制# Windows系统下启用DPI感知 if os.name == 'nt': from ctypes import windll windll.shcore.SetProcessDpiAwareness(1)
5. 完整代码与打包发布
5.1 完整实现代码
python复制import tkinter as tk
from time import strftime
import time
import math
import os
class FloatingClock:
def __init__(self):
# DPI适配(Windows)
if os.name == 'nt':
from ctypes import windll
windll.shcore.SetProcessDpiAwareness(1)
# 主窗口设置
self.root = tk.Tk()
self.root.title("浮动时钟")
self.root.attributes('-topmost', True)
self.root.overrideredirect(True)
self.root.config(bg='black')
# 初始位置(右下角)
screen_width = self.root.winfo_screenwidth()
screen_height = self.root.winfo_screenheight()
self.root.geometry(f"200x200+{screen_width-220}+{screen_height-220}")
# Canvas设置
self.canvas = tk.Canvas(self.root, bg='black', highlightthickness=0)
self.canvas.pack(fill=tk.BOTH, expand=True)
# 表盘参数
self.radius = 80
self.center_x = 100
self.center_y = 100
# 绘制表盘
self.draw_face()
# 创建指针
self.hour_hand = self.canvas.create_line(0, 0, 0, 0, fill='white', width=4)
self.minute_hand = self.canvas.create_line(0, 0, 0, 0, fill='white', width=2)
self.second_hand = self.canvas.create_line(0, 0, 0, 0, fill='red', width=1)
# 数字时间显示
self.time_label = tk.Label(self.root, font=('Arial', 12),
fg='white', bg='black')
self.time_label.pack(pady=10)
# 绑定拖动事件
self._drag_data = {"x": 0, "y": 0}
self.canvas.bind("<ButtonPress-1>", self.start_move)
self.canvas.bind("<ButtonRelease-1>", self.stop_move)
self.canvas.bind("<B1-Motion>", self.on_move)
# 右键菜单(退出)
self.menu = tk.Menu(self.root, tearoff=0)
self.menu.add_command(label="退出", command=self.root.destroy)
self.canvas.bind("<Button-3>", self.show_menu)
# 启动时钟
self.update_clock()
self.update_digital()
def draw_face(self):
"""绘制表盘和刻度"""
# 外圆
self.canvas.create_oval(
self.center_x - self.radius,
self.center_y - self.radius,
self.center_x + self.radius,
self.center_y + self.radius,
outline='white', width=2
)
# 刻度
for i in range(12):
angle = i * 30
rad = math.radians(angle)
inner_x = self.center_x + (self.radius-10) * math.sin(rad)
inner_y = self.center_y - (self.radius-10) * math.cos(rad)
outer_x = self.center_x + self.radius * math.sin(rad)
outer_y = self.center_y - self.radius * math.cos(rad)
self.canvas.create_line(
inner_x, inner_y, outer_x, outer_y,
fill='white', width=2
)
def update_clock(self):
"""更新指针位置"""
now = time.localtime()
hour = now.tm_hour % 12
minute = now.tm_min
second = now.tm_sec
# 计算角度
hour_angle = math.radians((hour + minute/60) * 30 - 90)
minute_angle = math.radians(minute * 6 - 90)
second_angle = math.radians(second * 6 - 90)
# 更新指针
self.update_hand(self.hour_hand, hour_angle, 0.5)
self.update_hand(self.minute_hand, minute_angle, 0.7)
self.update_hand(self.second_hand, second_angle, 0.9)
self.root.after(1000, self.update_clock)
def update_hand(self, hand, angle, length_factor):
"""更新单个指针位置"""
x = self.center_x + self.radius * length_factor * math.cos(angle)
y = self.center_y + self.radius * length_factor * math.sin(angle)
self.canvas.coords(hand, self.center_x, self.center_y, x, y)
def update_digital(self):
"""更新数字时间"""
current_time = strftime('%H:%M:%S')
self.time_label.config(text=current_time)
self.root.after(1000, self.update_digital)
def start_move(self, event):
"""开始拖动"""
self._drag_data["x"] = event.x
self._drag_data["y"] = event.y
def stop_move(self, event):
"""停止拖动"""
self._drag_data["x"] = 0
self._drag_data["y"] = 0
def on_move(self, event):
"""处理拖动"""
delta_x = event.x - self._drag_data["x"]
delta_y = event.y - self._drag_data["y"]
x = self.root.winfo_x() + delta_x
y = self.root.winfo_y() + delta_y
self.root.geometry(f"+{x}+{y}")
def show_menu(self, event):
"""显示右键菜单"""
try:
self.menu.tk_popup(event.x_root, event.y_root)
finally:
self.menu.grab_release()
def run(self):
self.root.mainloop()
if __name__ == "__main__":
clock = FloatingClock()
clock.run()
5.2 打包为独立可执行文件
使用PyInstaller将脚本打包为exe文件,方便在没有Python环境的电脑上运行:
- 安装PyInstaller:
bash复制pip install pyinstaller
- 创建打包配置文件
clock.spec:
python复制# clock.spec
block_cipher = None
a = Analysis(['clock.py'],
pathex=[],
binaries=[],
datas=[],
hiddenimports=[],
hookspath=[],
runtime_hooks=[],
excludes=[],
win_no_prefer_redirects=False,
win_private_assemblies=False,
cipher=block_cipher,
noarchive=False)
pyz = PYZ(a.pure, a.zipped_data,
cipher=block_cipher)
exe = EXE(pyz,
a.scripts,
[],
exclude_binaries=True,
name='FloatingClock',
debug=False,
bootloader_ignore_signals=False,
strip=False,
upx=True,
console=False, # 不显示控制台窗口
icon='clock.ico') # 可选的图标文件
coll = COLLECT(exe,
a.binaries,
a.zipfiles,
a.datas,
strip=False,
upx=True,
name='FloatingClock')
- 执行打包命令:
bash复制pyinstaller clock.spec
打包完成后,dist目录下会生成可执行文件,可以直接运行或在开机时自动启动。
6. 样式自定义与扩展思路
6.1 自定义时钟外观
- 修改颜色方案:
python复制# 深色模式
self.root.config(bg='#222222')
self.canvas.config(bg='#222222')
self.time_label.config(bg='#222222', fg='#00FF00') # 绿色数字
# 指针颜色
self.hour_hand = self.canvas.create_line(..., fill='#FF9900') # 橙色
self.minute_hand = self.canvas.create_line(..., fill='#0099FF') # 蓝色
- 添加背景图片:
python复制from PIL import Image, ImageTk
# 加载图片
self.bg_image = Image.open("background.png")
self.bg_photo = ImageTk.PhotoImage(self.bg_image)
self.canvas.create_image(100, 100, image=self.bg_photo)
- 透明背景设置:
python复制# Windows系统支持
self.root.attributes('-transparentcolor', 'black') # 黑色部分透明
# Linux系统可能需要compositing窗口管理器支持
self.root.wm_attributes('-type', 'splash')
6.2 功能扩展思路
- 添加闹钟功能:
python复制def set_alarm(self, hour, minute):
self.alarm_time = (hour, minute)
def check_alarm(self):
now = time.localtime()
if hasattr(self, 'alarm_time') and \
(now.tm_hour, now.tm_min) == self.alarm_time and \
now.tm_sec == 0:
self.trigger_alarm()
self.root.after(1000, self.check_alarm)
def trigger_alarm(self):
# 闪烁效果
for i in range(5):
self.canvas.config(bg='red')
self.root.update()
time.sleep(0.5)
self.canvas.config(bg='black')
self.root.update()
time.sleep(0.5)
- 添加日期显示:
python复制def update_digital(self):
current_time = strftime('%H:%M:%S')
current_date = strftime('%Y-%m-%d %a')
self.time_label.config(text=f"{current_date}\n{current_time}")
self.root.after(1000, self.update_digital)
- 多时区支持:
python复制import pytz
from datetime import datetime
def update_clock(self):
# 获取指定时区时间
tz = pytz.timezone('America/New_York')
now = datetime.now(tz)
# 后续处理与之前相同...
- 系统托盘图标集成:
python复制import pystray
from PIL import Image
def create_tray_icon(self):
# 创建托盘图标
image = Image.new('RGB', (64, 64), 'black')
menu = pystray.Menu(
pystray.MenuItem('显示/隐藏', self.toggle_window),
pystray.MenuItem('退出', self.quit_app)
)
self.icon = pystray.Icon("clock_icon", image, "浮动时钟", menu)
def toggle_window(self, icon, item):
if self.root.state() == 'normal':
self.root.withdraw()
else:
self.root.deiconify()
def quit_app(self, icon, item):
self.icon.stop()
self.root.destroy()
在实际使用中,我发现这个时钟最实用的场景是作为第二显示器的时间参考,特别是在全屏演示或编码时。通过调整透明度,可以让它既不影响主要工作,又能随时查看时间。对于需要精确计时的任务,可以开启秒针显示;对于日常使用,隐藏秒针可以减少视觉干扰。
