Skip to content

Fix:pinning-induced window position drift on Windows - #241

Closed
hesphoros wants to merge 2 commits into
stdware:mainfrom
hesphoros:fix/pin
Closed

Fix:pinning-induced window position drift on Windows#241
hesphoros wants to merge 2 commits into
stdware:mainfrom
hesphoros:fix/pin

Conversation

@hesphoros

Copy link
Copy Markdown
Contributor

Fix pinning-induced window position drift on Windows

Summary

Fix the Widgets MainWindow example moving downward when the Pin button is toggled repeatedly on Windows.

The Pin action now changes only the native window z-order instead of changing Qt::WindowStaysOnTopHint and showing the window again. The selected Pin state is cached and restored if Qt recreates the native window handle.

Problem

The previous Pin callback used:

setWindowFlag(Qt::WindowStaysOnTopHint, pin);
show();

Changing a top-level window flag can recreate its native HWND. For a frameless window with custom non-client handling, recreating and showing the window can alter the conversion between client and frame coordinates. Repeated Pin/Unpin operations therefore caused the window to drift downward by approximately one title-bar height per toggle.

Changes

  • Use SetWindowPos() with HWND_TOPMOST or HWND_NOTOPMOST on Windows.
  • Pass SWP_NOMOVE | SWP_NOSIZE | SWP_NOACTIVATE | SWP_NOOWNERZORDER so the operation changes only z-order.
  • Use internalWinId() to avoid creating a native handle as a side effect.
  • Cache the current stay-on-top state in the example window.
  • Reapply the cached state after QEvent::WinIdChange so pinning survives unrelated native handle recreation.
  • Guard against reentrant state application while handling native window changes.
  • Restore the Pin button's checked state when the request cannot be applied, including requests made while the window is hidden, minimized, maximized, or full screen.
  • Keep a geometry-preserving Qt fallback for non-Windows platforms.

This change is limited to the Widgets example and does not modify QWindowKit's public API.

Verification

  • Built QWKExample_MainWindow in Release mode with Qt 6.8.3 and MSVC.
  • On Windows, toggle Pin/Unpin and confirm the frame position and size do not change.
  • Confirm the window becomes topmost when pinned and returns to non-topmost when unpinned
  • Confirm the Pin button remains synchronized with the applied native state.
  • Confirm pinning is restored after native handle recreation.
  • Confirm minimize, maximize, restore, close, title-bar hit testing, and menu activation styling still work.

Scope

No core WindowAgent behavior or public API is changed. The fix only replaces the example application's Pin policy implementation.

Copilot AI review requested due to automatic review settings July 28, 2026 11:26

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR updates the Widgets MainWindow example’s “Pin” (stay-on-top) behavior on Windows to avoid repeated top-level flag changes that can recreate the native window handle and cause gradual frame position drift.

Changes:

  • On Windows, implement pin/unpin by changing native z-order via SetWindowPos(HWND_TOPMOST/HWND_NOTOPMOST) instead of toggling Qt::WindowStaysOnTopHint + show().
  • Cache the requested stay-on-top state and reapply it on QEvent::WinIdChange to survive native handle recreation.
  • Keep a geometry-preserving Qt-based fallback for non-Windows platforms and improve UI synchronization when pin requests can’t be applied.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 2 comments.

File Description
examples/mainwindow/mainwindow.h Adds helper API + state flags to track and reapply stay-on-top behavior.
examples/mainwindow/mainwindow.cpp Implements Windows z-order pinning, WinIdChange reapply, and updated pin button handling.
Comments suppressed due to low confidence (2)

examples/mainwindow/mainwindow.cpp:141

  • This Windows-only event handling is guarded by Q_OS_WINDOWS, but other Windows-specific blocks in this file use Q_OS_WIN. Using a single Windows macro consistently within the file avoids platform-guard mismatches.
#ifdef Q_OS_WINDOWS
        case QEvent::WinIdChange:
            if (!applyingStayOnTop) {
                setStayOnTop(stayOnTop);
            }
            break;
