mirror of
https://github.com/Blinue/Magpie.git
synced 2026-06-24 02:04:10 +00:00
优化 Graphics Capture 对 Kirikiri 窗口的支持 (#1238)
* feat: 尝试模拟 kirikiri 窗口行为 * feat: 完善模拟 kirikiri 窗口 * feat: 优化 WGC 对 kirikiri 窗口的处理 * chore: 优化注释 * fix: 优化错误处理 * chore: 添加注释
This commit is contained in:
parent
ffdc3e772b
commit
fb2270ec89
13 changed files with 490 additions and 100 deletions
|
|
@ -175,6 +175,26 @@ static bool CalcWindowCapturedFrameBounds(HWND hWnd, RECT& rect) noexcept {
|
|||
return true;
|
||||
}
|
||||
|
||||
// 部分使用 Kirikiri 引擎的游戏有着这样的架构: 游戏窗口并非顶级窗口,而是被一个零尺寸
|
||||
// 的窗口所有。此时 Alt+Tab 列表中的窗口和任务栏图标实际上是所有者窗口,这会导致 WGC
|
||||
// 捕获失败。我们特殊处理这类窗口。
|
||||
static bool IsKirikiriWindow(HWND hwndSrc) noexcept {
|
||||
const HWND hwndOwner = GetWindowOwner(hwndSrc);
|
||||
if (!hwndOwner) {
|
||||
return false;
|
||||
}
|
||||
|
||||
RECT ownerRect;
|
||||
if (!GetWindowRect(hwndOwner, &ownerRect)) {
|
||||
Logger::Get().Win32Error("GetWindowRect 失败");
|
||||
return false;
|
||||
}
|
||||
|
||||
// 所有者窗口尺寸为零,而且是顶级窗口
|
||||
return ownerRect.left == ownerRect.right && ownerRect.top == ownerRect.bottom &&
|
||||
!GetWindowOwner(hwndOwner);
|
||||
}
|
||||
|
||||
bool GraphicsCaptureFrameSource::_CaptureWindow(IGraphicsCaptureItemInterop* interop) noexcept {
|
||||
const SrcTracker& srcTracker = ScalingWindow::Get().SrcTracker();
|
||||
const HWND hwndSrc = srcTracker.Handle();
|
||||
|
|
@ -202,64 +222,67 @@ bool GraphicsCaptureFrameSource::_CaptureWindow(IGraphicsCaptureItemInterop* int
|
|||
1
|
||||
};
|
||||
|
||||
const DWORD srcExStyle = GetWindowExStyle(hwndSrc);
|
||||
// WS_EX_APPWINDOW 样式使窗口始终在 Alt+Tab 列表中显示
|
||||
if (srcExStyle & WS_EX_APPWINDOW) {
|
||||
return _TryCreateGraphicsCaptureItem(interop);
|
||||
}
|
||||
|
||||
const bool isSrcKirikiri = IsKirikiriWindow(hwndSrc);
|
||||
if (isSrcKirikiri) {
|
||||
Logger::Get().Info("源窗口有零尺寸的所有者窗口");
|
||||
} else {
|
||||
// 第一次尝试捕获。Kirikiri 窗口必定失败,无需尝试
|
||||
if (_TryCreateGraphicsCaptureItem(interop)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// 添加 WS_EX_APPWINDOW 样式
|
||||
if (!SetWindowLongPtr(hwndSrc, GWL_EXSTYLE, srcExStyle | WS_EX_APPWINDOW)) {
|
||||
Logger::Get().Win32Error("SetWindowLongPtr 失败");
|
||||
return false;
|
||||
}
|
||||
|
||||
Logger::Get().Info("已改变源窗口样式");
|
||||
_isSrcStyleChanged = true;
|
||||
|
||||
// Kirikiri 窗口改变样式后所有者窗口和游戏窗口将同时出现在 Alt+Tab 列表和任务栏中。
|
||||
// 虽然所有窗口都会如此,但 Kirikiri 的特殊之处在于两个窗口的图标和标题相同,为了不
|
||||
// 引起困惑应隐藏所有者窗口的图标。
|
||||
if (isSrcKirikiri) {
|
||||
_taskbarList = winrt::try_create_instance<ITaskbarList>(CLSID_TaskbarList);
|
||||
if (_taskbarList) {
|
||||
HRESULT hr = _taskbarList->HrInit();
|
||||
if (SUCCEEDED(hr)) {
|
||||
// 修正任务栏图标
|
||||
_taskbarList->DeleteTab(GetWindowOwner(hwndSrc));
|
||||
_taskbarList->AddTab(hwndSrc);
|
||||
|
||||
// 修正 Alt+Tab 切换顺序
|
||||
if (GetForegroundWindow() == hwndSrc) {
|
||||
SetForegroundWindow(GetDesktopWindow());
|
||||
SetForegroundWindow(hwndSrc);
|
||||
}
|
||||
} else {
|
||||
Logger::Get().ComError("ITaskbarList::HrInit 失败", hr);
|
||||
_taskbarList = nullptr;
|
||||
}
|
||||
} else {
|
||||
Logger::Get().Error("创建 ITaskbarList 失败");
|
||||
}
|
||||
}
|
||||
|
||||
// 再次尝试捕获
|
||||
if (_TryCreateGraphicsCaptureItem(interop)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// 尝试设置源窗口样式,因为 WGC 只能捕获位于 Alt+Tab 列表中的窗口
|
||||
LONG_PTR srcExStyle = GetWindowLongPtr(hwndSrc, GWL_EXSTYLE);
|
||||
if ((srcExStyle & WS_EX_APPWINDOW) == 0) {
|
||||
// 添加 WS_EX_APPWINDOW 样式,确保源窗口可被 Alt+Tab 选中
|
||||
if (SetWindowLongPtr(hwndSrc, GWL_EXSTYLE, srcExStyle | WS_EX_APPWINDOW)) {
|
||||
Logger::Get().Info("已改变源窗口样式");
|
||||
_originalSrcExStyle = srcExStyle;
|
||||
|
||||
if (_TryCreateGraphicsCaptureItem(interop)) {
|
||||
_RemoveOwnerFromAltTabList(hwndSrc);
|
||||
return true;
|
||||
}
|
||||
} else {
|
||||
Logger::Get().Win32Error("SetWindowLongPtr 失败");
|
||||
}
|
||||
}
|
||||
|
||||
// 如果窗口使用 ITaskbarList 隐藏了任务栏图标也不会出现在 Alt+Tab 列表。这种情况很罕见
|
||||
_taskbarList = winrt::try_create_instance<ITaskbarList>(CLSID_TaskbarList);
|
||||
if (_taskbarList && SUCCEEDED(_taskbarList->HrInit())) {
|
||||
HRESULT hr = _taskbarList->AddTab(hwndSrc);
|
||||
if (SUCCEEDED(hr)) {
|
||||
Logger::Get().Info("已添加任务栏图标");
|
||||
|
||||
if (_TryCreateGraphicsCaptureItem(interop)) {
|
||||
_RemoveOwnerFromAltTabList(hwndSrc);
|
||||
return true;
|
||||
}
|
||||
} else {
|
||||
_taskbarList = nullptr;
|
||||
Logger::Get().Error("ITaskbarList::AddTab 失败");
|
||||
}
|
||||
} else {
|
||||
_taskbarList = nullptr;
|
||||
Logger::Get().Error("创建 ITaskbarList 失败");
|
||||
}
|
||||
|
||||
// 上面的尝试失败了则还原更改
|
||||
if (_taskbarList) {
|
||||
_taskbarList->DeleteTab(hwndSrc);
|
||||
_taskbarList = nullptr;
|
||||
}
|
||||
if (_originalSrcExStyle) {
|
||||
// 首先还原所有者窗口的样式以压制任务栏的动画
|
||||
if (_originalOwnerExStyle) {
|
||||
SetWindowLongPtr(GetWindowOwner(hwndSrc), GWL_EXSTYLE, _originalOwnerExStyle);
|
||||
_originalOwnerExStyle = 0;
|
||||
if (_isSrcStyleChanged) {
|
||||
// 恢复源窗口样式
|
||||
SetWindowLongPtr(hwndSrc, GWL_EXSTYLE, srcExStyle);
|
||||
}
|
||||
|
||||
SetWindowLongPtr(hwndSrc, GWL_EXSTYLE, _originalSrcExStyle);
|
||||
_originalSrcExStyle = 0;
|
||||
return false;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
bool GraphicsCaptureFrameSource::_TryCreateGraphicsCaptureItem(IGraphicsCaptureItemInterop* interop) noexcept {
|
||||
|
|
@ -281,43 +304,6 @@ bool GraphicsCaptureFrameSource::_TryCreateGraphicsCaptureItem(IGraphicsCaptureI
|
|||
return true;
|
||||
}
|
||||
|
||||
// 部分使用 Kirikiri 引擎的游戏有着这样的架构: 游戏窗口并非根窗口,它被一个尺寸为 0 的窗口
|
||||
// 所有。此时 Alt+Tab 列表中的窗口和任务栏图标实际上是所有者窗口,这会导致 WGC 捕获游戏窗
|
||||
// 口时失败。_CaptureWindow 在初次捕获失败后会将 WS_EX_APPWINDOW 样式添加到游戏窗口,这
|
||||
// 可以工作,但也导致所有者窗口和游戏窗口同时出现在 Alt+Tab 列表中,引起用户的困惑。
|
||||
//
|
||||
// 此函数检测这种情况并改变所有者窗口的样式将它从 Alt+Tab 列表中移除。
|
||||
void GraphicsCaptureFrameSource::_RemoveOwnerFromAltTabList(HWND hwndSrc) noexcept {
|
||||
HWND hwndOwner = GetWindowOwner(hwndSrc);
|
||||
if (!hwndOwner) {
|
||||
return;
|
||||
}
|
||||
|
||||
RECT ownerRect{};
|
||||
if (!GetWindowRect(hwndOwner, &ownerRect)) {
|
||||
Logger::Get().Win32Error("GetWindowRect 失败");
|
||||
return;
|
||||
}
|
||||
|
||||
// 检查所有者窗口尺寸
|
||||
if (ownerRect.right != ownerRect.left || ownerRect.bottom != ownerRect.top) {
|
||||
return;
|
||||
}
|
||||
|
||||
LONG_PTR ownerExStyle = GetWindowLongPtr(hwndOwner, GWL_EXSTYLE);
|
||||
if (ownerExStyle == 0) {
|
||||
Logger::Get().Win32Error("GetWindowLongPtr 失败");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!SetWindowLongPtr(hwndOwner, GWL_EXSTYLE, ownerExStyle | WS_EX_TOOLWINDOW)) {
|
||||
Logger::Get().Win32Error("SetWindowLongPtr 失败");
|
||||
return;
|
||||
}
|
||||
|
||||
_originalOwnerExStyle = ownerExStyle;
|
||||
}
|
||||
|
||||
bool GraphicsCaptureFrameSource::_StartCapture() noexcept {
|
||||
if (_captureSession) {
|
||||
return true;
|
||||
|
|
@ -386,18 +372,22 @@ GraphicsCaptureFrameSource::~GraphicsCaptureFrameSource() {
|
|||
|
||||
const HWND hwndSrc = ScalingWindow::Get().SrcTracker().Handle();
|
||||
|
||||
// 还原源窗口样式
|
||||
if (_isSrcStyleChanged) {
|
||||
const DWORD srcExStyle = GetWindowExStyle(hwndSrc);
|
||||
SetWindowLongPtr(hwndSrc, GWL_EXSTYLE, srcExStyle & ~WS_EX_APPWINDOW);
|
||||
}
|
||||
|
||||
// 还原 Kirikiri 窗口
|
||||
if (_taskbarList) {
|
||||
_taskbarList->DeleteTab(hwndSrc);
|
||||
}
|
||||
_taskbarList->AddTab(GetWindowOwner(hwndSrc));
|
||||
|
||||
// 还原源窗口样式
|
||||
if (_originalSrcExStyle) {
|
||||
// 首先还原所有者窗口的样式以压制任务栏的动画
|
||||
if (_originalOwnerExStyle) {
|
||||
SetWindowLongPtr(GetWindowOwner(hwndSrc), GWL_EXSTYLE, _originalOwnerExStyle);
|
||||
// 修正任务栏焦点窗口和 Alt+Tab 切换顺序
|
||||
if (GetForegroundWindow() == hwndSrc) {
|
||||
SetForegroundWindow(GetDesktopWindow());
|
||||
SetForegroundWindow(hwndSrc);
|
||||
}
|
||||
|
||||
SetWindowLongPtr(hwndSrc, GWL_EXSTYLE, _originalSrcExStyle);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue