From 67518dd7547bd2fa28fdf41f6709446f15bebcee Mon Sep 17 00:00:00 2001 From: Kai Tao <69313318+vanzue@users.noreply.github.com> Date: Mon, 2 Feb 2026 09:34:50 +0800 Subject: [PATCH] Workspace: Fix an overlay issue for workspace snapshot draw (#45183) ## Summary of the Pull Request Root cause: Workspaces uses DPI-unaware coordinates (via GetDpiUnawareScreens() which runs in a temporary DPI-unaware thread) to store/match window positions across different DPI settings. However, WorkspacesEditor itself uses PerMonitorV2 DPI awareness for UI clarity. When assigning these DPI-unaware coordinates directly to WPF window properties, WPF automatically scaled them again based on current DPI, causing incorrect overlay positioning. Fix: Use SetWindowPositionDpiUnaware() to bypass WPF's automatic DPI scaling by temporarily switching to DPI-unaware context when calling Win32 SetWindowPos. Fix #45174 ## PR Checklist - [ ] Closes: #45174 - [ ] **Communication:** I've discussed this with core contributors already. If the work hasn't been agreed, this work might be rejected - [ ] **Tests:** Added/updated and all pass - [ ] **Localization:** All end-user-facing strings can be localized - [ ] **Dev docs:** Added/updated - [ ] **New binaries:** Added on the required places - [ ] [JSON for signing](https://github.com/microsoft/PowerToys/blob/main/.pipelines/ESRPSigning_core.json) for new binaries - [ ] [WXS for installer](https://github.com/microsoft/PowerToys/blob/main/installer/PowerToysSetup/Product.wxs) for new binaries and localization folder - [ ] [YML for CI pipeline](https://github.com/microsoft/PowerToys/blob/main/.pipelines/ci/templates/build-powertoys-steps.yml) for new test projects - [ ] [YML for signed pipeline](https://github.com/microsoft/PowerToys/blob/main/.pipelines/release.yml) - [ ] **Documentation updated:** If checked, please file a pull request on [our docs repo](https://github.com/MicrosoftDocs/windows-uwp/tree/docs/hub/powertoys) and link it here: #xxx ## Detailed Description of the Pull Request / Additional comments ## Validation Steps Performed Verified in local build vs production build, and the problem fixed in local build. --- .github/actions/spell-check/expect.txt | 1 + .../WorkspacesEditor/OverlayWindow.xaml.cs | 34 ++++++++++++++++++ .../WorkspacesEditor/Utils/NativeMethods.cs | 35 +++++++++++++++++++ .../ViewModels/MainViewModel.cs | 8 ++--- 4 files changed, 74 insertions(+), 4 deletions(-) diff --git a/.github/actions/spell-check/expect.txt b/.github/actions/spell-check/expect.txt index 58556bb989..2f1bcde7f8 100644 --- a/.github/actions/spell-check/expect.txt +++ b/.github/actions/spell-check/expect.txt @@ -1017,6 +1017,7 @@ MERGEPAINT Metacharacter metadatamatters Metadatas +Metacharacter metafile mfc Mgmt diff --git a/src/modules/Workspaces/WorkspacesEditor/OverlayWindow.xaml.cs b/src/modules/Workspaces/WorkspacesEditor/OverlayWindow.xaml.cs index 54e892d9bd..d1646e7282 100644 --- a/src/modules/Workspaces/WorkspacesEditor/OverlayWindow.xaml.cs +++ b/src/modules/Workspaces/WorkspacesEditor/OverlayWindow.xaml.cs @@ -2,8 +2,11 @@ // The Microsoft Corporation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. +using System; using System.Windows; +using WorkspacesEditor.Utils; + namespace WorkspacesEditor { /// @@ -11,9 +14,40 @@ namespace WorkspacesEditor /// public partial class OverlayWindow : Window { + private int _targetX; + private int _targetY; + private int _targetWidth; + private int _targetHeight; + public OverlayWindow() { InitializeComponent(); + SourceInitialized += OnWindowSourceInitialized; + } + + /// + /// Sets the target bounds for the overlay window. + /// The window will be positioned using DPI-unaware context after initialization. + /// + public void SetTargetBounds(int x, int y, int width, int height) + { + _targetX = x; + _targetY = y; + _targetWidth = width; + _targetHeight = height; + + // Set initial WPF properties (will be corrected after HWND creation) + Left = x; + Top = y; + Width = width; + Height = height; + } + + private void OnWindowSourceInitialized(object sender, EventArgs e) + { + // Reposition window using DPI-unaware context to match the virtual coordinates. + // This fixes overlay positioning on mixed-DPI multi-monitor setups. + NativeMethods.SetWindowPositionDpiUnaware(this, _targetX, _targetY, _targetWidth, _targetHeight); } } } diff --git a/src/modules/Workspaces/WorkspacesEditor/Utils/NativeMethods.cs b/src/modules/Workspaces/WorkspacesEditor/Utils/NativeMethods.cs index 4105cbe959..9687aeac63 100644 --- a/src/modules/Workspaces/WorkspacesEditor/Utils/NativeMethods.cs +++ b/src/modules/Workspaces/WorkspacesEditor/Utils/NativeMethods.cs @@ -4,6 +4,8 @@ using System; using System.Runtime.InteropServices; +using System.Windows; +using System.Windows.Interop; namespace WorkspacesEditor.Utils { @@ -17,6 +19,39 @@ namespace WorkspacesEditor.Utils [return: MarshalAs(UnmanagedType.Bool)] public static extern bool MoveWindow(IntPtr hWnd, int X, int Y, int nWidth, int nHeight, bool bRepaint); + [DllImport("user32.dll", SetLastError = true)] + private static extern bool SetWindowPos(IntPtr hWnd, IntPtr hWndInsertAfter, int X, int Y, int cx, int cy, uint uFlags); + + [DllImport("user32.dll")] + private static extern IntPtr SetThreadDpiAwarenessContext(IntPtr dpiContext); + + private const uint SWP_NOZORDER = 0x0004; + private const uint SWP_NOACTIVATE = 0x0010; + + private static readonly IntPtr DPI_AWARENESS_CONTEXT_UNAWARE = new IntPtr(-1); + + /// + /// Positions a WPF window using DPI-unaware context to match the virtual coordinates. + /// This fixes overlay positioning on mixed-DPI multi-monitor setups. + /// + public static void SetWindowPositionDpiUnaware(Window window, int x, int y, int width, int height) + { + var helper = new WindowInteropHelper(window).Handle; + if (helper != IntPtr.Zero) + { + // Temporarily switch to DPI-unaware context to position window. + IntPtr oldContext = SetThreadDpiAwarenessContext(DPI_AWARENESS_CONTEXT_UNAWARE); + try + { + SetWindowPos(helper, IntPtr.Zero, x, y, width, height, SWP_NOZORDER | SWP_NOACTIVATE); + } + finally + { + SetThreadDpiAwarenessContext(oldContext); + } + } + } + [DllImport("USER32.DLL")] public static extern bool SetForegroundWindow(IntPtr hWnd); diff --git a/src/modules/Workspaces/WorkspacesEditor/ViewModels/MainViewModel.cs b/src/modules/Workspaces/WorkspacesEditor/ViewModels/MainViewModel.cs index 9c76c26fa0..5741fd65ab 100644 --- a/src/modules/Workspaces/WorkspacesEditor/ViewModels/MainViewModel.cs +++ b/src/modules/Workspaces/WorkspacesEditor/ViewModels/MainViewModel.cs @@ -495,10 +495,10 @@ namespace WorkspacesEditor.ViewModels { var bounds = screen.Bounds; OverlayWindow overlayWindow = new OverlayWindow(); - overlayWindow.Top = bounds.Top; - overlayWindow.Left = bounds.Left; - overlayWindow.Width = bounds.Width; - overlayWindow.Height = bounds.Height; + + // Use DPI-unaware positioning to fix overlay on mixed-DPI multi-monitor setups + overlayWindow.SetTargetBounds(bounds.Left, bounds.Top, bounds.Width, bounds.Height); + overlayWindow.ShowActivated = true; overlayWindow.Topmost = true; overlayWindow.Show();