#endif

examples/mainwindow/mainwindow.cpp:476

  • setStayOnTop() is currently compiled under Q_OS_WINDOWS while other Windows-specific logic in this file is under Q_OS_WIN. Consider using the same macro consistently to prevent any platform-guard mismatches.
bool MainWindow::setStayOnTop(bool enabled) {
#ifdef Q_OS_WINDOWS
    if (applyingStayOnTop) {
        return false;

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread examples/mainwindow/mainwindow.h
Comment thread examples/mainwindow/mainwindow.cpp Outdated
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
@wangwenx190

Copy link
Copy Markdown
Collaborator

看起来有道理,但我觉得应该是Qt内部有bug,你这样实际上只是从表层进行workaround吧?

@hesphoros

Copy link
Copy Markdown
Contributor Author

@wangwenx190 是的,这个改动属于 example 层的 workaround。问题发生在一个已经显示的顶层 QWidget 上调用 setWindowFlag(Qt::WindowStaysOnTopHint, ...) 并重新 show() 时;该过程可能重建 HWND。在 QWindowKit 自定义 non-client frame 的场景下,重建前后的 frame/client 坐标换算会导致窗口位置发生偏移,这部分可能是 Qt Windows platform plugin 的问题

@wangwenx190

Copy link
Copy Markdown
Collaborator

你这个PR其实改了不少地方,我有点不确定为了workaround示例程序的问题,引入这种改动是否有必要

@hesphoros

Copy link
Copy Markdown
Contributor Author

造成窗口整体32px的偏移的具体原因如下:

setWindowFlag(Qt::WindowStaysOnTopHint) + show()
让 Qt 临时走了一遍“带系统标题栏”的 native geometry 流程,Qt 把 client top = frame top + titleBarHeight 记成了新的窗口位置,随后 QWK 再把标题栏移除,于是窗口 frame 被整体往下推了一个标题栏高度。

从调试日志中得出窗口整体偏移32px的来源.

titleBarHeight=31
visibleFrameBorder=1

@wangwenx190

Copy link
Copy Markdown
Collaborator

感谢你的分析!这个问题我觉得应该在QWK层面进行workaround,而不是在示例程序那边,不知道你有没有什么好办法呢

@hesphoros

Copy link
Copy Markdown
Contributor Author

具体流程如下:

初始 QWK 接管后,client 几乎贴着 frame 顶部:

clientOrigin.y = 186

只差 1px,这是 Win11 visible frame border。

点击pin后调用了

setWindowFlag(Qt::WindowStaysOnTopHint, pin);
show();

从日志中得出setWindowFlag() 会让 Qt 隐藏/重建/刷新 native window.此阶段的HWND是不稳定的.

...
[NativeTrace] pinRequested after setWindowFlag hwnd=0x0 windowRect= <failed> clientRect= <failed> clientOrigin= <failed
...

在 QWK hook 尚未重新完整接管时,系统/Qt 处理了一次普通窗口的 WM_NCCALCSIZE:
此时两者之差正好是标题栏的高度.

clientOrigin.y = 216
windowRect.top = 185

Qt 接下来把这个带标题栏偏移的 client 坐标当成 QWidget 的 geometry,后续又把窗口移动到这个位置:

WM_WINDOWPOSCHANGING x=552 y=217

这也就造成了后续的持续累加.

@wangwenx190

Copy link
Copy Markdown
Collaborator

或许我们应该用QWindow句柄作为一个窗口的唯一标志,然后观察HWND的变化

@hesphoros

hesphoros commented Jul 31, 2026

Copy link
Copy Markdown
Contributor Author

@hesphoros

Copy link
Copy Markdown
Contributor Author

感谢你的分析!这个问题我觉得应该在QWK层面进行workaround,而不是在示例程序那边,不知道你有没有什么好办法呢

我觉得我们可以在agent中新增stay-on-top属性,以方便的提供pin功能.

windowAgent->setWindowAttribute("stay-on-top", true);
windowAgent->setWindowAttribute("stay-on-top", false);

Windows下实现为

SetWindowPos(hwnd,
             enabled ? HWND_TOPMOST : HWND_NOTOPMOST,
             0, 0, 0, 0,
             SWP_NOMOVE | SWP_NOSIZE | SWP_NOACTIVATE | SWP_NOOWNERZORDER);

这样应用层仍然只依赖 QWK,不需要碰 HWND,而且 QWK 避开了 Qt 的setWindowFlag()+show()重建路径.
PIN操作的只是 Z-order 变化,不应该触发 frame recalculation / native window recreate.

@wangwenx190

Copy link
Copy Markdown
Collaborator

只修复pin的话其实治标不治本,看起来只要Qt重建native窗口,就会触发问题

@hesphoros

Copy link
Copy Markdown
Contributor Author

或许我们应该用QWindow句柄作为一个窗口的唯一标志,然后观察HWND的变化

但是此种方法好像改动太大

@hesphoros

Copy link
Copy Markdown
Contributor Author

或许我们应该用QWindow句柄作为一个窗口的唯一标志,然后观察HWND的变化

或许Win32WindowContext应该稳定持有QWindow *m_windowHandle.让其存储内部状态.

@wangwenx190

Copy link
Copy Markdown
Collaborator

这个问题如果要在QWK里根治其实有点麻烦,因为我们可能要一直存储窗口的位置,在原生窗口重建后re-apply一下

@wangwenx190

Copy link
Copy Markdown
Collaborator

我可以试着让AI帮忙搞个方法看看,如果你那边有更好的办法也可以提出来。改动大点暂时是可以接受的,毕竟也是为了修复QWK导致的问题

@hesphoros

Copy link
Copy Markdown
Contributor Author

这个问题如果要在QWK里根治其实有点麻烦,因为我们可能要一直存储窗口的位置,在原生窗口重建后re-apply一下

对的.而且我觉得我们的agent安装是不稳定的.

@wangwenx190

Copy link
Copy Markdown
Collaborator

这个问题如果要在QWK里根治其实有点麻烦,因为我们可能要一直存储窗口的位置,在原生窗口重建后re-apply一下

对的.而且我觉得我们的agent安装是不稳定的.

不稳定?这个倒没考虑过,你是有什么看法呢

@hesphoros

Copy link
Copy Markdown
Contributor Author

这个问题如果要在QWK里根治其实有点麻烦,因为我们可能要一直存储窗口的位置,在原生窗口重建后re-apply一下

对的.而且我觉得我们的agent安装是不稳定的.

不稳定?这个倒没考虑过,你是有什么看法呢

准确的来说是它对 HWND 生命周期的接管存在一个空窗期.在 old HWND 移除、新 HWND 完整接管之间,会有一段消息流不经过 QWK hook.

@hesphoros

Copy link
Copy Markdown
Contributor Author

我可以试着让AI帮忙搞个方法看看,如果你那边有更好的办法也可以提出来。改动大点暂时是可以接受的,毕竟也是为了修复QWK导致的问题

  • 以 QWindow 作为稳定身份,观察 HWND 变化:
  • old HWND 失效前缓存 native frame rect
  • new HWND 安装 hook 后 replay QWK 状态
  • 在 event loop 下一次检查是否出现 titleBarHeight + visibleFrameBorder 级别漂移
  • 如果出现,re-apply cached frame rect

@wangwenx190

Copy link
Copy Markdown
Collaborator

#243 已经合了,这个PR应该没必要了吧?

@hesphoros

Copy link
Copy Markdown
Contributor Author

#243 已经合了,这个PR应该没必要了吧?

好的,感谢您的合并。

@hesphoros hesphoros closed this Aug 1, 2026
@hesphoros
hesphoros deleted the fix/pin branch August 1, 2026 21:41
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants