Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion desktop/windows/runner/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
6 changes: 6 additions & 0 deletions desktop/windows/runner/main.cpp
Original file line number Diff line number Diff line change
@@ -1,12 +1,18 @@
#include <flutter/dart_project.h>
#include <flutter/flutter_view_controller.h>
#include <shlobj.h>
#include <windows.h>

#include "flutter_window.h"
#include "utils.h"

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()) {
Expand Down
Binary file modified desktop/windows/runner/resources/app_icon.ico
Binary file not shown.
24 changes: 22 additions & 2 deletions desktop/windows/runner/win32_window.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,25 @@ void EnableFullDpiSupportIfAvailable(HWND hwnd) {
FreeLibrary(user32_module);
}

HICON LoadAppIconSized(int cx, int cy) {
return static_cast<HICON>(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<LPARAM>(big));
}
if (small) {
SendMessage(hwnd, WM_SETICON, ICON_SMALL, reinterpret_cast<LPARAM>(small));
}
}

} // namespace

// Manages the Win32Window's window class registration.
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -144,6 +163,7 @@ bool Win32Window::Create(const std::wstring& title,
return false;
}

ApplyWindowIcons(window);
UpdateTheme(window);

return OnCreate();
Expand Down
6 changes: 4 additions & 2 deletions packaging/windows/conch-setup.nsi
Original file line number Diff line number Diff line change
Expand Up @@ -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}"
Expand Down
141 changes: 119 additions & 22 deletions scripts/gen_icons.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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(
"<IiiHHIIiiII",
40,
w,
h * 2,
1,
32,
0,
len(xor) + len(and_mask),
0,
0,
0,
0,
)
return header + xor + and_mask


def write_windows_ico(win_master: Image.Image, dest: Path = WIN_ICO) -> 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("<HHH", 0, 1, len(entries)))
blobs = bytearray()
for w, h, data in entries:
out += struct.pack(
"<BBBBHHII",
0 if w >= 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__":
Expand Down