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();