170 lines
7.0 KiB
Markdown
170 lines
7.0 KiB
Markdown
# 图标生成(Pillow 纯 Python 绘制 → 多尺寸 ICO)
|
||
|
||
用 Pillow 直接绘制图标 → 多尺寸 `.ico`,嵌入 exe。**纯 Python,零原生依赖**(不依赖 cairo/svglib,那些需要 `libcairo-2.dll`,Windows 上通常没有)。
|
||
|
||
> 本模板在 GCS 铜排壳体计算项目中验证过,产出 `app.ico`(16/32/48/64/128/256 六尺寸),通过 `/win32icon:app.ico` 嵌入 exe,并在所有窗体构造函数里 `ExtractAssociatedIcon` 设置窗口/任务栏图标。
|
||
|
||
## 为什么不用 SVG→ICO
|
||
|
||
常规路径 cairosvg / svglib+reportlab 都依赖原生 `libcairo-2.dll`,Windows 默认没有,装起来麻烦。**Pillow 直接画** 是零依赖、最稳的方案,缺点是只能画几何图形(渐变、滤镜要自己用代码模拟)。
|
||
|
||
## 核心思路
|
||
|
||
1. **高分辨率绘制**:在 512×512 画布上画完整图标(细节清晰)
|
||
2. **Pillow 自动缩放**:保存 ICO 时传 `sizes=[(16,16),(32,32),...]`,Pillow 自动从大图缩出各尺寸
|
||
3. **模拟渐变**:逐行/逐列插值颜色(`lerp_color`),Pillow 没有原生渐变填充
|
||
4. **圆角矩形**:用临时 mask 层 `rounded_rectangle` 做遮罩,再 paste
|
||
|
||
## 完整模板(铜排+闪电图标,可直接改色改形)
|
||
|
||
```python
|
||
# -*- coding: utf-8 -*-
|
||
"""用 Pillow 绘制图标 -> 多尺寸 app.ico。纯 Python,零原生依赖。
|
||
设计:深蓝→青蓝渐变圆角背景 + 三根铜排 + 接线端子 + 闪电(电流)。"""
|
||
import os
|
||
from PIL import Image, ImageDraw
|
||
|
||
SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__))
|
||
ICO_PATH = os.path.join(SCRIPT_DIR, "app.ico")
|
||
SIZES = [16, 32, 48, 64, 128, 256]
|
||
RENDER_SIZE = 512 # 高分辨率绘制
|
||
|
||
|
||
def lerp_color(c1, c2, t):
|
||
"""线性插值两个 (r,g,b) 颜色,t∈[0,1]。"""
|
||
return (
|
||
int(c1[0] + (c2[0] - c1[0]) * t),
|
||
int(c1[1] + (c2[1] - c1[1]) * t),
|
||
int(c1[2] + (c2[2] - c1[2]) * t),
|
||
)
|
||
|
||
|
||
def draw_gradient_rect(img, box, c_top, c_mid, c_bot, radius):
|
||
"""带垂直渐变的圆角矩形。用逐行填充近似渐变 + 圆角 mask。"""
|
||
x0, y0, x1, y1 = box
|
||
w, h = x1 - x0, y1 - y0
|
||
grad = Image.new("RGBA", (w, h), (0, 0, 0, 0))
|
||
gp = grad.load()
|
||
for y in range(h):
|
||
t = y / max(h - 1, 1)
|
||
if t < 0.55:
|
||
c = lerp_color(c_top, c_mid, t / 0.55)
|
||
else:
|
||
c = lerp_color(c_mid, c_bot, (t - 0.55) / 0.45)
|
||
for x in range(w):
|
||
gp[x, y] = (c[0], c[1], c[2], 255)
|
||
mask = Image.new("L", (w, h), 0)
|
||
ImageDraw.Draw(mask).rounded_rectangle([0, 0, w - 1, h - 1], radius=radius, fill=255)
|
||
img.paste(grad, (x0, y0), mask)
|
||
|
||
|
||
def draw_copper_bar(img, box, radius):
|
||
"""画一根铜排:铜橙→金黄渐变 + 左侧高光条。box=(x,y,w,h)。"""
|
||
bx, by, bw, bh = box
|
||
c_top, c_mid, c_bot = (255, 210, 127), (232, 163, 61), (181, 101, 29)
|
||
grad = Image.new("RGBA", (bw, bh), (0, 0, 0, 0))
|
||
gp = grad.load()
|
||
for y in range(bh):
|
||
t = y / max(bh - 1, 1)
|
||
c = lerp_color(c_top, c_mid, t / 0.45) if t < 0.45 else lerp_color(c_mid, c_bot, (t - 0.45) / 0.55)
|
||
for x in range(bw):
|
||
gp[x, y] = (c[0], c[1], c[2], 255)
|
||
mask = Image.new("L", (bw, bh), 0)
|
||
ImageDraw.Draw(mask).rounded_rectangle([0, 0, bw - 1, bh - 1], radius=radius, fill=255)
|
||
img.paste(grad, (bx, by), mask)
|
||
# 高光条
|
||
shine_w = max(bw // 4, 6)
|
||
shine = Image.new("RGBA", (shine_w, bh - 10), (0, 0, 0, 0))
|
||
sp = shine.load()
|
||
for y in range(shine.height):
|
||
a = int(180 * (1 - y / shine.height))
|
||
for x in range(shine_w):
|
||
sp[x, y] = (255, 255, 255, a)
|
||
smask = Image.new("L", (shine_w, shine.height), 0)
|
||
ImageDraw.Draw(smask).rounded_rectangle([0, 0, shine_w - 1, shine.height - 1], radius=max(radius - 2, 2), fill=255)
|
||
img.paste(shine, (bx + bw // 6, by + 5), smask)
|
||
|
||
|
||
def draw_lightning(img, cx, cy, scale):
|
||
"""画一道闪电。金黄色,带白色描边。"""
|
||
pts = [(4, -34), (-10, 4), (2, 4), (-8, 30), (16, -4), (4, -4)]
|
||
pts = [(cx + p[0] * scale, cy + p[1] * scale) for p in pts]
|
||
d = ImageDraw.Draw(img)
|
||
for dx, dy in [(-2, 0), (2, 0), (0, -2), (0, 2), (-2, -2), (2, 2), (-2, 2), (2, -2)]:
|
||
d.polygon([(p[0] + dx, p[1] + dy) for p in pts], fill=(255, 255, 255, 255))
|
||
d.polygon(pts, fill=(255, 224, 102, 255))
|
||
|
||
|
||
def draw_terminal(img, cx, cy, r):
|
||
"""铜排顶部接线端子。"""
|
||
d = ImageDraw.Draw(img)
|
||
d.ellipse([cx - r, cy - r, cx + r, cy + r], fill=(244, 228, 188, 255),
|
||
outline=(139, 90, 43, 255), width=2)
|
||
d.ellipse([cx - r // 3, cy - r // 3, cx + r // 3, cy + r // 3], fill=(58, 36, 16, 255))
|
||
|
||
|
||
def draw_icon(size):
|
||
s = size
|
||
img = Image.new("RGBA", (s, s), (0, 0, 0, 0))
|
||
margin = int(s * 0.03)
|
||
radius = int(s * 0.17)
|
||
# 背景渐变
|
||
draw_gradient_rect(img, (margin, margin, s - margin, s - margin),
|
||
c_top=(15, 61, 94), c_mid=(27, 108, 168), c_bot=(42, 157, 143),
|
||
radius=radius)
|
||
# 三根铜排(长短递减)
|
||
bar_w = int(s * 0.133)
|
||
bar_radius = max(int(s * 0.023), 3)
|
||
bars = [
|
||
(int(s * 0.227), int(s * 0.203), int(s * 0.594)), # 最长
|
||
(int(s * 0.434), int(s * 0.281), int(s * 0.516)),
|
||
(int(s * 0.641), int(s * 0.375), int(s * 0.422)), # 最短
|
||
]
|
||
term_r = int(s * 0.027)
|
||
for bx, by, bh in bars:
|
||
draw_terminal(img, bx + bar_w // 2, by - term_r + 2, term_r)
|
||
draw_copper_bar(img, (bx, by, bar_w, bh), bar_radius)
|
||
# 闪电
|
||
draw_lightning(img, int(s * 0.512), int(s * 0.613), s / 512.0)
|
||
return img
|
||
|
||
|
||
def main():
|
||
hi = draw_icon(RENDER_SIZE)
|
||
hi.save(ICO_PATH, format="ICO", sizes=[(sz, sz) for sz in SIZES])
|
||
print("已生成:", ICO_PATH, "尺寸:", SIZES)
|
||
|
||
if __name__ == "__main__":
|
||
main()
|
||
```
|
||
|
||
## 改造成其他图标
|
||
|
||
要换图标主题时,改 3 处:
|
||
1. **背景色**:`draw_gradient_rect` 的 `c_top/c_mid/c_bot`(RGB 三元组)
|
||
2. **主体元素**:替换 `draw_copper_bar` / `draw_lightning` 为你要画的元素(用 `ImageDraw` 的 ellipse/rectangle/polygon/line)
|
||
3. **布局坐标**:`draw_icon` 里各元素的 x/y/w/h(按 `s * 比例` 计算,这样任意尺寸都自适应)
|
||
|
||
## 嵌入 exe 的两步(配合 SKILL.md 主文档)
|
||
|
||
**1) 编译加参数**:`build.rsp` 或 csc 命令加 `/win32icon:app.ico`
|
||
|
||
**2) 窗体加图标加载代码**(exe 嵌入图标后,Form 默认不自动用它):
|
||
|
||
```csharp
|
||
private void LoadAppIcon()
|
||
{
|
||
try { this.Icon = System.Drawing.Icon.ExtractAssociatedIcon(
|
||
System.Windows.Forms.Application.ExecutablePath); }
|
||
catch { }
|
||
}
|
||
// 在每个窗体构造函数 InitializeComponent() 之后调用 LoadAppIcon();
|
||
```
|
||
|
||
这样 exe 文件图标、窗口标题栏、任务栏、Alt+Tab 都显示这个图标。
|
||
|
||
## 小尺寸清晰度技巧
|
||
|
||
- 16×16 这种小尺寸,复杂图案会糊成一片。设计时**主体元素要大、要少**(1~3 个),避免细节。
|
||
- Pillow 从 512 缩到 16 用的是抗锯齿,比手画 16 清晰。如果还是糊,可以 `draw_icon(16)` 单独画一版简化图标(只保留最核心的轮廓)再合并到 ICO。
|