Files
90CNServer/client-patch/90CN_gm_window.cpp
T
hanfuit 7f41f9797a GM 控制台跟随游戏窗口:窗口模式下按游戏客户区收小并居中
- 新增 FindGameWindow/FitInsideGame:取本进程最大可见顶层窗口的客户区作为游戏画面
- 开窗口时按 min(配置尺寸, 客户区-2倍边距) 定尺寸并居中;游戏画面小于配置时自动缩
- 新增 GmWindowFollowGame:切全屏/改分辨率/挪游戏窗口时重新摆放,玩家手动拖过的窗口不动
- 服务端指纹回绑 186A2C95… (1260032)
2026-11-05 13:29:05 +08:00

672 lines
23 KiB
C++

// ============================================================================
// 90CN_gm_window.cpp -- 我们自己绘制的 GM 窗口(自绘外壳 + Edge 内核内容)
//
// 为什么要自己画:游戏内嵌的 IE 控件在这个进程里输入法候选框出不来,
// 弹一个浏览器窗口又太“外部”。这里自己建一个无系统边框的窗口,标题栏、
// 边框、按钮全部由 GDI 绘制成和游戏一致的深色风格;内容区放一个 WebView2
// 控件(Edge 内核),中文输入由内核处理,候选框正常。
//
// 窗口生命周期:
// GmWindowShow() 第一次调用时创建窗口并初始化内嵌浏览器,之后复用
// GmWindowHide() 只是隐藏,不销毁,下次打开秒开
// GmPumpMessages() 由重定向线程每轮调用,负责把消息泵喂给窗口和 WebView2
//
// 失败不影响游戏:WebView2 初始化失败会把 g_failed 置位,由调用方回退到
// Edge 应用窗口或内嵌 IE 方案。
// ============================================================================
#include <windows.h>
#include <windowsx.h>
#include <wrl.h>
#include <string>
#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<ICoreWebView2Environment> g_env;
ComPtr<ICoreWebView2Controller> g_ctrl;
ComPtr<ICoreWebView2> 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<GameSearch*>(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<LPARAM>(&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<int>(fit.right - fit.left), static_cast<int>(fit.bottom - fit.top),
static_cast<long>(fit.left), static_cast<long>(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<HBRUSH>(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<HFONT>(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<HPEN>(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<HPEN>(SelectObject(dc, linePen));
MoveToEx(dc, 0, kTitleH - 1, nullptr);
LineTo(dc, rc.right, kTitleH - 1);
HBRUSH hollow = static_cast<HBRUSH>(GetStockObject(NULL_BRUSH));
HBRUSH prevBrush = static_cast<HBRUSH>(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<ICoreWebView2CreateCoreWebView2EnvironmentCompletedHandler>(
[](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<ICoreWebView2CreateCoreWebView2ControllerCompletedHandler>(
[](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<ICoreWebView2Settings> 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<MINMAXINFO*>(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<int>(fit.right - fit.left), static_cast<int>(fit.bottom - fit.top),
static_cast<long>(fit.left), static_cast<long>(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);
}
}