[windows] Restore Windows Snap for custom title bar#3069
Conversation
There was a problem hiding this comment.
Thanks for this @jensenpat — nice, focused fix. The approach (preserving native overlapped-window style bits and hiding the frame via WM_NCCALCSIZE instead of leaning on Qt::FramelessWindowHint) is the standard Windows custom-frame contract and is the right way to get Snap Assist back. AppSettings usage matches our conventions, scope is appropriate (4 files, all Windows-frame related), and the non-Windows behavior is untouched.
A few observations / questions — none blocking, just want to make sure they're intentional:
1. setFramelessWindow(bool on) ignores on on Windows — src/gui/MainWindow.cpp:12895
The Windows branch always calls applyWindowsCustomFrame() regardless of on. Tracing it through:
- The
onparameter only controls what gets persisted toAppSettings("FramelessWindow")at the top of the function. applyWindowsCustomFrame()triggersSWP_FRAMECHANGED, which firesWM_NCCALCSIZE, andnativeEventthen re-reads the (just-saved) setting viamainWindowCustomFrameEnabled()to decide whether to swallow the message.
So toggling on=false on Windows does end up restoring native chrome via the redraw path, which is correct — but it's pretty non-obvious. A one-line comment in the Windows branch explaining that applyWindowsCustomFrame() works for both directions because nativeEvent gates on the persisted setting would save the next reader a lot of head-scratching.
2. m_sizeGrip visibility when on=true on Windows — src/gui/MainWindow.cpp:12916
With the new Windows path, edge/corner hits from WM_NCHITTEST already give native resize, so the bottom-right QSizeGrip is redundant (and may double up visually with the snap-aware resize edges). Worth confirming this isn't visually awkward, or hiding the size grip unconditionally on Windows when custom-frame is active.
3. applyWindowsCustomFrame() ORs style bits but never removes WS_POPUP — src/gui/MainWindow.cpp (the new function)
desiredStyle = style | WS_CAPTION | WS_THICKFRAME | .... If style happens to still have WS_POPUP set (e.g. from a stale flag state), this preserves it alongside WS_CAPTION, which Win32 treats as conflicting. With the constructor change to skip Qt::FramelessWindowHint on Windows this shouldn't happen in practice, but explicitly masking off WS_POPUP (desiredStyle = (style & ~WS_POPUP) | WS_OVERLAPPEDWINDOW) would make it robust against future changes that might re-introduce the flag.
4. QCursor::pos() vs the WM_NCHITTEST lParam — src/gui/MainWindow.cpp (in nativeEvent)
You correctly use nativePos (from GET_X_LPARAM/GET_Y_LPARAM) for the resize-edge math because it's in physical pixels, then switch to QCursor::pos() for the isSystemMoveAreaAt check because that helper operates in Qt logical coordinates. Just confirming this is intentional — fine in practice, but worth a one-line comment so a future reader doesn't "fix" the inconsistency.
Otherwise looks good. The TitleBar::isSystemMoveAreaAt helper is a clean separation, and routing the title-bar drag through WM_NCLBUTTONDOWN / HTCAPTION (instead of startSystemMove) is the right call given the Qt 6.8.3 hit-test skip.
🤖 aethersdr-agent · cost: $4.7125 · model: claude-opus-4-7
## Summary - Preserve native Windows overlapped-window styles for the main window so Snap Assist can recognize it as a normal resizable window. - On Windows, avoid Qt 6.8.3's `Qt::FramelessWindowHint`/`WS_POPUP` path for the main window while frameless mode is enabled; instead hide the native frame with `WM_NCCALCSIZE`. - Return Windows non-client hit-test values (`HTCAPTION`, resize edges/corners) for the custom title bar and window edges so the center title-bar handle invokes the OS move/snap UI. - Keep existing frameless propagation for dialogs/floating child windows unchanged. ## Why Qt 6.8.3 creates `Qt::FramelessWindowHint` top-level windows as `WS_POPUP` and its Windows platform hit-test path skips frameless windows. That prevents Windows from taking over the drag operation for Snap Assist even when the app returns `HTCAPTION`. Keeping native frame style bits and hiding the frame through `WM_NCCALCSIZE` matches the Windows custom-frame contract and restores the native snap preview. ## Testing - Built on Windows/MSVC with 8-way parallel build: `powershell -NoProfile -ExecutionPolicy Bypass -Command ". 'C:\Users\patj\Documents\AetherSDR\scripts\enter-msvc.ps1' -Arch x64; cmake --build build-msvc --config RelWithDebInfo --parallel 8"` - Ran `windeployqt` after build: `C:\Users\patj\Documents\AetherSDR\Qt\6.8.3\msvc2022_64\bin\windeployqt.exe --release --compiler-runtime C:\Users\patj\Documents\AetherSDR\upstream\AetherSDR-windows-window-snap\build-msvc\AetherSDR.exe` - Manual Windows verification: dragging the center handle in the frameless title bar now shows the Windows Snap UI and snaps to top/left/right. `windeployqt` emitted the existing local warnings about missing `dxcompiler.dll`/`dxil.dll` and unset `VCINSTALLDIR`; deployment otherwise completed.
Summary
Qt::FramelessWindowHint/WS_POPUPpath for the main window while frameless mode is enabled; instead hide the native frame withWM_NCCALCSIZE.HTCAPTION, resize edges/corners) for the custom title bar and window edges so the center title-bar handle invokes the OS move/snap UI.Why
Qt 6.8.3 creates
Qt::FramelessWindowHinttop-level windows asWS_POPUPand its Windows platform hit-test path skips frameless windows. That prevents Windows from taking over the drag operation for Snap Assist even when the app returnsHTCAPTION. Keeping native frame style bits and hiding the frame throughWM_NCCALCSIZEmatches the Windows custom-frame contract and restores the native snap preview.Testing
powershell -NoProfile -ExecutionPolicy Bypass -Command ". 'C:\Users\patj\Documents\AetherSDR\scripts\enter-msvc.ps1' -Arch x64; cmake --build build-msvc --config RelWithDebInfo --parallel 8"windeployqtafter build:C:\Users\patj\Documents\AetherSDR\Qt\6.8.3\msvc2022_64\bin\windeployqt.exe --release --compiler-runtime C:\Users\patj\Documents\AetherSDR\upstream\AetherSDR-windows-window-snap\build-msvc\AetherSDR.exewindeployqtemitted the existing local warnings about missingdxcompiler.dll/dxil.dlland unsetVCINSTALLDIR; deployment otherwise completed.