From 42e1f4345507d3d9dc55c0923ece9f2601deb6cc Mon Sep 17 00:00:00 2001 From: Z2549 <1211940555@qq.com> Date: Tue, 29 Sep 2026 12:54:43 +0800 Subject: [PATCH 1/2] =?UTF-8?q?fix:=20=E4=BF=AE=E6=AD=A3=E7=AA=97=E5=8F=A3?= =?UTF-8?q?=E5=9B=BE=E6=A0=87=E6=98=BE=E7=A4=BA=E4=B8=BA=E9=BB=98=E8=AE=A4?= =?UTF-8?q?=E7=A8=8B=E5=BA=8F=E5=9B=BE=E6=A0=87=E3=80=81=E6=B7=B1=E8=89=B2?= =?UTF-8?q?=E6=A8=A1=E5=BC=8F=E6=A0=87=E9=A2=98=E6=A0=8F=E6=8C=89=E9=92=AE?= =?UTF-8?q?=E4=B8=8D=E5=8F=AF=E8=A7=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 窗口创建后按 ICO 内嵌帧设置 WM_SETICON(16/32),不再使用默认程序图标 - 深色模式下用 DwmSetWindowAttribute(DWMWA_USE_IMMERSIVE_DARK_MODE) 让右上角按钮可见 - xmake: 链接 dwmapi,并通过 utils.bin2c 把 resources/injector.ico 编入只读数据 --- src/shell/script/binding_types_breeze_ui.cc | 95 +++++++++++++++++++++ xmake.lua | 6 +- 2 files changed, 100 insertions(+), 1 deletion(-) diff --git a/src/shell/script/binding_types_breeze_ui.cc b/src/shell/script/binding_types_breeze_ui.cc index bf26aa32..aa0112f7 100644 --- a/src/shell/script/binding_types_breeze_ui.cc +++ b/src/shell/script/binding_types_breeze_ui.cc @@ -10,10 +10,22 @@ #include #include #include +#include #include "../utils.h" #include "spdlog/spdlog.h" +// Windows 10 20H1 之前的系统用属性 19,SDK 头文件里可能还没有定义 +#ifndef DWMWA_USE_IMMERSIVE_DARK_MODE +#define DWMWA_USE_IMMERSIVE_DARK_MODE 20 +#endif +#define DWMWA_USE_IMMERSIVE_DARK_MODE_PRE_20H1 19 + +// resources/injector.ico 的原始内容,由 xmake 的 utils.bin2c 规则生成 +static unsigned char g_injector_ico[] = { +#include "injector.ico.h" +}; + namespace mb_shell::js { namespace { @@ -27,6 +39,87 @@ void set_owner_rt_recursive(const std::shared_ptr &widget, set_owner_rt_recursive(child, owner_rt); } } + +// 从 ico 数据里挑一帧最接近 want 尺寸的,直接交给系统造 HICON。 +// ico 里每一帧本身就是一份 RT_ICON 资源(BITMAPINFOHEADER + 像素 + AND 掩码), +// 所以 CreateIconFromResourceEx 可以直接吃,不需要自己解码。 +HICON load_icon_from_ico(const unsigned char *data, size_t size, int want) { + if (!data || size < 6) { + return nullptr; + } + auto rd16 = [](const unsigned char *p) { + return static_cast(p[0] | (p[1] << 8)); + }; + auto rd32 = [](const unsigned char *p) { + return static_cast(p[0] | (p[1] << 8) | (p[2] << 16) | + (static_cast(p[3]) << 24)); + }; + // ICONDIR: reserved=0, type=1(ICON), count + if (rd16(data) != 0 || rd16(data + 2) != 1) { + return nullptr; + } + const unsigned count = rd16(data + 4); + const unsigned char *frame = nullptr; + unsigned frame_size = 0, best_delta = ~0u; + for (unsigned i = 0; i < count; ++i) { + const unsigned char *entry = data + 6 + i * 16; + if (static_cast(entry - data) + 16 > size) { + break; + } + const unsigned w = entry[0] ? entry[0] : 256; + const unsigned h = entry[1] ? entry[1] : 256; + const unsigned bytes = rd32(entry + 8); + const unsigned offset = rd32(entry + 12); + if (w != h || static_cast(offset) + bytes > size) { + continue; + } + const unsigned delta = w > static_cast(want) + ? w - static_cast(want) + : static_cast(want) - w; + if (delta < best_delta) { + best_delta = delta; + frame = data + offset; + frame_size = bytes; + } + } + if (!frame) { + return nullptr; + } + return static_cast(CreateIconFromResourceEx( + const_cast(frame), frame_size, TRUE, 0x00030000, 0, 0, + LR_DEFAULTCOLOR)); +} + +// 窗口建好后补上系统标题栏相关的设置。GLFW 注册窗口类时找不到 GLFW_ICON 资源就会退回 +// IDI_APPLICATION(也就是 Windows 程序的那个通用图标),另外它也不会开启沉浸式深色标题栏, +// 于是深色主题下标题栏文字和最小化/最大化/关闭三个按钮会画成黑字黑底,跟背景糊在一起。 +void apply_native_window_style(HWND hwnd) { + if (!hwnd) { + return; + } + + const BOOL use_dark = is_light_mode() ? FALSE : TRUE; + if (FAILED(DwmSetWindowAttribute(hwnd, DWMWA_USE_IMMERSIVE_DARK_MODE, &use_dark, + sizeof(use_dark)))) { + DwmSetWindowAttribute(hwnd, DWMWA_USE_IMMERSIVE_DARK_MODE_PRE_20H1, &use_dark, + sizeof(use_dark)); + } + + // 图标做成进程内共享,省得每开一个窗口都多留两个句柄。 + // 也刻意不 DestroyIcon:窗口整个生命周期都会引用它,等进程退出由系统回收。 + static HICON small_icon = + load_icon_from_ico(g_injector_ico, sizeof(g_injector_ico), 16); + static HICON big_icon = + load_icon_from_ico(g_injector_ico, sizeof(g_injector_ico), 32); + if (small_icon) { + SendMessageW(hwnd, WM_SETICON, ICON_SMALL, + reinterpret_cast(small_icon)); + } + if (big_icon) { + SendMessageW(hwnd, WM_SETICON, ICON_BIG, + reinterpret_cast(big_icon)); + } +} } // namespace // Macro for getter/setter pairs with animation support @@ -741,6 +834,8 @@ breeze_ui::window::create_ex(std::string title, int width, int height, std::thread([win, on_close = std::move(on_close)]() { set_thread_name("breeze::js_window_renderer"); if (auto res = win->$render_target->init(); res) { + apply_native_window_style( + static_cast(win->$render_target->hwnd())); config::current->apply_fonts_to_nvg(win->$render_target->nvg); win->$render_target->show(); win->$render_target->start_loop(); diff --git a/xmake.lua b/xmake.lua index 6bb4a801..e3cc10e9 100644 --- a/xmake.lua +++ b/xmake.lua @@ -57,10 +57,14 @@ target("shell") add_defines("NOMINMAX", "WIN32_LEAN_AND_MEAN") add_packages("blook", "reflect-cpp", "wintoast", "yalantinglibs", "breeze-ui", "sentry-native", "watcher", "spdlog", "fmt", "breeze-js-runtime") - add_syslinks("oleacc", "ole32", "oleaut32", "uuid", "comctl32", "comdlg32", "gdi32", "user32", "shell32", "kernel32", "advapi32", "psapi", "Winhttp", "dbghelp") + add_syslinks("oleacc", "ole32", "oleaut32", "uuid", "comctl32", "comdlg32", "gdi32", "user32", "shell32", "kernel32", "advapi32", "psapi", "Winhttp", "dbghelp", "dwmapi") add_rules("utils.bin2obj", { extensions = {".js"} }) + add_rules("utils.bin2c", { + extensions = {".ico"} + }) + add_files("resources/injector.ico") set_version(version) set_configdir("src/shell") add_configfiles("src/shell/build_info.h.in") From b00a25347d7fe4ea9527443cb750d2d48e5fb4bb Mon Sep 17 00:00:00 2001 From: Z2549 <1211940555@qq.com> Date: Tue, 29 Sep 2026 16:12:29 +0800 Subject: [PATCH 2/2] =?UTF-8?q?fix:=20=E4=BF=AE=E6=AD=A3=E6=B3=A8=E5=85=A5?= =?UTF-8?q?=E5=99=A8=E7=AA=97=E5=8F=A3=E7=9A=84=E6=A0=87=E9=A2=98=E6=A0=8F?= =?UTF-8?q?=E5=9B=BE=E6=A0=87=E4=B8=8E=E6=B7=B1=E8=89=B2=E6=A8=A1=E5=BC=8F?= =?UTF-8?q?=E6=8C=89=E9=92=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 运行 breeze.exe 弹出的那个注入器窗口,标题栏图标仍是 Windows 通用程序图标, 深色主题下右上角最小化/最大化/关闭三个按钮是黑的、跟背景糊在一起。 两个原因: - GLFW 注册窗口类时按名字去 exe 资源里找 "GLFW_ICON" (breeze-glfw/src/win32_window.c),找不到就退回 IDI_APPLICATION; 而 src/inject/inject.rc 里图标组的名字实际是 "IDI_APP_ICON",两边对不上。 - GLFW 不会开启沉浸式深色标题栏,DWM 按浅色渲染标题栏, 于是深色主题下按钮和文字是黑的。 修法与 shell 侧(binding_types_breeze_ui.cc)保持一致:窗口建好后用 WM_SETICON 补上大小图标、用 DwmSetWindowAttribute 开沉浸式深色标题栏 (属性 20,失败回退 19)。StartInjectUI 和 ShowCrashDialog 两个窗口都处理。 xmake.lua 给 inject 目标补上 dwmapi。 shell.dll 那边的配置页窗口走 breeze_ui::window::create_ex,上一版已修,本次不动。 --- src/inject/inject.cc | 61 ++++++++++++++++++++++++++++++++++++++++++++ xmake.lua | 2 +- 2 files changed, 62 insertions(+), 1 deletion(-) diff --git a/src/inject/inject.cc b/src/inject/inject.cc index 3a072a4b..65f959a7 100644 --- a/src/inject/inject.cc +++ b/src/inject/inject.cc @@ -31,6 +31,8 @@ static unsigned char g_icon_png[] = { #include "Shlobj.h" +#include + namespace fs = std::filesystem; void init_inject_logger() { @@ -1123,6 +1125,63 @@ std::string getFontPath() { throw std::runtime_error("no font found"); } +// ── 原生标题栏修正 ────────────────────────────────────────────── +// GLFW 注册窗口类时按名字去 exe 资源里找 "GLFW_ICON" +// (见 breeze-glfw/src/win32_window.c 的 createNativeWindow),找不到就退回 +// IDI_APPLICATION —— 也就是 Windows 那个通用程序图标。而 src/inject/inject.rc +// 里图标组的名字实际是 "IDI_APP_ICON",两边对不上,所以标题栏图标一直是错的。 +// 另外 GLFW 不会开沉浸式深色标题栏,深色主题下最小化/最大化/关闭这三个按钮 +// 会按浅色标题栏渲染成黑字黑底,跟内容糊在一起。 +// +// Windows 10 20H1 之前的系统用属性 19,SDK 头文件里可能还没有定义 +#ifndef DWMWA_USE_IMMERSIVE_DARK_MODE +#define DWMWA_USE_IMMERSIVE_DARK_MODE 20 +#endif +#define DWMWA_USE_IMMERSIVE_DARK_MODE_PRE_20H1 19 + +static bool is_light_mode() { + DWORD data = 0; + DWORD size = sizeof(data); + auto res = RegGetValueW( + HKEY_CURRENT_USER, + L"Software\\Microsoft\\Windows\\CurrentVersion\\Themes\\Personalize", + L"AppsUseLightTheme", RRF_RT_REG_DWORD, nullptr, &data, &size); + return res == ERROR_SUCCESS && data == 1; +} + +static void apply_native_window_style(HWND hwnd) { + if (!hwnd) { + return; + } + + const BOOL use_dark = is_light_mode() ? FALSE : TRUE; + if (FAILED(DwmSetWindowAttribute(hwnd, DWMWA_USE_IMMERSIVE_DARK_MODE, + &use_dark, sizeof(use_dark)))) { + DwmSetWindowAttribute(hwnd, DWMWA_USE_IMMERSIVE_DARK_MODE_PRE_20H1, + &use_dark, sizeof(use_dark)); + } + + // 按资源组的真实名字取图标,系统会从图标组里挑最接近请求尺寸的那一帧。 + // 做成 static 共享,也刻意不 DestroyIcon —— 窗口整个生命周期都在引用它, + // 等进程退出由系统回收。 + static HICON small_icon = static_cast( + LoadImageW(GetModuleHandleW(nullptr), L"IDI_APP_ICON", IMAGE_ICON, + GetSystemMetrics(SM_CXSMICON), + GetSystemMetrics(SM_CYSMICON), LR_DEFAULTCOLOR)); + static HICON big_icon = static_cast( + LoadImageW(GetModuleHandleW(nullptr), L"IDI_APP_ICON", IMAGE_ICON, + GetSystemMetrics(SM_CXICON), GetSystemMetrics(SM_CYICON), + LR_DEFAULTCOLOR)); + if (small_icon) { + SendMessageW(hwnd, WM_SETICON, ICON_SMALL, + reinterpret_cast(small_icon)); + } + if (big_icon) { + SendMessageW(hwnd, WM_SETICON, ICON_BIG, + reinterpret_cast(big_icon)); + } +} + void StartInjectUI() { if (auto r = ui::render_target::init_global(); !r) { spdlog::error("Failed to initialize global render target."); @@ -1138,6 +1197,7 @@ void StartInjectUI() { spdlog::error("Failed to initialize render target."); return; } + apply_native_window_style(static_cast(rt.hwnd())); nvgCreateFont(rt.nvg, "main", getFontPath().c_str()); rt.root->emplace_child(); rt.start_loop(); @@ -1183,6 +1243,7 @@ void ShowCrashDialog() { return; } + apply_native_window_style(static_cast(rt.hwnd())); nvgCreateFont(rt.nvg, "main", getFontPath().c_str()); auto error_ui = rt.root->emplace_child(); diff --git a/xmake.lua b/xmake.lua index e3cc10e9..9c7d8ec5 100644 --- a/xmake.lua +++ b/xmake.lua @@ -100,7 +100,7 @@ target("asan_test") target("inject") set_kind("binary") - add_syslinks("psapi", "user32", "shell32", "kernel32", "advapi32", "taskschd", "ole32", "oleaut32", "taskschd", "comsupp") + add_syslinks("psapi", "user32", "shell32", "kernel32", "advapi32", "taskschd", "ole32", "oleaut32", "taskschd", "comsupp", "dwmapi") add_files("src/inject/*.cc", "src/inject/*.rc") add_packages("breeze-ui", "spdlog", "fmt") set_basename("breeze")