GM 控制台跟随游戏窗口:窗口模式下按游戏客户区收小并居中

- 新增 FindGameWindow/FitInsideGame:取本进程最大可见顶层窗口的客户区作为游戏画面
- 开窗口时按 min(配置尺寸, 客户区-2倍边距) 定尺寸并居中;游戏画面小于配置时自动缩
- 新增 GmWindowFollowGame:切全屏/改分辨率/挪游戏窗口时重新摆放,玩家手动拖过的窗口不动
- 服务端指纹回绑 186A2C95… (1260032)
This commit is contained in:
hanfuit committed 2026-11-05 13:29:05 +08:00
1 parent 0c7ef6d645
commit 7f41f9797a
3 files changed
+162 -9

No files matched your search

+157 -6
View File
@@ -92,6 +92,125 @@ void LayoutCenterOnOwner()
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 = {};
@@ -450,20 +569,45 @@ 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);
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);
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) {
LayoutCenterOnOwner();
if (!fitted) LayoutCenterOnOwner();
StartWebView();
} else if (g_web && !g_url.empty()) {
// 页面服务可能重启过,重新打开时刷一次,避免停在错误页
@@ -504,6 +648,13 @@ extern "C" void GmWindowAssertTop()
}
}
// 游戏窗口换了尺寸/位置(切全屏、改分辨率、拖窗口)时把控制台重新摆回游戏画面里。
// 主循环每轮调一次,内部只在游戏矩形变化或窗口超出游戏范围时才真正动窗口。
extern "C" void GmWindowFollowGame()
{
FollowGameWindow();
}
extern "C" bool GmWindowFailed()
{
return g_failed;
+3 -1
View File
@@ -39,6 +39,7 @@ extern "C" void GmWindowHide();
extern "C" bool GmWindowIsVisible();
extern "C" bool GmWindowFailed();
extern "C" void GmPumpMessages(unsigned timeoutMs);
extern "C" void GmWindowFollowGame();
extern "C" void GmWindowAssertTop();
namespace {
@@ -699,7 +700,8 @@ DWORD WINAPI WorkerThread(void*)
ShowWindow(panel, SW_HIDE);
LogLine("web window: panel suppressed hwnd=%p", (void*)panel);
}
// 全屏游戏会抢窗口栈,每轮确认一次置顶
// 全屏游戏会抢窗口栈,每轮确认一次置顶;同时跟着游戏窗口尺寸走
GmWindowFollowGame();
GmWindowAssertTop();
continue;
}