图标素材准备:如何制作和准备托盘图标?
托盘图标这东西,看着小,其实门道不少。我刚开始做系统托盘程序时,随便找了个 16x16 的 PNG 就往上怼,结果在 Windows 上显示模糊,在 macOS 上直接不认。嗯,后来才搞明白——不同平台对图标的格式、尺寸、透明度都有各自的脾气。
今天咱们就聊聊,怎么用 Python 的 PIL 库(Pillow)动态生成托盘图标。说白了,就是让你不用依赖外部图片文件,代码里直接画一个出来。
为什么需要动态生成图标?
你想想看,如果你的程序需要根据状态切换图标——比如网络连接正常显示绿色,断开显示红色——难道要准备几十个图标文件?太蠢了。动态生成,一个函数搞定所有状态。
我在项目中遇到过需要实时显示 CPU 占用率的托盘图标。每次刷新都重新画一个带数字的小图标,用 PIL 几行代码就解决了。
PIL 基础:创建空白图标
先来个最简单的。创建一个 32x32 的图标,背景透明,画个蓝色圆点。
from PIL import Image, ImageDraw
# 创建一个 RGBA 模式的图像,背景透明
icon_size = (32, 32)
img = Image.new('RGBA', icon_size, (0, 0, 0, 0))
# 获取绘图对象
draw = ImageDraw.Draw(img)
# 画一个蓝色圆点
draw.ellipse([4, 4, 28, 28], fill=(0, 120, 255, 255))
# 保存为 PNG
img.save('tray_icon.png')
这里要注意:Image.new 的第一个参数必须是 'RGBA',否则不支持透明背景。第二个参数是尺寸,第三个是背景色——(0,0,0,0) 表示全透明黑色。
绘制复杂形状:状态指示器
实际项目中,图标往往需要表达状态。比如绿色代表正常,黄色代表警告,红色代表错误。
def create_status_icon(status):
"""status: 'ok', 'warn', 'error'"""
colors = {
'ok': (0, 200, 83, 255), # 绿色
'warn': (255, 193, 7, 255), # 黄色
'error': (244, 67, 54, 255) # 红色
}
size = 32
img = Image.new('RGBA', (size, size), (0, 0, 0, 0))
draw = ImageDraw.Draw(img)
color = colors.get(status, colors['ok'])
# 画一个圆角矩形(模拟 macOS 风格)
draw.rounded_rectangle(
[2, 2, size-2, size-2],
radius=6,
fill=color
)
# 在中间画一个白色小图标(比如对勾或感叹号)
if status == 'ok':
# 画一个简单的对勾
draw.line([10, 16, 14, 20, 22, 12], fill=(255,255,255,255), width=3)
elif status == 'warn':
# 画感叹号
draw.rectangle([14, 8, 18, 18], fill=(255,255,255,255))
draw.ellipse([14, 20, 18, 24], fill=(255,255,255,255))
elif status == 'error':
# 画叉号
draw.line([10, 10, 22, 22], fill=(255,255,255,255), width=3)
draw.line([22, 10, 10, 22], fill=(255,255,255,255), width=3)
return img
# 使用示例
ok_icon = create_status_icon('ok')
ok_icon.save('status_ok.png')
这段代码里,我用 rounded_rectangle 画圆角矩形,比直接画圆形更现代。对勾和叉号用简单的线条实现,不用加载字体。
width 参数在 Pillow 8.0 以上才支持。如果你用的旧版本,线条会特别细,看不清。建议升级到最新版:pip install --upgrade Pillow。
添加文字:动态数字图标
有时候需要在图标上显示数字,比如未读消息数。用 PIL 的 ImageFont 可以加载系统字体。
from PIL import ImageFont
def create_number_icon(number, max_number=99):
"""生成带数字的图标,数字超过 max_number 显示 '+'"""
size = 32
img = Image.new('RGBA', (size, size), (0, 0, 0, 0))
draw = ImageDraw.Draw(img)
# 画红色圆形背景
draw.ellipse([0, 0, size-1, size-1], fill=(255, 59, 48, 255))
# 准备文字
text = str(number) if number <= max_number else f'{max_number}+'
# 加载字体(这里用默认字体,实际建议指定路径)
try:
font = ImageFont.truetype('arial.ttf', 16)
except:
font = ImageFont.load_default()
# 计算文字居中位置
bbox = draw.textbbox((0, 0), text, font=font)
text_width = bbox[2] - bbox[0]
text_height = bbox[3] - bbox[1]
x = (size - text_width) // 2
y = (size - text_height) // 2 - 1 # 微调垂直居中
# 绘制白色文字
draw.text((x, y), text, fill=(255, 255, 255, 255), font=font)
return img
# 生成示例
for num in [5, 23, 100]:
icon = create_number_icon(num)
icon.save(f'badge_{num}.png')
这里有个细节:textbbox 用来计算文字的实际尺寸,确保文字居中。不同字体、不同字号,bbox 返回的值不一样,所以每次都要算。
os.path.join 拼接路径,确保跨平台兼容。
多状态组合:复合图标
有时候一个图标需要表达多个信息。比如主图标 + 角标。像手机 App 那样,主图标是应用 logo,右上角有个小红点表示通知。
def create_composite_icon(main_color, badge_number):
"""主图标 + 数字角标"""
size = 32
img = Image.new('RGBA', (size, size), (0, 0, 0, 0))
draw = ImageDraw.Draw(img)
# 主图标:一个圆角矩形
draw.rounded_rectangle(
[0, 0, size-1, size-1],
radius=8,
fill=main_color
)
# 角标:在右上角画一个小红点
badge_size = 10
badge_x = size - badge_size - 2
badge_y = 2
draw.ellipse(
[badge_x, badge_y, badge_x + badge_size, badge_y + badge_size],
fill=(255, 59, 48, 255)
)
# 如果角标数字大于0,显示数字
if badge_number > 0:
try:
font = ImageFont.truetype('arial.ttf', 8)
except:
font = ImageFont.load_default()
text = str(badge_number) if badge_number < 10 else '9+'
bbox = draw.textbbox((0, 0), text, font=font)
tw = bbox[2] - bbox[0]
th = bbox[3] - bbox[1]
tx = badge_x + (badge_size - tw) // 2
ty = badge_y + (badge_size - th) // 2
draw.text((tx, ty), text, fill=(255,255,255,255), font=font)
return img
# 生成示例
icon = create_composite_icon((0, 122, 255, 255), 3)
icon.save('composite_icon.png')
这段代码展示了如何在一个图标里叠加多个元素。主图标是蓝色圆角矩形,右上角是红色角标,角标里还有数字。实际项目中,你可以把主图标换成你的应用 logo。
图标格式与平台适配
不同平台对图标格式要求不同。我整理了一个表格,方便你对照:
| 平台 | 推荐格式 | 推荐尺寸 | 注意事项 |
|---|---|---|---|
| Windows | .ico 或 .png | 16x16, 32x32, 48x48 | .ico 可包含多尺寸;.png 需设置透明度 |
| macOS | .png | 22x22, 44x44 (Retina) | 必须带 alpha 通道;建议用 @2x 高清图 |
| Linux | .png | 24x24, 32x32, 48x48 | 部分桌面环境支持 .svg |
| 跨平台 (通用) | .png | 32x32 | 兼容性最好,推荐首选 |
如果你需要生成 .ico 文件,PIL 也支持:
# 将 PNG 转换为 ICO
img = Image.open('tray_icon.png')
img.save('tray_icon.ico', format='ICO', sizes=[(16,16), (32,32), (48,48)])
sizes 参数可以指定多个尺寸。但 Windows 对 16x16 和 32x32 支持最好,48x48 在某些旧系统上可能不显示。我一般只生成 16 和 32 两个尺寸。
完整示例:动态状态切换
最后,给你一个完整的实战例子。假设你的程序需要根据网络状态切换托盘图标:
import time
from PIL import Image, ImageDraw
class TrayIconGenerator:
def __init__(self):
self.size = 32
def _create_base(self):
img = Image.new('RGBA', (self.size, self.size), (0, 0, 0, 0))
draw = ImageDraw.Draw(img)
return img, draw
def connected(self):
"""网络已连接:绿色圆点"""
img, draw = self._create_base()
draw.ellipse([4, 4, 28, 28], fill=(0, 200, 83, 255))
# 中间画个白色对勾
draw.line([11, 16, 14, 20, 21, 12], fill=(255,255,255,255), width=3)
return img
def disconnected(self):
"""网络断开:红色圆点"""
img, draw = self._create_base()
draw.ellipse([4, 4, 28, 28], fill=(244, 67, 54, 255))
# 中间画白色叉号
draw.line([11, 11, 21, 21], fill=(255,255,255,255), width=3)
draw.line([21, 11, 11, 21], fill=(255,255,255,255), width=3)
return img
def loading(self):
"""加载中:黄色圆点"""
img, draw = self._create_base()
draw.ellipse([4, 4, 28, 28], fill=(255, 193, 7, 255))
# 画三个小点表示加载
for i, offset in enumerate([10, 15, 20]):
draw.ellipse([offset, 14, offset+3, 17], fill=(255,255,255,255))
return img
# 使用示例
gen = TrayIconGenerator()
gen.connected().save('status_connected.png')
gen.disconnected().save('status_disconnected.png')
gen.loading().save('status_loading.png')
这个类封装了三种状态。实际使用时,你只需要根据网络状态调用对应方法,返回的 Image 对象可以直接传给系统托盘库(比如 pystray 或 infi.systray)。
好了,关于图标素材准备就聊这么多。说白了,PIL 就是你的画笔,想画什么画什么。下次你的程序需要根据状态换图标时,别再傻傻地准备几十个图片文件了——几行代码,动态生成,干净利落。