CmdPal: Fix window centering when moving to a display with different DPI (#45057)

## Summary of the Pull Request

This PR fixes centering of main window, when the window also moves
between move display with a different DPI.

- The centered position was calculated using the current window width
and height, but those values change after the window is moved to
accommodate the new display’s DPI.
- Calculations have been refactored out of main window to a helper
class.

<!-- Please review the items on the PR checklist before submitting-->
## PR Checklist

- [x] Closes: #44932
<!-- - [ ] Closes: #yyy (add separate lines for additional resolved
issues) -->
- [ ] **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

<!-- Provide a more detailed description of the PR, other things fixed,
or any additional comments/features here -->
## Detailed Description of the Pull Request / Additional comments

<!-- Describe how you validated the behavior. Add automated tests
wherever possible, but list manual validation steps taken as well -->
## Validation Steps Performed
This commit is contained in:
Jiří Polášek
2026-01-29 04:21:11 +01:00
committed by GitHub
parent 989e005500
commit 64cabc8789
2 changed files with 177 additions and 139 deletions

View File

@@ -21,7 +21,6 @@ using Microsoft.CmdPal.UI.ViewModels.Messages;
using Microsoft.CmdPal.UI.ViewModels.Services;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.PowerToys.Telemetry;
using Microsoft.UI;
using Microsoft.UI.Composition;
using Microsoft.UI.Composition.SystemBackdrops;
using Microsoft.UI.Input;
@@ -32,13 +31,9 @@ using Windows.ApplicationModel.Activation;
using Windows.Foundation;
using Windows.Graphics;
using Windows.System;
using Windows.UI;
using Windows.UI.WindowManagement;
using Windows.Win32;
using Windows.Win32.Foundation;
using Windows.Win32.Graphics.Dwm;
using Windows.Win32.Graphics.Gdi;
using Windows.Win32.UI.HiDpi;
using Windows.Win32.UI.Input.KeyboardAndMouse;
using Windows.Win32.UI.WindowsAndMessaging;
using WinRT;
@@ -60,9 +55,6 @@ public sealed partial class MainWindow : WindowEx,
IRecipient<DragCompletedMessage>,
IDisposable
{
private const int DefaultWidth = 800;
private const int DefaultHeight = 480;
[System.Diagnostics.CodeAnalysis.SuppressMessage("StyleCop.CSharp.NamingRules", "SA1310:Field names should not contain underscore", Justification = "Stylistically, window messages are WM_")]
[System.Diagnostics.CodeAnalysis.SuppressMessage("StyleCop.CSharp.NamingRules", "SA1306:Field names should begin with lower-case letter", Justification = "Stylistically, window messages are WM_")]
private readonly uint WM_TASKBAR_RESTART;
@@ -226,39 +218,40 @@ public sealed partial class MainWindow : WindowEx,
PositionCentered(displayArea);
}
private void PositionCentered(DisplayArea displayArea)
{
var position = WindowPositionHelper.CalculateCenteredPosition(
displayArea,
AppWindow.Size,
(int)this.GetDpiForWindow());
if (position is not null)
{
// Use Move(), not MoveAndResize(). Windows auto-resizes on DPI change via WM_DPICHANGED;
// the helper already accounts for this when calculating the centered position.
AppWindow.Move((PointInt32)position);
}
}
private void RestoreWindowPosition()
{
var settings = App.Current.Services.GetService<SettingsModel>();
if (settings?.LastWindowPosition is not WindowPosition savedPosition)
if (settings?.LastWindowPosition is not { Width: > 0, Height: > 0 } savedPosition)
{
PositionCentered();
return;
}
if (savedPosition.Width <= 0 || savedPosition.Height <= 0)
{
PositionCentered();
return;
}
// MoveAndResize is safe here—we're restoring a saved state at startup,
// not moving a live window between displays.
var newRect = WindowPositionHelper.AdjustRectForVisibility(
savedPosition.ToPhysicalWindowRectangle(),
new SizeInt32(savedPosition.ScreenWidth, savedPosition.ScreenHeight),
savedPosition.Dpi);
var newRect = EnsureWindowIsVisible(savedPosition.ToPhysicalWindowRectangle(), new SizeInt32(savedPosition.ScreenWidth, savedPosition.ScreenHeight), savedPosition.Dpi);
AppWindow.MoveAndResize(newRect);
}
private void PositionCentered(DisplayArea displayArea)
{
if (displayArea is not null)
{
var centeredPosition = AppWindow.Position;
centeredPosition.X = (displayArea.WorkArea.Width - AppWindow.Size.Width) / 2;
centeredPosition.Y = (displayArea.WorkArea.Height - AppWindow.Size.Height) / 2;
centeredPosition.X += displayArea.WorkArea.X;
centeredPosition.Y += displayArea.WorkArea.Y;
AppWindow.Move(centeredPosition);
}
}
private void UpdateWindowPositionInMemory()
{
var displayArea = DisplayArea.GetFromWindowId(AppWindow.Id, DisplayAreaFallback.Nearest) ?? DisplayArea.Primary;
@@ -352,7 +345,8 @@ public sealed partial class MainWindow : WindowEx,
if (target == MonitorBehavior.ToLast)
{
var newRect = EnsureWindowIsVisible(_currentWindowPosition.ToPhysicalWindowRectangle(), new SizeInt32(_currentWindowPosition.ScreenWidth, _currentWindowPosition.ScreenHeight), _currentWindowPosition.Dpi);
var originalScreen = new SizeInt32(_currentWindowPosition.ScreenWidth, _currentWindowPosition.ScreenHeight);
var newRect = WindowPositionHelper.AdjustRectForVisibility(_currentWindowPosition.ToPhysicalWindowRectangle(), originalScreen, _currentWindowPosition.Dpi);
AppWindow.MoveAndResize(newRect);
}
else
@@ -382,115 +376,7 @@ public sealed partial class MainWindow : WindowEx,
PInvoke.SetWindowPos(hwnd, HWND.HWND_TOPMOST, 0, 0, 0, 0, SET_WINDOW_POS_FLAGS.SWP_NOMOVE | SET_WINDOW_POS_FLAGS.SWP_NOSIZE);
}
/// <summary>
/// Ensures that the window rectangle is visible on-screen.
/// </summary>
/// <param name="windowRect">The window rectangle in physical pixels.</param>
/// <param name="originalScreen">The desktop area the window was positioned on.</param>
/// <param name="originalDpi">The window's original DPI.</param>
/// <returns>
/// A window rectangle in physical pixels, moved to the nearest display and resized
/// if the DPI has changed.
/// </returns>
private static RectInt32 EnsureWindowIsVisible(RectInt32 windowRect, SizeInt32 originalScreen, int originalDpi)
{
var displayArea = DisplayArea.GetFromRect(windowRect, DisplayAreaFallback.Nearest);
if (displayArea is null)
{
return windowRect;
}
var workArea = displayArea.WorkArea;
if (workArea.Width <= 0 || workArea.Height <= 0)
{
// Fallback, nothing reasonable to do
return windowRect;
}
var effectiveDpi = GetEffectiveDpiFromDisplayId(displayArea);
if (originalDpi <= 0)
{
originalDpi = effectiveDpi; // use current DPI as baseline (no scaling adjustment needed)
}
var hasInvalidSize = windowRect.Width <= 0 || windowRect.Height <= 0;
if (hasInvalidSize)
{
windowRect = new RectInt32(windowRect.X, windowRect.Y, DefaultWidth, DefaultHeight);
}
// If we have a DPI change, scale the window rectangle accordingly
if (effectiveDpi != originalDpi)
{
var scalingFactor = effectiveDpi / (double)originalDpi;
windowRect = new RectInt32(
(int)Math.Round(windowRect.X * scalingFactor),
(int)Math.Round(windowRect.Y * scalingFactor),
(int)Math.Round(windowRect.Width * scalingFactor),
(int)Math.Round(windowRect.Height * scalingFactor));
}
var targetWidth = Math.Min(windowRect.Width, workArea.Width);
var targetHeight = Math.Min(windowRect.Height, workArea.Height);
// Ensure at least some minimum visible area (e.g., 100 pixels)
// This helps prevent the window from being entirely offscreen, regardless of display scaling.
const int minimumVisibleSize = 100;
var isOffscreen =
windowRect.X + minimumVisibleSize > workArea.X + workArea.Width ||
windowRect.X + windowRect.Width - minimumVisibleSize < workArea.X ||
windowRect.Y + minimumVisibleSize > workArea.Y + workArea.Height ||
windowRect.Y + windowRect.Height - minimumVisibleSize < workArea.Y;
// if the work area size has changed, re-center the window
var workAreaSizeChanged =
originalScreen.Width != workArea.Width ||
originalScreen.Height != workArea.Height;
int targetX;
int targetY;
var recenter = isOffscreen || workAreaSizeChanged || hasInvalidSize;
if (recenter)
{
targetX = workArea.X + ((workArea.Width - targetWidth) / 2);
targetY = workArea.Y + ((workArea.Height - targetHeight) / 2);
}
else
{
targetX = windowRect.X;
targetY = windowRect.Y;
}
return new RectInt32(targetX, targetY, targetWidth, targetHeight);
}
private static int GetEffectiveDpiFromDisplayId(DisplayArea displayArea)
{
var effectiveDpi = 96;
var hMonitor = (HMONITOR)Win32Interop.GetMonitorFromDisplayId(displayArea.DisplayId);
if (!hMonitor.IsNull)
{
var hr = PInvoke.GetDpiForMonitor(hMonitor, MONITOR_DPI_TYPE.MDT_EFFECTIVE_DPI, out var dpiX, out _);
if (hr == 0)
{
effectiveDpi = (int)dpiX;
}
else
{
Logger.LogWarning($"GetDpiForMonitor failed with HRESULT: 0x{hr.Value:X8} on display {displayArea.DisplayId}");
}
}
if (effectiveDpi <= 0)
{
effectiveDpi = 96;
}
return effectiveDpi;
}
private DisplayArea GetScreen(HWND currentHwnd, MonitorBehavior target)
private static DisplayArea GetScreen(HWND currentHwnd, MonitorBehavior target)
{
// Leaving a note here, in case we ever need it:
// https://github.com/microsoft/microsoft-ui-xaml/issues/6454