// ============================================================================ // 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); } // ---------------------------------------------------------------- 跟随游戏窗口 // // 需求:控制台要“长”在游戏画面里。全屏(无边框最大化)时用配置里的目标尺寸, // 窗口模式下按游戏窗口的实际客户区等比收小并居中;玩家改分辨率、切全屏/窗口, // 都跟着重新摆一次。 int g_desiredW = 1040; int g_desiredH = 720; RECT g_lastGameRect = { 0, 0, 0, 0 }; // 上次布局时游戏客户区的位置 bool g_fitValid = false; struct GameSearch { DWORD pid; HWND best; LONG area; }; BOOL CALLBACK OnGameCandidate(HWND hwnd, LPARAM param) { GameSearch* s = reinterpret_cast(param); DWORD pid = 0; GetWindowThreadProcessId(hwnd, &pid); if (pid != s->pid) return TRUE; if (!IsWindowVisible(hwnd)) return TRUE; if (GetWindow(hwnd, GW_OWNER)) return TRUE; wchar_t cls[128] = {}; if (GetClassNameW(hwnd, cls, 128)) { if (_wcsicmp(cls, kWindowClass) == 0) return TRUE; // 我们自己 if (_wcsicmp(cls, L"Browser Example") == 0) return TRUE; // 游戏自带面板 if (_wcsicmp(cls, L"Internet Explorer_Server") == 0) return TRUE; } RECT rc = {}; if (!GetWindowRect(hwnd, &rc)) return TRUE; const LONG area = (rc.right - rc.left) * (rc.bottom - rc.top); if (area > s->area) { s->area = area; s->best = hwnd; } return TRUE; } // 游戏主窗口 —— 本进程里可见、没有 owner、面积最大的那个顶层窗口。 HWND FindGameWindow() { GameSearch s = { GetCurrentProcessId(), nullptr, 0 }; EnumWindows(OnGameCandidate, reinterpret_cast(&s)); return s.best; } // 算一个「装得进游戏窗口」的矩形。gameOut 回传游戏客户区,供变化检测用。 bool FitInsideGame(int desiredW, int desiredH, RECT* out, RECT* gameOut) { const HWND game = FindGameWindow(); if (!game) return false; RECT client = {}; if (!GetClientRect(game, &client)) return false; POINT tl = { 0, 0 }; if (!ClientToScreen(game, &tl)) return false; RECT g = { client.left + tl.x, client.top + tl.y, client.right + tl.x, client.bottom + tl.y }; const int gw = g.right - g.left; const int gh = g.bottom - g.top; if (gw < 320 || gh < 240) return false; // 还没进游戏/窗口还没建好 int margin = gw / 60; if (margin < 12) margin = 12; if (margin > 28) margin = 28; int w = desiredW > 0 ? desiredW : gw; int h = desiredH > 0 ? desiredH : gh; if (w > gw - 2 * margin) w = gw - 2 * margin; if (h > gh - 2 * margin) h = gh - 2 * margin; // 窗口小到一定程度就别再缩了,否则控件挤得没法用;宁可稍微溢出一点。 if (w < 720) w = 720; if (h < 500) h = 500; if (w > gw) w = gw; if (h > gh) h = gh; out->left = g.left + (gw - w) / 2; out->top = g.top + (gh - h) / 2; out->right = out->left + w; out->bottom = out->top + h; if (gameOut) *gameOut = g; return true; } // 游戏窗口变了(切全屏/改分辨率/挪窗口)就重新摆一次。玩家自己拖过窗口、 // 游戏没变的情况下不动它,避免跟玩家较劲。 void FollowGameWindow() { if (!g_hwnd || !IsWindow(g_hwnd) || !IsWindowVisible(g_hwnd) || !g_fitValid) return; RECT fit = {}; RECT game = {}; if (!FitInsideGame(g_desiredW, g_desiredH, &fit, &game)) return; RECT cur = {}; GetWindowRect(g_hwnd, &cur); const bool gameChanged = !EqualRect(&game, &g_lastGameRect); const bool outside = cur.left < game.left - 4 || cur.top < game.top - 4 || cur.right > game.right + 4 || cur.bottom > game.bottom + 4; if (!gameChanged && !outside) return; SetWindowPos(g_hwnd, HWND_TOPMOST, fit.left, fit.top, fit.right - fit.left, fit.bottom - fit.top, SWP_NOACTIVATE | SWP_SHOWWINDOW); ResizeWebView(); InvalidateRect(g_hwnd, nullptr, FALSE); g_lastGameRect = game; LogLine("gm window: refit to game %ldx%ld -> %dx%d at %ld,%ld", game.right - game.left, game.bottom - game.top, static_cast(fit.right - fit.left), static_cast(fit.bottom - fit.top), static_cast(fit.left), static_cast(fit.top)); } 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; if (width > 0) g_desiredW = width; if (height > 0) g_desiredH = height; // 优先让窗口落在游戏画面里:全屏时就是配置的目标尺寸,窗口模式下自动收小。 RECT fit = {}; RECT game = {}; const bool fitted = FitInsideGame(g_desiredW, g_desiredH, &fit, &game); if (!IsWindowVisible(g_hwnd)) { if (fitted) { SetWindowPos(g_hwnd, HWND_TOPMOST, fit.left, fit.top, fit.right - fit.left, fit.bottom - fit.top, SWP_SHOWWINDOW | SWP_NOACTIVATE); g_lastGameRect = game; g_fitValid = true; LogLine("gm window: show inside game %ldx%ld -> %dx%d at %ld,%ld", game.right - game.left, game.bottom - game.top, static_cast(fit.right - fit.left), static_cast(fit.bottom - fit.top), static_cast(fit.left), static_cast(fit.top)); } else { SetWindowPos(g_hwnd, HWND_TOPMOST, 0, 0, g_desiredW, g_desiredH, SWP_NOMOVE | SWP_SHOWWINDOW | SWP_NOACTIVATE); g_fitValid = false; LogLine("gm window: game window not found, show at %dx%d", g_desiredW, g_desiredH); } } else { SetWindowPos(g_hwnd, HWND_TOPMOST, 0, 0, 0, 0, SWP_NOMOVE | SWP_NOSIZE | SWP_NOACTIVATE); if (fitted) { SetWindowPos(g_hwnd, HWND_TOPMOST, fit.left, fit.top, fit.right - fit.left, fit.bottom - fit.top, SWP_NOACTIVATE); g_lastGameRect = game; g_fitValid = true; ResizeWebView(); } } if (!g_envStarted) { if (!fitted) 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" void GmWindowFollowGame() { FollowGameWindow(); } 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); } }