源窗口最小化不再终止缩放 (#1219)

* fix: 优化窗口移动检测

* fix: 优化拖拽窗口时缩放行为
窗口模式立即缩放,全屏模式将等待拖拽结束

* feat: 源窗口最小化后等待其还原

* fix: 修复显示消息时窗口被销毁然后立即显示新消息会导致崩溃

* feat: 源窗口在最小化然后还原后可以还原缩放尺寸

* refactor: 微小重构

* feat: 缩放时禁用源窗口的窗口动画

* refactor: 禁用/还原窗口动画的逻辑集中在 WindowAnimationDisabler 类

* feat: 不再禁用窗口动画
Win10 的动画很突兀,Win11 却还行
This commit is contained in:
Xu 2025-07-29 20:16:35 +08:00 committed by GitHub
commit f116629169
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 140 additions and 82 deletions

View file

@ -54,9 +54,14 @@ bool ScalingRuntime::Start(HWND hwndSrc, ScalingOptions&& options) {
assert(!options.screenshotsDir.empty() && options.showToast && options.showError && options.save);
_Dispatcher().TryEnqueue([this, hwndSrc, options(std::move(options))]() mutable {
ScalingWindow& scalingWindow = ScalingWindow::Get();
if (scalingWindow.IsSrcRepositioning()) {
scalingWindow.CleanAfterSrcRepositioned();
}
// 初始化时视为处于缩放状态
_IsScaling(true);
ScalingWindow::Get().Start(hwndSrc, std::move(options));
scalingWindow.Start(hwndSrc, std::move(options));
});
return true;
@ -88,15 +93,24 @@ void ScalingRuntime::Stop() {
// -1: 应取消缩放
// 0: 仍在调整中
// 1: 调整完毕
static int GetSrcRepositionState(HWND hwndSrc, bool allowScalingMaximized) noexcept {
if (!IsWindow(hwndSrc) || GetForegroundWindow() != hwndSrc) {
static int GetSrcRepositionState(HWND hwndSrc) noexcept {
if (!IsWindow(hwndSrc)) {
return -1;
}
if (UINT showCmd = Win32Helper::GetWindowShowCmd(hwndSrc); showCmd != SW_NORMAL) {
if (showCmd != SW_SHOWMAXIMIZED || !allowScalingMaximized) {
return -1;
}
if (Win32Helper::IsWindowHung(hwndSrc)) {
return -1;
}
const UINT showCmd = Win32Helper::GetWindowShowCmd(hwndSrc);
if (showCmd == SW_HIDE) {
return -1;
} else if (showCmd == SW_SHOWMAXIMIZED) {
// 窗口最大化则尝试缩放,失败会显示错误消息
return 1;
} else if (showCmd == SW_SHOWMINIMIZED) {
// 窗口最小化则继续等待
return 0;
}
// 检查源窗口是否正在调整大小或移动
@ -156,7 +170,7 @@ void ScalingRuntime::_ScalingThreadProc() noexcept {
DispatchMessage(&msg);
}
_IsScaling(scalingWindow || scalingWindow.IsSrcRepositioning());
_IsScaling(scalingWindow);
if (scalingWindow) {
const auto now = steady_clock::now();
@ -175,10 +189,7 @@ void ScalingRuntime::_ScalingThreadProc() noexcept {
const DWORD restMs = DWORD((rest.count() + ratio - 1) / ratio);
MsgWaitForMultipleObjectsEx(0, nullptr, restMs, QS_ALLINPUT, MWMO_INPUTAVAILABLE);
} else if (scalingWindow.IsSrcRepositioning()) {
const int state = GetSrcRepositionState(
scalingWindow.SrcTracker().Handle(),
scalingWindow.Options().IsAllowScalingMaximized()
);
const int state = GetSrcRepositionState(scalingWindow.SrcTracker().Handle());
if (state == 0) {
// 等待调整完成
MsgWaitForMultipleObjectsEx(0, nullptr, 10, QS_ALLINPUT, MWMO_INPUTAVAILABLE);

View file

@ -57,6 +57,20 @@ ScalingError ScalingWindow::_StartImpl(HWND hwndSrc) noexcept {
Win32Helper::IsProcessElevated() ? "" : ""
));
#if _DEBUG
OutputDebugString(fmt::format(L"可执行文件路径: {}\n窗口类: {}\n",
Win32Helper::GetWindowPath(hwndSrc), Win32Helper::GetWindowClassName(hwndSrc)).c_str());
#endif
_runtimeError = ScalingError::NoError;
_isFirstFrame = true;
_isResizingOrMoving = false;
_isPreparingForResizing = false;
_isMovingDueToSrcMoved = false;
_shouldWaitForRender = false;
_areResizeHelperWindowsVisible = false;
_isSrcRepositioning = false;
if (_options.IsWindowedMode()) {
if (_options.Is3DGameMode()) {
return ScalingError::Windowed3DGameMode;
@ -72,15 +86,6 @@ ScalingError ScalingWindow::_StartImpl(HWND hwndSrc) noexcept {
InitMessage();
_runtimeError = ScalingError::NoError;
_isFirstFrame = true;
_isResizingOrMoving = false;
_isPreparingForResizing = false;
_isMovingDueToSrcMoved = false;
_shouldWaitForRender = false;
_areResizeHelperWindowsVisible = false;
_isSrcRepositioning = false;
if (ScalingError error = _srcTracker.Set(hwndSrc, _options); error != ScalingError::NoError) {
Logger::Get().Error("初始化 SrcTracker 失败");
return error;
@ -96,10 +101,10 @@ ScalingError ScalingWindow::_StartImpl(HWND hwndSrc) noexcept {
}
}
#if _DEBUG
OutputDebugString(fmt::format(L"可执行文件路径: {}\n窗口类: {}\n",
Win32Helper::GetWindowPath(hwndSrc), Win32Helper::GetWindowClassName(hwndSrc)).c_str());
#endif
if (_srcTracker.IsMoving() && !_options.IsWindowedMode()) {
_isSrcRepositioning = true;
return ScalingError::NoError;
}
[[maybe_unused]] static Ignore _ = []() {
WNDCLASSEXW wcex{
@ -158,25 +163,30 @@ ScalingError ScalingWindow::_StartImpl(HWND hwndSrc) noexcept {
// 填入渲染矩形尺寸
int windowWidth = 0;
int windowHeight = 0;
if (_options.initialWindowedScaleFactor < 1.0f) {
// 根据屏幕的工作区尺寸计算
MONITORINFO mi{ .cbSize = sizeof(mi) };
if (GetMonitorInfo(hMon, &mi)) {
const SIZE monitorSize = Win32Helper::GetSizeOfRect(mi.rcWork);
const float srcAspectRatio = (float)srcSize.cy / srcSize.cx;
if (_lastWindowedRendererWidth == 0) {
if (_options.initialWindowedScaleFactor < 1.0f) {
// 根据屏幕的工作区尺寸计算
MONITORINFO mi{ .cbSize = sizeof(mi) };
if (GetMonitorInfo(hMon, &mi)) {
const SIZE monitorSize = Win32Helper::GetSizeOfRect(mi.rcWork);
const float srcAspectRatio = (float)srcSize.cy / srcSize.cx;
// 放大到显示器的 3/4且最少放大 1/3 倍
if ((float)monitorSize.cy / monitorSize.cx > srcAspectRatio) {
windowWidth = std::max(monitorSize.cx * 3 / 4, srcSize.cx * 4 / 3);
// 放大到显示器的 3/4且最少放大 1/3 倍
if ((float)monitorSize.cy / monitorSize.cx > srcAspectRatio) {
windowWidth = std::max(monitorSize.cx * 3 / 4, srcSize.cx * 4 / 3);
} else {
windowHeight = std::max(monitorSize.cy * 3 / 4, srcSize.cy * 4 / 3);
}
} else {
windowHeight = std::max(monitorSize.cy * 3 / 4, srcSize.cy * 4 / 3);
Logger::Get().Win32Error("GetMonitorInfo 失败");
windowWidth = srcSize.cx;
}
} else {
Logger::Get().Win32Error("GetMonitorInfo 失败");
windowWidth = srcSize.cx;
windowWidth = (LONG)std::lroundf(srcSize.cx * _options.initialWindowedScaleFactor);
}
} else {
windowWidth = (LONG)std::lroundf(srcSize.cx * _options.initialWindowedScaleFactor);
// 恢复上次窗口模式缩放尺寸
windowWidth = _lastWindowedRendererWidth;
}
if (!_CalcWindowedScalingWindowSize(windowWidth, windowHeight, true)) {
@ -282,7 +292,7 @@ ScalingError ScalingWindow::_StartImpl(HWND hwndSrc) noexcept {
LogRects(_srcTracker.SrcRect(), _rendererRect, _windowRect);
if (!_options.IsWindowedMode() && !_options.IsAllowScalingMaximized()) {
if (!_options.RealIsAllowScalingMaximized()) {
// 检查源窗口是否是无边框全屏窗口
if (srcWindowKind == SrcWindowKind::NoDecoration && _srcTracker.WindowRect() == _rendererRect) {
Logger::Get().Info("源窗口已全屏");
@ -343,6 +353,9 @@ void ScalingWindow::SwitchScalingState(bool isWindowedMode) noexcept {
// 源窗口在前台时按快捷键可以切换全屏/窗口模式缩放
_isSrcRepositioning = true;
if (_options.IsWindowedMode()) {
_lastWindowedRendererWidth = _rendererRect.right - _rendererRect.left;
}
Destroy();
_options.IsWindowedMode(isWindowedMode);
RestartAfterSrcRepositioned();
@ -386,6 +399,7 @@ void ScalingWindow::RestartAfterSrcRepositioned() noexcept {
void ScalingWindow::CleanAfterSrcRepositioned() noexcept {
_options = {};
_lastWindowedRendererWidth = 0;
_isSrcRepositioning = false;
}
@ -831,11 +845,10 @@ LRESULT ScalingWindow::_MessageHandler(UINT msg, WPARAM wParam, LPARAM lParam) n
_renderer.reset();
Logger::Get().Info("Renderer 已析构");
// 如果正在源窗口正在调整,暂时不清理这些成员
if (!_isSrcRepositioning) {
// 缩放结束时保存配置
_options.save(_options, NULL);
_options = {};
CleanAfterSrcRepositioned();
}
// 还原时钟精度
@ -1256,23 +1269,32 @@ bool ScalingWindow::_UpdateSrcState(
return false;
}
bool srcMinimized = false;
bool srcRectChanged = false;
bool srcSizeChanged = false;
bool srcMovingChanged = false;
if (!_srcTracker.UpdateState(hwndFore, _options.IsWindowedMode(), _isResizingOrMoving,
srcFocusedChanged, srcOwnedWindowFocusedChanged,
srcRectChanged, srcSizeChanged, srcMovingChanged)) {
srcMinimized, srcRectChanged, srcSizeChanged, srcMovingChanged)) {
return false;
}
if (srcSizeChanged || (!_options.IsWindowedMode() && srcRectChanged)) {
// 不要立刻设置 _isSrcRepositioning销毁窗口是异步的
if (srcMinimized || srcSizeChanged || (!_options.IsWindowedMode() && srcRectChanged)) {
// 不要立刻设置 _isSrcSizing销毁窗口是异步的
isSrcRepositioning = true;
if (srcSizeChanged) {
// 源窗口大小改变则清除记忆
_lastWindowedRendererWidth = 0;
} else if (srcMinimized) {
if (_options.IsWindowedMode()) {
_lastWindowedRendererWidth = _rendererRect.right - _rendererRect.left;
}
}
return false;
}
// DirectFlip 可能使窗口移动很卡,目前发现缩放 Magpie 主窗口有这个
// 问题。因此源窗口移动过程中临时禁用 DirectFlip。
if (srcMovingChanged) {
assert(_options.IsWindowedMode());

View file

@ -153,7 +153,7 @@ private:
void _UpdateWindowRectFromWindowPos(const WINDOWPOS& windowPos) noexcept;
void _DelayedStop(bool onSrcHung = false, bool isSrcRepositioning = false) const noexcept;
void _DelayedStop(bool onSrcHung = false, bool onSrcRepositioning = false) const noexcept;
static inline std::atomic<uint32_t> _runId = 0;
static inline winrt::DispatcherQueue _dispatcher{ nullptr };
@ -182,6 +182,9 @@ private:
ScalingError _runtimeError = ScalingError::NoError;
// 窗口缩放时切换到全屏缩放或最小化前保存尺寸供以后恢复
LONG _lastWindowedRendererWidth = 0;
// 第一帧渲染完成后再显示
bool _isFirstFrame = false;
bool _isResizingOrMoving = false;

View file

@ -37,10 +37,19 @@ static bool CheckIL(HWND hwndSrc) noexcept {
return GetWindowIntegrityLevel(hwndSrc, windowIL) && windowIL <= thisIL;
}
static bool IsWindowMoving(HWND hWnd) noexcept {
GUITHREADINFO guiThreadInfo{ .cbSize = sizeof(GUITHREADINFO) };
if (GetGUIThreadInfo(GetWindowThreadProcessId(hWnd, nullptr), &guiThreadInfo)) {
return guiThreadInfo.flags & GUI_INMOVESIZE;
} else {
Logger::Get().Win32Error("GetGUIThreadInfo 失败");
return false;
}
}
ScalingError SrcTracker::Set(HWND hWnd, const ScalingOptions& options) noexcept {
_hWnd = hWnd;
_isMoving = false;
// 这里不检查源窗口是否挂起,将在创建缩放窗口前检查
if (!IsWindow(_hWnd)) {
@ -81,6 +90,8 @@ ScalingError SrcTracker::Set(HWND hWnd, const ScalingOptions& options) noexcept
_isFocused = hwndFore == hWnd;
_UpdateIsOwnedWindowFocused(hwndFore);
_isMoving = IsWindowMoving(_hWnd);
if (!GetWindowRect(hWnd, &_windowRect)) {
Logger::Get().Win32Error("GetWindowRect 失败");
return ScalingError::ScalingFailedGeneral;
@ -173,6 +184,7 @@ bool SrcTracker::UpdateState(
bool isResizingOrMoving,
bool& focusedChanged,
bool& ownedWindowFocusedChanged,
bool& minimized,
bool& rectChanged,
bool& sizeChanged,
bool& movingChanged
@ -180,12 +192,7 @@ bool SrcTracker::UpdateState(
assert(!focusedChanged && !ownedWindowFocusedChanged && !rectChanged && !sizeChanged && !movingChanged);
if (!IsWindow(_hWnd)) {
Logger::Get().Error("源窗口已销毁");
return false;
}
if (!IsWindowVisible(_hWnd)) {
Logger::Get().Error("源窗口已隐藏");
Logger::Get().Info("源窗口已销毁");
return false;
}
@ -194,7 +201,7 @@ bool SrcTracker::UpdateState(
// 格,因此即使源窗口挂起一段时间,只要用户不做额外的操作就不会结束缩放,
// 直到源窗口被替换为幽灵窗口。
if (IsHungAppWindow(_hWnd)) {
Logger::Get().Error("源窗口已挂起");
Logger::Get().Info("源窗口已挂起");
return false;
}
@ -206,21 +213,39 @@ bool SrcTracker::UpdateState(
ownedWindowFocusedChanged = _UpdateIsOwnedWindowFocused(hwndFore);
const bool oldMaximized = _isMaximized;
UINT showCmd = Win32Helper::GetWindowShowCmd(_hWnd);
if (showCmd == SW_SHOWMINIMIZED) {
Logger::Get().Error("源窗口处于最小化状态");
const UINT showCmd = Win32Helper::GetWindowShowCmd(_hWnd);
if (showCmd == SW_HIDE) {
Logger::Get().Info("源窗口已隐藏");
return false;
} else if (showCmd == SW_SHOWMINIMIZED) {
Logger::Get().Info("源窗口已最小化");
_isMaximized = false;
minimized = true;
} else {
_isMaximized = showCmd == SW_SHOWMAXIMIZED;
}
_isMaximized = showCmd == SW_SHOWMAXIMIZED;
RECT curWindowRect;
if (!GetWindowRect(_hWnd, &curWindowRect)) {
Logger::Get().Win32Error("GetWindowRect 失败");
return false;
if (minimized) {
WINDOWPLACEMENT wp{ sizeof(wp) };
if (!GetWindowPlacement(_hWnd, &wp)) {
Logger::Get().Win32Error("GetWindowPlacement 失败");
return false;
}
curWindowRect = wp.rcNormalPosition;
} else {
if (!GetWindowRect(_hWnd, &curWindowRect)) {
Logger::Get().Win32Error("GetWindowRect 失败");
return false;
}
}
sizeChanged = oldMaximized != _isMaximized ||
Win32Helper::GetSizeOfRect(curWindowRect) != Win32Helper::GetSizeOfRect(_windowRect);
if (sizeChanged) {
rectChanged = true;
return true;
}
// 缩放窗口正在调整大小或被拖动时源窗口的移动是异步的,暂时不检查源窗口是否移动
if (isResizingOrMoving) {
@ -232,20 +257,6 @@ bool SrcTracker::UpdateState(
rectChanged = oldMaximized != _isMaximized || curWindowRect != _windowRect;
if (isWindowedMode && !sizeChanged) {
bool isMoving = false;
GUITHREADINFO guiThreadInfo{ .cbSize = sizeof(GUITHREADINFO) };
if (GetGUIThreadInfo(GetWindowThreadProcessId(_hWnd, nullptr), &guiThreadInfo)) {
isMoving = guiThreadInfo.flags & GUI_INMOVESIZE;
} else {
Logger::Get().Win32Error("GetGUIThreadInfo 失败");
}
// 处理自己实现拖拽逻辑的窗口:将鼠标左键按下视为开始拖拽,释放视为拖拽结束。
// 可能会有误判,但幸好后果不太严重。
if (_isMoving || (!_isMoving && rectChanged)) {
isMoving = isMoving || IsPrimaryMouseButtonDown();
}
if (rectChanged) {
const LONG offsetX = curWindowRect.left - _windowRect.left;
const LONG offsetY = curWindowRect.top - _windowRect.top;
@ -253,6 +264,10 @@ bool SrcTracker::UpdateState(
Win32Helper::OffsetRect(_srcRect, offsetX, offsetY);
}
// 处理自己实现拖拽逻辑的窗口:将鼠标左键按下视为开始拖拽,释放视为拖拽结束。
// 可能会有误判,但幸好后果不太严重。
const bool isMoving = !minimized &&
(IsWindowMoving(_hWnd) || (rectChanged && IsPrimaryMouseButtonDown()));
if (_isMoving != isMoving) {
movingChanged = true;
_isMoving = isMoving;

View file

@ -35,6 +35,7 @@ public:
bool isResizingOrMoving,
bool& focusedChanged,
bool& ownedWindowFocusedChanged,
bool& minimized,
bool& rectChanged,
bool& sizeChanged,
bool& movingChanged

View file

@ -247,10 +247,16 @@ fire_and_forget ToastPage::ShowMessageOnWindow(std::wstring title, std::wstring
co_return;
}
if (!IsWindow((HWND)hwndTarget) || !IsWindow(_hwndToast) || !Win32Helper::GetWindowFrameRect((HWND)hwndTarget, frameRect)) {
// 附加的窗口已经关闭toast 也应关闭_oldTeachingTip 用于延长生命周期避免崩溃
UnloadObject(curTeachingTip);
_oldTeachingTip = std::move(curTeachingTip);
if (!IsWindow((HWND)hwndTarget) || !IsWindow(_hwndToast) ||
!Win32Helper::GetWindowFrameRect((HWND)hwndTarget, frameRect))
{
// 附加的窗口关闭后 toast 也应关闭。应检查 curTeachingTip 是否已经在新的调用中被卸载,
// 见函数开头的 UnloadObject。
if (curTeachingTip.IsLoaded()) {
UnloadObject(curTeachingTip);
// 延长生命周期避免崩溃
_oldTeachingTip = std::move(curTeachingTip);
}
co_return;
}