// ============================================================================ // 90CN_gm_window.cpp -- 我们自己绘制的 GM 窗口(自绘外壳 + Edge 内核内容) // // 为什么要自己画:游戏内嵌的 IE 控件在这个进程里输入法候选框出不来, // 弹一个浏览器窗口又太“外部”。这里自己建一个无系统边框的窗口,标题栏、 // 边框、按钮全部由 GDI 绘制成和游戏一致的深色风格;内容区放一个 WebView2 // 控件(Edge 内核),中文输入由内核处理,候选框正常。 // // 窗口生命周期: // GmWindowShow() 第一次调用时创建窗口并初始化内嵌浏览器,之后复用 // GmWindowHide() 只是隐藏,不销毁,下次打开秒开 // GmPumpMessages() 由重定向线程每轮调用,负责把消息泵喂给窗口和 WebView2 // // 失败不影响游戏:WebView2 初始化失败会把 g_failed 置位,由调用方回退到 // Edge 应用窗口或内嵌 IE 方案。 // ============================================================================ #include #include #include #include #include "WebView2.h" extern "C" void __cdecl CnmLogLine(const char* format, ...); #define LogLine CnmLogLine using Microsoft::WRL::Callback; using Microsoft::WRL::ComPtr; namespace { const wchar_t kWindowClass[] = L"90CN_GMHostWindow"; const wchar_t kTitleText[] = L"GM 控制台"; const int kTitleH = 38; // 自绘标题栏高度 const int kBtnW = 44; // 标题栏按钮宽度 const int kEdge = 5; // 拉伸手柄厚度 const int kMinW = 760; const int kMinH = 520; HWND g_hwnd = nullptr; HFONT g_fontTitle = nullptr; HFONT g_fontSmall = nullptr; HBRUSH g_brBg = nullptr; ComPtr g_env; ComPtr g_ctrl; ComPtr g_web; std::wstring g_url; std::wstring g_userDataDir; bool g_envStarted = false; bool g_failed = false; bool g_trackingLeave = false; int g_hover = 0; // 1 = 关闭按钮, 2 = 刷新按钮 // 颜色:和 gmweb 的深色主题对齐 const COLORREF kColBg = RGB(0x12, 0x15, 0x1c); const COLORREF kColBar = RGB(0x1a, 0x1e, 0x27); const COLORREF kColLine = RGB(0x2f, 0x35, 0x47); const COLORREF kColTitle = RGB(0xe8, 0xc9, 0x79); const COLORREF kColMuted = RGB(0x8d, 0x94, 0xa6); std::wstring EnvValue(const wchar_t* name) { wchar_t buf[MAX_PATH] = {}; const DWORD got = GetEnvironmentVariableW(name, buf, MAX_PATH); return got ? std::wstring(buf, got) : std::wstring(); } void ResizeWebView() { if (!g_ctrl || !g_hwnd) return; RECT rc = {}; GetClientRect(g_hwnd, &rc); RECT bounds = { 0, kTitleH, rc.right, rc.bottom }; if (bounds.bottom < bounds.top) bounds.bottom = bounds.top; g_ctrl->put_Bounds(bounds); } void LayoutCenterOnOwner() { RECT work = {}; SystemParametersInfoW(SPI_GETWORKAREA, 0, &work, 0); RECT rc = {}; GetWindowRect(g_hwnd, &rc); const int w = rc.right - rc.left; const int h = rc.bottom - rc.top; const int x = work.left + (work.right - work.left - w) / 2; const int y = work.top + (work.bottom - work.top - h) / 2; SetWindowPos(g_hwnd, nullptr, x, y, 0, 0, SWP_NOSIZE | SWP_NOZORDER | SWP_NOACTIVATE); } void CloseButtonRect(RECT* out) { RECT rc = {}; GetClientRect(g_hwnd, &rc); out->right = rc.right; out->left = rc.right - kBtnW; out->top = 0; out->bottom = kTitleH; } void RefreshButtonRect(RECT* out) { RECT close = {}; CloseButtonRect(&close); out->right = close.left; out->left = close.left - kBtnW; out->top = 0; out->bottom = kTitleH; } void PaintWindow(HWND hwnd) { PAINTSTRUCT ps = {}; HDC dc = BeginPaint(hwnd, &ps); if (!dc) return; RECT rc = {}; GetClientRect(hwnd, &rc); // 整窗底色 HBRUSH bg = g_brBg ? g_brBg : static_cast(GetStockObject(BLACK_BRUSH)); FillRect(dc, &rc, bg); // 标题栏 RECT bar = { 0, 0, rc.right, kTitleH }; HBRUSH barBrush = CreateSolidBrush(kColBar); FillRect(dc, &bar, barBrush); DeleteObject(barBrush); // 金色小竖条 + 标题 RECT accent = { 14, 11, 17, kTitleH - 11 }; HBRUSH accentBrush = CreateSolidBrush(kColTitle); FillRect(dc, &accent, accentBrush); DeleteObject(accentBrush); SetBkMode(dc, TRANSPARENT); SetTextColor(dc, kColTitle); HFONT old = static_cast(SelectObject(dc, g_fontTitle ? g_fontTitle : GetStockObject(DEFAULT_GUI_FONT))); RECT textRect = { 24, 0, rc.right - 2 * kBtnW - 8, kTitleH }; DrawTextW(dc, kTitleText, -1, &textRect, DT_LEFT | DT_VCENTER | DT_SINGLELINE | DT_END_ELLIPSIS); // 刷新按钮 RECT refresh = {}; RefreshButtonRect(&refresh); if (g_hover == 2) { HBRUSH hb = CreateSolidBrush(RGB(0x26, 0x2c, 0x3a)); FillRect(dc, &refresh, hb); DeleteObject(hb); } SetTextColor(dc, kColMuted); SelectObject(dc, g_fontSmall ? g_fontSmall : GetStockObject(DEFAULT_GUI_FONT)); DrawTextW(dc, L"刷新", -1, &refresh, DT_CENTER | DT_VCENTER | DT_SINGLELINE); // 关闭按钮(自己画 ×,不依赖字体里有没有这个字形) RECT close = {}; CloseButtonRect(&close); if (g_hover == 1) { HBRUSH hb = CreateSolidBrush(RGB(0xc0, 0x39, 0x2b)); FillRect(dc, &close, hb); DeleteObject(hb); } const int cx = (close.left + close.right) / 2; const int cy = (close.top + close.bottom) / 2; const int r = 6; HPEN pen = CreatePen(PS_SOLID, 2, g_hover == 1 ? RGB(0xff, 0xff, 0xff) : kColMuted); HPEN oldPen = static_cast(SelectObject(dc, pen)); MoveToEx(dc, cx - r, cy - r, nullptr); LineTo(dc, cx + r + 1, cy + r + 1); MoveToEx(dc, cx + r, cy - r, nullptr); LineTo(dc, cx - r - 1, cy + r + 1); SelectObject(dc, oldPen); DeleteObject(pen); // 分隔线与外框 HPEN linePen = CreatePen(PS_SOLID, 1, kColLine); HPEN prevPen = static_cast(SelectObject(dc, linePen)); MoveToEx(dc, 0, kTitleH - 1, nullptr); LineTo(dc, rc.right, kTitleH - 1); HBRUSH hollow = static_cast(GetStockObject(NULL_BRUSH)); HBRUSH prevBrush = static_cast(SelectObject(dc, hollow)); Rectangle(dc, 0, 0, rc.right, rc.bottom); SelectObject(dc, prevBrush); SelectObject(dc, prevPen); DeleteObject(linePen); if (g_failed) { SetTextColor(dc, kColMuted); RECT tip = { 0, kTitleH, rc.right, rc.bottom }; DrawTextW(dc, L"内嵌浏览器初始化失败,请检查 WebView2 运行时是否安装。", -1, &tip, DT_CENTER | DT_VCENTER | DT_SINGLELINE); } else if (!g_web) { SetTextColor(dc, kColMuted); RECT tip = { 0, kTitleH, rc.right, rc.bottom }; DrawTextW(dc, L"正在加载 GM 页面…", -1, &tip, DT_CENTER | DT_VCENTER | DT_SINGLELINE); } SelectObject(dc, old); EndPaint(hwnd, &ps); } // ---------------------------------------------------------------- WebView2 std::wstring UserDataDir() { const std::wstring base = EnvValue(L"LOCALAPPDATA"); if (base.empty()) return std::wstring(); const std::wstring root = base + L"\\90CN"; CreateDirectoryW(root.c_str(), nullptr); const std::wstring dir = root + L"\\wv2"; CreateDirectoryW(dir.c_str(), nullptr); return dir; } void StartWebView() { if (g_envStarted || g_failed || !g_hwnd) return; g_envStarted = true; g_userDataDir = UserDataDir(); if (g_userDataDir.empty()) { g_failed = true; LogLine("gm window: LOCALAPPDATA missing, webview cannot start"); InvalidateRect(g_hwnd, nullptr, FALSE); return; } const HRESULT hr = CreateCoreWebView2EnvironmentWithOptions(nullptr, g_userDataDir.c_str(), nullptr, Callback( [](HRESULT result, ICoreWebView2Environment* env) -> HRESULT { if (FAILED(result) || !env) { g_failed = true; LogLine("gm window: environment failed hr=0x%08X", (unsigned)result); if (g_hwnd) InvalidateRect(g_hwnd, nullptr, FALSE); return S_OK; } g_env = env; g_env->CreateCoreWebView2Controller(g_hwnd, Callback( [](HRESULT result2, ICoreWebView2Controller* controller) -> HRESULT { if (FAILED(result2) || !controller) { g_failed = true; LogLine("gm window: controller failed hr=0x%08X", (unsigned)result2); if (g_hwnd) InvalidateRect(g_hwnd, nullptr, FALSE); return S_OK; } g_ctrl = controller; g_ctrl->get_CoreWebView2(&g_web); if (g_web) { // 关掉右键菜单和开发者工具入口,看起来更像应用而不是网页 ComPtr settings; if (SUCCEEDED(g_web->get_Settings(&settings)) && settings) { settings->put_AreDefaultContextMenusEnabled(FALSE); settings->put_IsStatusBarEnabled(FALSE); settings->put_AreDevToolsEnabled(FALSE); } if (!g_url.empty()) g_web->Navigate(g_url.c_str()); } ResizeWebView(); if (g_hwnd) InvalidateRect(g_hwnd, nullptr, FALSE); LogLine("gm window: webview ready"); return S_OK; }).Get()); return S_OK; }).Get()); if (FAILED(hr)) { g_failed = true; LogLine("gm window: CreateCoreWebView2EnvironmentWithOptions hr=0x%08X (runtime missing?)", (unsigned)hr); if (g_hwnd) InvalidateRect(g_hwnd, nullptr, FALSE); } } // ---------------------------------------------------------------- 窗口过程 LRESULT HitTest(HWND hwnd) { POINT pt = {}; GetCursorPos(&pt); ScreenToClient(hwnd, &pt); RECT rc = {}; GetClientRect(hwnd, &rc); RECT close = {}; CloseButtonRect(&close); RECT refresh = {}; RefreshButtonRect(&refresh); if (PtInRect(&close, pt) || PtInRect(&refresh, pt)) return HTCLIENT; // 先判边缘,方便拉伸 const bool left = pt.x < kEdge; const bool right = pt.x >= rc.right - kEdge; const bool top = pt.y < kEdge; const bool bottom = pt.y >= rc.bottom - kEdge; if (top && left) return HTTOPLEFT; if (top && right) return HTTOPRIGHT; if (bottom && left) return HTBOTTOMLEFT; if (bottom && right) return HTBOTTOMRIGHT; if (left) return HTLEFT; if (right) return HTRIGHT; if (top) return HTTOP; if (bottom) return HTBOTTOM; if (pt.y < kTitleH) return HTCAPTION; return HTCLIENT; } LRESULT CALLBACK WindowProc(HWND hwnd, UINT msg, WPARAM wparam, LPARAM lparam) { switch (msg) { case WM_NCCALCSIZE: if (wparam) return 0; // 去掉系统非客户区,边框和标题栏由我们自己画 break; case WM_NCHITTEST: return HitTest(hwnd); case WM_ERASEBKGND: return 1; case WM_PAINT: PaintWindow(hwnd); return 0; case WM_SIZE: ResizeWebView(); InvalidateRect(hwnd, nullptr, FALSE); return 0; case WM_MOUSEMOVE: { if (!g_trackingLeave) { TRACKMOUSEEVENT tme = { sizeof(tme), TME_LEAVE, hwnd, 0 }; TrackMouseEvent(&tme); g_trackingLeave = true; } POINT pt = { GET_X_LPARAM(lparam), GET_Y_LPARAM(lparam) }; RECT close = {}; CloseButtonRect(&close); RECT refresh = {}; RefreshButtonRect(&refresh); int hover = 0; if (PtInRect(&close, pt)) hover = 1; else if (PtInRect(&refresh, pt)) hover = 2; if (hover != g_hover) { g_hover = hover; InvalidateRect(hwnd, nullptr, FALSE); } return 0; } case WM_MOUSELEAVE: g_trackingLeave = false; if (g_hover != 0) { g_hover = 0; InvalidateRect(hwnd, nullptr, FALSE); } return 0; case WM_LBUTTONDOWN: { POINT pt = { GET_X_LPARAM(lparam), GET_Y_LPARAM(lparam) }; RECT close = {}; CloseButtonRect(&close); RECT refresh = {}; RefreshButtonRect(&refresh); if (PtInRect(&close, pt)) { PostMessageW(hwnd, WM_CLOSE, 0, 0); return 0; } if (PtInRect(&refresh, pt)) { if (g_web) g_web->Reload(); return 0; } break; } case WM_CLOSE: ShowWindow(hwnd, SW_HIDE); SetWindowPos(hwnd, HWND_NOTOPMOST, 0, 0, 0, 0, SWP_NOMOVE | SWP_NOSIZE | SWP_NOACTIVATE); LogLine("gm window: hidden by user"); return 0; case WM_GETMINMAXINFO: { MINMAXINFO* info = reinterpret_cast(lparam); info->ptMinTrackSize.x = kMinW; info->ptMinTrackSize.y = kMinH; return 0; } case WM_DESTROY: g_hwnd = nullptr; return 0; default: break; } return DefWindowProcW(hwnd, msg, wparam, lparam); } bool EnsureWindow() { if (g_hwnd && IsWindow(g_hwnd)) return true; WNDCLASSEXW wc = {}; wc.cbSize = sizeof(wc); wc.style = CS_DROPSHADOW; wc.lpfnWndProc = WindowProc; wc.hInstance = GetModuleHandleW(nullptr); wc.hCursor = LoadCursorW(nullptr, IDC_ARROW); wc.hbrBackground = nullptr; wc.lpszClassName = kWindowClass; if (!RegisterClassExW(&wc) && GetLastError() != ERROR_CLASS_ALREADY_EXISTS) { LogLine("gm window: RegisterClassEx failed err=%u", GetLastError()); return false; } if (!g_brBg) g_brBg = CreateSolidBrush(kColBg); if (!g_fontTitle) { g_fontTitle = CreateFontW(-16, 0, 0, 0, FW_SEMIBOLD, FALSE, FALSE, FALSE, DEFAULT_CHARSET, OUT_DEFAULT_PRECIS, CLIP_DEFAULT_PRECIS, CLEARTYPE_QUALITY, DEFAULT_PITCH | FF_DONTCARE, L"Microsoft YaHei UI"); } if (!g_fontSmall) { g_fontSmall = CreateFontW(-13, 0, 0, 0, FW_NORMAL, FALSE, FALSE, FALSE, DEFAULT_CHARSET, OUT_DEFAULT_PRECIS, CLIP_DEFAULT_PRECIS, CLEARTYPE_QUALITY, DEFAULT_PITCH | FF_DONTCARE, L"Microsoft YaHei UI"); } g_hwnd = CreateWindowExW(WS_EX_TOOLWINDOW, kWindowClass, kTitleText, WS_POPUP | WS_THICKFRAME | WS_CLIPCHILDREN, CW_USEDEFAULT, CW_USEDEFAULT, 1040, 720, nullptr, nullptr, wc.hInstance, nullptr); if (!g_hwnd) { LogLine("gm window: CreateWindowEx failed err=%u", GetLastError()); return false; } return true; } } // namespace // ---------------------------------------------------------------- 对外接口 extern "C" bool GmWindowShow(const wchar_t* url, int width, int height) { if (!EnsureWindow()) return false; if (url && *url) g_url = url; RECT rc = {}; GetWindowRect(g_hwnd, &rc); const int w = width > 0 ? width : (rc.right - rc.left); const int h = height > 0 ? height : (rc.bottom - rc.top); if (!IsWindowVisible(g_hwnd)) { SetWindowPos(g_hwnd, HWND_TOPMOST, 0, 0, w, h, SWP_NOMOVE | SWP_SHOWWINDOW); } else { SetWindowPos(g_hwnd, HWND_TOPMOST, 0, 0, 0, 0, SWP_NOMOVE | SWP_NOSIZE | SWP_NOACTIVATE); } if (!g_envStarted) { LayoutCenterOnOwner(); StartWebView(); } else if (g_web && !g_url.empty()) { // 页面服务可能重启过,重新打开时刷一次,避免停在错误页 g_web->Reload(); } ShowWindow(g_hwnd, SW_SHOW); SetForegroundWindow(g_hwnd); ResizeWebView(); InvalidateRect(g_hwnd, nullptr, FALSE); return true; } extern "C" void GmWindowHide() { if (g_hwnd && IsWindow(g_hwnd) && IsWindowVisible(g_hwnd)) { ShowWindow(g_hwnd, SW_HIDE); SetWindowPos(g_hwnd, HWND_NOTOPMOST, 0, 0, 0, 0, SWP_NOMOVE | SWP_NOSIZE | SWP_NOACTIVATE); } } extern "C" bool GmWindowIsVisible() { return g_hwnd && IsWindow(g_hwnd) && IsWindowVisible(g_hwnd); } // 游戏有时会在切图、恢复全屏时把窗口栈重排一遍,把我们的窗口压回普通层。 // 主循环每轮调一次,发现置顶丢失就重新提上去(不抢焦点)。 extern "C" void GmWindowAssertTop() { if (!g_hwnd || !IsWindow(g_hwnd)) return; const LONG_PTR ex = GetWindowLongPtrW(g_hwnd, GWL_EXSTYLE); if ((ex & WS_EX_TOPMOST) == 0) { SetWindowPos(g_hwnd, HWND_TOPMOST, 0, 0, 0, 0, SWP_NOMOVE | SWP_NOSIZE | SWP_NOACTIVATE); LogLine("gm window: re-asserted topmost"); } } extern "C" bool GmWindowFailed() { return g_failed; } extern "C" void GmPumpMessages(unsigned timeoutMs) { MsgWaitForMultipleObjects(0, nullptr, FALSE, timeoutMs, QS_ALLINPUT); MSG msg = {}; while (PeekMessageW(&msg, nullptr, 0, 0, PM_REMOVE)) { TranslateMessage(&msg); DispatchMessageW(&msg); } }