diff --git a/desktop/windows/runner/CMakeLists.txt b/desktop/windows/runner/CMakeLists.txt index 394917c..e2475cd 100644 --- a/desktop/windows/runner/CMakeLists.txt +++ b/desktop/windows/runner/CMakeLists.txt @@ -33,7 +33,7 @@ target_compile_definitions(${BINARY_NAME} PRIVATE "NOMINMAX") # Add dependency libraries and include directories. Add any application-specific # dependencies here. target_link_libraries(${BINARY_NAME} PRIVATE flutter flutter_wrapper_app) -target_link_libraries(${BINARY_NAME} PRIVATE "dwmapi.lib") +target_link_libraries(${BINARY_NAME} PRIVATE "dwmapi.lib" "shell32.lib") target_include_directories(${BINARY_NAME} PRIVATE "${CMAKE_SOURCE_DIR}") # Run the Flutter tool portions of the build. This must not be removed. diff --git a/desktop/windows/runner/main.cpp b/desktop/windows/runner/main.cpp index 7fd07aa..f952624 100644 --- a/desktop/windows/runner/main.cpp +++ b/desktop/windows/runner/main.cpp @@ -1,5 +1,6 @@ #include #include +#include #include #include "flutter_window.h" @@ -7,6 +8,11 @@ int APIENTRY wWinMain(_In_ HINSTANCE instance, _In_opt_ HINSTANCE prev, _In_ wchar_t *command_line, _In_ int show_command) { + // Bind the process to a stable AppUserModelID before any window is created, + // otherwise Windows 11 may show the generic document icon on the taskbar + // (especially when launched from a shortcut whose .ico path is stale). + SetCurrentProcessExplicitAppUserModelID(L"cn.apizero.Conch"); + // Attach to console when present (e.g., 'flutter run') or create a // new console when running with a debugger. if (!::AttachConsole(ATTACH_PARENT_PROCESS) && ::IsDebuggerPresent()) { diff --git a/desktop/windows/runner/resources/app_icon.ico b/desktop/windows/runner/resources/app_icon.ico index 1428994..354ec33 100644 Binary files a/desktop/windows/runner/resources/app_icon.ico and b/desktop/windows/runner/resources/app_icon.ico differ diff --git a/desktop/windows/runner/win32_window.cpp b/desktop/windows/runner/win32_window.cpp index 60608d0..bf6c7fb 100644 --- a/desktop/windows/runner/win32_window.cpp +++ b/desktop/windows/runner/win32_window.cpp @@ -53,6 +53,25 @@ void EnableFullDpiSupportIfAvailable(HWND hwnd) { FreeLibrary(user32_module); } +HICON LoadAppIconSized(int cx, int cy) { + return static_cast(LoadImage(GetModuleHandle(nullptr), + MAKEINTRESOURCE(IDI_APP_ICON), IMAGE_ICON, + cx, cy, LR_DEFAULTCOLOR)); +} + +void ApplyWindowIcons(HWND hwnd) { + HICON big = LoadAppIconSized(GetSystemMetrics(SM_CXICON), + GetSystemMetrics(SM_CYICON)); + HICON small = LoadAppIconSized(GetSystemMetrics(SM_CXSMICON), + GetSystemMetrics(SM_CYSMICON)); + if (big) { + SendMessage(hwnd, WM_SETICON, ICON_BIG, reinterpret_cast(big)); + } + if (small) { + SendMessage(hwnd, WM_SETICON, ICON_SMALL, reinterpret_cast(small)); + } +} + } // namespace // Manages the Win32Window's window class registration. @@ -95,8 +114,8 @@ const wchar_t* WindowClassRegistrar::GetWindowClass() { window_class.cbClsExtra = 0; window_class.cbWndExtra = 0; window_class.hInstance = GetModuleHandle(nullptr); - window_class.hIcon = - LoadIcon(window_class.hInstance, MAKEINTRESOURCE(IDI_APP_ICON)); + window_class.hIcon = LoadAppIconSized(GetSystemMetrics(SM_CXICON), + GetSystemMetrics(SM_CYICON)); window_class.hbrBackground = 0; window_class.lpszMenuName = nullptr; window_class.lpfnWndProc = Win32Window::WndProc; @@ -144,6 +163,7 @@ bool Win32Window::Create(const std::wstring& title, return false; } + ApplyWindowIcons(window); UpdateTheme(window); return OnCreate(); diff --git a/packaging/windows/conch-setup.nsi b/packaging/windows/conch-setup.nsi index 25bbcc3..5efd8e1 100644 --- a/packaging/windows/conch-setup.nsi +++ b/packaging/windows/conch-setup.nsi @@ -74,8 +74,10 @@ Section "Install" WriteRegStr HKLM "Software\${APP_NAME}" "InstallDir" "$INSTDIR" CreateDirectory "$SMPROGRAMS\${APP_NAME}" - CreateShortcut "$SMPROGRAMS\${APP_NAME}\${APP_NAME}.lnk" "$INSTDIR\${APP_EXE}" - CreateShortcut "$DESKTOP\${APP_NAME}.lnk" "$INSTDIR\${APP_EXE}" + ; Pin icon to the exe itself. A standalone .ico path goes stale when the + ; portable folder is moved, and Windows then shows the generic document icon. + CreateShortcut "$SMPROGRAMS\${APP_NAME}\${APP_NAME}.lnk" "$INSTDIR\${APP_EXE}" "" "$INSTDIR\${APP_EXE}" 0 + CreateShortcut "$DESKTOP\${APP_NAME}.lnk" "$INSTDIR\${APP_EXE}" "" "$INSTDIR\${APP_EXE}" 0 WriteUninstaller "$INSTDIR\uninstall.exe" WriteRegStr HKLM "${APP_REGKEY}" "DisplayName" "${APP_NAME}" diff --git a/scripts/gen_icons.py b/scripts/gen_icons.py index 3d01c2d..be68f02 100644 --- a/scripts/gen_icons.py +++ b/scripts/gen_icons.py @@ -8,24 +8,32 @@ - Windows:近满幅(砖≈96%)、适中圆角; 3. 扇出到 macOS AppIcon.appiconset 的各尺寸 PNG + Windows app_icon.ico。 -用法: python3 scripts/gen_icons.py [源图路径] +Pillow 的 ICO 写入会丢掉除 16x16 以外的尺寸(曾经产出 899 字节坏文件), +Windows 任务栏因此回落到空白文档图标。这里用手写 ICO: +小尺寸用 32-bit BMP,256 用 PNG。 + +用法: + python3 scripts/gen_icons.py [源图路径] + python3 scripts/gen_icons.py --from-branding # 只用已有 Windows 母版重做 ico """ +from __future__ import annotations + +import io +import struct import sys from pathlib import Path from PIL import Image, ImageDraw ROOT = Path(__file__).resolve().parent.parent -SRC = Path(sys.argv[1]) if len(sys.argv) > 1 else ( - Path.home() - / ".cursor/projects/Users-shcodegojo-Project/assets/conch_icon.png" -) BRAND_DIR = ROOT / "desktop/assets/branding" MAC_SET = ROOT / "desktop/macos/Runner/Assets.xcassets/AppIcon.appiconset" WIN_ICO = ROOT / "desktop/windows/runner/resources/app_icon.ico" +WIN_MASTER_PNG = BRAND_DIR / "conch_icon_windows.png" MAC_SIZES = [16, 32, 64, 128, 256, 512, 1024] -ICO_SIZES = [16, 24, 32, 48, 64, 128, 256] +# 20/40 覆盖 125%/150% 任务栏缩放;256 给资源管理器大图标。 +ICO_SIZES = [16, 20, 24, 32, 40, 48, 64, 128, 256] def crop_tile(src: Image.Image) -> Image.Image: @@ -58,33 +66,122 @@ def rounded(square: Image.Image, canvas: int, margin_frac: float, return out +def _png_bytes(im: Image.Image) -> bytes: + buf = io.BytesIO() + im.save(buf, format="PNG") + return buf.getvalue() + + +def _bmp_icon_bytes(im: Image.Image) -> bytes: + """ICO 用的 32-bit DIB: BITMAPINFOHEADER + 自下而上 BGRA XOR + AND mask。""" + im = im.convert("RGBA") + w, h = im.size + raw = im.tobytes("raw", "BGRA") + stride = w * 4 + xor = bytearray() + for y in range(h - 1, -1, -1): + xor += raw[y * stride:(y + 1) * stride] + and_stride = ((w + 31) // 32) * 4 + and_mask = bytes(and_stride * h) + header = struct.pack( + " None: + """写带多尺寸的合法 ICO,避免 Pillow ICO 只留下 16x16。""" + dest.parent.mkdir(parents=True, exist_ok=True) + images = [ + win_master.resize((s, s), Image.LANCZOS).convert("RGBA") + for s in ICO_SIZES + ] + entries: list[tuple[int, int, bytes]] = [] + for im in images: + w, h = im.size + data = _png_bytes(im) if max(w, h) >= 256 else _bmp_icon_bytes(im) + entries.append((w, h, data)) + + offset = 6 + 16 * len(entries) + out = bytearray(struct.pack("= 256 else w, + 0 if h >= 256 else h, + 0, + 0, + 1, + 32, + len(data), + offset, + ) + offset += len(data) + blobs += data + dest.write_bytes(out + blobs) + print(f"windows ico updated: {dest} ({dest.stat().st_size} bytes, {len(entries)} sizes)") + + +def resolve_source() -> Path | None: + args = [a for a in sys.argv[1:] if not a.startswith("-")] + if args: + return Path(args[0]) + default = ( + Path.home() + / ".cursor/projects/Users-shcodegojo-Project/assets/conch_icon.png" + ) + if default.exists(): + return default + return None + + def main() -> None: - if not SRC.exists(): - sys.exit(f"source image not found: {SRC}") - BRAND_DIR.mkdir(parents=True, exist_ok=True) + from_branding = "--from-branding" in sys.argv + src_path = resolve_source() + + if from_branding or src_path is None: + if not WIN_MASTER_PNG.exists(): + sys.exit( + "source image not found, and " + f"{WIN_MASTER_PNG} is missing (cannot rebuild ico)" + ) + print(f"rebuilding ICO from {WIN_MASTER_PNG}") + write_windows_ico(Image.open(WIN_MASTER_PNG).convert("RGBA")) + return + + if not src_path.exists(): + sys.exit(f"source image not found: {src_path}") - src = Image.open(SRC).convert("RGB") + BRAND_DIR.mkdir(parents=True, exist_ok=True) + src = Image.open(src_path).convert("RGB") square = crop_tile(src) mac_master = rounded(square, 1024, margin_frac=0.098, radius_frac=0.224) win_master = rounded(square, 1024, margin_frac=0.012, radius_frac=0.16) mac_master.save(BRAND_DIR / "conch_icon_macos.png") - win_master.save(BRAND_DIR / "conch_icon_windows.png") + win_master.save(WIN_MASTER_PNG) square.save(BRAND_DIR / "conch_icon_source_square.png") print(f"masters -> {BRAND_DIR}") - for size in MAC_SIZES: - img = mac_master.resize((size, size), Image.LANCZOS) - img.save(MAC_SET / f"app_icon_{size}.png") - print(f"macOS appiconset updated: {MAC_SET}") + if MAC_SET.exists(): + for size in MAC_SIZES: + img = mac_master.resize((size, size), Image.LANCZOS) + img.save(MAC_SET / f"app_icon_{size}.png") + print(f"macOS appiconset updated: {MAC_SET}") - frames = [win_master.resize((s, s), Image.LANCZOS) for s in ICO_SIZES] - frames[0].save( - WIN_ICO, format="ICO", - sizes=[(s, s) for s in ICO_SIZES], - append_images=frames[1:], - ) - print(f"windows ico updated: {WIN_ICO}") + write_windows_ico(win_master) if __name__ == "__main__":