Files
ctbjrj/.agents/skills/winforms-designer-package/references/icon-generation.md
T

7.0 KiB
Raw Blame History

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

完整模板(铜排+闪电图标,可直接改色改形)

# -*- 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 默认不自动用它):

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。