修复源窗口被最小化时偶尔没有移出屏幕 (#1335)

* fix: 修复源窗口被最小化后没有移出屏幕的错误

* fix: 位于中间状态时不再继续检查

* fix: 修复切换窗口时偶尔源窗口不会被带到顶部

* refactor: 常用 SWP 组合定义为宏

* perf: 提高细粒度

* fix: 置顶窗口可能失败,需多次尝试
This commit is contained in:
Xu 2025-11-10 18:41:37 +08:00 committed by GitHub
commit 15d3e392c4
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 34 additions and 31 deletions

View file

@ -1219,7 +1219,7 @@ void ScalingWindow::_Show() noexcept {
Handle(),
NULL,
0, 0, 0, 0,
SWP_SHOWWINDOW | SWP_NOACTIVATE | SWP_NOMOVE | SWP_NOSIZE
SWP_SHOWWINDOW | SWP_NO_ACTIVATE_MOVE_SIZE
);
// 广播开始缩放
@ -1888,43 +1888,39 @@ void ScalingWindow::_UpdateFocusState() const noexcept {
return;
}
const bool topmost = _CalcTopmostState();
if (IsTopmostWindow(Handle()) != topmost) {
const bool oldTopmost = IsTopmostWindow(Handle());
const bool newTopmost = _CalcTopmostState();
if (oldTopmost != newTopmost) {
// 由于同步问题可能需要尝试多次
for (int i = 0; i < 10; ++i) {
SetWindowPos(Handle(), topmost ? HWND_TOPMOST : HWND_NOTOPMOST,
0, 0, 0, 0, SWP_NOACTIVATE | SWP_NOMOVE | SWP_NOSIZE | SWP_NOOWNERZORDER);
// 切换到其他窗口时不要改变源窗口 Z 顺序,当前台窗口权限更高时需要依赖源窗口位置
SetWindowPos(Handle(), newTopmost ? HWND_TOPMOST : HWND_NOTOPMOST, 0, 0, 0, 0,
SWP_NO_ACTIVATE_MOVE_SIZE | (_srcTracker.IsFocused() ? 0 : SWP_NOOWNERZORDER));
if (IsTopmostWindow(Handle()) == topmost) {
if (IsTopmostWindow(Handle()) == newTopmost) {
break;
}
}
}
if (_srcTracker.IsFocused()) {
if (!_options.IsWindowedMode()) {
// 全屏模式缩放时确保缩放窗口在所有置顶窗口之上,这使不支持 MPO 的显卡更容易激
// 活 DirectFlip。
HDWP hDwp = BeginDeferWindowPos(2);
if (hDwp) {
hDwp = DeferWindowPos(hDwp, _srcTracker.Handle(), HWND_TOP, 0, 0, 0, 0,
SWP_NOACTIVATE | SWP_NOMOVE | SWP_NOSIZE);
hDwp = DeferWindowPos(hDwp, Handle(), HWND_TOP, 0, 0, 0, 0,
SWP_NOACTIVATE | SWP_NOMOVE | SWP_NOSIZE | SWP_NOOWNERZORDER);
EndDeferWindowPos(hDwp);
}
// 全屏模式缩放时确保缩放窗口在所有置顶窗口之上,这使不支持 MPO 的显卡更容易激
// 活 DirectFlip。
if (!_options.IsWindowedMode() && newTopmost) {
SetWindowPos(Handle(), HWND_TOP, 0, 0, 0, 0, SWP_NO_ACTIVATE_MOVE_SIZE);
}
} else {
} else if (oldTopmost && !newTopmost) {
// 如果缩放窗口之前是置顶的,此时会在前台窗口之上,应将前台窗口置于顶部
if (const HWND hwndFore = GetForegroundWindow()) {
if (!SetWindowPos(hwndFore, HWND_TOP, 0, 0, 0, 0, SWP_NOACTIVATE | SWP_NOMOVE | SWP_NOSIZE)) {
if (!SetWindowPos(hwndFore, HWND_TOP, 0, 0, 0, 0, SWP_NO_ACTIVATE_MOVE_SIZE)) {
// 如果前台窗口权限更高SetWindowPos 会失败。这时用其他方法将缩放窗口放到
// 前台窗口之后,缺点是偶尔会有一瞬间源窗口出现在缩放窗口前。
HDWP hDwp = BeginDeferWindowPos(2);
if (hDwp) {
hDwp = DeferWindowPos(hDwp, Handle(), _srcTracker.Handle(),
0, 0, 0, 0, SWP_NOACTIVATE | SWP_NOMOVE | SWP_NOSIZE | SWP_NOOWNERZORDER);
0, 0, 0, 0, SWP_NO_ACTIVATE_MOVE_SIZE | SWP_NOOWNERZORDER);
hDwp = DeferWindowPos(hDwp, _srcTracker.Handle(), Handle(),
0, 0, 0, 0, SWP_NOACTIVATE | SWP_NOMOVE | SWP_NOSIZE | SWP_NOOWNERZORDER);
0, 0, 0, 0, SWP_NO_ACTIVATE_MOVE_SIZE | SWP_NOOWNERZORDER);
EndDeferWindowPos(hDwp);
}
}
@ -2084,10 +2080,10 @@ void ScalingWindow::_UpdateWindowRectFromWindowPos(const WINDOWPOS& windowPos) n
void ScalingWindow::_DelayedStop(bool onSrcHung, bool onSrcRepositioning) const noexcept {
if (!onSrcHung) {
const HWND hwndSrc = _srcTracker.Handle();
if (!(IsWindow(hwndSrc) && Win32Helper::IsWindowHung(hwndSrc))) {
if (IsTopmostWindow(Handle()) && !(IsWindow(hwndSrc) && Win32Helper::IsWindowHung(hwndSrc))) {
// 提前取消置顶,这样销毁时出现问题不会影响和桌面环境交互
SetWindowPos(Handle(), HWND_NOTOPMOST, 0, 0, 0, 0,
SWP_NOACTIVATE | SWP_NOMOVE | SWP_NOSIZE | SWP_NOOWNERZORDER);
SWP_NO_ACTIVATE_MOVE_SIZE | SWP_NOOWNERZORDER);
}
}

View file

@ -234,6 +234,14 @@ bool SrcTracker::UpdateState(
RECT curWindowRect;
if (wp.showCmd == SW_SHOWMINIMIZED) {
// 窗口最小化有两步:先将窗口状态设为最小化,然后将窗口移出屏幕 (左上角坐标
// (-32000,-32000))。如果我们刚好在两步之间停止缩放,第二步将无法执行,这和缩
// 放窗口被源窗口所有有关,不确定是否是 OS 的 bug。这个检查确保第二步完成后再
// 停止缩放。
if (wp.rcNormalPosition.left == wp.ptMinPosition.x) {
return true;
}
isInvisibleOrMinimized = true;
// rcNormalPosition 使用工作区坐标,应转换为屏幕坐标

View file

@ -134,12 +134,10 @@ fire_and_forget ToastPage::ShowMessageOnWindow(std::wstring title, std::wstring
}
if (isTargetTopMost || !isOwned) {
SetWindowPos(_hwndToast, HWND_TOPMOST, 0, 0, 0, 0,
SWP_NOACTIVATE | SWP_NOMOVE | SWP_NOSIZE);
SetWindowPos(_hwndToast, HWND_TOPMOST, 0, 0, 0, 0, SWP_NO_ACTIVATE_MOVE_SIZE);
}
if (!isTargetTopMost) {
SetWindowPos(_hwndToast, HWND_NOTOPMOST, 0, 0, 0, 0,
SWP_NOACTIVATE | SWP_NOMOVE | SWP_NOSIZE);
SetWindowPos(_hwndToast, HWND_NOTOPMOST, 0, 0, 0, 0, SWP_NO_ACTIVATE_MOVE_SIZE);
}
// 更改所有者后应更新 Z 轴顺序
@ -268,11 +266,11 @@ fire_and_forget ToastPage::ShowMessageOnWindow(std::wstring title, std::wstring
// 如果 hwndTarget 位于前台定期将弹窗置顶。SWP_NOOWNERZORDER 可以避免修改 hwndTarget
// 的 Z 顺序,理论上不需要这个标志,可能是 OS 的 bug。
SetWindowPos(_hwndToast, HWND_TOPMOST, 0, 0, 0, 0,
SWP_NOACTIVATE | SWP_NOMOVE | SWP_NOSIZE | SWP_NOOWNERZORDER);
SWP_NO_ACTIVATE_MOVE_SIZE | SWP_NOOWNERZORDER);
}
if (!isTargetTopMost) {
SetWindowPos(_hwndToast, HWND_NOTOPMOST, 0, 0, 0, 0,
SWP_NOACTIVATE | SWP_NOMOVE | SWP_NOSIZE | SWP_NOOWNERZORDER);
SWP_NO_ACTIVATE_MOVE_SIZE | SWP_NOOWNERZORDER);
}
// 窗口没有移动则无需更新

View file

@ -85,8 +85,7 @@ void ToastService::_ToastThreadProc() noexcept {
wil::GetModuleInstanceHandle(),
nullptr
);
SetWindowPos(_hwndToast, NULL, 0, 0, 0, 0,
SWP_SHOWWINDOW | SWP_NOACTIVATE | SWP_NOMOVE | SWP_NOSIZE);
SetWindowPos(_hwndToast, NULL, 0, 0, 0, 0, SWP_SHOWWINDOW | SWP_NO_ACTIVATE_MOVE_SIZE);
// DesktopWindowXamlSource 在控件之前创建则无需调用 WindowsXamlManager::InitializeForCurrentThread
DesktopWindowXamlSource xamlSource;

View file

@ -19,6 +19,8 @@ using winrt::operator co_await;
#define _STRING_HELPER(x) #x
#define STRING(x) _STRING_HELPER(x)
#define SWP_NO_ACTIVATE_MOVE_SIZE (SWP_NOACTIVATE | SWP_NOMOVE | SWP_NOSIZE)
struct Ignore {
constexpr Ignore() noexcept = default;