Files
desktop-plugin/tool/gen_app_icons.dart
T
dzc 527baf7ec5 fix(windows): 托盘图标不显示——tray_manager 原生 LoadImage 只认 .ico
用户 Windows 实测:日志"托盘已就绪"无报错、不在折叠区,但任务栏无图标。
源码级定位:tray_manager 0.5.3 Windows 原生端用
LoadImage(IMAGE_ICON, LR_LOADFROMFILE) 加载图标文件——该 Win32 API 只认
.ico,传入 png 返回 NULL → Shell_NotifyIcon 添加成功但图标为空 → 任务栏
不显示且全程无任何报错(mac 走 base64+NSImage 另一条路径所以正常)。

修复:Windows 托盘改用 assets/tray_icon_win.ico(16/24/32 三尺寸 PNG-in-ICO,
gen_app_icons.dart 生成,python struct 校验目录项 offset 严格对齐;ico 随
flutter_assets 打包,Debug/Release/Inno 全形态生效);删除无效的
tray_icon_win.png;AppTray 按平台选路径。
2026-09-21 16:27:03 +08:00

176 lines
6.5 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 生成全部应用图标(源:资安信 logo 1024x1024 深蓝盾牌+白色人形,透明底)
// dart run tool/gen_app_icons.dart <源图路径>
// 产物:
// macos/Runner/Assets.xcassets/AppIcon.appiconset/*.png (mac App 图标,白底圆角托+盾牌)
// windows/runner/resources/app_icon.ico (16~256 多尺寸 PNG-in-ICO)
// assets/tray_icon_mac.png / assets/tray_icon_win.png (托盘:白底圆盘+盾牌,深浅菜单栏均可见)
// assets/logo.png (原样拷贝,侧栏显示用)
// ignore_for_file: avoid_print
import 'dart:io';
import 'dart:typed_data';
import 'package:image/image.dart';
/// 是否在圆角矩形外(>0 外)
bool _cornerOutside(int x, int y, int x0, int y0, int x1, int y1, int r) {
final cx = x < x0 + r ? x0 + r : (x > x1 - r ? x1 - r : x);
final cy = y < y0 + r ? y0 + r : (y > y1 - r ? y1 - r : y);
final dx = x - cx, dy = y - cy;
if (dx > 0 || dy > 0) {
return dx * dx + dy * dy > r * r;
}
return false;
}
void main(List<String> args) {
final src = args.isNotEmpty ? args.first : 'logo-1024.png';
final logo = decodeImage(File(src).readAsBytesSync())!;
print('源图 ${logo.width}x${logo.height}');
// ---------- mac App 图标:白底圆角方托 + 盾牌居中 72% ----------
Image macIcon(int size) {
final img = Image(width: size, height: size, numChannels: 4);
final r = (size * 0.18).round(); // macOS squircle 风格圆角
final inset = (size * 0.14).round();
final inner = copyResize(logo, width: size - inset * 2);
for (var y = 0; y < size; y++) {
for (var x = 0; x < size; x++) {
if (!_cornerOutside(x, y, 0, 0, size - 1, size - 1, r)) {
final hasPixel = x >= inset &&
x < inset + inner.width &&
y >= inset &&
y < inset + inner.height;
final p = hasPixel ? inner.getPixel(x - inset, y - inset) : null;
if (p != null && p.a.toInt() > 0) {
img.setPixel(x, y, p);
} else {
img.setPixel(x, y, ColorRgba8(255, 255, 255, 255));
}
}
}
}
return img;
}
final appIconDir = Directory('macos/Runner/Assets.xcassets/AppIcon.appiconset');
const macSizes = <String, int>{
'app_icon_16.png': 16,
'app_icon_32.png': 32,
'app_icon_64.png': 64,
'app_icon_128.png': 128,
'app_icon_256.png': 256,
'app_icon_512.png': 512,
'app_icon_1024.png': 1024,
};
for (final e in macSizes.entries) {
final f = File('${appIconDir.path}/${e.key}');
if (!f.existsSync()) {
print('跳过(Contents.json 未声明):${e.key}');
continue;
}
f.writeAsBytesSync(encodePng(macIcon(e.value)));
print('written ${f.path} (${e.value}px)');
}
// ---------- Windows ICO(PNG-in-ICO) ----------
Uint8List buildIco(List<int> sizes) {
final pngs = <int, Uint8List>{};
for (final s in sizes) {
pngs[s] = Uint8List.fromList(encodePng(macIcon(s)));
}
final b = BytesBuilder();
b.addByte(0); b.addByte(0); // reserved
b.addByte(1); b.addByte(0); // type = icon
b.addByte(sizes.length); b.addByte(0); // count
var offset = 6 + 16 * sizes.length;
for (final s in sizes) {
final png = pngs[s]!;
b.addByte(s >= 256 ? 0 : s); // width
b.addByte(s >= 256 ? 0 : s); // height
b.addByte(0); // colors
b.addByte(0); // reserved
b.addByte(1); b.addByte(0); // planes
b.addByte(32); b.addByte(0); // bitcount
for (var i = 0; i < 4; i++) {
b.addByte((png.length >> (8 * i)) & 0xff);
}
for (var i = 0; i < 4; i++) {
b.addByte((offset >> (8 * i)) & 0xff);
}
offset += png.length;
}
for (final s in sizes) {
b.add(pngs[s]!);
}
return b.toBytes();
}
final ico = buildIco([16, 24, 32, 48, 64, 128, 256]);
File('windows/runner/resources/app_icon.ico').writeAsBytesSync(ico);
print('written windows/runner/resources/app_icon.ico (${ico.length} bytes)');
// ---------- 托盘:白底圆盘 + 盾牌居中 ----------
Image trayIcon(int size) {
final img = Image(width: size, height: size, numChannels: 4);
final c = (size - 1) / 2;
final rOuter = c;
final inset = (size * 0.13).round();
final inner = copyResize(logo, width: size - inset * 2);
for (var y = 0; y < size; y++) {
for (var x = 0; x < size; x++) {
final d = ((x - c) * (x - c) + (y - c) * (y - c));
if (d > rOuter * rOuter) continue; // 圆外透明
final hasPixel = x >= inset && x < inset + inner.width && y >= inset && y < inset + inner.height;
final p = hasPixel ? inner.getPixel(x - inset, y - inset) : null;
if (p != null && p.a.toInt() > 0) {
img.setPixel(x, y, p);
} else {
img.setPixel(x, y, ColorRgba8(255, 255, 255, 255));
}
}
}
return img;
}
Directory('assets').createSync(recursive: true);
File('assets/tray_icon_mac.png').writeAsBytesSync(encodePng(trayIcon(64)));
print('written assets/tray_icon_mac.png');
// Windows 托盘必须用 .ico:tray_manager 的原生端 LoadImage(IMAGE_ICON) 只认
// ico 文件(传 png 返回 NULL → 图标空占位、任务栏不显示且无任何报错)
Uint8List buildIcoFrom(Image Function(int) generator, List<int> sizes) {
final pngs = <int, Uint8List>{
for (final s in sizes) s: Uint8List.fromList(encodePng(generator(s))),
};
final b = BytesBuilder();
// ICONDIR:reserved=0, type=icon, count
b.addByte(0); b.addByte(0); b.addByte(1); b.addByte(0);
b.addByte(sizes.length); b.addByte(0);
var offset = 6 + 16 * sizes.length;
// 目录项全部在前
for (final s in sizes) {
final png = pngs[s]!;
b.addByte(s); b.addByte(s); b.addByte(0); b.addByte(0);
b.addByte(1); b.addByte(0); b.addByte(32); b.addByte(0);
for (var i = 0; i < 4; i++) {
b.addByte((png.length >> (8 * i)) & 0xff);
}
for (var i = 0; i < 4; i++) {
b.addByte((offset >> (8 * i)) & 0xff);
}
offset += png.length;
}
// PNG 数据全部在后
for (final s in sizes) {
b.add(pngs[s]!);
}
return b.toBytes();
}
final trayIco = buildIcoFrom(trayIcon, [16, 24, 32]);
File('assets/tray_icon_win.ico').writeAsBytesSync(trayIco);
print('written assets/tray_icon_win.ico (${trayIco.length} bytes)');
// ---------- 原样 logo ----------
File('assets/logo.png').writeAsBytesSync(encodePng(logo));
print('written assets/logo.png');
}