打包技能入仓库:winforms-designer-package(含成套报价软件 8 步发版专章:版本号递增/组装/ISCC/更新包/git/Gitea Release/验收/故障速查)放入项目 .agents\skills,随项目走

This commit is contained in:
dqb-dev
2026-10-02 09:00:19 +08:00
parent ea9f3b6c29
commit 32076839d1
3 changed files with 513 additions and 0 deletions
@@ -0,0 +1,330 @@
---
name: winforms-designer-package
description: 把编译好的 .NET Framework 4.8 WinForms 项目(exe + 依赖 DLL + 图标)用 Inno Setup 6 打包成可分发的单文件安装包(setup.exe)。当用户说"打包""安装包""setup""发布""做成安装程序""Inno Setup""生成 setup.exe""打安装包""做成 exe 安装包""封装发布""更新包",或要求把 F:\pycode\wpf\src\WinFormsDesigner.Fx48 下 Samples\COOO\scr\<项目名> 里编译好的 exe 打包成安装包,或对"成套报价软件"项目发版打包(含版本号递增/Gitea Release/更新包的完整 8 步发版链,见文末专章)时,必须使用本技能。即使用户没说"Inno Setup",只要是要把编译产物做成可分发/可安装的形式就触发。涵盖:识别要打包的文件、写 setup.iss 脚本、中文语言包安装、编译安装包、图标嵌入、Program Files 权限处理、卸载清理等完整发布流程。
---
# WinForms 设计器项目打包器
把 winforms-designer-compile 技能编译出的 `<项目名>.exe` + 依赖 DLL + 图标,用 **Inno Setup 6** 打包成一个**单文件 setup.exe** 安装包,带中文安装向导、开始菜单/桌面快捷方式、控制面板卸载项。
> 本技能流程在 GCS 铜排壳体计算项目中**完整实战验证过**,每个步骤都真实跑通。
## ⛔ 铁律:不做测试安装,直接交付正式版(用户强制,2026-09-24)
**禁止 AI 自己做任何安装测试**——包括但不限于:
- 编译"测试变体"安装包(降权限 PrivilegesRequired=lowest / 改临时目录)再静默安装验证
- 把 setup.exe 装到本机临时目录、虚拟机去验证启动/卸载
- 安装后自动运行程序验证功能
**正确做法**:按需求直接编译**正式版** setup.exe(含正确的默认安装目录、权限、快捷方式),**立即交付给用户**,由**用户本人**安装和测试。测试中发现的任何问题由用户反馈后在下轮修复。
**原因**(xuanxing 项目实测教训):AI 侧测试变体要在用户桌面弹 UAC/错误框(如降权限后写公共桌面快捷方式报 `IPersistFile::Save 失败 0x80070005 拒绝访问`)、留残留目录和卸载表项,还要反复等待静默安装——比用户直接装一遍正式版**更慢更吵**。打包环节唯一要"验证"的只有 ISCC 编译输出 `Successful compile` 和产物存在。
## 前提:这是编译之后的环节
本技能接在 winforms-designer-compile 之后。进本技能前,`scr\<项目名>\` 目录里应该已经有:
- `<项目名>.exe`(csc 编译产物)
- 第三方 `.dll`(如 `AdvancedDataGridView.dll`)
- 可选 `app.ico`(程序图标)
如果还没有 exe,先去用 winforms-designer-compile 技能编译。
## 触发前提
- Inno Setup 6 已安装(默认在 `C:\Program Files (x86)\Inno Setup 6\`)。没装的话提示用户先装。
- 项目目录约定在 `F:\pycode\wpf\src\WinFormsDesigner.Fx48\bin\Debug\net48\Samples\COOO\scr\<项目名>\`。
---
## 📋 完整打包流程(5 步)
### 第 1 步:识别需要打包的文件
进项目目录 `scr\<项目名>\`,按下表分类:
| 文件类型 | 来源 | 是否打包 |
|---------|------|---------|
| `<项目名>.exe` | csc 编译产物 | ✅ 必需 |
| 第三方 `.dll`(如 `AdvancedDataGridView.dll`) | 设计器/代码依赖 | ✅ 必需(缺了程序跑不起来) |
| `app.ico`(若有) | 自制图标 | ✅ 建议带(用于快捷方式) |
| `.cs` / `.ui.cs` / `.xml` 源码 | — | ❌ 不打包 |
| `build.bat` / `build.rsp` / `*.log` | — | ❌ 不打包 |
| `单价.cfg` / `*.cfg` 配置 | 运行时生成 | ⚠️ 一般不打包(程序用默认值重建) |
**判断要打包哪些 DLL**:读 `<窗体名>.dependencies.txt`(设计器生成的权威清单),或直接 `ls *.dll` 看项目里实际存在的 DLL。
### 第 2 步:建发布目录 + 复制运行时文件
在 `Samples\COOO\` 下建 `release\<项目名>\`,**只复制运行时必需文件**(源码和构建文件不进):
```bash
mkdir -p 'F:\pycode\wpf\src\WinFormsDesigner.Fx48\bin\Debug\net48\Samples\COOO\release\<项目名>'
cp 'F:\...\scr\<项目名>\<项目名>.exe' 'F:\...\release\<项目名>\/'
cp 'F:\...\scr\<项目名>\AdvancedDataGridView.dll' 'F:\...\release\<项目名>\/' # 按实际依赖
cp 'F:\...\scr\<项目名>\app.ico' 'F:\...\release\<项目名>\/' # 若有图标
```
### 第 3 步:检查 Inno Setup 环境 + 中文语言包
**检查 Inno Setup**:
```bash
ls 'C:\Program Files (x86)\Inno Setup 6\ISCC.exe' 2>&1 && echo 'Inno Setup 已安装' || echo '未安装,需先装 Inno Setup 6'
```
**中文语言包**:Inno Setup 6 **默认不带中文简体语言包**。要做中文界面必须下载 `ChineseSimplified.isl` 放到 `C:\Program Files (x86)\Inno Setup 6\Languages\`(需管理员权限)。下载用 kira-96 社区维护版(已验证可用):
```bash
# 1. 下载到可写位置
curl -sL -o ChineseSimplified.isl 'https://raw.githubusercontent.com/kira-96/Inno-Setup-Chinese-Simplified-Translation/main/ChineseSimplified.isl'
# 2. 需管理员权限复制到 Program Files
powershell -NoProfile -Command "Start-Process cmd -ArgumentList '/c copy /Y ChineseSimplified.isl \"C:\Program Files (x86)\Inno Setup 6\Languages\\\"' -Verb RunAs -Wait"
# 3. 验证
ls 'C:\Program Files (x86)\Inno Setup 6\Languages\ChineseSimplified.isl'
```
> ⚠️ **下载的语言包必须带 UTF-8 BOM**。下载后检查,没有 BOM 就补一个,否则中文界面会乱码:
> ```bash
> python -c "
> import os
> p=r'C:\Program Files (x86)\Inno Setup 6\Languages\ChineseSimplified.isl'
> d=open(p,'rb').read()
> if d[:3]!=b'\xef\xbb\xbf': open(p,'wb').write(b'\xef\xbb\xbf'+d); print('已补 BOM')
> "
> ```
> (Program Files 需要管理员权限写入,可能要提权 PowerShell 跑。)
### 第 4 步:编写 setup.iss 脚本
在 `release\<项目名>\` 目录下建 `setup.iss`。下面是**实战验证过的完整模板**(GCS 铜排壳体计算项目),按需替换 `<...>` 占位:
```iss
; ============================================================
; <项目名> 安装包脚本 — Inno Setup 6
; 文件必须保存为 UTF-8 with BOM 编码(中文才能正确显示)
; ============================================================
[Setup]
AppName=<项目显示名>
AppVersion=1.0
AppPublisher=<作者或公司>
DefaultDirName=C:\<项目目录名>
DefaultGroupName=<项目显示名>
DisableProgramGroupPage=yes
OutputDir=Output
OutputBaseFilename=<项目名>_Setup
Compression=lzma2
SolidCompression=yes
WizardStyle=modern
ArchitecturesAllowed=x64
ArchitecturesInstallIn64BitMode=x64
PrivilegesRequired=admin
; 安装包自身图标(用程序的 app.ico)
SetupIconFile=app.ico
UninstallDisplayIcon={app}\app.ico
UninstallDisplayName=<项目显示名>
[Languages]
Name: "chinesesimp"; MessagesFile: "compiler:Languages\ChineseSimplified.isl"
[Tasks]
Name: "desktopicon"; Description: "创建桌面快捷方式(&D)"; GroupDescription: "附加图标:"
[Files]
Source: "<项目名>.exe"; DestDir: "{app}"; Flags: ignoreversion
Source: "<第三方DLL>.dll"; DestDir: "{app}"; Flags: ignoreversion
Source: "app.ico"; DestDir: "{app}"; Flags: ignoreversion
[Icons]
; 开始菜单快捷方式
Name: "{group}\<项目显示名>"; Filename: "{app}\<项目名>.exe"; IconFilename: "{app}\app.ico"; Comment: "<项目描述>"
Name: "{group}\卸载 <项目显示名>"; Filename: "{uninstallexe}"
; 桌面快捷方式
Name: "{commondesktop}\<项目显示名>"; Filename: "{app}\<项目名>.exe"; IconFilename: "{app}\app.ico"; Tasks: desktopicon
[Run]
; 安装完成可选启动
Filename: "{app}\<项目名>.exe"; Description: "立即启动 <项目显示名>"; Flags: nowait postinstall skipifsilent
[UninstallDelete]
; 卸载时彻底清理 {app} 目录
Type: filesandordirs; Name: "{app}"
```
#### setup.iss 段语法说明
| 段 | 作用 |
|----|------|
| `[Setup]` | 安装包基本信息、压缩、平台、权限。`DefaultDirName` = 默认安装位置(`{pf}` = Program Files,或直接写 `C:\xxx`)|
| `[Languages]` | 界面语言。中文用 `chinesesimp` + `ChineseSimplified.isl` |
| `[Tasks]` | 可勾选任务。桌面快捷方式任务默认勾选(**不要加 `Flags: checked`**,新版不认)|
| `[Files]` | 要打包的文件。`Flags: ignoreversion` = 不比对版本直接覆盖 |
| `[Icons]` | 快捷方式。`{group}` = 开始菜单文件夹,`{app}` = 安装目录,`{commondesktop}` = 公共桌面 |
| `[Run]` | 安装后动作。`postinstall` = 勾选"立即启动",`skipifsilent` = 静默安装时不启动 |
| `[UninstallDelete]` | 卸载时额外删除。`filesandordirs` 删整个目录及内容 |
#### ⚠️ setup.iss 的两个必做细节
**细节 1:必须 UTF-8 with BOM 编码**。中文(AppName、说明等)才能正确显示,否则装界面是乱码。写完 .iss 后检查并补 BOM:
```bash
python -c "
data = open('setup.iss','rb').read()
if data[:3] != b'\xef\xbb\xbf':
open('setup.iss','wb').write(b'\xef\xbb\xbf' + data)
print('已加 UTF-8 BOM')
"
```
**细节 2:`[Tasks]` 桌面快捷方式不要写 `Flags: checked`**。新版 Inno Setup 不认这个 flag,编译报 "Parameter Flags includes an unknown flag"。任务默认就是勾选的,直接省略 Flags。
### 第 5 步:编译安装包
```bash
cd 'F:\pycode\wpf\src\WinFormsDesigner.Fx48\bin\Debug\net48\Samples\COOO\release\<项目名>'
'C:\Program Files (x86)\Inno Setup 6\ISCC.exe' setup.iss 2>&1 | tail -5
```
**成功标志**:末尾输出 `Successful compile`,并在 `Output\<项目名>_Setup.exe` 生成安装包(通常 1~3 MB,LZMA2 压缩后远小于源文件总和)。
> 编译时 `Warning: A message named "..." has not been defined for "chinesesimp"` 是**正常的**——中文语言包版本与 Inno Setup 主程序版本不完全同步,缺失的消息自动用英文 Default.isl 回退,不影响功能。
---
## 发布目录最终结构
```
Samples\COOO\release\<项目名>\
├── <项目名>.exe ← 编译产物
├── <第三方DLL>.dll ← 依赖
├── app.ico ← 图标
├── setup.iss ← Inno Setup 脚本(源文件,留着方便改版重打包)
└── Output\
└── <项目名>_Setup.exe ← 最终交付的安装包
```
**交付物就是 `Output\<项目名>_Setup.exe` 这一个文件**,用户双击即可安装,自带中文向导、快捷方式、卸载项。
---
## 图标制作(可选,让 exe 有专属图标)
如果想给程序做专属图标嵌入 exe,详见 `references/icon-generation.md`(Pillow 纯 Python 绘制多尺寸 ICO 的完整模板,零原生依赖)。
简要流程:
1. 用 Pillow 绘制 512×512 图标 → 存成多尺寸 `app.ico`
2. 编译时在 `build.rsp` 加 `/win32icon:app.ico`
3. 窗体构造函数加 `LoadAppIcon()`(`Icon.ExtractAssociatedIcon`),让窗口/任务栏也显示图标
> 图标嵌入 exe 是**编译环节**做的事(/win32icon 是 csc 参数),打包环节只是把已有的 app.ico 一起带进安装包用于快捷方式。两环节配合。
---
## 常见问题排查
| 现象 | 原因 | 解决 |
|------|------|------|
| 安装界面中文乱码 | setup.iss 不是 UTF-8 BOM 编码 | 按第 4 步"细节 1"补 BOM 后重新编译 |
| `Couldn't open include file "...ChineseSimplified.isl"` | 没装中文语言包 | 按第 3 步下载 ChineseSimplified.isl 到 Inno Setup 的 Languages 目录 |
| 中文语言包装了还乱码 | 语言包 .isl 没 UTF-8 BOM | 给 .isl 文件补 BOM(需管理员权限)|
| `Parameter "Flags" includes an unknown flag` | `[Tasks]` 写了 `Flags: checked` | 删掉 `Flags: checked`,任务默认就勾选 |
| 安装到 Program Files 后程序保存配置报错 | exe 目录无写入权限 | 程序里配置文件改存 `%APPDATA%\<项目名>\`(用 `Environment.SpecialFolder.ApplicationData`),并在 `[UninstallDelete]` 加 `{userappdata}\<项目名>` 卸载时清理 |
| 装的 exe 双击没反应 | 缺少第三方 DLL | 检查 `[Files]` 是否把所有依赖 DLL 都打包了 |
| 卸载后配置文件残留 | `[UninstallDelete]` 没覆盖配置目录 | 加 `Type: filesandordirs; Name: "{userappdata}\<项目名>"` |
| `error CS0016: 未能写入输出文件`(重新编译 exe 时) | exe 被安装包运行的进程占用 | 先关掉运行中的程序再重新编译 |
---
## 发布前的完整流程(汇总)
```
1. [编译] 改完 .cs → 用 winforms-designer-compile 编译出 exe
2. [识别] 读 dependencies.txt 确定依赖 DLL
3. [建目录] 建 release\<项目名>\,复制 exe + DLL + ico
4. [写脚本] 写 setup.iss(用模板,改占位)+ 确保有 UTF-8 BOM
5. [语言包] 确认装了中文语言包(没有就下载,记得带 BOM)
6. [编译包] ISCC.exe setup.iss → 生成 Output\<项目名>_Setup.exe
7. [交付] 不做任何测试安装(见顶部铁律)——把 Output\<项目名>_Setup.exe 直接交给用户,由用户自行安装测试
```
---
## 🏭 项目专章:成套报价软件 完整发版体系(2026-10-01 定稿,用户强制)
> 仓库根:`C:\Users\099978\Documents\SharpDevelop Projects\成套报价软件`。
> 该项目发版**不走上面的通用 5 步**,走下面的 **8 步固定链**(多了版本号递增、更新包、git、Gitea Release 环节)。
> 用户说"打包/发版/上传到 git"针对本项目时,必须按本节执行,顺序不可换。
```
① 递增版本号 → ② 清进程+编译 → ③ make_package 组装 → ④ ISCC 安装包
→ ⑤ make_update_package 更新包 → ⑥ git 提交推送 → ⑦ Gitea Release → ⑧ 验收
```
### ① 递增版本号(版本号铁律:每次发版必须递增)
```powershell
powershell -File tools\递增版本号.ps1 # 修订号+1;大版本: tools\递增版本号.ps1 1.1.0
```
唯一事实源 = `成套报价软件\Properties\AssemblyInfo.cs` 的 AssemblyVersion(脚本同步 setup.iss 的 AppVersion 与安装包文件名)。关于软件页运行时自动读程序集版本。输出必须显示两处 `[OK]`。
### ② 清进程 + 编译
```powershell
Get-Process | Where-Object { $_.Path -like '*成套报价软件*' -or $_.Path -like '*SharpDevelop Projects*' } | Stop-Process -Force
cd 成套报价软件 # ★ rsp 相对路径基于此子目录,在仓库根跑必报 CS2001
& "C:\Windows\Microsoft.NET\Framework\v4.0.30319\csc.exe" "@..\tools\verify_build.rsp" /warn:1
```
ExitCode=0 且无 warning 才继续(rsp 默认 /warn:0 压警告,排查必须 /warn:1)。
### ③ 组装发布目录
```powershell
python tools\make_package.py # 仓库根跑
```
核对输出:`DLL 复制完成,缺失: 无`;cadbt 段只有 node+CAD-Viewer(独立扒图 exe 已于 2026-10-01 退役);无 `!!` 警告。
### ④ ISCC 安装包
```powershell
& "C:\Program Files (x86)\Inno Setup 6\ISCC.exe" "release\成套报价软件\setup.iss"
```
lzma2 约 2 分钟;产物 `release\成套报价软件\Output\成套报价软件_Setup_<版本>.exe`(约 120MB);`Successful compile` 才算成。中文语言包已装(chinesesimp 消息缺失 Warning 属正常)。
### ⑤ 更新包
先改 `tools\make_update_package.py` 两处:`PKG` 目录名带版本(如 `更新包_1.0.4`)+ `说明` 本期文案(只写用户可感知变化;使用方法写清覆盖哪个文件、怎么验证版本)。再跑 `python tools\make_update_package.py`。当前只含 成套报价软件.exe+使用说明(<1MB)。
### ⑥ git 提交推送
`git add -A` → `git commit -m "发版 v<版本>:..."` → `git push`(remote 已内嵌令牌)。
### ⑦ Gitea Release
tag=`v<版本>`(与 AssemblyInfo 一致)。PowerShell API 模式(令牌与仓库地址见 sd-enhanced-components 技能第 8 条或项目记忆):
- 建 Release:`Invoke-RestMethod -Method Post -Headers @{Authorization="token <令牌>"} -ContentType 'application/json; charset=utf-8' -Body ([Text.Encoding]::UTF8.GetBytes($body))`——**中文 body 必须 UTF8.GetBytes 否则乱码**
- 传附件:`System.Net.Http.HttpClient` + `MultipartFormDataContent`(字段名 `attachment`,PS5.1 无 -Form)
- Release 说明与更新包说明同源
### ⑧ 验收
附件大小合理(Setup ~120MB、更新包 <1MB);提醒用户装机验证:**关于软件页显示 v<版本>**、CAD/PDF 扒图窗口可用。
### 故障速查(本项目专属)
| 现象 | 处置 |
|------|------|
| 组装/替换文件报"正在使用" | ② 的进程没杀干净(查 node/AI 引擎/主程序) |
| 编译报 CS2001 全部源文件找不到 | 工作目录错了——必须 `成套报价软件\` 子目录 |
| ISCC 报文件找不到 | make_package 没跑或发布目录被清,回到 ③ |
| Release 附件上传 401 | 令牌失效——请用户在 Gitea 设置→应用重新生成(git remote 和 API 同步换) |
| push 被拒 fetch first | 用户网页传过文件——`git pull --rebase` 再 push,绝不 force push |
---
## 配套技能
- **winforms-designer-compile**:本技能的上游。先用它把 .cs + .ui.cs 编译成 exe,再交给本技能打包。
- **winforms-designer-xml**:生成/校验窗体 XML 布局,是整个流水线(XML → 编译 → 打包)的起点。
@@ -0,0 +1,169 @@
# 图标生成(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。