# 图标生成(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。