bun-app-window 0.1.0 → 0.2.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +202 -0
- package/README.md +35 -3
- package/README.zh-CN.md +35 -3
- package/README.zh-TW.md +35 -3
- package/dist/libshell.dll +0 -0
- package/package.json +8 -10
- package/src/shell/native-asset.d.ts +18 -0
- package/src/shell/resolve.ts +1 -1
- package/native/build.ps1 +0 -67
- package/native/fetch.ps1 +0 -66
- package/native/shell.cc +0 -2134
- package/vendor/webview/compatibility/CMakeLists.txt +0 -3
- package/vendor/webview/compatibility/mingw/CMakeLists.txt +0 -14
- package/vendor/webview/compatibility/mingw/include/EventToken.h +0 -25
- package/vendor/webview/core/CMakeLists.txt +0 -94
- package/vendor/webview/core/include/webview/api.h +0 -251
- package/vendor/webview/core/include/webview/backends.hh +0 -40
- package/vendor/webview/core/include/webview/c_api_impl.hh +0 -258
- package/vendor/webview/core/include/webview/detail/backends/cocoa_webkit.hh +0 -626
- package/vendor/webview/core/include/webview/detail/backends/gtk_webkitgtk.hh +0 -355
- package/vendor/webview/core/include/webview/detail/backends/win32_edge.hh +0 -909
- package/vendor/webview/core/include/webview/detail/basic_result.hh +0 -119
- package/vendor/webview/core/include/webview/detail/engine_base.hh +0 -382
- package/vendor/webview/core/include/webview/detail/exceptions.hh +0 -42
- package/vendor/webview/core/include/webview/detail/json.hh +0 -337
- package/vendor/webview/core/include/webview/detail/native_library.hh +0 -170
- package/vendor/webview/core/include/webview/detail/optional.hh +0 -118
- package/vendor/webview/core/include/webview/detail/platform/darwin/cocoa/NSApplication.hh +0 -107
- package/vendor/webview/core/include/webview/detail/platform/darwin/cocoa/NSBundle.hh +0 -56
- package/vendor/webview/core/include/webview/detail/platform/darwin/cocoa/NSEvent.hh +0 -71
- package/vendor/webview/core/include/webview/detail/platform/darwin/cocoa/NSInvocation.hh +0 -66
- package/vendor/webview/core/include/webview/detail/platform/darwin/cocoa/NSMethodSignature.hh +0 -52
- package/vendor/webview/core/include/webview/detail/platform/darwin/cocoa/NSNotification.hh +0 -51
- package/vendor/webview/core/include/webview/detail/platform/darwin/cocoa/NSNumber.hh +0 -53
- package/vendor/webview/core/include/webview/detail/platform/darwin/cocoa/NSObject.hh +0 -51
- package/vendor/webview/core/include/webview/detail/platform/darwin/cocoa/NSOpenPanel.hh +0 -71
- package/vendor/webview/core/include/webview/detail/platform/darwin/cocoa/NSPoint.hh +0 -53
- package/vendor/webview/core/include/webview/detail/platform/darwin/cocoa/NSRect.hh +0 -53
- package/vendor/webview/core/include/webview/detail/platform/darwin/cocoa/NSSavePanel.hh +0 -52
- package/vendor/webview/core/include/webview/detail/platform/darwin/cocoa/NSSize.hh +0 -53
- package/vendor/webview/core/include/webview/detail/platform/darwin/cocoa/NSString.hh +0 -87
- package/vendor/webview/core/include/webview/detail/platform/darwin/cocoa/NSURL.hh +0 -63
- package/vendor/webview/core/include/webview/detail/platform/darwin/cocoa/NSURLRequest.hh +0 -52
- package/vendor/webview/core/include/webview/detail/platform/darwin/cocoa/NSValue.hh +0 -56
- package/vendor/webview/core/include/webview/detail/platform/darwin/cocoa/NSView.hh +0 -91
- package/vendor/webview/core/include/webview/detail/platform/darwin/cocoa/NSWindow.hh +0 -137
- package/vendor/webview/core/include/webview/detail/platform/darwin/cocoa/cocoa.hh +0 -59
- package/vendor/webview/core/include/webview/detail/platform/darwin/cocoa/types.hh +0 -49
- package/vendor/webview/core/include/webview/detail/platform/darwin/objc/Class.hh +0 -55
- package/vendor/webview/core/include/webview/detail/platform/darwin/objc/autoreleasepool.hh +0 -70
- package/vendor/webview/core/include/webview/detail/platform/darwin/objc/invoke.hh +0 -75
- package/vendor/webview/core/include/webview/detail/platform/darwin/objc/memory.hh +0 -57
- package/vendor/webview/core/include/webview/detail/platform/darwin/objc/objc.hh +0 -47
- package/vendor/webview/core/include/webview/detail/platform/darwin/webkit/WKOpenPanelParameters.hh +0 -57
- package/vendor/webview/core/include/webview/detail/platform/darwin/webkit/WKScriptMessage.hh +0 -51
- package/vendor/webview/core/include/webview/detail/platform/darwin/webkit/WKUserContentController.hh +0 -61
- package/vendor/webview/core/include/webview/detail/platform/darwin/webkit/WKUserScript.hh +0 -71
- package/vendor/webview/core/include/webview/detail/platform/darwin/webkit/WKWebView.hh +0 -108
- package/vendor/webview/core/include/webview/detail/platform/darwin/webkit/WKWebViewConfiguration.hh +0 -60
- package/vendor/webview/core/include/webview/detail/platform/darwin/webkit/webkit.hh +0 -46
- package/vendor/webview/core/include/webview/detail/platform/linux/gtk/compat.hh +0 -137
- package/vendor/webview/core/include/webview/detail/platform/linux/webkitgtk/compat.hh +0 -142
- package/vendor/webview/core/include/webview/detail/platform/linux/webkitgtk/dmabuf.hh +0 -167
- package/vendor/webview/core/include/webview/detail/platform/windows/com_init_wrapper.hh +0 -126
- package/vendor/webview/core/include/webview/detail/platform/windows/dpi.hh +0 -161
- package/vendor/webview/core/include/webview/detail/platform/windows/dwmapi.hh +0 -67
- package/vendor/webview/core/include/webview/detail/platform/windows/iid.hh +0 -78
- package/vendor/webview/core/include/webview/detail/platform/windows/ntdll.hh +0 -58
- package/vendor/webview/core/include/webview/detail/platform/windows/reg_key.hh +0 -135
- package/vendor/webview/core/include/webview/detail/platform/windows/shcore.hh +0 -59
- package/vendor/webview/core/include/webview/detail/platform/windows/theme.hh +0 -76
- package/vendor/webview/core/include/webview/detail/platform/windows/user32.hh +0 -90
- package/vendor/webview/core/include/webview/detail/platform/windows/version.hh +0 -151
- package/vendor/webview/core/include/webview/detail/platform/windows/webview2/loader.hh +0 -391
- package/vendor/webview/core/include/webview/detail/user_script.hh +0 -76
- package/vendor/webview/core/include/webview/detail/utility/string.hh +0 -99
- package/vendor/webview/core/include/webview/errors.h +0 -67
- package/vendor/webview/core/include/webview/errors.hh +0 -85
- package/vendor/webview/core/include/webview/json_deprecated.hh +0 -61
- package/vendor/webview/core/include/webview/macros.h +0 -123
- package/vendor/webview/core/include/webview/types.h +0 -81
- package/vendor/webview/core/include/webview/types.hh +0 -48
- package/vendor/webview/core/include/webview/version.h +0 -67
- package/vendor/webview/core/include/webview/webview.h +0 -32
- package/vendor/webview/core/include/webview.h +0 -15
- package/vendor/webview/core/src/webview.cc +0 -1
- package/vendor/webview/core/tests/CMakeLists.txt +0 -18
- package/vendor/webview/core/tests/src/functional_tests.cc +0 -316
- package/vendor/webview/core/tests/src/unit_tests.cc +0 -188
- package/vendor/webview2/build/native/include/WebView2.h +0 -23568
- package/vendor/webview2/build/native/include/WebView2EnvironmentOptions.h +0 -144
package/native/shell.cc
DELETED
|
@@ -1,2134 +0,0 @@
|
|
|
1
|
-
/*
|
|
2
|
-
* shell.cc — 最小窗口 ABI(含无边框模式)
|
|
3
|
-
*
|
|
4
|
-
* 设计要点:
|
|
5
|
-
* 1. UI 线程由本文件创建,webview_run 在其上阻塞;JS 侧 shell_start 立即返回。
|
|
6
|
-
* 2. 无边框模式自己建窗口,再把 HWND 交给 webview(webview_create 的嵌入模式),
|
|
7
|
-
* COM 样板、DPI、WebView2 初始化仍由平台层负责。
|
|
8
|
-
* 3. 宿主 -> 页面的控制命令经队列 + webview_dispatch 投递到 UI 线程,
|
|
9
|
-
* 避免跨线程触碰线程亲和的 WebView2。
|
|
10
|
-
* 4. 页面 -> 宿主走本地 HTTP/WebSocket,不暴露 C -> JS 回调。
|
|
11
|
-
* 5. 不使用 webview_set_size:它会 SetWindowPos 顶层窗口,嵌入模式下会打乱几何。
|
|
12
|
-
* 改为直接 SetWindowPos 子 widget,由其 WM_SIZE 驱动 put_Bounds。
|
|
13
|
-
*/
|
|
14
|
-
|
|
15
|
-
#define WIN32_LEAN_AND_MEAN
|
|
16
|
-
#define NOMINMAX
|
|
17
|
-
// MinGW 的 winuser.h 用 WINVER 门控 DPI 相关 API:
|
|
18
|
-
// AdjustWindowRectExForDpi 需要 >= 0x0605,GetDpiForWindow 需要 >= 0x0A00
|
|
19
|
-
#define WINVER 0x0A00
|
|
20
|
-
#define _WIN32_WINNT 0x0A00
|
|
21
|
-
#include <windows.h>
|
|
22
|
-
#include <windowsx.h>
|
|
23
|
-
#include <dwmapi.h>
|
|
24
|
-
#include <shellapi.h>
|
|
25
|
-
#include <shlobj.h>
|
|
26
|
-
#include <shlwapi.h>
|
|
27
|
-
|
|
28
|
-
#include <cstdio>
|
|
29
|
-
#include <cstdlib>
|
|
30
|
-
#include <atomic>
|
|
31
|
-
#include <deque>
|
|
32
|
-
#include <mutex>
|
|
33
|
-
#include <string>
|
|
34
|
-
#include <vector>
|
|
35
|
-
|
|
36
|
-
#include "webview/webview.h"
|
|
37
|
-
|
|
38
|
-
// WebView2 的 COM 头。只为了拿 ICoreWebView2Controller2::put_DefaultBackgroundColor:
|
|
39
|
-
// WebView2 表面在页面首帧绘制之前显示的是它自己的默认白底,深色应用启动会白闪一下。
|
|
40
|
-
// MinGW 下没有 IID 符号库,所以下面的 QueryInterface 一律用本文件内的 GUID 字面量。
|
|
41
|
-
#include <WebView2.h>
|
|
42
|
-
|
|
43
|
-
namespace {
|
|
44
|
-
|
|
45
|
-
// ---- 对外常量(与 TS 侧保持字面一致) ----
|
|
46
|
-
enum ShellFlag : int {
|
|
47
|
-
SHELL_FLAG_FRAMELESS = 1,
|
|
48
|
-
SHELL_FLAG_TOPMOST = 2,
|
|
49
|
-
SHELL_FLAG_CENTER = 4,
|
|
50
|
-
SHELL_FLAG_RESIZABLE = 8,
|
|
51
|
-
// 创建后先不显示,等调用方确认页面就绪再 shell_show()。
|
|
52
|
-
// 用于消除「空窗口先出现、内容后填上」的首帧闪烁。
|
|
53
|
-
SHELL_FLAG_DEFER_SHOW = 16,
|
|
54
|
-
// 接受文件/文件夹拖放,落下的路径以 shell-msg 的 {t:'drop',paths:[...]} 推给页面。
|
|
55
|
-
SHELL_FLAG_ACCEPT_DROP = 32,
|
|
56
|
-
// 首次显示时直接以最大化出现。
|
|
57
|
-
//
|
|
58
|
-
// 为什么不「先 show 再 maximize」:那会有一次「小窗闪一下再撑满」的跳变,
|
|
59
|
-
// 交付观感很差。这里把最大化合成到 show_now() 的那一次 ShowWindow,
|
|
60
|
-
// 窗口从隐藏直接变成最大化,中间没有任何可见帧。
|
|
61
|
-
// 与 FLAG_CENTER 同时给时以本项为准——最大化会覆盖位置。
|
|
62
|
-
SHELL_FLAG_START_MAXIMIZED = 64,
|
|
63
|
-
};
|
|
64
|
-
|
|
65
|
-
enum ShellState : int {
|
|
66
|
-
SHELL_STATE_MAXIMIZED = 1,
|
|
67
|
-
SHELL_STATE_MINIMIZED = 2,
|
|
68
|
-
SHELL_STATE_FOCUSED = 4,
|
|
69
|
-
};
|
|
70
|
-
|
|
71
|
-
enum ShellEdge : int {
|
|
72
|
-
SHELL_EDGE_LEFT = 0,
|
|
73
|
-
SHELL_EDGE_RIGHT = 1,
|
|
74
|
-
SHELL_EDGE_TOP = 2,
|
|
75
|
-
SHELL_EDGE_BOTTOM = 3,
|
|
76
|
-
SHELL_EDGE_TOPLEFT = 4,
|
|
77
|
-
SHELL_EDGE_TOPRIGHT = 5,
|
|
78
|
-
SHELL_EDGE_BOTTOMLEFT = 6,
|
|
79
|
-
SHELL_EDGE_BOTTOMRIGHT = 7,
|
|
80
|
-
};
|
|
81
|
-
|
|
82
|
-
enum class cmd_kind { eval, title, minimize, maximize_toggle, close, drag, resize, show, zoom, reload, devtools, fullscreen, restore_bounds, browser_ui, app_menu };
|
|
83
|
-
|
|
84
|
-
struct cmd {
|
|
85
|
-
cmd_kind kind;
|
|
86
|
-
std::string arg;
|
|
87
|
-
};
|
|
88
|
-
|
|
89
|
-
std::atomic<webview_t> g_webview{nullptr};
|
|
90
|
-
HWND g_hwnd = nullptr; // 顶层窗口(自有或 webview 创建)
|
|
91
|
-
HWND g_widget = nullptr; // WebView2 容器子窗口
|
|
92
|
-
WNDPROC g_widget_orig = nullptr;
|
|
93
|
-
WNDPROC g_toplevel_orig = nullptr; // 仅带边框模式:webview 自建窗口的子类化
|
|
94
|
-
HANDLE g_ready = nullptr;
|
|
95
|
-
|
|
96
|
-
bool g_frameless = false;
|
|
97
|
-
std::atomic<bool> g_shown{false}; // 是否已完成首次显示(忽略创建期 WM_DPICHANGED 建议矩形;跨线程读)
|
|
98
|
-
std::atomic<int> g_round_state{-1}; // 圆角状态缓存:-1 未知 / 0 直角 / 1 圆角(跨线程读)
|
|
99
|
-
bool g_resizable = true;
|
|
100
|
-
bool g_defer_show = false; // 延迟显示:等 shell_show() 再 ShowWindow
|
|
101
|
-
bool g_accept_drop = false; // 接受文件拖放
|
|
102
|
-
bool g_start_maximized = false; // 首次显示即以最大化出现
|
|
103
|
-
std::wstring g_app_id; // AppUserModelID:决定任务栏分组与图标归属
|
|
104
|
-
int g_min_w = 320;
|
|
105
|
-
int g_min_h = 200;
|
|
106
|
-
int g_start_w = 1200;
|
|
107
|
-
int g_start_h = 800;
|
|
108
|
-
|
|
109
|
-
// 两把锁,锁序固定为 life → queue,避免任何自锁路径:
|
|
110
|
-
// g_life_mu — 保护 g_webview 的生死:enqueue 的「检查后投递」与 teardown 的「销毁后清空」互斥
|
|
111
|
-
// g_queue_mu — 只保护命令队列本身
|
|
112
|
-
// 注意 drain() 只碰 g_queue_mu;teardown 会持 g_life_mu 调 webview_destroy,
|
|
113
|
-
// 若两者共用一把锁,销毁过程中若同步派发了 WM_APP 就会死锁。
|
|
114
|
-
std::mutex g_life_mu;
|
|
115
|
-
std::mutex g_queue_mu;
|
|
116
|
-
std::deque<cmd> g_queue;
|
|
117
|
-
|
|
118
|
-
std::string g_url;
|
|
119
|
-
std::string g_title;
|
|
120
|
-
|
|
121
|
-
// 错误槽。跨线程写:启动期由 UI 线程写(CoInitializeEx / webview_create 失败等),
|
|
122
|
-
// 运行期任何线程都可能写。放在 mutex 后面,避免两条错误互相覆盖成半截字符串。
|
|
123
|
-
// 读侧必须走 shell_error_text() 拿拷贝——shell_error() 返回的是共享缓冲区的指针。
|
|
124
|
-
std::mutex g_err_mu;
|
|
125
|
-
char g_err[1024] = {0};
|
|
126
|
-
|
|
127
|
-
void set_err(const char *msg) {
|
|
128
|
-
std::lock_guard<std::mutex> lock(g_err_mu);
|
|
129
|
-
lstrcpynA(g_err, msg ? msg : "", static_cast<int>(sizeof(g_err)));
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
// 启动前清空:让「启动失败」读到的一定是本次启动写下的那条,而不是上一次的残留。
|
|
133
|
-
void clear_err() {
|
|
134
|
-
std::lock_guard<std::mutex> lock(g_err_mu);
|
|
135
|
-
g_err[0] = 0;
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
// 把当前错误拷进调用方的缓冲区并返回长度。shell_error() 只保留 ABI 兼容,
|
|
139
|
-
// 它返回的指针在解锁后可能被别的线程改写,新代码一律用这个。
|
|
140
|
-
int copy_err(char *buf, int cap) {
|
|
141
|
-
if (!buf || cap <= 0) return 0;
|
|
142
|
-
std::lock_guard<std::mutex> lock(g_err_mu);
|
|
143
|
-
const int n = lstrlenA(g_err);
|
|
144
|
-
const int copy = n < cap - 1 ? n : cap - 1;
|
|
145
|
-
if (copy > 0) memcpy(buf, g_err, static_cast<size_t>(copy));
|
|
146
|
-
buf[copy] = 0;
|
|
147
|
-
return copy;
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
// ---- 外观与系统集成辅助 ----
|
|
151
|
-
|
|
152
|
-
// 从宿主 exe 的资源里取图标。
|
|
153
|
-
// Bun 的 --windows-icon 会把 ICO 的每个尺寸各写成一个 RT_GROUP_ICON 条目,
|
|
154
|
-
// 编号不保证从 1 开始,因此这里枚举第一个可用的图标组,而不是猜测 ID。
|
|
155
|
-
HICON load_exe_icon(int cx, int cy) {
|
|
156
|
-
HINSTANCE inst = GetModuleHandleW(nullptr);
|
|
157
|
-
const int w = cx > 0 ? cx : GetSystemMetrics(SM_CXICON);
|
|
158
|
-
const int h = cy > 0 ? cy : GetSystemMetrics(SM_CYICON);
|
|
159
|
-
|
|
160
|
-
HICON icon = static_cast<HICON>(LoadImageW(inst, IDI_APPLICATION, IMAGE_ICON, w, h,
|
|
161
|
-
LR_SHARED));
|
|
162
|
-
|
|
163
|
-
// 枚举本模块的图标组资源,取第一个能加载成功的
|
|
164
|
-
struct Ctx {
|
|
165
|
-
HINSTANCE inst;
|
|
166
|
-
int w, h;
|
|
167
|
-
HICON out;
|
|
168
|
-
} ctx{inst, w, h, nullptr};
|
|
169
|
-
|
|
170
|
-
EnumResourceNamesW(
|
|
171
|
-
inst, RT_GROUP_ICON,
|
|
172
|
-
[](HMODULE, LPCWSTR, LPWSTR name, LONG_PTR lp) -> BOOL {
|
|
173
|
-
auto *c = reinterpret_cast<Ctx *>(lp);
|
|
174
|
-
HICON ic =
|
|
175
|
-
static_cast<HICON>(LoadImageW(c->inst, name, IMAGE_ICON, c->w, c->h, 0));
|
|
176
|
-
if (ic) {
|
|
177
|
-
c->out = ic;
|
|
178
|
-
return FALSE; // 找到即停
|
|
179
|
-
}
|
|
180
|
-
return TRUE;
|
|
181
|
-
},
|
|
182
|
-
reinterpret_cast<LONG_PTR>(&ctx));
|
|
183
|
-
|
|
184
|
-
if (ctx.out) icon = ctx.out;
|
|
185
|
-
return icon;
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
// exe 里嵌了图标,但窗口不会自动继承;不显式设置的话任务栏/Alt-Tab/标题栏
|
|
189
|
-
// 全是系统默认图标。大图标给 Alt-Tab 与任务栏,小图标给标题栏与 Alt-Tab 缩略。
|
|
190
|
-
void apply_window_icons(HWND hwnd) {
|
|
191
|
-
if (!hwnd) return;
|
|
192
|
-
HICON big = load_exe_icon(GetSystemMetrics(SM_CXICON), GetSystemMetrics(SM_CYICON));
|
|
193
|
-
HICON small =
|
|
194
|
-
load_exe_icon(GetSystemMetrics(SM_CXSMICON), GetSystemMetrics(SM_CYSMICON));
|
|
195
|
-
if (big) SendMessageW(hwnd, WM_SETICON, ICON_BIG, reinterpret_cast<LPARAM>(big));
|
|
196
|
-
if (small) SendMessageW(hwnd, WM_SETICON, ICON_SMALL, reinterpret_cast<LPARAM>(small));
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
// 任务栏分组按 AppUserModelID 归属。不设的话窗口会被归到承载进程(bun.exe)名下,
|
|
200
|
-
// 图标与跳转列表都跟着错。必须在窗口创建前调用。
|
|
201
|
-
void apply_app_user_model_id() {
|
|
202
|
-
if (g_app_id.empty()) return;
|
|
203
|
-
using SetFn = HRESULT(WINAPI *)(PCWSTR);
|
|
204
|
-
auto fn = reinterpret_cast<SetFn>(GetProcAddress(
|
|
205
|
-
GetModuleHandleW(L"shell32.dll"), "SetCurrentProcessExplicitAppUserModelID"));
|
|
206
|
-
if (fn) fn(g_app_id.c_str());
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
// AppsUseLightTheme: 0 = 深色,1 = 浅色。缺省按浅色处理。
|
|
210
|
-
bool system_uses_dark() {
|
|
211
|
-
DWORD value = 1;
|
|
212
|
-
DWORD size = sizeof(value);
|
|
213
|
-
const LONG rc = RegGetValueW(
|
|
214
|
-
HKEY_CURRENT_USER,
|
|
215
|
-
L"Software\\Microsoft\\Windows\\CurrentVersion\\Themes\\Personalize",
|
|
216
|
-
L"AppsUseLightTheme", RRF_RT_REG_DWORD, nullptr, &value, &size);
|
|
217
|
-
return rc == ERROR_SUCCESS && value == 0;
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
// 标题栏、边框、系统菜单由 DWM 绘制;只有把这个属性设上,它们才跟随系统深色。
|
|
221
|
-
// 20 是 1903+ 的属性号,19 对应更早的 1809,两个都试一遍不影响结果。
|
|
222
|
-
void apply_dark_mode(HWND hwnd) {
|
|
223
|
-
if (!hwnd) return;
|
|
224
|
-
const BOOL dark = system_uses_dark() ? TRUE : FALSE;
|
|
225
|
-
if (FAILED(DwmSetWindowAttribute(hwnd, 20, &dark, sizeof(dark)))) {
|
|
226
|
-
DwmSetWindowAttribute(hwnd, 19, &dark, sizeof(dark));
|
|
227
|
-
}
|
|
228
|
-
}
|
|
229
|
-
|
|
230
|
-
// 让 WebView2 在页面首帧绘制之前显示与系统主题一致的底色。
|
|
231
|
-
//
|
|
232
|
-
// 不设的话,未绘制区域是 WebView2 的默认**白底**:深色应用窗口一显示,屏幕会先闪
|
|
233
|
-
// 一下白(实测首帧亮度 mean=236/255,约 200 ms),这才是「启动闪屏」的真正来源——
|
|
234
|
-
// 光把窗口藏到内容就绪只能减轻一半,因为窗口一显示、页面还没合成,露出的是白底。
|
|
235
|
-
//
|
|
236
|
-
// 需要 ICoreWebView2Controller2。MinGW 不提供 IID 符号库,所以用 GUID 字面量
|
|
237
|
-
// (同仓库既有约定:见 pitfalls 3.2)。失败不影响其余功能。
|
|
238
|
-
void apply_default_background(webview_t w, bool dark) {
|
|
239
|
-
if (!w) {
|
|
240
|
-
return;
|
|
241
|
-
}
|
|
242
|
-
auto *ctrl = static_cast<ICoreWebView2Controller *>(
|
|
243
|
-
webview_get_native_handle(w, WEBVIEW_NATIVE_HANDLE_KIND_BROWSER_CONTROLLER));
|
|
244
|
-
if (!ctrl) {
|
|
245
|
-
return;
|
|
246
|
-
}
|
|
247
|
-
|
|
248
|
-
// {c979903e-d4ca-4228-92eb-47ee3fa96eab} = ICoreWebView2Controller2
|
|
249
|
-
static const GUID kIIDController2 = {
|
|
250
|
-
0xc979903e, 0xd4ca, 0x4228, {0x92, 0xeb, 0x47, 0xee, 0x3f, 0xa9, 0x6e, 0xab}};
|
|
251
|
-
|
|
252
|
-
ICoreWebView2Controller2 *ctrl2 = nullptr;
|
|
253
|
-
if (FAILED(ctrl->QueryInterface(kIIDController2, reinterpret_cast<void **>(&ctrl2))) ||
|
|
254
|
-
!ctrl2) {
|
|
255
|
-
return;
|
|
256
|
-
}
|
|
257
|
-
|
|
258
|
-
COREWEBVIEW2_COLOR color{};
|
|
259
|
-
color.A = 255;
|
|
260
|
-
if (dark) {
|
|
261
|
-
// 与常见深色应用底色同量级,差值小到看不出来
|
|
262
|
-
color.R = 32;
|
|
263
|
-
color.G = 32;
|
|
264
|
-
color.B = 32;
|
|
265
|
-
} else {
|
|
266
|
-
color.R = 255;
|
|
267
|
-
color.G = 255;
|
|
268
|
-
color.B = 255;
|
|
269
|
-
}
|
|
270
|
-
ctrl2->put_DefaultBackgroundColor(color);
|
|
271
|
-
ctrl2->Release();
|
|
272
|
-
}
|
|
273
|
-
|
|
274
|
-
// ---- WebView2 的「浏览器 vs 应用」设置 ----
|
|
275
|
-
|
|
276
|
-
// WebView2 的默认设置是**浏览器**语义:右键是 Edge 菜单、Ctrl+P 打印网页、
|
|
277
|
-
// 左下角跳状态栏、触控板横滑会前进后退、表单有自动填充与密码提示。
|
|
278
|
-
// 这些在浏览器里都对,在桌面应用里每一条都是「这是个网页」的破绽。
|
|
279
|
-
//
|
|
280
|
-
// 位定义与 TS 侧 `BROWSER_UI_*` 逐位一致,不要单独改。
|
|
281
|
-
enum ShellBrowserUi : int {
|
|
282
|
-
SHELL_BROWSER_MENU = 1, // 默认右键菜单(重新加载 / 检查 / 另存为 / 翻译…)
|
|
283
|
-
SHELL_BROWSER_KEYS = 2, // 浏览器快捷键(Ctrl+P 打印、Ctrl+F 查找、Ctrl+S 保存…)
|
|
284
|
-
SHELL_BROWSER_STATUSBAR = 4, // 悬停时左下角的链接状态栏
|
|
285
|
-
SHELL_BROWSER_SWIPE = 8, // 触控板横滑导航
|
|
286
|
-
SHELL_BROWSER_AUTOFILL = 16, // 表单自动填充与密码保存提示
|
|
287
|
-
};
|
|
288
|
-
|
|
289
|
-
// 各 Settings 接口的 IID(MinGW 无 IID 符号库,按本仓约定用字面量)。
|
|
290
|
-
// 它们是链式继承(Settings5 : Settings4 : Settings3 : Settings2 : Settings),
|
|
291
|
-
// 所以 QueryInterface 到最高版本即可访问全部属性。
|
|
292
|
-
static const GUID kIIDSettings = {
|
|
293
|
-
0xe562e4f0, 0xd7fa, 0x43ac, {0x8d, 0x71, 0xc0, 0x51, 0x50, 0x49, 0x9f, 0x00}};
|
|
294
|
-
static const GUID kIIDSettings3 = {
|
|
295
|
-
0xfdb5ab74, 0xaf33, 0x4854, {0x84, 0xf0, 0x0a, 0x63, 0x1d, 0xeb, 0x5e, 0xba}};
|
|
296
|
-
static const GUID kIIDSettings4 = {
|
|
297
|
-
0xcb56846c, 0x4168, 0x4d53, {0xb0, 0x4f, 0x03, 0xb6, 0xd6, 0x79, 0x6f, 0xf2}};
|
|
298
|
-
static const GUID kIIDSettings5 = {
|
|
299
|
-
0x183e7052, 0x1d03, 0x43a0, {0xab, 0x99, 0x98, 0xe0, 0x43, 0xb6, 0x6b, 0x39}};
|
|
300
|
-
// IsSwipeNavigationEnabled 在 Settings6(实测:头文件 21168 行),不在 Settings5
|
|
301
|
-
static const GUID kIIDSettings6 = {
|
|
302
|
-
0x11cb3acd, 0x9bc8, 0x43b8, {0x83, 0xbf, 0xf4, 0x07, 0x53, 0x71, 0x4f, 0x87}};
|
|
303
|
-
|
|
304
|
-
// 逐项关闭 mask 里的浏览器行为。
|
|
305
|
-
//
|
|
306
|
-
// 每一项都单独判 SUCCEEDED:老版本 WebView2 Runtime 可能没有 Settings4/5,
|
|
307
|
-
// 那时对应的 IF 会失败,但前面的项已经生效——不能因为一个失败就整体放弃。
|
|
308
|
-
void apply_browser_ui(webview_t w, int mask) {
|
|
309
|
-
if (!w || mask == 0) {
|
|
310
|
-
return;
|
|
311
|
-
}
|
|
312
|
-
auto *ctrl = static_cast<ICoreWebView2Controller *>(
|
|
313
|
-
webview_get_native_handle(w, WEBVIEW_NATIVE_HANDLE_KIND_BROWSER_CONTROLLER));
|
|
314
|
-
if (!ctrl) {
|
|
315
|
-
return;
|
|
316
|
-
}
|
|
317
|
-
ICoreWebView2 *core = nullptr;
|
|
318
|
-
if (FAILED(ctrl->get_CoreWebView2(&core)) || !core) {
|
|
319
|
-
return;
|
|
320
|
-
}
|
|
321
|
-
ICoreWebView2Settings *s1 = nullptr;
|
|
322
|
-
if (FAILED(core->get_Settings(&s1)) || !s1) {
|
|
323
|
-
core->Release();
|
|
324
|
-
return;
|
|
325
|
-
}
|
|
326
|
-
|
|
327
|
-
// ---- Settings(基线版本)----
|
|
328
|
-
if (mask & (SHELL_BROWSER_MENU | SHELL_BROWSER_STATUSBAR)) {
|
|
329
|
-
if (mask & SHELL_BROWSER_MENU) {
|
|
330
|
-
s1->put_AreDefaultContextMenusEnabled(FALSE);
|
|
331
|
-
}
|
|
332
|
-
if (mask & SHELL_BROWSER_STATUSBAR) {
|
|
333
|
-
s1->put_IsStatusBarEnabled(FALSE);
|
|
334
|
-
}
|
|
335
|
-
}
|
|
336
|
-
|
|
337
|
-
// ---- Settings3:浏览器快捷键 ----
|
|
338
|
-
if (mask & SHELL_BROWSER_KEYS) {
|
|
339
|
-
ICoreWebView2Settings3 *s3 = nullptr;
|
|
340
|
-
if (SUCCEEDED(s1->QueryInterface(kIIDSettings3, reinterpret_cast<void **>(&s3))) && s3) {
|
|
341
|
-
s3->put_AreBrowserAcceleratorKeysEnabled(FALSE);
|
|
342
|
-
s3->Release();
|
|
343
|
-
}
|
|
344
|
-
}
|
|
345
|
-
|
|
346
|
-
// ---- Settings4:自动填充与密码保存 ----
|
|
347
|
-
if (mask & SHELL_BROWSER_AUTOFILL) {
|
|
348
|
-
ICoreWebView2Settings4 *s4 = nullptr;
|
|
349
|
-
if (SUCCEEDED(s1->QueryInterface(kIIDSettings4, reinterpret_cast<void **>(&s4))) && s4) {
|
|
350
|
-
s4->put_IsGeneralAutofillEnabled(FALSE);
|
|
351
|
-
s4->put_IsPasswordAutosaveEnabled(FALSE);
|
|
352
|
-
s4->Release();
|
|
353
|
-
}
|
|
354
|
-
}
|
|
355
|
-
|
|
356
|
-
// ---- Settings6:滑动导航 ----
|
|
357
|
-
if (mask & SHELL_BROWSER_SWIPE) {
|
|
358
|
-
ICoreWebView2Settings6 *s6 = nullptr;
|
|
359
|
-
if (SUCCEEDED(s1->QueryInterface(kIIDSettings6, reinterpret_cast<void **>(&s6))) && s6) {
|
|
360
|
-
s6->put_IsSwipeNavigationEnabled(FALSE);
|
|
361
|
-
s6->Release();
|
|
362
|
-
}
|
|
363
|
-
}
|
|
364
|
-
|
|
365
|
-
(void)kIIDSettings; // 基接口走 get_Settings,保留字面量供文档对照
|
|
366
|
-
(void)kIIDSettings5; // Settings5 无本项目所需属性,保留以便将来扩展
|
|
367
|
-
s1->Release();
|
|
368
|
-
core->Release();
|
|
369
|
-
}
|
|
370
|
-
|
|
371
|
-
// 右键标题栏 / Alt+Space 的系统菜单。原生窗口靠它给出还原、移动、大小、关闭;
|
|
372
|
-
// 无边框模式删掉了非客户区,必须自己补回来,否则这几条交互全缺。
|
|
373
|
-
void show_system_menu(HWND hwnd, POINT screen_pt) {
|
|
374
|
-
if (!hwnd) return;
|
|
375
|
-
HMENU menu = GetSystemMenu(hwnd, FALSE);
|
|
376
|
-
if (!menu) return;
|
|
377
|
-
|
|
378
|
-
const bool maximized = IsZoomed(hwnd) != 0;
|
|
379
|
-
EnableMenuItem(menu, SC_RESTORE, MF_BYCOMMAND | (maximized ? MF_ENABLED : MF_GRAYED));
|
|
380
|
-
EnableMenuItem(menu, SC_MOVE, MF_BYCOMMAND | (maximized ? MF_GRAYED : MF_ENABLED));
|
|
381
|
-
EnableMenuItem(menu, SC_SIZE,
|
|
382
|
-
MF_BYCOMMAND | (maximized || !g_resizable ? MF_GRAYED : MF_ENABLED));
|
|
383
|
-
SetMenuDefaultItem(menu, SC_CLOSE, FALSE);
|
|
384
|
-
|
|
385
|
-
SetForegroundWindow(hwnd);
|
|
386
|
-
const int cmd = TrackPopupMenu(menu, TPM_RETURNCMD | TPM_RIGHTBUTTON, screen_pt.x,
|
|
387
|
-
screen_pt.y, 0, hwnd, nullptr);
|
|
388
|
-
// 交给系统执行,避免自己重复实现最小化/最大化/关闭
|
|
389
|
-
if (cmd) PostMessageW(hwnd, WM_SYSCOMMAND, static_cast<WPARAM>(cmd), 0);
|
|
390
|
-
}
|
|
391
|
-
|
|
392
|
-
// ---- 几何辅助 ----
|
|
393
|
-
|
|
394
|
-
// DPI 感知开启后,窗口尺寸按物理像素解释;对外仍以 96 DPI 的逻辑像素为准。
|
|
395
|
-
UINT current_dpi() {
|
|
396
|
-
return g_hwnd ? GetDpiForWindow(g_hwnd) : GetDpiForSystem();
|
|
397
|
-
}
|
|
398
|
-
|
|
399
|
-
int frame_border_x() {
|
|
400
|
-
const UINT dpi = current_dpi();
|
|
401
|
-
return GetSystemMetricsForDpi(SM_CXSIZEFRAME, dpi) +
|
|
402
|
-
GetSystemMetricsForDpi(SM_CXPADDEDBORDER, dpi);
|
|
403
|
-
}
|
|
404
|
-
|
|
405
|
-
int frame_border_y() {
|
|
406
|
-
const UINT dpi = current_dpi();
|
|
407
|
-
return GetSystemMetricsForDpi(SM_CYSIZEFRAME, dpi) +
|
|
408
|
-
GetSystemMetricsForDpi(SM_CXPADDEDBORDER, dpi);
|
|
409
|
-
}
|
|
410
|
-
|
|
411
|
-
// 返回 HT* 代码;HTCLIENT 表示不落在缩放边框上。坐标为顶层窗口客户区坐标(设备像素)。
|
|
412
|
-
LRESULT hit_test_client(HWND hwnd, POINT pt) {
|
|
413
|
-
if (!g_frameless || !g_resizable || IsZoomed(hwnd)) {
|
|
414
|
-
return HTCLIENT;
|
|
415
|
-
}
|
|
416
|
-
RECT rc{};
|
|
417
|
-
GetClientRect(hwnd, &rc);
|
|
418
|
-
|
|
419
|
-
const int bx = frame_border_x();
|
|
420
|
-
const int by = frame_border_y();
|
|
421
|
-
|
|
422
|
-
const bool left = pt.x < bx;
|
|
423
|
-
const bool right = pt.x >= rc.right - bx;
|
|
424
|
-
const bool top = pt.y < by;
|
|
425
|
-
const bool bottom = pt.y >= rc.bottom - by;
|
|
426
|
-
|
|
427
|
-
if (top && left) return HTTOPLEFT;
|
|
428
|
-
if (top && right) return HTTOPRIGHT;
|
|
429
|
-
if (bottom && left) return HTBOTTOMLEFT;
|
|
430
|
-
if (bottom && right) return HTBOTTOMRIGHT;
|
|
431
|
-
if (left) return HTLEFT;
|
|
432
|
-
if (right) return HTRIGHT;
|
|
433
|
-
if (top) return HTTOP;
|
|
434
|
-
if (bottom) return HTBOTTOM;
|
|
435
|
-
return HTCLIENT;
|
|
436
|
-
}
|
|
437
|
-
|
|
438
|
-
LRESULT hit_test(HWND hwnd, POINT screen_pt) {
|
|
439
|
-
POINT pt = screen_pt;
|
|
440
|
-
ScreenToClient(hwnd, &pt);
|
|
441
|
-
return hit_test_client(hwnd, pt);
|
|
442
|
-
}
|
|
443
|
-
|
|
444
|
-
// 把 widget 铺满顶层窗口客户区。widget 的 WM_SIZE 会驱动 put_Bounds。
|
|
445
|
-
void fit_widget() {
|
|
446
|
-
if (!g_hwnd || !g_widget) {
|
|
447
|
-
return;
|
|
448
|
-
}
|
|
449
|
-
RECT rc{};
|
|
450
|
-
GetClientRect(g_hwnd, &rc);
|
|
451
|
-
SetWindowPos(g_widget, nullptr, 0, 0, rc.right - rc.left, rc.bottom - rc.top,
|
|
452
|
-
SWP_NOZORDER | SWP_NOACTIVATE);
|
|
453
|
-
}
|
|
454
|
-
|
|
455
|
-
void post_json(const std::string &json) {
|
|
456
|
-
if (!g_webview) {
|
|
457
|
-
return;
|
|
458
|
-
}
|
|
459
|
-
std::string expr = "window.__shell && window.__shell.recv(";
|
|
460
|
-
expr += json;
|
|
461
|
-
expr += ")";
|
|
462
|
-
webview_eval(g_webview, expr.c_str());
|
|
463
|
-
}
|
|
464
|
-
|
|
465
|
-
// 上一次真正推送出去的状态。放在这里而不是函数内,供变化检测比对。
|
|
466
|
-
struct PushedState {
|
|
467
|
-
int px_w = -1;
|
|
468
|
-
int px_h = -1;
|
|
469
|
-
unsigned int dpi = 0;
|
|
470
|
-
bool maxed = false;
|
|
471
|
-
bool minned = false;
|
|
472
|
-
bool focused = false;
|
|
473
|
-
bool dark = false;
|
|
474
|
-
ULONGLONG at = 0;
|
|
475
|
-
};
|
|
476
|
-
PushedState g_pushed;
|
|
477
|
-
bool g_push_pending = false; // 有变化被节流掉了,等定时器补齐
|
|
478
|
-
|
|
479
|
-
const UINT_PTR kPushTimerId = 1;
|
|
480
|
-
const ULONGLONG kPushMinIntervalMs = 16; // 约一帧
|
|
481
|
-
|
|
482
|
-
// 推送窗口状态(仅限 UI 线程调用)
|
|
483
|
-
// 尺寸对外统一用 96 DPI 逻辑像素(即页面 CSS px),另附物理像素与 dpi,
|
|
484
|
-
// 避免前端每次自行按 dpr 折算。
|
|
485
|
-
//
|
|
486
|
-
// 两道闸,因为 WM_SIZE 在拖动缩放时每帧都触发,而每次推送都是一次跨进程
|
|
487
|
-
// webview_eval(宿主侧 39µs,渲染器还要执行一段 JS):
|
|
488
|
-
// 1) 变化检测:状态一模一样就不发。停下来之后不会再有 WM_SIZE,这里直接吸收掉。
|
|
489
|
-
// 2) 16ms 节流 + 尾随补齐:拖动过程中尺寸每帧都在变,靠第 1 道拦不住;
|
|
490
|
-
// 被节流掉时挂一个 16ms 定时器,到点补发最后一次状态,保证不会丢。
|
|
491
|
-
void push_state(bool force = false) {
|
|
492
|
-
if (!g_hwnd || !g_webview) {
|
|
493
|
-
return;
|
|
494
|
-
}
|
|
495
|
-
const bool maxed = IsZoomed(g_hwnd) != FALSE;
|
|
496
|
-
const bool minned = IsIconic(g_hwnd) != FALSE;
|
|
497
|
-
const bool focused = GetForegroundWindow() == g_hwnd;
|
|
498
|
-
RECT rc{};
|
|
499
|
-
GetClientRect(g_hwnd, &rc);
|
|
500
|
-
|
|
501
|
-
const int px_w = rc.right - rc.left;
|
|
502
|
-
const int px_h = rc.bottom - rc.top;
|
|
503
|
-
const UINT dpi = GetDpiForWindow(g_hwnd);
|
|
504
|
-
const bool dark = system_uses_dark();
|
|
505
|
-
|
|
506
|
-
const bool same = px_w == g_pushed.px_w && px_h == g_pushed.px_h &&
|
|
507
|
-
dpi == g_pushed.dpi && maxed == g_pushed.maxed &&
|
|
508
|
-
minned == g_pushed.minned && focused == g_pushed.focused &&
|
|
509
|
-
dark == g_pushed.dark;
|
|
510
|
-
if (same && !force) {
|
|
511
|
-
return;
|
|
512
|
-
}
|
|
513
|
-
|
|
514
|
-
const ULONGLONG now = GetTickCount64();
|
|
515
|
-
if (!force && now - g_pushed.at < kPushMinIntervalMs) {
|
|
516
|
-
if (!g_push_pending) {
|
|
517
|
-
g_push_pending = true;
|
|
518
|
-
SetTimer(g_hwnd, kPushTimerId, static_cast<UINT>(kPushMinIntervalMs), nullptr);
|
|
519
|
-
}
|
|
520
|
-
return;
|
|
521
|
-
}
|
|
522
|
-
|
|
523
|
-
if (g_push_pending) {
|
|
524
|
-
g_push_pending = false;
|
|
525
|
-
KillTimer(g_hwnd, kPushTimerId);
|
|
526
|
-
}
|
|
527
|
-
|
|
528
|
-
g_pushed = PushedState{px_w, px_h, dpi, maxed, minned, focused, dark, now};
|
|
529
|
-
|
|
530
|
-
const int css_w = dpi ? MulDiv(px_w, 96, static_cast<int>(dpi)) : px_w;
|
|
531
|
-
const int css_h = dpi ? MulDiv(px_h, 96, static_cast<int>(dpi)) : px_h;
|
|
532
|
-
|
|
533
|
-
char buf[448];
|
|
534
|
-
wsprintfA(buf,
|
|
535
|
-
"{\"t\":\"win\",\"maximized\":%s,\"minimized\":%s,\"focused\":%s,"
|
|
536
|
-
"\"width\":%d,\"height\":%d,\"pixelWidth\":%d,\"pixelHeight\":%d,"
|
|
537
|
-
"\"dpi\":%u,\"frameless\":%s,\"dark\":%s}",
|
|
538
|
-
maxed ? "true" : "false", minned ? "true" : "false",
|
|
539
|
-
focused ? "true" : "false", css_w, css_h, px_w, px_h, dpi,
|
|
540
|
-
g_frameless ? "true" : "false", dark ? "true" : "false");
|
|
541
|
-
post_json(buf);
|
|
542
|
-
}
|
|
543
|
-
|
|
544
|
-
// 节流定时器到点:补发一次(force,因为此时往往与上次推送的状态已经不同了)。
|
|
545
|
-
void flush_push_state() {
|
|
546
|
-
if (!g_push_pending) {
|
|
547
|
-
return;
|
|
548
|
-
}
|
|
549
|
-
g_push_pending = false;
|
|
550
|
-
if (g_hwnd) {
|
|
551
|
-
KillTimer(g_hwnd, kPushTimerId);
|
|
552
|
-
}
|
|
553
|
-
push_state(true);
|
|
554
|
-
}
|
|
555
|
-
|
|
556
|
-
void apply_round_corners(HWND hwnd, bool round) {
|
|
557
|
-
// DWMWA_WINDOW_CORNER_PREFERENCE = 33;DWMWCP_DONOTROUND = 1, DWMWCP_ROUND = 2
|
|
558
|
-
// 带缓存:拖动缩放时 WM_SIZE 触发极频繁,没必要每次都调 DWM。
|
|
559
|
-
const int key = round ? 1 : 0;
|
|
560
|
-
if (g_round_state == key) {
|
|
561
|
-
return;
|
|
562
|
-
}
|
|
563
|
-
const int pref = round ? 2 : 1;
|
|
564
|
-
if (SUCCEEDED(DwmSetWindowAttribute(hwnd, 33, &pref, sizeof(pref)))) {
|
|
565
|
-
g_round_state = key;
|
|
566
|
-
}
|
|
567
|
-
}
|
|
568
|
-
|
|
569
|
-
// 首次显示。g_shown 必须先置位再 ShowWindow:
|
|
570
|
-
// 显示之后的 WM_DPICHANGED 才允许跟随系统建议矩形;同时也让 toplevel_proc 的
|
|
571
|
-
// WM_SHOWWINDOW 拦截放行(它只拦 g_shown 为假的显示请求)。
|
|
572
|
-
//
|
|
573
|
-
// 激活动作是必须的:带边框模式下原本由 webview 的 window_show() 负责 SetFocus,
|
|
574
|
-
// 现在我们接管了显示,不补这一步窗口出现时不会成为前台窗口(标题栏呈未激活灰态,
|
|
575
|
-
// 用起来像「卡住了」)。SetForegroundWindow 可能因本机策略失败,属尽力而为。
|
|
576
|
-
void show_now() {
|
|
577
|
-
if (!g_hwnd) return;
|
|
578
|
-
// 先置 g_shown:之后的 WM_DPICHANGED 才允许跟随建议矩形,
|
|
579
|
-
// 也让 toplevel_proc 的 WM_SHOWWINDOW 拦截放行。
|
|
580
|
-
g_shown = true;
|
|
581
|
-
// 最大化也走这一次 ShowWindow:窗口从隐藏直接变成最大化,
|
|
582
|
-
// 没有「先小窗再撑满」的中间帧。
|
|
583
|
-
ShowWindow(g_hwnd, g_start_maximized ? SW_SHOWMAXIMIZED : SW_SHOW);
|
|
584
|
-
SetForegroundWindow(g_hwnd);
|
|
585
|
-
SetFocus(g_hwnd);
|
|
586
|
-
UpdateWindow(g_hwnd);
|
|
587
|
-
fit_widget();
|
|
588
|
-
push_state(true);
|
|
589
|
-
}
|
|
590
|
-
|
|
591
|
-
// ---- 原生上下文菜单 ----
|
|
592
|
-
//
|
|
593
|
-
// 关掉 WebView2 的默认右键菜单之后,右键就成了「点了没反应」——那也不像原生应用。
|
|
594
|
-
// 原生桌面应用右键是有菜单的,只是菜单属于应用而非浏览器。
|
|
595
|
-
//
|
|
596
|
-
// 这里由页面定义菜单项,壳负责用 TrackPopupMenu 弹出系统菜单(真正的 Win32 菜单,
|
|
597
|
-
// 跟随系统主题、支持键盘导航、带阴影)。选中结果通过 shell-msg 推回页面:
|
|
598
|
-
// { t: "menu", id: "<选中的 id>" }
|
|
599
|
-
//
|
|
600
|
-
// 菜单项格式(页面侧构造,壳只做切分,不做 JSON 解析):
|
|
601
|
-
// "open|打开题库\nrefresh|刷新\n-\nquit|退出"
|
|
602
|
-
// 每行 `id|label`,单独的 `-` 表示分隔线。
|
|
603
|
-
|
|
604
|
-
struct MenuItem {
|
|
605
|
-
std::string id;
|
|
606
|
-
std::wstring label;
|
|
607
|
-
bool separator;
|
|
608
|
-
};
|
|
609
|
-
|
|
610
|
-
std::wstring widen_utf8(const std::string &s) {
|
|
611
|
-
if (s.empty()) return std::wstring();
|
|
612
|
-
const int need = MultiByteToWideChar(CP_UTF8, 0, s.c_str(), -1, nullptr, 0);
|
|
613
|
-
if (need <= 1) return std::wstring();
|
|
614
|
-
std::wstring out(static_cast<size_t>(need - 1), L'\0');
|
|
615
|
-
MultiByteToWideChar(CP_UTF8, 0, s.c_str(), -1, &out[0], need);
|
|
616
|
-
return out;
|
|
617
|
-
}
|
|
618
|
-
|
|
619
|
-
// 解析菜单描述。空行忽略;`-` 是分隔线。
|
|
620
|
-
std::vector<MenuItem> parse_menu(const std::string &spec) {
|
|
621
|
-
std::vector<MenuItem> out;
|
|
622
|
-
size_t start = 0;
|
|
623
|
-
while (start <= spec.size()) {
|
|
624
|
-
const size_t nl = spec.find('\n', start);
|
|
625
|
-
const std::string line =
|
|
626
|
-
spec.substr(start, nl == std::string::npos ? std::string::npos : nl - start);
|
|
627
|
-
if (!line.empty()) {
|
|
628
|
-
if (line == "-") {
|
|
629
|
-
out.push_back(MenuItem{"", L"", true});
|
|
630
|
-
} else {
|
|
631
|
-
const size_t bar = line.find('|');
|
|
632
|
-
if (bar != std::string::npos && bar > 0 && bar + 1 < line.size()) {
|
|
633
|
-
out.push_back(MenuItem{line.substr(0, bar), widen_utf8(line.substr(bar + 1)), false});
|
|
634
|
-
}
|
|
635
|
-
}
|
|
636
|
-
}
|
|
637
|
-
if (nl == std::string::npos) break;
|
|
638
|
-
start = nl + 1;
|
|
639
|
-
}
|
|
640
|
-
return out;
|
|
641
|
-
}
|
|
642
|
-
|
|
643
|
-
// 弹出原生菜单并把选中项推回页面。仅 UI 线程调用。
|
|
644
|
-
//
|
|
645
|
-
// 为什么不在返回值里给结果:菜单是模态的,选中前拿不到结果;而跨线程同步返回
|
|
646
|
-
// 又要多一套等待机制。推回 shell-msg 与既有的消息通道同构,页面侧处理起来更自然。
|
|
647
|
-
void show_app_menu(HWND hwnd, POINT screen_pt, const std::string &spec) {
|
|
648
|
-
const std::vector<MenuItem> items = parse_menu(spec);
|
|
649
|
-
if (items.empty() || !hwnd) {
|
|
650
|
-
return;
|
|
651
|
-
}
|
|
652
|
-
|
|
653
|
-
HMENU menu = CreatePopupMenu();
|
|
654
|
-
if (!menu) {
|
|
655
|
-
return;
|
|
656
|
-
}
|
|
657
|
-
|
|
658
|
-
// 菜单项 id 从 1 开始(0 在 TrackPopupMenu 里表示「没选」)
|
|
659
|
-
std::vector<std::string> ids(items.size());
|
|
660
|
-
int next_id = 1;
|
|
661
|
-
for (size_t i = 0; i < items.size(); ++i) {
|
|
662
|
-
const MenuItem &it = items[i];
|
|
663
|
-
if (it.separator) {
|
|
664
|
-
AppendMenuW(menu, MF_SEPARATOR, 0, nullptr);
|
|
665
|
-
continue;
|
|
666
|
-
}
|
|
667
|
-
ids[static_cast<size_t>(next_id - 1)] = it.id;
|
|
668
|
-
AppendMenuW(menu, MF_STRING, static_cast<UINT_PTR>(next_id), it.label.c_str());
|
|
669
|
-
++next_id;
|
|
670
|
-
}
|
|
671
|
-
|
|
672
|
-
SetForegroundWindow(hwnd);
|
|
673
|
-
const UINT cmd = static_cast<UINT>(TrackPopupMenu(
|
|
674
|
-
menu, TPM_RETURNCMD | TPM_RIGHTBUTTON | TPM_NONOTIFY, screen_pt.x, screen_pt.y, 0, hwnd,
|
|
675
|
-
nullptr));
|
|
676
|
-
DestroyMenu(menu);
|
|
677
|
-
|
|
678
|
-
// TPM_NONOTIFY 下必须补这一条,否则菜单关掉后窗口收不到激活,
|
|
679
|
-
// 表现为「菜单没了但窗口半死」——点哪都不响应,直到 Alt+Tab 切走再回来。
|
|
680
|
-
PostMessageW(hwnd, WM_NULL, 0, 0);
|
|
681
|
-
|
|
682
|
-
if (cmd == 0 || cmd > ids.size()) {
|
|
683
|
-
return;
|
|
684
|
-
}
|
|
685
|
-
const std::string &picked = ids[cmd - 1];
|
|
686
|
-
if (picked.empty()) {
|
|
687
|
-
return;
|
|
688
|
-
}
|
|
689
|
-
std::string json = "{\"t\":\"menu\",\"id\":\"";
|
|
690
|
-
for (char ch : picked) {
|
|
691
|
-
if (ch == '\\' || ch == '"') json.push_back('\\');
|
|
692
|
-
json.push_back(ch);
|
|
693
|
-
}
|
|
694
|
-
json += "\"}";
|
|
695
|
-
post_json(json);
|
|
696
|
-
}
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
// ---- 缩放 / 全屏 / 刷新 / 开发者工具 / 拖放 ----
|
|
700
|
-
|
|
701
|
-
// 缩放级别阶梯。与 Edge 的量级接近:小端更密,大端更疏,避免高倍率下 ±0.1 看不出变化。
|
|
702
|
-
const double kZoomLadder[] = {0.25, 0.33, 0.5, 0.67, 0.8, 0.9, 1.0,
|
|
703
|
-
1.1, 1.25, 1.5, 1.75, 2.0, 2.5, 3.0};
|
|
704
|
-
const int kZoomCount = static_cast<int>(sizeof(kZoomLadder) / sizeof(kZoomLadder[0]));
|
|
705
|
-
double g_zoom = 1.0;
|
|
706
|
-
|
|
707
|
-
// 全屏状态:进入前保存样式与矩形,退出时原样还原。
|
|
708
|
-
bool g_fullscreen = false;
|
|
709
|
-
LONG_PTR g_saved_style = 0;
|
|
710
|
-
RECT g_saved_rect{};
|
|
711
|
-
|
|
712
|
-
// 取 CoreWebView2。调用方负责 Release()。失败返回 nullptr(老 runtime 或缺 COM 上下文)。
|
|
713
|
-
ICoreWebView2 *core_webview() {
|
|
714
|
-
if (!g_webview) {
|
|
715
|
-
return nullptr;
|
|
716
|
-
}
|
|
717
|
-
auto *ctrl = static_cast<ICoreWebView2Controller *>(
|
|
718
|
-
webview_get_native_handle(g_webview, WEBVIEW_NATIVE_HANDLE_KIND_BROWSER_CONTROLLER));
|
|
719
|
-
if (!ctrl) {
|
|
720
|
-
return nullptr;
|
|
721
|
-
}
|
|
722
|
-
ICoreWebView2 *core = nullptr;
|
|
723
|
-
if (FAILED(ctrl->get_CoreWebView2(&core))) {
|
|
724
|
-
return nullptr;
|
|
725
|
-
}
|
|
726
|
-
return core;
|
|
727
|
-
}
|
|
728
|
-
|
|
729
|
-
// 设页面缩放。必须在 UI 线程调用(WebView2 是 STA)。
|
|
730
|
-
void apply_zoom(double factor) {
|
|
731
|
-
if (!g_webview) {
|
|
732
|
-
return;
|
|
733
|
-
}
|
|
734
|
-
if (factor < kZoomLadder[0]) factor = kZoomLadder[0];
|
|
735
|
-
if (factor > kZoomLadder[kZoomCount - 1]) factor = kZoomLadder[kZoomCount - 1];
|
|
736
|
-
|
|
737
|
-
auto *ctrl = static_cast<ICoreWebView2Controller *>(
|
|
738
|
-
webview_get_native_handle(g_webview, WEBVIEW_NATIVE_HANDLE_KIND_BROWSER_CONTROLLER));
|
|
739
|
-
if (!ctrl) {
|
|
740
|
-
return;
|
|
741
|
-
}
|
|
742
|
-
if (SUCCEEDED(ctrl->put_ZoomFactor(factor))) {
|
|
743
|
-
g_zoom = factor;
|
|
744
|
-
}
|
|
745
|
-
}
|
|
746
|
-
|
|
747
|
-
// 往上/下走一档。direction > 0 放大。
|
|
748
|
-
void zoom_step(int direction) {
|
|
749
|
-
int nearest = 0;
|
|
750
|
-
for (int i = 0; i < kZoomCount; ++i) {
|
|
751
|
-
if (g_zoom <= kZoomLadder[i] + 1e-9) {
|
|
752
|
-
nearest = i;
|
|
753
|
-
break;
|
|
754
|
-
}
|
|
755
|
-
nearest = i;
|
|
756
|
-
}
|
|
757
|
-
int next = nearest + (direction > 0 ? 1 : -1);
|
|
758
|
-
if (next < 0) next = 0;
|
|
759
|
-
if (next >= kZoomCount) next = kZoomCount - 1;
|
|
760
|
-
apply_zoom(kZoomLadder[next]);
|
|
761
|
-
}
|
|
762
|
-
|
|
763
|
-
void reload_page() {
|
|
764
|
-
ICoreWebView2 *core = core_webview();
|
|
765
|
-
if (!core) {
|
|
766
|
-
return;
|
|
767
|
-
}
|
|
768
|
-
core->Reload();
|
|
769
|
-
core->Release();
|
|
770
|
-
}
|
|
771
|
-
|
|
772
|
-
void open_dev_tools() {
|
|
773
|
-
ICoreWebView2 *core = core_webview();
|
|
774
|
-
if (!core) {
|
|
775
|
-
return;
|
|
776
|
-
}
|
|
777
|
-
core->OpenDevToolsWindow();
|
|
778
|
-
core->Release();
|
|
779
|
-
}
|
|
780
|
-
|
|
781
|
-
// F11 全屏:去掉窗口样式钉到当前显示器整屏,再按一次还原。
|
|
782
|
-
// 与「最大化」不同——最大化仍保留边框与任务栏,全屏两者都不要。
|
|
783
|
-
void toggle_fullscreen() {
|
|
784
|
-
if (!g_hwnd) {
|
|
785
|
-
return;
|
|
786
|
-
}
|
|
787
|
-
HWND hwnd = g_hwnd;
|
|
788
|
-
|
|
789
|
-
if (!g_fullscreen) {
|
|
790
|
-
GetWindowRect(hwnd, &g_saved_rect);
|
|
791
|
-
g_saved_style = GetWindowLongPtrW(hwnd, GWL_STYLE);
|
|
792
|
-
|
|
793
|
-
MONITORINFO mi{};
|
|
794
|
-
mi.cbSize = sizeof(mi);
|
|
795
|
-
if (!GetMonitorInfoW(MonitorFromWindow(hwnd, MONITOR_DEFAULTTONEAREST), &mi)) {
|
|
796
|
-
return;
|
|
797
|
-
}
|
|
798
|
-
const int w = mi.rcMonitor.right - mi.rcMonitor.left;
|
|
799
|
-
const int h = mi.rcMonitor.bottom - mi.rcMonitor.top;
|
|
800
|
-
|
|
801
|
-
// 无边框模式本来就是 WS_POPUP 风格,只需铺满;带边框模式要去掉整个非客户区。
|
|
802
|
-
SetWindowLongPtrW(hwnd, GWL_STYLE, WS_POPUP | WS_VISIBLE);
|
|
803
|
-
SetWindowPos(hwnd, HWND_TOP, mi.rcMonitor.left, mi.rcMonitor.top, w, h,
|
|
804
|
-
SWP_FRAMECHANGED | SWP_NOOWNERZORDER);
|
|
805
|
-
g_fullscreen = true;
|
|
806
|
-
} else {
|
|
807
|
-
SetWindowLongPtrW(hwnd, GWL_STYLE, g_saved_style);
|
|
808
|
-
SetWindowPos(hwnd, nullptr, g_saved_rect.left, g_saved_rect.top,
|
|
809
|
-
g_saved_rect.right - g_saved_rect.left,
|
|
810
|
-
g_saved_rect.bottom - g_saved_rect.top,
|
|
811
|
-
SWP_FRAMECHANGED | SWP_NOZORDER | SWP_NOOWNERZORDER);
|
|
812
|
-
g_fullscreen = false;
|
|
813
|
-
}
|
|
814
|
-
fit_widget();
|
|
815
|
-
push_state(true);
|
|
816
|
-
}
|
|
817
|
-
|
|
818
|
-
// 把宽字符串转成 UTF-8 并写进 JSON 字符串字面量(转义 \ 与 ")。
|
|
819
|
-
void append_json_string(std::string &out, const std::wstring &w) {
|
|
820
|
-
out.push_back('"');
|
|
821
|
-
const int need = WideCharToMultiByte(CP_UTF8, 0, w.c_str(), -1, nullptr, 0, nullptr, nullptr);
|
|
822
|
-
if (need > 1) {
|
|
823
|
-
std::string utf8(static_cast<size_t>(need - 1), '\0');
|
|
824
|
-
WideCharToMultiByte(CP_UTF8, 0, w.c_str(), -1, &utf8[0], need, nullptr, nullptr);
|
|
825
|
-
for (char ch : utf8) {
|
|
826
|
-
if (ch == '\\' || ch == '"') {
|
|
827
|
-
out.push_back('\\');
|
|
828
|
-
}
|
|
829
|
-
out.push_back(ch);
|
|
830
|
-
}
|
|
831
|
-
}
|
|
832
|
-
out.push_back('"');
|
|
833
|
-
}
|
|
834
|
-
|
|
835
|
-
// 拖放:把落进来的路径推给页面。
|
|
836
|
-
//
|
|
837
|
-
// 注意提权场景:本进程若是 High 完整性,资源管理器(Medium)
|
|
838
|
-
// 发来的 WM_DROPFILES 会被 UIPI 拦掉。所以窗口建好后要显式放行这三条消息,
|
|
839
|
-
// 否则「拖进来没反应」且完全没有错误提示。
|
|
840
|
-
void enable_drop(HWND hwnd) {
|
|
841
|
-
if (!hwnd) {
|
|
842
|
-
return;
|
|
843
|
-
}
|
|
844
|
-
DragAcceptFiles(hwnd, TRUE);
|
|
845
|
-
ChangeWindowMessageFilterEx(hwnd, WM_DROPFILES, MSGFLT_ALLOW, nullptr);
|
|
846
|
-
ChangeWindowMessageFilterEx(hwnd, WM_COPYDATA, MSGFLT_ALLOW, nullptr);
|
|
847
|
-
// 0x0049 = WM_COPYGLOBALDATA,Win7 起改名后未再公开常量
|
|
848
|
-
ChangeWindowMessageFilterEx(hwnd, 0x0049, MSGFLT_ALLOW, nullptr);
|
|
849
|
-
}
|
|
850
|
-
|
|
851
|
-
void handle_drop(HDROP hd) {
|
|
852
|
-
if (!hd) {
|
|
853
|
-
return;
|
|
854
|
-
}
|
|
855
|
-
const UINT count = DragQueryFileW(hd, 0xFFFFFFFF, nullptr, 0);
|
|
856
|
-
std::string json = "{\"t\":\"drop\",\"paths\":[";
|
|
857
|
-
for (UINT i = 0; i < count; ++i) {
|
|
858
|
-
const UINT len = DragQueryFileW(hd, i, nullptr, 0);
|
|
859
|
-
if (len == 0) {
|
|
860
|
-
continue;
|
|
861
|
-
}
|
|
862
|
-
std::wstring path(static_cast<size_t>(len) + 1, L'\0');
|
|
863
|
-
if (DragQueryFileW(hd, i, &path[0], len + 1) == 0) {
|
|
864
|
-
continue;
|
|
865
|
-
}
|
|
866
|
-
path.resize(len);
|
|
867
|
-
if (json[json.size() - 1] != '[') {
|
|
868
|
-
json.push_back(',');
|
|
869
|
-
}
|
|
870
|
-
append_json_string(json, path);
|
|
871
|
-
}
|
|
872
|
-
json += "]}";
|
|
873
|
-
DragFinish(hd);
|
|
874
|
-
if (count > 0) {
|
|
875
|
-
post_json(json);
|
|
876
|
-
}
|
|
877
|
-
}
|
|
878
|
-
|
|
879
|
-
// 键盘 / 滚轮 / 拖放的共用前置处理。两种窗口模式都要,抽出来避免两份漂移。
|
|
880
|
-
// 返回 true 表示已处理并回填 *out。
|
|
881
|
-
bool handle_input_message(HWND hwnd, UINT msg, WPARAM wp, LPARAM lp, LRESULT *out) {
|
|
882
|
-
switch (msg) {
|
|
883
|
-
case WM_KEYDOWN: {
|
|
884
|
-
const bool ctrl = (GetKeyState(VK_CONTROL) & 0x8000) != 0;
|
|
885
|
-
switch (wp) {
|
|
886
|
-
case VK_F11:
|
|
887
|
-
toggle_fullscreen();
|
|
888
|
-
*out = 0;
|
|
889
|
-
return true;
|
|
890
|
-
case VK_F12:
|
|
891
|
-
open_dev_tools();
|
|
892
|
-
*out = 0;
|
|
893
|
-
return true;
|
|
894
|
-
case VK_F5:
|
|
895
|
-
reload_page();
|
|
896
|
-
*out = 0;
|
|
897
|
-
return true;
|
|
898
|
-
case 'R':
|
|
899
|
-
if (ctrl) {
|
|
900
|
-
reload_page();
|
|
901
|
-
*out = 0;
|
|
902
|
-
return true;
|
|
903
|
-
}
|
|
904
|
-
break;
|
|
905
|
-
case VK_OEM_PLUS: // 主键盘 =/+(Shift 不改变虚拟键码)
|
|
906
|
-
case VK_ADD: // 小键盘 +
|
|
907
|
-
if (ctrl) {
|
|
908
|
-
zoom_step(+1);
|
|
909
|
-
*out = 0;
|
|
910
|
-
return true;
|
|
911
|
-
}
|
|
912
|
-
break;
|
|
913
|
-
case VK_OEM_MINUS:
|
|
914
|
-
case VK_SUBTRACT:
|
|
915
|
-
if (ctrl) {
|
|
916
|
-
zoom_step(-1);
|
|
917
|
-
*out = 0;
|
|
918
|
-
return true;
|
|
919
|
-
}
|
|
920
|
-
break;
|
|
921
|
-
case '0':
|
|
922
|
-
case VK_NUMPAD0:
|
|
923
|
-
if (ctrl) {
|
|
924
|
-
apply_zoom(1.0);
|
|
925
|
-
*out = 0;
|
|
926
|
-
return true;
|
|
927
|
-
}
|
|
928
|
-
break;
|
|
929
|
-
default:
|
|
930
|
-
break;
|
|
931
|
-
}
|
|
932
|
-
break;
|
|
933
|
-
}
|
|
934
|
-
|
|
935
|
-
case WM_MOUSEWHEEL: {
|
|
936
|
-
if ((GET_KEYSTATE_WPARAM(wp) & MK_CONTROL) != 0) {
|
|
937
|
-
const short delta = GET_WHEEL_DELTA_WPARAM(wp);
|
|
938
|
-
zoom_step(delta > 0 ? +1 : -1);
|
|
939
|
-
*out = 0;
|
|
940
|
-
return true;
|
|
941
|
-
}
|
|
942
|
-
break;
|
|
943
|
-
}
|
|
944
|
-
|
|
945
|
-
case WM_DROPFILES:
|
|
946
|
-
handle_drop(reinterpret_cast<HDROP>(wp));
|
|
947
|
-
*out = 0;
|
|
948
|
-
return true;
|
|
949
|
-
|
|
950
|
-
default:
|
|
951
|
-
break;
|
|
952
|
-
}
|
|
953
|
-
return false;
|
|
954
|
-
}
|
|
955
|
-
|
|
956
|
-
// setMinSize 的入参是逻辑像素(96 DPI),而系统的 track size 要物理像素。
|
|
957
|
-
// WM_GETMINMAXINFO 与居中夹取都要这个换算,抽出来避免两处口径分叉
|
|
958
|
-
// (此前直接在 native 侧按物理像素用,高 DPI 下最小尺寸只有预期的六成)。
|
|
959
|
-
void min_track_size(HWND hwnd, int *out_w, int *out_h) {
|
|
960
|
-
const UINT dpi = hwnd ? GetDpiForWindow(hwnd) : 96;
|
|
961
|
-
RECT rc{0, 0, MulDiv(g_min_w, static_cast<int>(dpi), 96),
|
|
962
|
-
MulDiv(g_min_h, static_cast<int>(dpi), 96)};
|
|
963
|
-
AdjustWindowRectExForDpi(&rc, WS_OVERLAPPEDWINDOW, FALSE, 0, dpi);
|
|
964
|
-
if (out_w) *out_w = rc.right - rc.left;
|
|
965
|
-
if (out_h) *out_h = rc.bottom - rc.top;
|
|
966
|
-
}
|
|
967
|
-
|
|
968
|
-
// 把窗口居中到它所在显示器的工作区。两种窗口模式(自有窗口 / webview 自建窗口)共用。
|
|
969
|
-
//
|
|
970
|
-
// 三处容易写错的地方:
|
|
971
|
-
// 1) 必须用 rcWork 而不是 rcMonitor——整屏居中会让窗口压到任务栏下面。
|
|
972
|
-
// 2) 窗口比工作区大时必须先收缩再居中。高 DPI 下这很常见(900x940 逻辑像素在 200%
|
|
973
|
-
// 就是 1800x1880,加边框 1950 > 工作区 1824)。不收缩的话居中算出的 y 为负,
|
|
974
|
-
// 夹取后窗口贴顶,看起来「没有垂直居中」;收缩后才是真正的居中。
|
|
975
|
-
// 3) 收缩不得低于 setMinSize 给的下限。
|
|
976
|
-
void center_on_work_area(HWND hwnd) {
|
|
977
|
-
if (!hwnd) {
|
|
978
|
-
return;
|
|
979
|
-
}
|
|
980
|
-
MONITORINFO mi{};
|
|
981
|
-
mi.cbSize = sizeof(mi);
|
|
982
|
-
if (!GetMonitorInfoW(MonitorFromWindow(hwnd, MONITOR_DEFAULTTONEAREST), &mi)) {
|
|
983
|
-
return;
|
|
984
|
-
}
|
|
985
|
-
|
|
986
|
-
RECT rc{};
|
|
987
|
-
if (!GetWindowRect(hwnd, &rc)) {
|
|
988
|
-
return;
|
|
989
|
-
}
|
|
990
|
-
int w = rc.right - rc.left;
|
|
991
|
-
int h = rc.bottom - rc.top;
|
|
992
|
-
const int work_w = mi.rcWork.right - mi.rcWork.left;
|
|
993
|
-
const int work_h = mi.rcWork.bottom - mi.rcWork.top;
|
|
994
|
-
|
|
995
|
-
// 先收缩到工作区内,但不下探到最小尺寸以下
|
|
996
|
-
if (w > work_w || h > work_h) {
|
|
997
|
-
int min_w = 0;
|
|
998
|
-
int min_h = 0;
|
|
999
|
-
min_track_size(hwnd, &min_w, &min_h);
|
|
1000
|
-
const int max_w = work_w > min_w ? work_w : min_w;
|
|
1001
|
-
const int max_h = work_h > min_h ? work_h : min_h;
|
|
1002
|
-
const int fit_w = w > max_w ? max_w : w;
|
|
1003
|
-
const int fit_h = h > max_h ? max_h : h;
|
|
1004
|
-
if (fit_w != w || fit_h != h) {
|
|
1005
|
-
SetWindowPos(hwnd, nullptr, 0, 0, fit_w, fit_h,
|
|
1006
|
-
SWP_NOMOVE | SWP_NOZORDER | SWP_NOACTIVATE);
|
|
1007
|
-
w = fit_w;
|
|
1008
|
-
h = fit_h;
|
|
1009
|
-
}
|
|
1010
|
-
}
|
|
1011
|
-
|
|
1012
|
-
int x = mi.rcWork.left + (work_w - w) / 2;
|
|
1013
|
-
int y = mi.rcWork.top + (work_h - h) / 2;
|
|
1014
|
-
|
|
1015
|
-
if (x + w > mi.rcWork.right) x = mi.rcWork.right - w;
|
|
1016
|
-
if (y + h > mi.rcWork.bottom) y = mi.rcWork.bottom - h;
|
|
1017
|
-
if (x < mi.rcWork.left) x = mi.rcWork.left;
|
|
1018
|
-
if (y < mi.rcWork.top) y = mi.rcWork.top;
|
|
1019
|
-
|
|
1020
|
-
SetWindowPos(hwnd, nullptr, x, y, 0, 0,
|
|
1021
|
-
SWP_NOSIZE | SWP_NOZORDER | SWP_NOACTIVATE);
|
|
1022
|
-
}
|
|
1023
|
-
|
|
1024
|
-
// ---- 窗口位置与尺寸的保存/恢复 ----
|
|
1025
|
-
|
|
1026
|
-
// 读取当前窗口矩形,写成 "x,y,w,h,max,dpi"。
|
|
1027
|
-
//
|
|
1028
|
-
// 最大化时必须取「还原后」的矩形(WINDOWPLACEMENT.rcNormalPosition):
|
|
1029
|
-
// 直接 GetWindowRect 拿到的是整屏尺寸,下次启动就会先铺满再最大化,
|
|
1030
|
-
// 用户点「还原」时看到的仍是全屏——这是同类实现最常见的错。
|
|
1031
|
-
int format_bounds(char *buf, int cap) {
|
|
1032
|
-
if (!buf || cap <= 0) return 0;
|
|
1033
|
-
buf[0] = 0;
|
|
1034
|
-
if (!g_hwnd) return 0;
|
|
1035
|
-
|
|
1036
|
-
const bool maxed = IsZoomed(g_hwnd) != 0;
|
|
1037
|
-
RECT rc{};
|
|
1038
|
-
WINDOWPLACEMENT wp{};
|
|
1039
|
-
wp.length = sizeof(wp);
|
|
1040
|
-
if (maxed && GetWindowPlacement(g_hwnd, &wp)) {
|
|
1041
|
-
rc = wp.rcNormalPosition;
|
|
1042
|
-
} else if (!GetWindowRect(g_hwnd, &rc)) {
|
|
1043
|
-
return 0;
|
|
1044
|
-
}
|
|
1045
|
-
|
|
1046
|
-
const UINT dpi = GetDpiForWindow(g_hwnd);
|
|
1047
|
-
char tmp[128];
|
|
1048
|
-
// 只用整数格式:wsprintfA 不吃 %f(见 pitfalls 2.4d)
|
|
1049
|
-
wsprintfA(tmp, "%d,%d,%d,%d,%d,%u", static_cast<int>(rc.left),
|
|
1050
|
-
static_cast<int>(rc.top), static_cast<int>(rc.right - rc.left),
|
|
1051
|
-
static_cast<int>(rc.bottom - rc.top), maxed ? 1 : 0, dpi);
|
|
1052
|
-
const int n = lstrlenA(tmp);
|
|
1053
|
-
if (n >= cap) return 0;
|
|
1054
|
-
lstrcpyA(buf, tmp);
|
|
1055
|
-
return n;
|
|
1056
|
-
}
|
|
1057
|
-
|
|
1058
|
-
long long intersect_area(const RECT &a, const RECT &b) {
|
|
1059
|
-
const int l = a.left > b.left ? a.left : b.left;
|
|
1060
|
-
const int t = a.top > b.top ? a.top : b.top;
|
|
1061
|
-
const int r = a.right < b.right ? a.right : b.right;
|
|
1062
|
-
const int bo = a.bottom < b.bottom ? a.bottom : b.bottom;
|
|
1063
|
-
if (r <= l || bo <= t) return 0;
|
|
1064
|
-
return static_cast<long long>(r - l) * (bo - t);
|
|
1065
|
-
}
|
|
1066
|
-
|
|
1067
|
-
// 找出与目标矩形交叠最多的那块显示器工作区。
|
|
1068
|
-
struct MonitorPick {
|
|
1069
|
-
HMONITOR mon = nullptr;
|
|
1070
|
-
long long area = 0;
|
|
1071
|
-
RECT work{};
|
|
1072
|
-
};
|
|
1073
|
-
|
|
1074
|
-
struct PickCtx {
|
|
1075
|
-
const RECT *want;
|
|
1076
|
-
MonitorPick *out;
|
|
1077
|
-
};
|
|
1078
|
-
|
|
1079
|
-
BOOL CALLBACK pick_monitor_proc(HMONITOR mon, HDC, LPRECT, LPARAM lp) {
|
|
1080
|
-
auto *ctx = reinterpret_cast<PickCtx *>(lp);
|
|
1081
|
-
MONITORINFO mi{};
|
|
1082
|
-
mi.cbSize = sizeof(mi);
|
|
1083
|
-
if (GetMonitorInfoW(mon, &mi)) {
|
|
1084
|
-
const long long a = intersect_area(*ctx->want, mi.rcWork);
|
|
1085
|
-
if (a > ctx->out->area) {
|
|
1086
|
-
ctx->out->area = a;
|
|
1087
|
-
ctx->out->mon = mon;
|
|
1088
|
-
ctx->out->work = mi.rcWork;
|
|
1089
|
-
}
|
|
1090
|
-
}
|
|
1091
|
-
return TRUE;
|
|
1092
|
-
}
|
|
1093
|
-
|
|
1094
|
-
// 把保存的矩形恢复到窗口上。
|
|
1095
|
-
//
|
|
1096
|
-
// 跨会话恢复有三种失败方式,每个都要处理,否则表现为「窗口不知道跑哪去了」:
|
|
1097
|
-
// 1) 缩放比例变了:保存的是物理像素,要按 DPI 比例换算,否则同一份配置在
|
|
1098
|
-
// 125% 和 200% 的机器上是两个大小。
|
|
1099
|
-
// 2) 显示器不在了(拔掉外接屏、换分辨率):矩形可能整块落在屏幕外。
|
|
1100
|
-
// 判据取「与某块工作区交叠至少 80×40」——够看见标题栏并抓住窗口。
|
|
1101
|
-
// 3) 窗口比工作区还大:先收缩,否则标题栏会被推到屏幕外。
|
|
1102
|
-
// 判据不过时在某块屏上居中,而不是沿用 (0,0)——那会让使用者以为程序没启动。
|
|
1103
|
-
void restore_bounds(int x, int y, int w, int h, int saved_dpi, bool maximized) {
|
|
1104
|
-
if (!g_hwnd) return;
|
|
1105
|
-
|
|
1106
|
-
const UINT dpi = GetDpiForWindow(g_hwnd);
|
|
1107
|
-
int tw = w;
|
|
1108
|
-
int th = h;
|
|
1109
|
-
if (saved_dpi > 0 && dpi > 0 && saved_dpi != static_cast<int>(dpi)) {
|
|
1110
|
-
tw = MulDiv(w, static_cast<int>(dpi), saved_dpi);
|
|
1111
|
-
th = MulDiv(h, static_cast<int>(dpi), saved_dpi);
|
|
1112
|
-
}
|
|
1113
|
-
if (tw < g_min_w) tw = g_min_w;
|
|
1114
|
-
if (th < g_min_h) th = g_min_h;
|
|
1115
|
-
|
|
1116
|
-
RECT want{x, y, x + tw, y + th};
|
|
1117
|
-
MonitorPick pick;
|
|
1118
|
-
PickCtx ctx{&want, &pick};
|
|
1119
|
-
EnumDisplayMonitors(nullptr, nullptr, pick_monitor_proc,
|
|
1120
|
-
reinterpret_cast<LPARAM>(&ctx));
|
|
1121
|
-
|
|
1122
|
-
const long long kMinVisible = 80LL * 40LL;
|
|
1123
|
-
const bool usable = pick.mon != nullptr && pick.area >= kMinVisible;
|
|
1124
|
-
|
|
1125
|
-
RECT work{};
|
|
1126
|
-
if (usable) {
|
|
1127
|
-
work = pick.work;
|
|
1128
|
-
} else {
|
|
1129
|
-
MONITORINFO mi{};
|
|
1130
|
-
mi.cbSize = sizeof(mi);
|
|
1131
|
-
if (!GetMonitorInfoW(MonitorFromWindow(g_hwnd, MONITOR_DEFAULTTOPRIMARY), &mi)) {
|
|
1132
|
-
return;
|
|
1133
|
-
}
|
|
1134
|
-
work = mi.rcWork;
|
|
1135
|
-
}
|
|
1136
|
-
|
|
1137
|
-
const int work_w = work.right - work.left;
|
|
1138
|
-
const int work_h = work.bottom - work.top;
|
|
1139
|
-
if (tw > work_w) tw = work_w;
|
|
1140
|
-
if (th > work_h) th = work_h;
|
|
1141
|
-
|
|
1142
|
-
int nx;
|
|
1143
|
-
int ny;
|
|
1144
|
-
if (usable) {
|
|
1145
|
-
nx = x;
|
|
1146
|
-
ny = y;
|
|
1147
|
-
} else {
|
|
1148
|
-
nx = work.left + (work_w - tw) / 2;
|
|
1149
|
-
ny = work.top + (work_h - th) / 2;
|
|
1150
|
-
}
|
|
1151
|
-
|
|
1152
|
-
if (nx + tw > work.right) nx = work.right - tw;
|
|
1153
|
-
if (ny + th > work.bottom) ny = work.bottom - th;
|
|
1154
|
-
if (nx < work.left) nx = work.left;
|
|
1155
|
-
if (ny < work.top) ny = work.top;
|
|
1156
|
-
|
|
1157
|
-
// 最大化状态下直接 SetWindowPos 只改「还原位置」,不移动窗口本身;
|
|
1158
|
-
// 先还原再定位,最后按需重新最大化。
|
|
1159
|
-
if (IsZoomed(g_hwnd)) {
|
|
1160
|
-
ShowWindow(g_hwnd, SW_RESTORE);
|
|
1161
|
-
}
|
|
1162
|
-
SetWindowPos(g_hwnd, nullptr, nx, ny, tw, th,
|
|
1163
|
-
SWP_NOZORDER | SWP_NOACTIVATE);
|
|
1164
|
-
if (maximized) {
|
|
1165
|
-
ShowWindow(g_hwnd, SW_MAXIMIZE);
|
|
1166
|
-
}
|
|
1167
|
-
fit_widget();
|
|
1168
|
-
push_state(true);
|
|
1169
|
-
}
|
|
1170
|
-
|
|
1171
|
-
// ---- 自有窗口的消息处理(仅 frameless 路径使用) ----
|
|
1172
|
-
|
|
1173
|
-
LRESULT CALLBACK shell_wnd_proc(HWND hwnd, UINT msg, WPARAM wp, LPARAM lp) {
|
|
1174
|
-
// 键盘 / Ctrl+滚轮 / 拖放:两种窗口模式共用同一套处理
|
|
1175
|
-
LRESULT handled = 0;
|
|
1176
|
-
if (handle_input_message(hwnd, msg, wp, lp, &handled)) {
|
|
1177
|
-
return handled;
|
|
1178
|
-
}
|
|
1179
|
-
switch (msg) {
|
|
1180
|
-
case WM_NCCALCSIZE:
|
|
1181
|
-
if (g_frameless && wp == TRUE) {
|
|
1182
|
-
// 客户区占满窗口。最大化时直接钉到工作区:
|
|
1183
|
-
// 系统给最大化窗口的矩形会带不可见边框,只做等差回缩依赖 SM_* 猜测;
|
|
1184
|
-
// 直接以 rcWork 覆盖更确定,也不受 DWM 边框宽度变化影响。
|
|
1185
|
-
if (IsZoomed(hwnd)) {
|
|
1186
|
-
auto *params = reinterpret_cast<NCCALCSIZE_PARAMS *>(lp);
|
|
1187
|
-
MONITORINFO mi{};
|
|
1188
|
-
mi.cbSize = sizeof(mi);
|
|
1189
|
-
if (GetMonitorInfoW(MonitorFromWindow(hwnd, MONITOR_DEFAULTTONEAREST), &mi)) {
|
|
1190
|
-
params->rgrc[0] = mi.rcWork;
|
|
1191
|
-
}
|
|
1192
|
-
}
|
|
1193
|
-
return 0;
|
|
1194
|
-
}
|
|
1195
|
-
break;
|
|
1196
|
-
|
|
1197
|
-
case WM_NCHITTEST: {
|
|
1198
|
-
POINT pt{GET_X_LPARAM(lp), GET_Y_LPARAM(lp)};
|
|
1199
|
-
const LRESULT code = hit_test(hwnd, pt);
|
|
1200
|
-
if (code != HTCLIENT) {
|
|
1201
|
-
return code;
|
|
1202
|
-
}
|
|
1203
|
-
break;
|
|
1204
|
-
}
|
|
1205
|
-
|
|
1206
|
-
case WM_GETMINMAXINFO: {
|
|
1207
|
-
auto *mmi = reinterpret_cast<MINMAXINFO *>(lp);
|
|
1208
|
-
|
|
1209
|
-
// 最小尺寸:入参是逻辑像素,换算成当前 DPI 的 track size 后回报
|
|
1210
|
-
int min_w = 0;
|
|
1211
|
-
int min_h = 0;
|
|
1212
|
-
min_track_size(hwnd, &min_w, &min_h);
|
|
1213
|
-
mmi->ptMinTrackSize.x = min_w;
|
|
1214
|
-
mmi->ptMinTrackSize.y = min_h;
|
|
1215
|
-
|
|
1216
|
-
// 最大化区域钉到工作区:否则系统按带边框的窗口外扩,无边框下会溢出屏幕。
|
|
1217
|
-
MONITORINFO mi{};
|
|
1218
|
-
mi.cbSize = sizeof(mi);
|
|
1219
|
-
if (GetMonitorInfoW(MonitorFromWindow(hwnd, MONITOR_DEFAULTTONEAREST), &mi)) {
|
|
1220
|
-
mmi->ptMaxPosition.x = mi.rcWork.left - mi.rcMonitor.left;
|
|
1221
|
-
mmi->ptMaxPosition.y = mi.rcWork.top - mi.rcMonitor.top;
|
|
1222
|
-
mmi->ptMaxSize.x = mi.rcWork.right - mi.rcWork.left;
|
|
1223
|
-
mmi->ptMaxSize.y = mi.rcWork.bottom - mi.rcWork.top;
|
|
1224
|
-
}
|
|
1225
|
-
break;
|
|
1226
|
-
}
|
|
1227
|
-
|
|
1228
|
-
case WM_TIMER:
|
|
1229
|
-
// 状态推送被节流时挂的一次性定时器:到点补发最后一次状态
|
|
1230
|
-
if (wp == kPushTimerId) {
|
|
1231
|
-
flush_push_state();
|
|
1232
|
-
return 0;
|
|
1233
|
-
}
|
|
1234
|
-
break;
|
|
1235
|
-
|
|
1236
|
-
case WM_SIZE:
|
|
1237
|
-
// 贴满屏幕时收起圆角:最大化窗口与屏幕边缘应严丝合缝,
|
|
1238
|
-
// 保留圆角会在四角露出桌面(尤其任务栏同色时更明显)。
|
|
1239
|
-
apply_round_corners(hwnd, !IsZoomed(hwnd));
|
|
1240
|
-
fit_widget();
|
|
1241
|
-
push_state();
|
|
1242
|
-
break;
|
|
1243
|
-
|
|
1244
|
-
case WM_ACTIVATE:
|
|
1245
|
-
case WM_SETFOCUS:
|
|
1246
|
-
push_state();
|
|
1247
|
-
break;
|
|
1248
|
-
|
|
1249
|
-
case WM_SETTINGCHANGE:
|
|
1250
|
-
case WM_THEMECHANGED:
|
|
1251
|
-
// 系统切换深/浅色时重新应用,并把新状态推给页面
|
|
1252
|
-
apply_dark_mode(hwnd);
|
|
1253
|
-
push_state();
|
|
1254
|
-
break;
|
|
1255
|
-
|
|
1256
|
-
case WM_NCRBUTTONUP:
|
|
1257
|
-
// 右键标题栏:系统菜单。无边框模式没有非客户区,命中由 hit_test 给出
|
|
1258
|
-
if (static_cast<WPARAM>(wp) == HTCAPTION) {
|
|
1259
|
-
POINT pt{GET_X_LPARAM(lp), GET_Y_LPARAM(lp)};
|
|
1260
|
-
show_system_menu(hwnd, pt);
|
|
1261
|
-
return 0;
|
|
1262
|
-
}
|
|
1263
|
-
break;
|
|
1264
|
-
|
|
1265
|
-
case WM_SYSCOMMAND:
|
|
1266
|
-
// Alt+Space 走 SC_KEYMENU,低半字节用于携带按键字符
|
|
1267
|
-
if ((static_cast<WPARAM>(wp) & 0xFFF0) == SC_KEYMENU) {
|
|
1268
|
-
if (static_cast<INT_PTR>(lp) == ' ') {
|
|
1269
|
-
RECT rc{};
|
|
1270
|
-
GetWindowRect(hwnd, &rc);
|
|
1271
|
-
POINT pt{rc.left + 8, rc.top + 8};
|
|
1272
|
-
show_system_menu(hwnd, pt);
|
|
1273
|
-
return 0;
|
|
1274
|
-
}
|
|
1275
|
-
}
|
|
1276
|
-
break;
|
|
1277
|
-
|
|
1278
|
-
case WM_DPICHANGED: {
|
|
1279
|
-
// 只在窗口已显示后跟随建议矩形。创建/首次布局阶段系统给出的建议矩形,
|
|
1280
|
-
// 是基于创建时的级联位置算出来的,会覆盖掉调用方的居中/定位意图。
|
|
1281
|
-
if (!g_shown) {
|
|
1282
|
-
break;
|
|
1283
|
-
}
|
|
1284
|
-
const auto *suggested = reinterpret_cast<RECT *>(lp);
|
|
1285
|
-
SetWindowPos(hwnd, nullptr, suggested->left, suggested->top,
|
|
1286
|
-
suggested->right - suggested->left,
|
|
1287
|
-
suggested->bottom - suggested->top,
|
|
1288
|
-
SWP_NOZORDER | SWP_NOACTIVATE);
|
|
1289
|
-
fit_widget();
|
|
1290
|
-
push_state();
|
|
1291
|
-
return 0;
|
|
1292
|
-
}
|
|
1293
|
-
|
|
1294
|
-
case WM_CLOSE:
|
|
1295
|
-
DestroyWindow(hwnd);
|
|
1296
|
-
return 0;
|
|
1297
|
-
|
|
1298
|
-
case WM_DESTROY:
|
|
1299
|
-
PostQuitMessage(0);
|
|
1300
|
-
return 0;
|
|
1301
|
-
|
|
1302
|
-
default:
|
|
1303
|
-
break;
|
|
1304
|
-
}
|
|
1305
|
-
return DefWindowProcW(hwnd, msg, wp, lp);
|
|
1306
|
-
}
|
|
1307
|
-
|
|
1308
|
-
// ---- widget 子类化:让缩放边框的命中透传给父窗口 ----
|
|
1309
|
-
|
|
1310
|
-
LRESULT CALLBACK widget_proc(HWND hwnd, UINT msg, WPARAM wp, LPARAM lp) {
|
|
1311
|
-
if (msg == WM_NCHITTEST) {
|
|
1312
|
-
POINT pt{GET_X_LPARAM(lp), GET_Y_LPARAM(lp)};
|
|
1313
|
-
if (hit_test(g_hwnd ? g_hwnd : GetParent(hwnd), pt) != HTCLIENT) {
|
|
1314
|
-
// HTTRANSPARENT:本窗口不吃这个点,交给同一线程的父窗口判定。
|
|
1315
|
-
return HTTRANSPARENT;
|
|
1316
|
-
}
|
|
1317
|
-
}
|
|
1318
|
-
return CallWindowProcW(g_widget_orig, hwnd, msg, wp, lp);
|
|
1319
|
-
}
|
|
1320
|
-
|
|
1321
|
-
void subclass_widget() {
|
|
1322
|
-
if (!g_widget || g_widget_orig) {
|
|
1323
|
-
return;
|
|
1324
|
-
}
|
|
1325
|
-
g_widget_orig = reinterpret_cast<WNDPROC>(
|
|
1326
|
-
SetWindowLongPtrW(g_widget, GWLP_WNDPROC,
|
|
1327
|
-
reinterpret_cast<LONG_PTR>(widget_proc)));
|
|
1328
|
-
}
|
|
1329
|
-
|
|
1330
|
-
// 带边框模式:顶层窗口由 webview 创建,要补系统菜单与主题跟随只能子类化。
|
|
1331
|
-
// 只拦这几类消息,其余原样转发,避免影响 webview 自身的窗口逻辑。
|
|
1332
|
-
LRESULT CALLBACK toplevel_proc(HWND hwnd, UINT msg, WPARAM wp, LPARAM lp) {
|
|
1333
|
-
LRESULT handled = 0;
|
|
1334
|
-
if (handle_input_message(hwnd, msg, wp, lp, &handled)) {
|
|
1335
|
-
return handled;
|
|
1336
|
-
}
|
|
1337
|
-
switch (msg) {
|
|
1338
|
-
case WM_SHOWWINDOW:
|
|
1339
|
-
// webview 的 set_size_impl 结尾会 window_show(),而 window_show() 里
|
|
1340
|
-
// ShowWindow + UpdateWindow 是**同步绘制**:窗口会先以未加载页面(纯黑)
|
|
1341
|
-
// 的姿态真画出一帧,再被调用方藏起来——这就是启动闪屏。
|
|
1342
|
-
// 延迟显示期间,任何来自外部的显示请求都在这里压回去:
|
|
1343
|
-
// 用 SWP_HIDEWINDOW 清掉 WS_VISIBLE,使随后的 UpdateWindow 无更新区域可画。
|
|
1344
|
-
// 我们自己的 shell_show() → show_now() 会先置 g_shown,故不会被拦截。
|
|
1345
|
-
if (wp == TRUE && g_defer_show && !g_shown) {
|
|
1346
|
-
SetWindowPos(hwnd, nullptr, 0, 0, 0, 0,
|
|
1347
|
-
SWP_HIDEWINDOW | SWP_NOMOVE | SWP_NOSIZE | SWP_NOZORDER |
|
|
1348
|
-
SWP_NOACTIVATE);
|
|
1349
|
-
return 0;
|
|
1350
|
-
}
|
|
1351
|
-
break;
|
|
1352
|
-
case WM_TIMER:
|
|
1353
|
-
if (wp == kPushTimerId) {
|
|
1354
|
-
flush_push_state();
|
|
1355
|
-
return 0;
|
|
1356
|
-
}
|
|
1357
|
-
break;
|
|
1358
|
-
case WM_SETTINGCHANGE:
|
|
1359
|
-
case WM_THEMECHANGED:
|
|
1360
|
-
apply_dark_mode(hwnd);
|
|
1361
|
-
push_state();
|
|
1362
|
-
break;
|
|
1363
|
-
case WM_NCRBUTTONUP:
|
|
1364
|
-
if (static_cast<WPARAM>(wp) == HTCAPTION) {
|
|
1365
|
-
POINT pt{GET_X_LPARAM(lp), GET_Y_LPARAM(lp)};
|
|
1366
|
-
show_system_menu(hwnd, pt);
|
|
1367
|
-
return 0;
|
|
1368
|
-
}
|
|
1369
|
-
break;
|
|
1370
|
-
default:
|
|
1371
|
-
break;
|
|
1372
|
-
}
|
|
1373
|
-
return CallWindowProcW(g_toplevel_orig, hwnd, msg, wp, lp);
|
|
1374
|
-
}
|
|
1375
|
-
|
|
1376
|
-
void subclass_toplevel() {
|
|
1377
|
-
if (!g_hwnd || g_toplevel_orig) {
|
|
1378
|
-
return;
|
|
1379
|
-
}
|
|
1380
|
-
g_toplevel_orig = reinterpret_cast<WNDPROC>(
|
|
1381
|
-
SetWindowLongPtrW(g_hwnd, GWLP_WNDPROC,
|
|
1382
|
-
reinterpret_cast<LONG_PTR>(toplevel_proc)));
|
|
1383
|
-
}
|
|
1384
|
-
|
|
1385
|
-
// ---- UI 线程 ----
|
|
1386
|
-
|
|
1387
|
-
void drain(webview_t /*w*/, void * /*arg*/) {
|
|
1388
|
-
std::deque<cmd> local;
|
|
1389
|
-
{
|
|
1390
|
-
std::lock_guard<std::mutex> lock(g_queue_mu);
|
|
1391
|
-
local.swap(g_queue);
|
|
1392
|
-
}
|
|
1393
|
-
for (const auto &c : local) {
|
|
1394
|
-
if (!g_hwnd) {
|
|
1395
|
-
break;
|
|
1396
|
-
}
|
|
1397
|
-
switch (c.kind) {
|
|
1398
|
-
case cmd_kind::eval:
|
|
1399
|
-
if (g_webview) webview_eval(g_webview, c.arg.c_str());
|
|
1400
|
-
break;
|
|
1401
|
-
case cmd_kind::title:
|
|
1402
|
-
// 必须做 UTF-8 → UTF-16 解码:std::wstring(arg.begin(), arg.end()) 会把每个
|
|
1403
|
-
// UTF-8 字节当成一个 wchar_t,中文标题直接变乱码(实测:「围棋习题册生成器」
|
|
1404
|
-
// 被显示成「¥ロᄡ₩ᆪヒ…」)。widen_utf8 见原生上下文菜单那一节。
|
|
1405
|
-
SetWindowTextW(g_hwnd, widen_utf8(c.arg).c_str());
|
|
1406
|
-
break;
|
|
1407
|
-
case cmd_kind::minimize:
|
|
1408
|
-
ShowWindow(g_hwnd, SW_MINIMIZE);
|
|
1409
|
-
break;
|
|
1410
|
-
case cmd_kind::maximize_toggle:
|
|
1411
|
-
ShowWindow(g_hwnd, IsZoomed(g_hwnd) ? SW_RESTORE : SW_MAXIMIZE);
|
|
1412
|
-
break;
|
|
1413
|
-
case cmd_kind::close:
|
|
1414
|
-
DestroyWindow(g_hwnd);
|
|
1415
|
-
break;
|
|
1416
|
-
case cmd_kind::drag:
|
|
1417
|
-
ReleaseCapture();
|
|
1418
|
-
SendMessageW(g_hwnd, WM_NCLBUTTONDOWN, HTCAPTION, 0);
|
|
1419
|
-
break;
|
|
1420
|
-
case cmd_kind::resize: {
|
|
1421
|
-
const int edge = std::atoi(c.arg.c_str());
|
|
1422
|
-
static const WPARAM codes[8] = {HTLEFT, HTRIGHT, HTTOP,
|
|
1423
|
-
HTBOTTOM, HTTOPLEFT, HTTOPRIGHT,
|
|
1424
|
-
HTBOTTOMLEFT, HTBOTTOMRIGHT};
|
|
1425
|
-
if (edge >= 0 && edge <= 7) {
|
|
1426
|
-
ReleaseCapture();
|
|
1427
|
-
SendMessageW(g_hwnd, WM_NCLBUTTONDOWN, codes[edge], 0);
|
|
1428
|
-
}
|
|
1429
|
-
break;
|
|
1430
|
-
}
|
|
1431
|
-
case cmd_kind::show:
|
|
1432
|
-
if (!g_shown) show_now();
|
|
1433
|
-
break;
|
|
1434
|
-
case cmd_kind::zoom:
|
|
1435
|
-
// 参数是千分数(见 shell_set_zoom)
|
|
1436
|
-
apply_zoom(static_cast<double>(std::atoi(c.arg.c_str())) / 1000.0);
|
|
1437
|
-
break;
|
|
1438
|
-
case cmd_kind::reload:
|
|
1439
|
-
reload_page();
|
|
1440
|
-
break;
|
|
1441
|
-
case cmd_kind::devtools:
|
|
1442
|
-
open_dev_tools();
|
|
1443
|
-
break;
|
|
1444
|
-
case cmd_kind::fullscreen:
|
|
1445
|
-
toggle_fullscreen();
|
|
1446
|
-
break;
|
|
1447
|
-
case cmd_kind::browser_ui:
|
|
1448
|
-
apply_browser_ui(g_webview, std::atoi(c.arg.c_str()));
|
|
1449
|
-
break;
|
|
1450
|
-
case cmd_kind::app_menu: {
|
|
1451
|
-
// 参数格式 "x,y|menu-spec":屏幕坐标 + 菜单描述
|
|
1452
|
-
const size_t bar = c.arg.find('|');
|
|
1453
|
-
if (bar != std::string::npos) {
|
|
1454
|
-
const std::string coord = c.arg.substr(0, bar);
|
|
1455
|
-
const std::string spec = c.arg.substr(bar + 1);
|
|
1456
|
-
int mx = 0;
|
|
1457
|
-
int my = 0;
|
|
1458
|
-
if (sscanf(coord.c_str(), "%d,%d", &mx, &my) == 2) {
|
|
1459
|
-
POINT pt{mx, my};
|
|
1460
|
-
show_app_menu(g_hwnd, pt, spec);
|
|
1461
|
-
}
|
|
1462
|
-
}
|
|
1463
|
-
break;
|
|
1464
|
-
}
|
|
1465
|
-
case cmd_kind::restore_bounds: {
|
|
1466
|
-
int bx = 0, by = 0, bw = 0, bh = 0, bdpi = 0, bmax = 0;
|
|
1467
|
-
// 纯整数解析:与 format_bounds 的输出格式成对
|
|
1468
|
-
if (sscanf(c.arg.c_str(), "%d,%d,%d,%d,%d,%d", &bx, &by, &bw, &bh, &bdpi,
|
|
1469
|
-
&bmax) == 6) {
|
|
1470
|
-
restore_bounds(bx, by, bw, bh, bdpi, bmax != 0);
|
|
1471
|
-
}
|
|
1472
|
-
break;
|
|
1473
|
-
}
|
|
1474
|
-
}
|
|
1475
|
-
}
|
|
1476
|
-
}
|
|
1477
|
-
|
|
1478
|
-
// 把命令投给 UI 线程执行。
|
|
1479
|
-
//
|
|
1480
|
-
// 生命周期锁 g_life_mu 让「检查 g_webview 非空」与「投递」成为原子对:
|
|
1481
|
-
// teardown 也必须持它才能销毁并清空,所以中间不会插进一次销毁,
|
|
1482
|
-
// 不会把 webview_dispatch 打到已释放的对象上。
|
|
1483
|
-
// webview_dispatch 内部只是 PostMessageW,不同步回调 drain,因此持锁安全。
|
|
1484
|
-
void enqueue(cmd_kind kind, std::string arg = {}) {
|
|
1485
|
-
std::lock_guard<std::mutex> life(g_life_mu);
|
|
1486
|
-
if (!g_webview) {
|
|
1487
|
-
return;
|
|
1488
|
-
}
|
|
1489
|
-
{
|
|
1490
|
-
std::lock_guard<std::mutex> q(g_queue_mu);
|
|
1491
|
-
g_queue.push_back(cmd{kind, std::move(arg)});
|
|
1492
|
-
}
|
|
1493
|
-
webview_dispatch(g_webview, drain, nullptr);
|
|
1494
|
-
}
|
|
1495
|
-
|
|
1496
|
-
HWND create_own_window(int width, int height, const wchar_t *title) {
|
|
1497
|
-
static bool registered = false;
|
|
1498
|
-
if (!registered) {
|
|
1499
|
-
WNDCLASSEXW wc{};
|
|
1500
|
-
wc.cbSize = sizeof(wc);
|
|
1501
|
-
wc.hInstance = GetModuleHandleW(nullptr);
|
|
1502
|
-
wc.lpszClassName = L"webview_shell";
|
|
1503
|
-
wc.lpfnWndProc = shell_wnd_proc;
|
|
1504
|
-
wc.hCursor = LoadCursorW(nullptr, IDC_ARROW);
|
|
1505
|
-
wc.hbrBackground = nullptr;
|
|
1506
|
-
// 窗口不会自动继承 exe 图标,必须在类上显式给,否则标题栏/Alt-Tab 是默认图标
|
|
1507
|
-
wc.hIcon = load_exe_icon(GetSystemMetrics(SM_CXICON), GetSystemMetrics(SM_CYICON));
|
|
1508
|
-
wc.hIconSm =
|
|
1509
|
-
load_exe_icon(GetSystemMetrics(SM_CXSMICON), GetSystemMetrics(SM_CYSMICON));
|
|
1510
|
-
// CS_HREDRAW/VREDRAW 会引发重绘闪烁,无边框自绘不需要
|
|
1511
|
-
wc.style = 0;
|
|
1512
|
-
if (!RegisterClassExW(&wc) && GetLastError() != ERROR_CLASS_ALREADY_EXISTS) {
|
|
1513
|
-
return nullptr;
|
|
1514
|
-
}
|
|
1515
|
-
registered = true;
|
|
1516
|
-
}
|
|
1517
|
-
|
|
1518
|
-
// 无边框窗口按「客户区尺寸」直接创建:不做 AdjustWindowRectEx,否则会多出边框,
|
|
1519
|
-
// 且最大化/还原时系统按带边框的矩形保存还原点,造成还原后尺寸漂移。
|
|
1520
|
-
// 尺寸按当前 DPI 换算:对外接口统一用 96 DPI 逻辑像素。
|
|
1521
|
-
const int scale_num = static_cast<int>(GetDpiForSystem());
|
|
1522
|
-
const int px_w = MulDiv(width, scale_num, 96);
|
|
1523
|
-
const int px_h = MulDiv(height, scale_num, 96);
|
|
1524
|
-
|
|
1525
|
-
HWND hwnd = CreateWindowExW(
|
|
1526
|
-
0, L"webview_shell", title, WS_OVERLAPPEDWINDOW & ~WS_VISIBLE,
|
|
1527
|
-
CW_USEDEFAULT, CW_USEDEFAULT, px_w, px_h, nullptr, nullptr,
|
|
1528
|
-
GetModuleHandleW(nullptr), nullptr);
|
|
1529
|
-
if (!hwnd) {
|
|
1530
|
-
return nullptr;
|
|
1531
|
-
}
|
|
1532
|
-
|
|
1533
|
-
// 创建期的 WM_NCCALCSIZE 不会被采纳(系统仍按带边框计算客户区),
|
|
1534
|
-
// 必须用 SWP_FRAMECHANGED 强制重算一次,之后 client == window。
|
|
1535
|
-
SetWindowPos(hwnd, nullptr, 0, 0, 0, 0,
|
|
1536
|
-
SWP_NOMOVE | SWP_NOSIZE | SWP_NOZORDER | SWP_NOACTIVATE |
|
|
1537
|
-
SWP_FRAMECHANGED);
|
|
1538
|
-
|
|
1539
|
-
apply_round_corners(hwnd, true);
|
|
1540
|
-
apply_window_icons(hwnd);
|
|
1541
|
-
apply_dark_mode(hwnd);
|
|
1542
|
-
return hwnd;
|
|
1543
|
-
}
|
|
1544
|
-
|
|
1545
|
-
DWORD WINAPI ui_thread(LPVOID param) {
|
|
1546
|
-
const int flags = static_cast<int>(reinterpret_cast<INT_PTR>(param));
|
|
1547
|
-
|
|
1548
|
-
g_frameless = (flags & SHELL_FLAG_FRAMELESS) != 0;
|
|
1549
|
-
g_resizable = (flags & SHELL_FLAG_RESIZABLE) != 0;
|
|
1550
|
-
g_defer_show = (flags & SHELL_FLAG_DEFER_SHOW) != 0;
|
|
1551
|
-
g_accept_drop = (flags & SHELL_FLAG_ACCEPT_DROP) != 0;
|
|
1552
|
-
g_start_maximized = (flags & SHELL_FLAG_START_MAXIMIZED) != 0;
|
|
1553
|
-
|
|
1554
|
-
// 任务栏归属必须在窗口创建前设定
|
|
1555
|
-
apply_app_user_model_id();
|
|
1556
|
-
|
|
1557
|
-
// 关键:webview 的 com_init_wrapper 只在「它自己创建窗口」时初始化 COM。
|
|
1558
|
-
// 嵌入模式(我们提供 HWND)必须由调用方先 CoInitializeEx,否则
|
|
1559
|
-
// CreateCoreWebView2Controller 会失败,最终表现为 webview_create 返回 nullptr。
|
|
1560
|
-
if (g_frameless) {
|
|
1561
|
-
const HRESULT hr = CoInitializeEx(nullptr, COINIT_APARTMENTTHREADED);
|
|
1562
|
-
if (FAILED(hr)) {
|
|
1563
|
-
set_err("CoInitializeEx failed");
|
|
1564
|
-
SetEvent(g_ready);
|
|
1565
|
-
return 1;
|
|
1566
|
-
}
|
|
1567
|
-
}
|
|
1568
|
-
|
|
1569
|
-
// 必须做 UTF-8 → UTF-16 解码(同 drain 里的 title 分支):
|
|
1570
|
-
// 无边框模式用它创建窗口标题,直接窄转宽会把中文变成乱码。
|
|
1571
|
-
const std::wstring wtitle = widen_utf8(g_title);
|
|
1572
|
-
|
|
1573
|
-
if (g_frameless) {
|
|
1574
|
-
g_hwnd = create_own_window(g_start_w, g_start_h, wtitle.c_str());
|
|
1575
|
-
if (!g_hwnd) {
|
|
1576
|
-
set_err("CreateWindowEx failed");
|
|
1577
|
-
SetEvent(g_ready);
|
|
1578
|
-
return 1;
|
|
1579
|
-
}
|
|
1580
|
-
if (!g_resizable) {
|
|
1581
|
-
SetWindowLongPtrW(g_hwnd, GWL_STYLE,
|
|
1582
|
-
GetWindowLongPtrW(g_hwnd, GWL_STYLE) &
|
|
1583
|
-
~(WS_THICKFRAME | WS_MAXIMIZEBOX));
|
|
1584
|
-
}
|
|
1585
|
-
if (flags & SHELL_FLAG_TOPMOST) {
|
|
1586
|
-
SetWindowPos(g_hwnd, HWND_TOPMOST, 0, 0, 0, 0,
|
|
1587
|
-
SWP_NOMOVE | SWP_NOSIZE | SWP_NOACTIVATE);
|
|
1588
|
-
}
|
|
1589
|
-
if (flags & SHELL_FLAG_CENTER) {
|
|
1590
|
-
center_on_work_area(g_hwnd);
|
|
1591
|
-
}
|
|
1592
|
-
}
|
|
1593
|
-
|
|
1594
|
-
g_webview = webview_create(1, g_frameless ? static_cast<void *>(g_hwnd) : nullptr);
|
|
1595
|
-
if (!g_webview) {
|
|
1596
|
-
set_err("webview_create failed (WebView2 runtime missing?)");
|
|
1597
|
-
SetEvent(g_ready);
|
|
1598
|
-
return 1;
|
|
1599
|
-
}
|
|
1600
|
-
|
|
1601
|
-
// 必须在 navigate 之前:这是「页面还没画出内容时露出的底色」,
|
|
1602
|
-
// 设晚了那一帧白底已经闪过去了。
|
|
1603
|
-
apply_default_background(g_webview, system_uses_dark());
|
|
1604
|
-
|
|
1605
|
-
if (!g_frameless) {
|
|
1606
|
-
g_hwnd = static_cast<HWND>(webview_get_window(g_webview));
|
|
1607
|
-
|
|
1608
|
-
// 子类化必须在 set_size 之前安装:webview 的 set_size_impl 结尾会调
|
|
1609
|
-
// window_show(),而我们要在 WM_SHOWWINDOW 里把这次显示压回去。
|
|
1610
|
-
subclass_toplevel();
|
|
1611
|
-
|
|
1612
|
-
webview_set_title(g_webview, g_title.c_str());
|
|
1613
|
-
// webview 自建的窗口同样不继承 exe 图标,且不会跟随系统深色
|
|
1614
|
-
apply_window_icons(g_hwnd);
|
|
1615
|
-
apply_dark_mode(g_hwnd);
|
|
1616
|
-
|
|
1617
|
-
// 这个调用不能省,哪怕尺寸我们后面会再改:
|
|
1618
|
-
// 它会置位 webview 内部的 m_is_size_set。不置位的话,webview 在消息循环里
|
|
1619
|
-
// 会补一个「默认 640x480」的 set_size,把尺寸覆盖掉(实测 client 变 1280x960,
|
|
1620
|
-
// 而不是预期的 1800x1640)。它顺带的显示动作由 WM_SHOWWINDOW 拦截。
|
|
1621
|
-
webview_set_size(g_webview, g_start_w, g_start_h, WEBVIEW_HINT_NONE);
|
|
1622
|
-
|
|
1623
|
-
// 带边框模式同样支持 FLAG_CENTER。此前只在无边框分支里居中,
|
|
1624
|
-
// 带边框窗口于是停在创建时的级联位置(实测 147,147 / 343,343)。
|
|
1625
|
-
if (flags & SHELL_FLAG_CENTER) {
|
|
1626
|
-
center_on_work_area(g_hwnd);
|
|
1627
|
-
}
|
|
1628
|
-
}
|
|
1629
|
-
|
|
1630
|
-
g_widget = static_cast<HWND>(
|
|
1631
|
-
webview_get_native_handle(g_webview, WEBVIEW_NATIVE_HANDLE_KIND_UI_WIDGET));
|
|
1632
|
-
|
|
1633
|
-
if (g_accept_drop) {
|
|
1634
|
-
enable_drop(g_hwnd);
|
|
1635
|
-
}
|
|
1636
|
-
|
|
1637
|
-
static const char *kInit =
|
|
1638
|
-
"window.__shell = window.__shell || {};"
|
|
1639
|
-
"window.__shell.recv = function (o) {"
|
|
1640
|
-
" window.dispatchEvent(new CustomEvent('shell-msg', { detail: o }));"
|
|
1641
|
-
"};";
|
|
1642
|
-
|
|
1643
|
-
if (webview_init(g_webview, kInit) != WEBVIEW_ERROR_OK) {
|
|
1644
|
-
set_err("webview_init failed");
|
|
1645
|
-
}
|
|
1646
|
-
if (webview_navigate(g_webview, g_url.c_str()) != WEBVIEW_ERROR_OK) {
|
|
1647
|
-
set_err("webview_navigate failed");
|
|
1648
|
-
}
|
|
1649
|
-
|
|
1650
|
-
if (g_frameless) {
|
|
1651
|
-
subclass_widget();
|
|
1652
|
-
fit_widget();
|
|
1653
|
-
if (!g_defer_show) show_now();
|
|
1654
|
-
} else {
|
|
1655
|
-
// subclass_toplevel() 已在上面(set_size 之前)安装
|
|
1656
|
-
if (!g_defer_show) g_shown = true;
|
|
1657
|
-
}
|
|
1658
|
-
|
|
1659
|
-
SetEvent(g_ready);
|
|
1660
|
-
|
|
1661
|
-
webview_run(g_webview); // 阻塞直到窗口关闭或 shell_stop()
|
|
1662
|
-
|
|
1663
|
-
// 销毁 + 清空必须在生命周期锁内完成:enqueue 在同一把锁下「检查非空并投递」,
|
|
1664
|
-
// 中间若插进一次销毁,就会把 webview_dispatch 打到已释放的对象上。
|
|
1665
|
-
{
|
|
1666
|
-
std::lock_guard<std::mutex> life(g_life_mu);
|
|
1667
|
-
webview_destroy(g_webview);
|
|
1668
|
-
g_webview = nullptr;
|
|
1669
|
-
{
|
|
1670
|
-
std::lock_guard<std::mutex> q(g_queue_mu);
|
|
1671
|
-
g_queue.clear();
|
|
1672
|
-
}
|
|
1673
|
-
}
|
|
1674
|
-
|
|
1675
|
-
g_widget = nullptr;
|
|
1676
|
-
g_widget_orig = nullptr;
|
|
1677
|
-
g_toplevel_orig = nullptr;
|
|
1678
|
-
g_hwnd = nullptr;
|
|
1679
|
-
g_shown = false;
|
|
1680
|
-
g_round_state = -1; // 窗口已销毁,缓存失效
|
|
1681
|
-
g_push_pending = false;
|
|
1682
|
-
g_pushed = PushedState{};
|
|
1683
|
-
|
|
1684
|
-
if (g_frameless) {
|
|
1685
|
-
CoUninitialize();
|
|
1686
|
-
}
|
|
1687
|
-
return 0;
|
|
1688
|
-
}
|
|
1689
|
-
|
|
1690
|
-
} // namespace
|
|
1691
|
-
|
|
1692
|
-
extern "C" {
|
|
1693
|
-
|
|
1694
|
-
__declspec(dllexport) int shell_start_ex(const char *url, const char *title,
|
|
1695
|
-
int width, int height, int flags) {
|
|
1696
|
-
if (!url || !*url) {
|
|
1697
|
-
set_err("url is required");
|
|
1698
|
-
return -1;
|
|
1699
|
-
}
|
|
1700
|
-
if (g_webview) {
|
|
1701
|
-
set_err("shell already started");
|
|
1702
|
-
return -1;
|
|
1703
|
-
}
|
|
1704
|
-
|
|
1705
|
-
// 尽力开启 Per-Monitor V2 DPI 感知:否则在 150%/200% 缩放下整个窗口会被系统
|
|
1706
|
-
// 位图拉伸(WebView2 内容一起变糊)。必须在任何窗口创建前调用,
|
|
1707
|
-
// 且宿进程若已设定感知级别则此处会失败(沿用宿主设定)。
|
|
1708
|
-
{
|
|
1709
|
-
auto setCtx = reinterpret_cast<BOOL(WINAPI *)(DPI_AWARENESS_CONTEXT)>(
|
|
1710
|
-
GetProcAddress(GetModuleHandleW(L"user32.dll"), "SetProcessDpiAwarenessContext"));
|
|
1711
|
-
if (!setCtx ||
|
|
1712
|
-
!setCtx(DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2)) {
|
|
1713
|
-
// 退一步尝试 system-aware
|
|
1714
|
-
auto setAware = reinterpret_cast<BOOL(WINAPI *)(int)>(
|
|
1715
|
-
GetProcAddress(GetModuleHandleW(L"shcore.dll"), "SetProcessDpiAwareness"));
|
|
1716
|
-
if (setAware) setAware(1 /* PROCESS_SYSTEM_DPI_AWARE */);
|
|
1717
|
-
}
|
|
1718
|
-
}
|
|
1719
|
-
|
|
1720
|
-
g_url = url;
|
|
1721
|
-
g_title = title ? title : "shell";
|
|
1722
|
-
g_start_w = width > 0 ? width : 1200;
|
|
1723
|
-
g_start_h = height > 0 ? height : 800;
|
|
1724
|
-
g_frameless = (flags & SHELL_FLAG_FRAMELESS) != 0;
|
|
1725
|
-
g_resizable = (flags & SHELL_FLAG_RESIZABLE) != 0;
|
|
1726
|
-
g_accept_drop = (flags & SHELL_FLAG_ACCEPT_DROP) != 0;
|
|
1727
|
-
g_start_maximized = (flags & SHELL_FLAG_START_MAXIMIZED) != 0;
|
|
1728
|
-
clear_err();
|
|
1729
|
-
|
|
1730
|
-
g_ready = CreateEventW(nullptr, TRUE, FALSE, nullptr);
|
|
1731
|
-
if (!g_ready) {
|
|
1732
|
-
set_err("CreateEvent failed");
|
|
1733
|
-
return -1;
|
|
1734
|
-
}
|
|
1735
|
-
|
|
1736
|
-
HANDLE thread = CreateThread(nullptr, 0, ui_thread,
|
|
1737
|
-
reinterpret_cast<LPVOID>(static_cast<INT_PTR>(flags)),
|
|
1738
|
-
0, nullptr);
|
|
1739
|
-
if (!thread) {
|
|
1740
|
-
set_err("CreateThread failed");
|
|
1741
|
-
CloseHandle(g_ready);
|
|
1742
|
-
g_ready = nullptr;
|
|
1743
|
-
return -1;
|
|
1744
|
-
}
|
|
1745
|
-
|
|
1746
|
-
const DWORD waited = WaitForSingleObject(g_ready, 20000);
|
|
1747
|
-
CloseHandle(g_ready);
|
|
1748
|
-
g_ready = nullptr;
|
|
1749
|
-
CloseHandle(thread);
|
|
1750
|
-
|
|
1751
|
-
if (waited != WAIT_OBJECT_0) {
|
|
1752
|
-
set_err("timeout waiting for webview creation");
|
|
1753
|
-
return -1;
|
|
1754
|
-
}
|
|
1755
|
-
return g_webview ? 0 : -1;
|
|
1756
|
-
}
|
|
1757
|
-
|
|
1758
|
-
__declspec(dllexport) int shell_start(const char *url, const char *title,
|
|
1759
|
-
int width, int height) {
|
|
1760
|
-
return shell_start_ex(url, title, width, height, SHELL_FLAG_RESIZABLE);
|
|
1761
|
-
}
|
|
1762
|
-
|
|
1763
|
-
__declspec(dllexport) int shell_post(const char *json) {
|
|
1764
|
-
if (!json || !*json) {
|
|
1765
|
-
set_err("payload is required");
|
|
1766
|
-
return -1;
|
|
1767
|
-
}
|
|
1768
|
-
if (!g_webview) {
|
|
1769
|
-
set_err("window is not running");
|
|
1770
|
-
return -1;
|
|
1771
|
-
}
|
|
1772
|
-
enqueue(cmd_kind::eval, std::string("window.__shell && window.__shell.recv(") + json + ")");
|
|
1773
|
-
return 0;
|
|
1774
|
-
}
|
|
1775
|
-
|
|
1776
|
-
__declspec(dllexport) int shell_title(const char *title) {
|
|
1777
|
-
if (!g_webview) return -1;
|
|
1778
|
-
enqueue(cmd_kind::title, title ? title : "");
|
|
1779
|
-
return 0;
|
|
1780
|
-
}
|
|
1781
|
-
|
|
1782
|
-
__declspec(dllexport) int shell_minimize(void) {
|
|
1783
|
-
if (!g_webview) return -1;
|
|
1784
|
-
enqueue(cmd_kind::minimize);
|
|
1785
|
-
return 0;
|
|
1786
|
-
}
|
|
1787
|
-
|
|
1788
|
-
__declspec(dllexport) int shell_toggle_maximize(void) {
|
|
1789
|
-
if (!g_webview) return -1;
|
|
1790
|
-
enqueue(cmd_kind::maximize_toggle);
|
|
1791
|
-
return 0;
|
|
1792
|
-
}
|
|
1793
|
-
|
|
1794
|
-
__declspec(dllexport) int shell_close(void) {
|
|
1795
|
-
if (!g_webview) return -1;
|
|
1796
|
-
enqueue(cmd_kind::close);
|
|
1797
|
-
return 0;
|
|
1798
|
-
}
|
|
1799
|
-
|
|
1800
|
-
// 由页面在拖拽区 pointerdown 时调用:转成一次原生标题栏拖拽(含贴边分屏)。
|
|
1801
|
-
__declspec(dllexport) int shell_drag(void) {
|
|
1802
|
-
if (!g_webview) return -1;
|
|
1803
|
-
enqueue(cmd_kind::drag);
|
|
1804
|
-
return 0;
|
|
1805
|
-
}
|
|
1806
|
-
|
|
1807
|
-
// 自定义缩放手柄用;正常边框命中走 WM_NCHITTEST,不需要调用。
|
|
1808
|
-
__declspec(dllexport) int shell_begin_resize(int edge) {
|
|
1809
|
-
if (!g_webview) return -1;
|
|
1810
|
-
if (edge < 0 || edge > 7) return -1;
|
|
1811
|
-
enqueue(cmd_kind::resize, std::to_string(edge));
|
|
1812
|
-
return 0;
|
|
1813
|
-
}
|
|
1814
|
-
|
|
1815
|
-
__declspec(dllexport) void shell_set_min_size(int width, int height) {
|
|
1816
|
-
if (width > 0) g_min_w = width;
|
|
1817
|
-
if (height > 0) g_min_h = height;
|
|
1818
|
-
}
|
|
1819
|
-
|
|
1820
|
-
// 延迟显示模式下由调用方在页面就绪后调用(消除首帧闪烁)。
|
|
1821
|
-
// 未启用 SHELL_FLAG_DEFER_SHOW 时是幂等 no-op。
|
|
1822
|
-
__declspec(dllexport) int shell_show(void) {
|
|
1823
|
-
if (!g_webview) return -1;
|
|
1824
|
-
if (g_shown) return 0;
|
|
1825
|
-
enqueue(cmd_kind::show);
|
|
1826
|
-
return 0;
|
|
1827
|
-
}
|
|
1828
|
-
|
|
1829
|
-
// 在屏幕坐标 (x, y) 弹出原生上下文菜单。items 是页面定义的描述串:
|
|
1830
|
-
// "open|打开题库\nrefresh|刷新\n-\nquit|退出"
|
|
1831
|
-
// 选中后通过 shell-msg 推回 { t: "menu", id: "<id>" }。
|
|
1832
|
-
__declspec(dllexport) int shell_show_app_menu(int x, int y, const char *items) {
|
|
1833
|
-
if (!g_webview || !items || !*items) return -1;
|
|
1834
|
-
char head[40];
|
|
1835
|
-
wsprintfA(head, "%d,%d|", x, y);
|
|
1836
|
-
enqueue(cmd_kind::app_menu, std::string(head) + items);
|
|
1837
|
-
return 0;
|
|
1838
|
-
}
|
|
1839
|
-
|
|
1840
|
-
// 关闭 mask 里指定的浏览器行为(右键默认菜单 / 浏览器快捷键 / 状态栏 /
|
|
1841
|
-
// 滑动导航 / 自动填充)。位定义见 ShellBrowserUi。
|
|
1842
|
-
// 运行时生效;建议在 start() 之后立即调用。
|
|
1843
|
-
__declspec(dllexport) int shell_set_browser_ui(int mask) {
|
|
1844
|
-
if (!g_webview) return -1;
|
|
1845
|
-
char buf[16];
|
|
1846
|
-
wsprintfA(buf, "%d", mask);
|
|
1847
|
-
enqueue(cmd_kind::browser_ui, std::string(buf));
|
|
1848
|
-
return 0;
|
|
1849
|
-
}
|
|
1850
|
-
|
|
1851
|
-
// ---- 窗口位置(供持久化 helper 使用)----
|
|
1852
|
-
|
|
1853
|
-
// 读取当前矩形,格式 "x,y,w,h,maximized,dpi"。返回写入长度,0 表示窗口不存在。
|
|
1854
|
-
__declspec(dllexport) int shell_get_bounds(char *buf, int cap) { return format_bounds(buf, cap); }
|
|
1855
|
-
|
|
1856
|
-
// 恢复矩形。坐标与尺寸均为物理像素,saved_dpi 是保存时的缩放 DPI。
|
|
1857
|
-
// 会按当前 DPI 换算尺寸、校验矩形是否仍在某块屏上、必要时居中。
|
|
1858
|
-
__declspec(dllexport) int shell_restore_bounds(int x, int y, int w, int h,
|
|
1859
|
-
int saved_dpi, int maximized) {
|
|
1860
|
-
if (!g_webview) return -1;
|
|
1861
|
-
char buf[96];
|
|
1862
|
-
wsprintfA(buf, "%d,%d,%d,%d,%d,%d", x, y, w, h, saved_dpi, maximized ? 1 : 0);
|
|
1863
|
-
enqueue(cmd_kind::restore_bounds, std::string(buf));
|
|
1864
|
-
return 0;
|
|
1865
|
-
}
|
|
1866
|
-
|
|
1867
|
-
// ---- 输入与视图能力 ----
|
|
1868
|
-
|
|
1869
|
-
// 设页面缩放倍率(0.25 ~ 3.0)。主线程调用,实际 COM 动作投到 UI 线程执行。
|
|
1870
|
-
__declspec(dllexport) int shell_set_zoom(double factor) {
|
|
1871
|
-
if (!g_webview) return -1;
|
|
1872
|
-
// 注意:不能在这里用 wsprintfA("...%f", factor)。user32 的 wsprintf 只支持
|
|
1873
|
-
// %c/%d/%u/%x/%s,没有浮点,%.4f 会产出空串,atof("")=0 再被钳到下限——
|
|
1874
|
-
// 实测表现为「设任何倍率都变成 0.25」。改用整数千分数编码。
|
|
1875
|
-
int milli = static_cast<int>(factor * 1000.0 + (factor >= 0 ? 0.5 : -0.5));
|
|
1876
|
-
char buf[24];
|
|
1877
|
-
wsprintfA(buf, "%d", milli);
|
|
1878
|
-
enqueue(cmd_kind::zoom, std::string(buf));
|
|
1879
|
-
return 0;
|
|
1880
|
-
}
|
|
1881
|
-
|
|
1882
|
-
// 查询当前缩放。读缓存,无 COM 调用。
|
|
1883
|
-
__declspec(dllexport) double shell_get_zoom(void) { return g_zoom; }
|
|
1884
|
-
|
|
1885
|
-
// 重新加载页面(等同 Ctrl+R / F5)。
|
|
1886
|
-
__declspec(dllexport) int shell_reload(void) {
|
|
1887
|
-
if (!g_webview) return -1;
|
|
1888
|
-
enqueue(cmd_kind::reload);
|
|
1889
|
-
return 0;
|
|
1890
|
-
}
|
|
1891
|
-
|
|
1892
|
-
// 打开 WebView2 开发者工具(等同 F12)。
|
|
1893
|
-
__declspec(dllexport) int shell_devtools(void) {
|
|
1894
|
-
if (!g_webview) return -1;
|
|
1895
|
-
enqueue(cmd_kind::devtools);
|
|
1896
|
-
return 0;
|
|
1897
|
-
}
|
|
1898
|
-
|
|
1899
|
-
// 切换全屏(等同 F11)。
|
|
1900
|
-
__declspec(dllexport) int shell_toggle_fullscreen(void) {
|
|
1901
|
-
if (!g_webview) return -1;
|
|
1902
|
-
enqueue(cmd_kind::fullscreen);
|
|
1903
|
-
return 0;
|
|
1904
|
-
}
|
|
1905
|
-
|
|
1906
|
-
// 同步查询系统深浅色。页面侧也会收到 win 推送里的 dark 字段;
|
|
1907
|
-
// 这个接口供宿主在推送之外按需读取。
|
|
1908
|
-
__declspec(dllexport) int shell_is_dark(void) { return system_uses_dark() ? 1 : 0; }
|
|
1909
|
-
|
|
1910
|
-
// AppUserModelID 决定任务栏分组与图标归属。
|
|
1911
|
-
// 进程级设定应在 shell_start_ex 之前调用;窗口已建时改写对当前窗口不生效。
|
|
1912
|
-
__declspec(dllexport) int shell_set_app_id(const char *app_id) {
|
|
1913
|
-
if (!app_id || !*app_id) {
|
|
1914
|
-
g_app_id.clear();
|
|
1915
|
-
return 0;
|
|
1916
|
-
}
|
|
1917
|
-
const int need = MultiByteToWideChar(CP_UTF8, 0, app_id, -1, nullptr, 0);
|
|
1918
|
-
if (need <= 0) {
|
|
1919
|
-
set_err("invalid app id (utf-8 decode failed)");
|
|
1920
|
-
return -1;
|
|
1921
|
-
}
|
|
1922
|
-
g_app_id.assign(static_cast<size_t>(need - 1), L'\0');
|
|
1923
|
-
MultiByteToWideChar(CP_UTF8, 0, app_id, -1, &g_app_id[0], need);
|
|
1924
|
-
apply_app_user_model_id();
|
|
1925
|
-
return 0;
|
|
1926
|
-
}
|
|
1927
|
-
|
|
1928
|
-
// 系统文件夹选择框。替代「每次 spawn 一个 powershell.exe 跑 WinForms」的做法:
|
|
1929
|
-
// 直接调 shell32,无子进程、无脚本落盘、无 STA 依赖外部进程。
|
|
1930
|
-
// 返回 0 表示用户选中并把路径写入 buf;1 表示取消;-1 表示失败。
|
|
1931
|
-
__declspec(dllexport) int shell_pick_folder(const char *title, char *buf, int cap) {
|
|
1932
|
-
if (!buf || cap <= 0) return -1;
|
|
1933
|
-
buf[0] = 0;
|
|
1934
|
-
|
|
1935
|
-
// SHBrowseForFolder 需要 COM 已初始化;重复初始化返回 S_FALSE 也无害
|
|
1936
|
-
const HRESULT hr = CoInitializeEx(nullptr, COINIT_APARTMENTTHREADED);
|
|
1937
|
-
|
|
1938
|
-
wchar_t wtitle[256] = L"选择文件夹";
|
|
1939
|
-
if (title && *title) {
|
|
1940
|
-
MultiByteToWideChar(CP_UTF8, 0, title, -1, wtitle, 256);
|
|
1941
|
-
}
|
|
1942
|
-
|
|
1943
|
-
BROWSEINFOW bi{};
|
|
1944
|
-
// hwndOwner 必须给:没有 owner 的对话框不模态于应用窗口,z 序上也压不住
|
|
1945
|
-
// 应用窗口——表现为「点了选择,对话框跑到窗口下面去了,看着像没反应」。
|
|
1946
|
-
// 给了 owner 之后它是该窗口的模态子级,必然在其之上,且打开期间应用窗口不可操作。
|
|
1947
|
-
//
|
|
1948
|
-
// 取 hwnd 时短暂持生命周期锁,但**不能**在 SHBrowseForFolder 阻塞期间持锁:
|
|
1949
|
-
// 那会挡住 teardown。所以只锁住「读值」这一步,再用 IsWindow 复核一次,
|
|
1950
|
-
// 防止读到的窗口在解锁后已被销毁(无效 owner 会退化为无 owner,不会崩)。
|
|
1951
|
-
HWND owner = nullptr;
|
|
1952
|
-
{
|
|
1953
|
-
std::lock_guard<std::mutex> life(g_life_mu);
|
|
1954
|
-
owner = g_hwnd;
|
|
1955
|
-
}
|
|
1956
|
-
if (owner && !IsWindow(owner)) {
|
|
1957
|
-
owner = nullptr;
|
|
1958
|
-
}
|
|
1959
|
-
bi.hwndOwner = owner;
|
|
1960
|
-
bi.lpszTitle = wtitle;
|
|
1961
|
-
bi.ulFlags = BIF_RETURNONLYFSDIRS | BIF_NEWDIALOGSTYLE | BIF_USENEWUI;
|
|
1962
|
-
|
|
1963
|
-
PIDLIST_ABSOLUTE pidl = SHBrowseForFolderW(&bi);
|
|
1964
|
-
if (!pidl) {
|
|
1965
|
-
if (SUCCEEDED(hr)) CoUninitialize();
|
|
1966
|
-
return 1; // 用户取消
|
|
1967
|
-
}
|
|
1968
|
-
|
|
1969
|
-
wchar_t wpath[MAX_PATH] = {0};
|
|
1970
|
-
const BOOL ok = SHGetPathFromIDListW(pidl, wpath);
|
|
1971
|
-
CoTaskMemFree(pidl);
|
|
1972
|
-
if (SUCCEEDED(hr)) CoUninitialize();
|
|
1973
|
-
|
|
1974
|
-
if (!ok) {
|
|
1975
|
-
set_err("SHGetPathFromIDList failed");
|
|
1976
|
-
return -1;
|
|
1977
|
-
}
|
|
1978
|
-
const int need = WideCharToMultiByte(CP_UTF8, 0, wpath, -1, buf, cap, nullptr, nullptr);
|
|
1979
|
-
if (need <= 0) {
|
|
1980
|
-
set_err("folder path too long");
|
|
1981
|
-
return -1;
|
|
1982
|
-
}
|
|
1983
|
-
return 0;
|
|
1984
|
-
}
|
|
1985
|
-
|
|
1986
|
-
__declspec(dllexport) int shell_get_state(void) {
|
|
1987
|
-
int state = 0;
|
|
1988
|
-
if (!g_hwnd) return 0;
|
|
1989
|
-
if (IsZoomed(g_hwnd)) state |= SHELL_STATE_MAXIMIZED;
|
|
1990
|
-
if (IsIconic(g_hwnd)) state |= SHELL_STATE_MINIMIZED;
|
|
1991
|
-
if (GetForegroundWindow() == g_hwnd) state |= SHELL_STATE_FOCUSED;
|
|
1992
|
-
return state;
|
|
1993
|
-
}
|
|
1994
|
-
|
|
1995
|
-
// 窗口是否仍然存在。1 = 存活,0 = 已销毁(用户点 X 或调用过 shell_stop)。
|
|
1996
|
-
//
|
|
1997
|
-
// 为什么不复用 shell_get_state:它返回 0 同时表示「未启动」与「已销毁」,
|
|
1998
|
-
// 调用方无法区分。宿主需要这个信号来回收进程——窗口销毁后 webview_run 会在
|
|
1999
|
-
// UI 线程返回,但宿主进程(以及它起的本地服务)不会自己退出。
|
|
2000
|
-
__declspec(dllexport) int shell_is_alive(void) { return g_webview ? 1 : 0; }
|
|
2001
|
-
|
|
2002
|
-
// 调试用:入参为顶层窗口客户区坐标(设备像素),与页面 CSS 像素按 dpr 换算。
|
|
2003
|
-
__declspec(dllexport) int shell_debug_hittest(int x, int y) {
|
|
2004
|
-
if (!g_hwnd) return -1;
|
|
2005
|
-
POINT pt{x, y};
|
|
2006
|
-
return static_cast<int>(hit_test_client(g_hwnd, pt));
|
|
2007
|
-
}
|
|
2008
|
-
|
|
2009
|
-
// 调试用:把窗口/客户区/工作区几何写进缓冲区,返回写入长度。
|
|
2010
|
-
// 格式:pos=X,Y win=WxH client=WxH work=WxH border=XxY
|
|
2011
|
-
__declspec(dllexport) int shell_debug_metrics(char *buf, int cap) {
|
|
2012
|
-
if (!buf || cap <= 0) return 0;
|
|
2013
|
-
buf[0] = 0;
|
|
2014
|
-
if (!g_hwnd) return 0;
|
|
2015
|
-
|
|
2016
|
-
RECT wr{}, cr{};
|
|
2017
|
-
GetWindowRect(g_hwnd, &wr);
|
|
2018
|
-
GetClientRect(g_hwnd, &cr);
|
|
2019
|
-
|
|
2020
|
-
MONITORINFO mi{};
|
|
2021
|
-
mi.cbSize = sizeof(mi);
|
|
2022
|
-
GetMonitorInfoW(MonitorFromWindow(g_hwnd, MONITOR_DEFAULTTONEAREST), &mi);
|
|
2023
|
-
|
|
2024
|
-
char tmp[320];
|
|
2025
|
-
wsprintfA(tmp,
|
|
2026
|
-
"pos=%ld,%ld win=%ldx%ld client=%ldx%ld work=%ldx%ld border=%dx%d round=%d",
|
|
2027
|
-
wr.left, wr.top, wr.right - wr.left, wr.bottom - wr.top,
|
|
2028
|
-
cr.right - cr.left, cr.bottom - cr.top,
|
|
2029
|
-
mi.rcWork.right - mi.rcWork.left, mi.rcWork.bottom - mi.rcWork.top,
|
|
2030
|
-
frame_border_x(), frame_border_y(), g_round_state.load());
|
|
2031
|
-
|
|
2032
|
-
const int n = lstrlenA(tmp);
|
|
2033
|
-
if (n >= cap) return 0;
|
|
2034
|
-
lstrcpyA(buf, tmp);
|
|
2035
|
-
return n;
|
|
2036
|
-
}
|
|
2037
|
-
|
|
2038
|
-
// 调试用:报告进程 DPI 感知与实际监视器 DPI。
|
|
2039
|
-
// 格式:awareness=N dpi=N scale=N%
|
|
2040
|
-
__declspec(dllexport) int shell_debug_dpi(char *buf, int cap) {
|
|
2041
|
-
if (!buf || cap <= 0) return 0;
|
|
2042
|
-
buf[0] = 0;
|
|
2043
|
-
|
|
2044
|
-
DPI_AWARENESS awareness = DPI_AWARENESS_INVALID;
|
|
2045
|
-
auto getAwareness = reinterpret_cast<HRESULT(WINAPI *)(DPI_AWARENESS *)>(
|
|
2046
|
-
GetProcAddress(GetModuleHandleW(L"user32.dll"), "GetProcessDpiAwareness"));
|
|
2047
|
-
if (getAwareness) {
|
|
2048
|
-
// GetProcessDpiAwareness 需要一个 handle;传 nullptr 表示当前进程
|
|
2049
|
-
HANDLE proc = GetCurrentProcess();
|
|
2050
|
-
auto getFromProc = reinterpret_cast<HRESULT(WINAPI *)(HANDLE, DPI_AWARENESS *)>(
|
|
2051
|
-
GetProcAddress(GetModuleHandleW(L"shcore.dll"), "GetProcessDpiAwareness"));
|
|
2052
|
-
(void)getAwareness;
|
|
2053
|
-
if (getFromProc) {
|
|
2054
|
-
getFromProc(proc, &awareness);
|
|
2055
|
-
}
|
|
2056
|
-
}
|
|
2057
|
-
|
|
2058
|
-
// 监视器原始 DPI(不受进程感知影响)
|
|
2059
|
-
const UINT monitorDpi = GetDpiForSystem();
|
|
2060
|
-
const int scale = MulDiv(static_cast<int>(monitorDpi), 100, 96);
|
|
2061
|
-
|
|
2062
|
-
char tmp[160];
|
|
2063
|
-
wsprintfA(tmp, "awareness=%d dpi=%u scale=%d%%", static_cast<int>(awareness),
|
|
2064
|
-
monitorDpi, scale);
|
|
2065
|
-
|
|
2066
|
-
const int n = lstrlenA(tmp);
|
|
2067
|
-
if (n >= cap) return 0;
|
|
2068
|
-
lstrcpyA(buf, tmp);
|
|
2069
|
-
return n;
|
|
2070
|
-
}
|
|
2071
|
-
|
|
2072
|
-
// 调试用:返回本 DLL 自身在内存中的模块基址。
|
|
2073
|
-
// 用来判断 Bun 的 ffi 是按标准模块加载(出现在 PEB 模块链)还是手动映射。
|
|
2074
|
-
__declspec(dllexport) unsigned long long shell_self_base(void) {
|
|
2075
|
-
HMODULE self = nullptr;
|
|
2076
|
-
if (GetModuleHandleExW(GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS |
|
|
2077
|
-
GET_MODULE_HANDLE_EX_FLAG_UNCHANGED_REFCOUNT,
|
|
2078
|
-
reinterpret_cast<LPCWSTR>(&shell_self_base), &self)) {
|
|
2079
|
-
return static_cast<unsigned long long>(reinterpret_cast<ULONG_PTR>(self));
|
|
2080
|
-
}
|
|
2081
|
-
return 0;
|
|
2082
|
-
}
|
|
2083
|
-
|
|
2084
|
-
// 调试用:把本 DLL 的完整文件路径写进缓冲区,返回写入长度。
|
|
2085
|
-
// 若为空,说明它是内存映射而非磁盘文件。
|
|
2086
|
-
__declspec(dllexport) int shell_self_path(char *buf, int cap) {
|
|
2087
|
-
if (!buf || cap <= 0) return 0;
|
|
2088
|
-
buf[0] = 0;
|
|
2089
|
-
|
|
2090
|
-
HMODULE self = nullptr;
|
|
2091
|
-
if (!GetModuleHandleExW(GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS |
|
|
2092
|
-
GET_MODULE_HANDLE_EX_FLAG_UNCHANGED_REFCOUNT,
|
|
2093
|
-
reinterpret_cast<LPCWSTR>(&shell_self_base), &self)) {
|
|
2094
|
-
return 0;
|
|
2095
|
-
}
|
|
2096
|
-
|
|
2097
|
-
wchar_t wpath[MAX_PATH] = {0};
|
|
2098
|
-
// 0x2 = GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS 不适用;用 GetModuleFileNameW
|
|
2099
|
-
const DWORD n = GetModuleFileNameW(self, wpath, MAX_PATH);
|
|
2100
|
-
if (n == 0) {
|
|
2101
|
-
lstrcpynA(buf, "(GetModuleFileNameW failed)", cap);
|
|
2102
|
-
return lstrlenA(buf);
|
|
2103
|
-
}
|
|
2104
|
-
const int need = WideCharToMultiByte(CP_UTF8, 0, wpath, -1, buf, cap, nullptr, nullptr);
|
|
2105
|
-
return need > 0 ? need - 1 : 0;
|
|
2106
|
-
}
|
|
2107
|
-
|
|
2108
|
-
// terminate 必须跑在 UI 线程:vendor 侧的实现是 PostQuitMessage(0),它只把
|
|
2109
|
-
// WM_QUIT 投给**调用线程**的消息队列。从宿主线程直接调,退出消息就落在一个
|
|
2110
|
-
// 没有消息循环的线程上,UI 线程的 webview_run 永远收不到——表现为
|
|
2111
|
-
// shell_stop()「什么都没发生」:窗口还在、is_alive 仍为真、再 start 会报
|
|
2112
|
-
// 「shell already started」。实测踩过。
|
|
2113
|
-
void terminate_on_ui(webview_t w, void * /*arg*/) {
|
|
2114
|
-
if (w) {
|
|
2115
|
-
webview_terminate(w);
|
|
2116
|
-
}
|
|
2117
|
-
}
|
|
2118
|
-
|
|
2119
|
-
__declspec(dllexport) void shell_stop(void) {
|
|
2120
|
-
// 持生命周期锁:teardown 也用它,所以不会在「检查非空」与「投递」之间
|
|
2121
|
-
// 插进一次销毁。
|
|
2122
|
-
std::lock_guard<std::mutex> life(g_life_mu);
|
|
2123
|
-
if (g_webview) {
|
|
2124
|
-
webview_dispatch(g_webview, terminate_on_ui, nullptr);
|
|
2125
|
-
}
|
|
2126
|
-
}
|
|
2127
|
-
|
|
2128
|
-
// 错误文本的拷贝式读出。shell_error() 返回的是共享缓冲区的指针,解锁后可能被
|
|
2129
|
-
// 别的线程改写;需要可靠读取时用这个(返回写入长度)。
|
|
2130
|
-
__declspec(dllexport) int shell_error_text(char *buf, int cap) { return copy_err(buf, cap); }
|
|
2131
|
-
|
|
2132
|
-
__declspec(dllexport) const char *shell_error(void) { return g_err; }
|
|
2133
|
-
|
|
2134
|
-
} // extern "C"
|