mirror of
https://github.com/microsoft/PowerToys.git
synced 2026-04-03 09:46:54 +02:00
CmdPal: Fixes and improve main window positioning (#45585)
## Summary of the Pull Request This PR improves main window positioning: - Fixes cases where an invalid window size or position was saved. - `UpdateWindowPositionInMemory` failed to capture correct values when the window was minimized or maximized (for example, a minimized window reports coordinates like `(-32000, -32000)`). - Improves repositioning logic to use relative anchors (corners and center). When switching displays, the window should reappear in the expected position. This also reduces cases that trigger the failsafe recentering. - Fixes the dragging rectangle size after switching DPIs - the rectangle was not adapting, so it when switching from 100 % to 200 % it covered only left half of the window and had teeny-tiny height. - Suppresses system DPI handling during summon to prevent double scaling. - Makes `WindowPosition` class immutable. - Adds light-weight failsafe preventing overwriting position with invalid data. - Hotfixes a min/max state conflict with the WinUIEx window manager. <!-- Please review the items on the PR checklist before submitting--> ## PR Checklist - [x] Closes: #45576 <!-- - [ ] 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:
@@ -11,37 +11,42 @@ public sealed class WindowPosition
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets left position in device pixels.
|
/// Gets or sets left position in device pixels.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public int X { get; set; }
|
public int X { get; init; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets top position in device pixels.
|
/// Gets or sets top position in device pixels.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public int Y { get; set; }
|
public int Y { get; init; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets width in device pixels.
|
/// Gets or sets width in device pixels.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public int Width { get; set; }
|
public int Width { get; init; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets height in device pixels.
|
/// Gets or sets height in device pixels.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public int Height { get; set; }
|
public int Height { get; init; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets width of the screen in device pixels where the window is located.
|
/// Gets or sets width of the screen in device pixels where the window is located.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public int ScreenWidth { get; set; }
|
public int ScreenWidth { get; init; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets height of the screen in device pixels where the window is located.
|
/// Gets or sets height of the screen in device pixels where the window is located.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public int ScreenHeight { get; set; }
|
public int ScreenHeight { get; init; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets DPI (dots per inch) of the display where the window is located.
|
/// Gets or sets DPI (dots per inch) of the display where the window is located.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public int Dpi { get; set; }
|
public int Dpi { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets a value indicating whether the width and height of the window are valid (greater than 0).
|
||||||
|
/// </summary>
|
||||||
|
public bool IsSizeValid => Width > 0 && Height > 0;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Converts the window position properties to a <see cref="RectInt32"/> structure representing the physical window rectangle.
|
/// Converts the window position properties to a <see cref="RectInt32"/> structure representing the physical window rectangle.
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ internal static class WindowPositionHelper
|
|||||||
private const int MinimumVisibleSize = 100;
|
private const int MinimumVisibleSize = 100;
|
||||||
private const int DefaultDpi = 96;
|
private const int DefaultDpi = 96;
|
||||||
|
|
||||||
public static PointInt32? CalculateCenteredPosition(DisplayArea? displayArea, SizeInt32 windowSize, int windowDpi)
|
public static RectInt32? CenterOnDisplay(DisplayArea? displayArea, SizeInt32 windowSize, int windowDpi)
|
||||||
{
|
{
|
||||||
if (displayArea is null)
|
if (displayArea is null)
|
||||||
{
|
{
|
||||||
@@ -32,15 +32,9 @@ internal static class WindowPositionHelper
|
|||||||
}
|
}
|
||||||
|
|
||||||
var targetDpi = GetDpiForDisplay(displayArea);
|
var targetDpi = GetDpiForDisplay(displayArea);
|
||||||
var predictedSize = ScaleSize(windowSize, windowDpi, targetDpi);
|
var scaledSize = ScaleSize(windowSize, windowDpi, targetDpi);
|
||||||
|
var clampedSize = ClampSize(scaledSize.Width, scaledSize.Height, workArea);
|
||||||
// Clamp to work area
|
return CenterRectInWorkArea(clampedSize, workArea);
|
||||||
var width = Math.Min(predictedSize.Width, workArea.Width);
|
|
||||||
var height = Math.Min(predictedSize.Height, workArea.Height);
|
|
||||||
|
|
||||||
return new PointInt32(
|
|
||||||
workArea.X + ((workArea.Width - width) / 2),
|
|
||||||
workArea.Y + ((workArea.Height - height) / 2));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -74,6 +68,10 @@ internal static class WindowPositionHelper
|
|||||||
savedRect = savedRect with { Width = DefaultWidth, Height = DefaultHeight };
|
savedRect = savedRect with { Width = DefaultWidth, Height = DefaultHeight };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Remember the original size before DPI scaling - needed to compute
|
||||||
|
// gaps relative to the old screen when repositioning across displays.
|
||||||
|
var originalSize = new SizeInt32(savedRect.Width, savedRect.Height);
|
||||||
|
|
||||||
if (targetDpi != savedDpi)
|
if (targetDpi != savedDpi)
|
||||||
{
|
{
|
||||||
savedRect = ScaleRect(savedRect, savedDpi, targetDpi);
|
savedRect = ScaleRect(savedRect, savedDpi, targetDpi);
|
||||||
@@ -81,12 +79,17 @@ internal static class WindowPositionHelper
|
|||||||
|
|
||||||
var clampedSize = ClampSize(savedRect.Width, savedRect.Height, workArea);
|
var clampedSize = ClampSize(savedRect.Width, savedRect.Height, workArea);
|
||||||
|
|
||||||
var shouldRecenter = hasInvalidSize ||
|
if (hasInvalidSize)
|
||||||
IsOffscreen(savedRect, workArea) ||
|
{
|
||||||
savedScreenSize.Width != workArea.Width ||
|
return CenterRectInWorkArea(clampedSize, workArea);
|
||||||
savedScreenSize.Height != workArea.Height;
|
}
|
||||||
|
|
||||||
if (shouldRecenter)
|
if (savedScreenSize.Width != workArea.Width || savedScreenSize.Height != workArea.Height)
|
||||||
|
{
|
||||||
|
return RepositionRelativeToWorkArea(savedRect, savedScreenSize, originalSize, clampedSize, workArea);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (IsOffscreen(savedRect, workArea))
|
||||||
{
|
{
|
||||||
return CenterRectInWorkArea(clampedSize, workArea);
|
return CenterRectInWorkArea(clampedSize, workArea);
|
||||||
}
|
}
|
||||||
@@ -126,27 +129,92 @@ internal static class WindowPositionHelper
|
|||||||
|
|
||||||
private static RectInt32 ScaleRect(RectInt32 rect, int fromDpi, int toDpi)
|
private static RectInt32 ScaleRect(RectInt32 rect, int fromDpi, int toDpi)
|
||||||
{
|
{
|
||||||
|
if (fromDpi <= 0 || toDpi <= 0 || fromDpi == toDpi)
|
||||||
|
{
|
||||||
|
return rect;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Don't scale position, that's absolute coordinates in virtual screen space
|
||||||
var scale = (double)toDpi / fromDpi;
|
var scale = (double)toDpi / fromDpi;
|
||||||
return new RectInt32(
|
return new RectInt32(
|
||||||
(int)Math.Round(rect.X * scale),
|
rect.X,
|
||||||
(int)Math.Round(rect.Y * scale),
|
rect.Y,
|
||||||
(int)Math.Round(rect.Width * scale),
|
(int)Math.Round(rect.Width * scale),
|
||||||
(int)Math.Round(rect.Height * scale));
|
(int)Math.Round(rect.Height * scale));
|
||||||
}
|
}
|
||||||
|
|
||||||
private static SizeInt32 ClampSize(int width, int height, RectInt32 workArea) =>
|
private static SizeInt32 ClampSize(int width, int height, RectInt32 workArea)
|
||||||
new(Math.Min(width, workArea.Width), Math.Min(height, workArea.Height));
|
{
|
||||||
|
return new SizeInt32(Math.Min(width, workArea.Width), Math.Min(height, workArea.Height));
|
||||||
|
}
|
||||||
|
|
||||||
private static RectInt32 CenterRectInWorkArea(SizeInt32 size, RectInt32 workArea) =>
|
private static RectInt32 RepositionRelativeToWorkArea(RectInt32 savedRect, SizeInt32 savedScreenSize, SizeInt32 originalSize, SizeInt32 clampedSize, RectInt32 workArea)
|
||||||
new(
|
{
|
||||||
|
// Treat each axis as a 3-zone grid (start / center / end) so that
|
||||||
|
// edge-snapped windows stay snapped and centered windows stay centered.
|
||||||
|
// We don't store the old work area origin, so we use the current one as a
|
||||||
|
// best estimate (correct when the same physical display changed resolution/DPI/taskbar).
|
||||||
|
var newX = ScaleAxisByZone(savedRect.X, originalSize.Width, clampedSize.Width, workArea.X, savedScreenSize.Width, workArea.Width);
|
||||||
|
var newY = ScaleAxisByZone(savedRect.Y, originalSize.Height, clampedSize.Height, workArea.Y, savedScreenSize.Height, workArea.Height);
|
||||||
|
|
||||||
|
newX = Math.Clamp(newX, workArea.X, Math.Max(workArea.X, workArea.X + workArea.Width - clampedSize.Width));
|
||||||
|
newY = Math.Clamp(newY, workArea.Y, Math.Max(workArea.Y, workArea.Y + workArea.Height - clampedSize.Height));
|
||||||
|
|
||||||
|
return new RectInt32(newX, newY, clampedSize.Width, clampedSize.Height);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Repositions a window along one axis using a 3-zone model (start / center / end).
|
||||||
|
/// The zone is determined by which third of the old screen the window center falls in.
|
||||||
|
/// Uses <paramref name="oldWindowSize"/> (pre-DPI-scaling) for gap calculations against
|
||||||
|
/// the old screen, and <paramref name="newWindowSize"/> (post-scaling) for placement on the new screen.
|
||||||
|
/// </summary>
|
||||||
|
private static int ScaleAxisByZone(int savedPos, int oldWindowSize, int newWindowSize, int workAreaOrigin, int oldScreenSize, int newScreenSize)
|
||||||
|
{
|
||||||
|
if (oldScreenSize <= 0 || newScreenSize <= 0)
|
||||||
|
{
|
||||||
|
return savedPos;
|
||||||
|
}
|
||||||
|
|
||||||
|
var gapFromStart = savedPos - workAreaOrigin;
|
||||||
|
var windowCenter = gapFromStart + (oldWindowSize / 2);
|
||||||
|
|
||||||
|
if (windowCenter >= oldScreenSize / 3 && windowCenter <= oldScreenSize * 2 / 3)
|
||||||
|
{
|
||||||
|
// Center zone - keep centered
|
||||||
|
return workAreaOrigin + ((newScreenSize - newWindowSize) / 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
var gapFromEnd = oldScreenSize - gapFromStart - oldWindowSize;
|
||||||
|
|
||||||
|
if (gapFromStart <= gapFromEnd)
|
||||||
|
{
|
||||||
|
// Start zone - preserve proportional distance from start edge
|
||||||
|
var rel = (double)gapFromStart / oldScreenSize;
|
||||||
|
return workAreaOrigin + (int)Math.Round(rel * newScreenSize);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// End zone - preserve proportional distance from end edge
|
||||||
|
var rel = (double)gapFromEnd / oldScreenSize;
|
||||||
|
return workAreaOrigin + newScreenSize - newWindowSize - (int)Math.Round(rel * newScreenSize);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static RectInt32 CenterRectInWorkArea(SizeInt32 size, RectInt32 workArea)
|
||||||
|
{
|
||||||
|
return new RectInt32(
|
||||||
workArea.X + ((workArea.Width - size.Width) / 2),
|
workArea.X + ((workArea.Width - size.Width) / 2),
|
||||||
workArea.Y + ((workArea.Height - size.Height) / 2),
|
workArea.Y + ((workArea.Height - size.Height) / 2),
|
||||||
size.Width,
|
size.Width,
|
||||||
size.Height);
|
size.Height);
|
||||||
|
}
|
||||||
|
|
||||||
private static bool IsOffscreen(RectInt32 rect, RectInt32 workArea) =>
|
private static bool IsOffscreen(RectInt32 rect, RectInt32 workArea)
|
||||||
rect.X + MinimumVisibleSize > workArea.X + workArea.Width ||
|
{
|
||||||
rect.X + rect.Width - MinimumVisibleSize < workArea.X ||
|
return rect.X + MinimumVisibleSize > workArea.X + workArea.Width ||
|
||||||
rect.Y + MinimumVisibleSize > workArea.Y + workArea.Height ||
|
rect.X + rect.Width - MinimumVisibleSize < workArea.X ||
|
||||||
rect.Y + rect.Height - MinimumVisibleSize < workArea.Y;
|
rect.Y + MinimumVisibleSize > workArea.Y + workArea.Height ||
|
||||||
|
rect.Y + rect.Height - MinimumVisibleSize < workArea.Y;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -72,6 +72,7 @@ public sealed partial class MainWindow : WindowEx,
|
|||||||
private readonly IThemeService _themeService;
|
private readonly IThemeService _themeService;
|
||||||
private readonly WindowThemeSynchronizer _windowThemeSynchronizer;
|
private readonly WindowThemeSynchronizer _windowThemeSynchronizer;
|
||||||
private bool _ignoreHotKeyWhenFullScreen = true;
|
private bool _ignoreHotKeyWhenFullScreen = true;
|
||||||
|
private bool _suppressDpiChange;
|
||||||
private bool _themeServiceInitialized;
|
private bool _themeServiceInitialized;
|
||||||
|
|
||||||
// Session tracking for telemetry
|
// Session tracking for telemetry
|
||||||
@@ -127,6 +128,16 @@ public sealed partial class MainWindow : WindowEx,
|
|||||||
|
|
||||||
_keyboardListener.SetProcessCommand(new CmdPalKeyboardService.ProcessCommand(HandleSummon));
|
_keyboardListener.SetProcessCommand(new CmdPalKeyboardService.ProcessCommand(HandleSummon));
|
||||||
|
|
||||||
|
WM_TASKBAR_RESTART = PInvoke.RegisterWindowMessage("TaskbarCreated");
|
||||||
|
|
||||||
|
// LOAD BEARING: If you don't stick the pointer to HotKeyPrc into a
|
||||||
|
// member (and instead like, use a local), then the pointer we marshal
|
||||||
|
// into the WindowLongPtr will be useless after we leave this function,
|
||||||
|
// and our **WindProc will explode**.
|
||||||
|
_hotkeyWndProc = HotKeyPrc;
|
||||||
|
var hotKeyPrcPointer = Marshal.GetFunctionPointerForDelegate(_hotkeyWndProc);
|
||||||
|
_originalWndProc = Marshal.GetDelegateForFunctionPointer<WNDPROC>(PInvoke.SetWindowLongPtr(_hwnd, WINDOW_LONG_PTR_INDEX.GWL_WNDPROC, hotKeyPrcPointer));
|
||||||
|
|
||||||
this.SetIcon();
|
this.SetIcon();
|
||||||
AppWindow.Title = RS_.GetString("AppName");
|
AppWindow.Title = RS_.GetString("AppName");
|
||||||
RestoreWindowPosition();
|
RestoreWindowPosition();
|
||||||
@@ -153,16 +164,6 @@ public sealed partial class MainWindow : WindowEx,
|
|||||||
SizeChanged += WindowSizeChanged;
|
SizeChanged += WindowSizeChanged;
|
||||||
RootElement.Loaded += RootElementLoaded;
|
RootElement.Loaded += RootElementLoaded;
|
||||||
|
|
||||||
WM_TASKBAR_RESTART = PInvoke.RegisterWindowMessage("TaskbarCreated");
|
|
||||||
|
|
||||||
// LOAD BEARING: If you don't stick the pointer to HotKeyPrc into a
|
|
||||||
// member (and instead like, use a local), then the pointer we marshal
|
|
||||||
// into the WindowLongPtr will be useless after we leave this function,
|
|
||||||
// and our **WindProc will explode**.
|
|
||||||
_hotkeyWndProc = HotKeyPrc;
|
|
||||||
var hotKeyPrcPointer = Marshal.GetFunctionPointerForDelegate(_hotkeyWndProc);
|
|
||||||
_originalWndProc = Marshal.GetDelegateForFunctionPointer<WNDPROC>(PInvoke.SetWindowLongPtr(_hwnd, WINDOW_LONG_PTR_INDEX.GWL_WNDPROC, hotKeyPrcPointer));
|
|
||||||
|
|
||||||
// Load our settings, and then also wire up a settings changed handler
|
// Load our settings, and then also wire up a settings changed handler
|
||||||
HotReloadSettings();
|
HotReloadSettings();
|
||||||
App.Current.Services.GetService<SettingsModel>()!.SettingsChanged += SettingsChangedHandler;
|
App.Current.Services.GetService<SettingsModel>()!.SettingsChanged += SettingsChangedHandler;
|
||||||
@@ -213,6 +214,11 @@ public sealed partial class MainWindow : WindowEx,
|
|||||||
// Now that our content has loaded, we can update our draggable regions
|
// Now that our content has loaded, we can update our draggable regions
|
||||||
UpdateRegionsForCustomTitleBar();
|
UpdateRegionsForCustomTitleBar();
|
||||||
|
|
||||||
|
// Also update regions when DPI changes. SizeChanged only fires when the logical
|
||||||
|
// (DIP) size changes — a DPI change that scales the physical size while preserving
|
||||||
|
// the DIP size won't trigger it, leaving drag regions at the old physical coordinates.
|
||||||
|
RootElement.XamlRoot.Changed += XamlRoot_Changed;
|
||||||
|
|
||||||
// Add dev ribbon if enabled
|
// Add dev ribbon if enabled
|
||||||
if (!BuildInfo.IsCiBuild)
|
if (!BuildInfo.IsCiBuild)
|
||||||
{
|
{
|
||||||
@@ -221,6 +227,8 @@ public sealed partial class MainWindow : WindowEx,
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void XamlRoot_Changed(XamlRoot sender, XamlRootChangedEventArgs args) => UpdateRegionsForCustomTitleBar();
|
||||||
|
|
||||||
private void WindowSizeChanged(object sender, WindowSizeChangedEventArgs args) => UpdateRegionsForCustomTitleBar();
|
private void WindowSizeChanged(object sender, WindowSizeChangedEventArgs args) => UpdateRegionsForCustomTitleBar();
|
||||||
|
|
||||||
private void PositionCentered()
|
private void PositionCentered()
|
||||||
@@ -231,16 +239,14 @@ public sealed partial class MainWindow : WindowEx,
|
|||||||
|
|
||||||
private void PositionCentered(DisplayArea displayArea)
|
private void PositionCentered(DisplayArea displayArea)
|
||||||
{
|
{
|
||||||
var position = WindowPositionHelper.CalculateCenteredPosition(
|
var rect = WindowPositionHelper.CenterOnDisplay(
|
||||||
displayArea,
|
displayArea,
|
||||||
AppWindow.Size,
|
AppWindow.Size,
|
||||||
(int)this.GetDpiForWindow());
|
(int)this.GetDpiForWindow());
|
||||||
|
|
||||||
if (position is not null)
|
if (rect is not null)
|
||||||
{
|
{
|
||||||
// Use Move(), not MoveAndResize(). Windows auto-resizes on DPI change via WM_DPICHANGED;
|
MoveAndResizeDpiAware(rect.Value);
|
||||||
// the helper already accounts for this when calculating the centered position.
|
|
||||||
AppWindow.Move((PointInt32)position);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -249,29 +255,62 @@ public sealed partial class MainWindow : WindowEx,
|
|||||||
var settings = App.Current.Services.GetService<SettingsModel>();
|
var settings = App.Current.Services.GetService<SettingsModel>();
|
||||||
if (settings?.LastWindowPosition is not { Width: > 0, Height: > 0 } savedPosition)
|
if (settings?.LastWindowPosition is not { Width: > 0, Height: > 0 } savedPosition)
|
||||||
{
|
{
|
||||||
|
// don't try to restore if the saved position is invalid, just recenter
|
||||||
PositionCentered();
|
PositionCentered();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// MoveAndResize is safe here—we're restoring a saved state at startup,
|
|
||||||
// not moving a live window between displays.
|
|
||||||
var newRect = WindowPositionHelper.AdjustRectForVisibility(
|
var newRect = WindowPositionHelper.AdjustRectForVisibility(
|
||||||
savedPosition.ToPhysicalWindowRectangle(),
|
savedPosition.ToPhysicalWindowRectangle(),
|
||||||
new SizeInt32(savedPosition.ScreenWidth, savedPosition.ScreenHeight),
|
new SizeInt32(savedPosition.ScreenWidth, savedPosition.ScreenHeight),
|
||||||
savedPosition.Dpi);
|
savedPosition.Dpi);
|
||||||
|
|
||||||
AppWindow.MoveAndResize(newRect);
|
MoveAndResizeDpiAware(newRect);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Moves and resizes the window while suppressing WM_DPICHANGED.
|
||||||
|
/// The caller is expected to provide a rect already scaled for the target display's DPI.
|
||||||
|
/// Without suppression, the framework would apply its own DPI scaling on top, double-scaling the window.
|
||||||
|
/// </summary>
|
||||||
|
private void MoveAndResizeDpiAware(RectInt32 rect)
|
||||||
|
{
|
||||||
|
var originalMinHeight = MinHeight;
|
||||||
|
var originalMinWidth = MinWidth;
|
||||||
|
|
||||||
|
_suppressDpiChange = true;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// WindowEx is uses current DPI to calculate the minimum window size
|
||||||
|
MinHeight = 0;
|
||||||
|
MinWidth = 0;
|
||||||
|
AppWindow.MoveAndResize(rect);
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
MinHeight = originalMinHeight;
|
||||||
|
MinWidth = originalMinWidth;
|
||||||
|
_suppressDpiChange = false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void UpdateWindowPositionInMemory()
|
private void UpdateWindowPositionInMemory()
|
||||||
{
|
{
|
||||||
|
var placement = new WINDOWPLACEMENT { length = (uint)Marshal.SizeOf<WINDOWPLACEMENT>() };
|
||||||
|
if (!PInvoke.GetWindowPlacement(_hwnd, ref placement))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var rect = placement.rcNormalPosition;
|
||||||
var displayArea = DisplayArea.GetFromWindowId(AppWindow.Id, DisplayAreaFallback.Nearest) ?? DisplayArea.Primary;
|
var displayArea = DisplayArea.GetFromWindowId(AppWindow.Id, DisplayAreaFallback.Nearest) ?? DisplayArea.Primary;
|
||||||
_currentWindowPosition = new WindowPosition
|
_currentWindowPosition = new WindowPosition
|
||||||
{
|
{
|
||||||
X = AppWindow.Position.X,
|
X = rect.X,
|
||||||
Y = AppWindow.Position.Y,
|
Y = rect.Y,
|
||||||
Width = AppWindow.Size.Width,
|
Width = rect.Width,
|
||||||
Height = AppWindow.Size.Height,
|
Height = rect.Height,
|
||||||
Dpi = (int)this.GetDpiForWindow(),
|
Dpi = (int)this.GetDpiForWindow(),
|
||||||
ScreenWidth = displayArea.WorkArea.Width,
|
ScreenWidth = displayArea.WorkArea.Width,
|
||||||
ScreenHeight = displayArea.WorkArea.Height,
|
ScreenHeight = displayArea.WorkArea.Height,
|
||||||
@@ -480,7 +519,7 @@ public sealed partial class MainWindow : WindowEx,
|
|||||||
{
|
{
|
||||||
var originalScreen = new SizeInt32(_currentWindowPosition.ScreenWidth, _currentWindowPosition.ScreenHeight);
|
var originalScreen = new SizeInt32(_currentWindowPosition.ScreenWidth, _currentWindowPosition.ScreenHeight);
|
||||||
var newRect = WindowPositionHelper.AdjustRectForVisibility(_currentWindowPosition.ToPhysicalWindowRectangle(), originalScreen, _currentWindowPosition.Dpi);
|
var newRect = WindowPositionHelper.AdjustRectForVisibility(_currentWindowPosition.ToPhysicalWindowRectangle(), originalScreen, _currentWindowPosition.Dpi);
|
||||||
AppWindow.MoveAndResize(newRect);
|
MoveAndResizeDpiAware(newRect);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
@@ -737,18 +776,12 @@ public sealed partial class MainWindow : WindowEx,
|
|||||||
var settings = serviceProvider.GetService<SettingsModel>();
|
var settings = serviceProvider.GetService<SettingsModel>();
|
||||||
if (settings is not null)
|
if (settings is not null)
|
||||||
{
|
{
|
||||||
settings.LastWindowPosition = new WindowPosition
|
// a quick sanity check, so we don't overwrite correct values
|
||||||
|
if (_currentWindowPosition.IsSizeValid)
|
||||||
{
|
{
|
||||||
X = _currentWindowPosition.X,
|
settings.LastWindowPosition = _currentWindowPosition;
|
||||||
Y = _currentWindowPosition.Y,
|
SettingsModel.SaveSettings(settings);
|
||||||
Width = _currentWindowPosition.Width,
|
}
|
||||||
Height = _currentWindowPosition.Height,
|
|
||||||
Dpi = _currentWindowPosition.Dpi,
|
|
||||||
ScreenWidth = _currentWindowPosition.ScreenWidth,
|
|
||||||
ScreenHeight = _currentWindowPosition.ScreenHeight,
|
|
||||||
};
|
|
||||||
|
|
||||||
SettingsModel.SaveSettings(settings);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var extensionService = serviceProvider.GetService<IExtensionService>()!;
|
var extensionService = serviceProvider.GetService<IExtensionService>()!;
|
||||||
@@ -1108,6 +1141,13 @@ public sealed partial class MainWindow : WindowEx,
|
|||||||
// Prevent the window from maximizing when double-clicking the title bar area
|
// Prevent the window from maximizing when double-clicking the title bar area
|
||||||
case PInvoke.WM_NCLBUTTONDBLCLK:
|
case PInvoke.WM_NCLBUTTONDBLCLK:
|
||||||
return (LRESULT)IntPtr.Zero;
|
return (LRESULT)IntPtr.Zero;
|
||||||
|
|
||||||
|
// When restoring a saved position across monitors with different DPIs,
|
||||||
|
// MoveAndResize already sets the correctly-scaled size. Suppress the
|
||||||
|
// framework's automatic DPI resize to avoid double-scaling.
|
||||||
|
case PInvoke.WM_DPICHANGED when _suppressDpiChange:
|
||||||
|
return (LRESULT)IntPtr.Zero;
|
||||||
|
|
||||||
case PInvoke.WM_HOTKEY:
|
case PInvoke.WM_HOTKEY:
|
||||||
{
|
{
|
||||||
var hotkeyIndex = (int)wParam.Value;
|
var hotkeyIndex = (int)wParam.Value;
|
||||||
|
|||||||
@@ -66,4 +66,8 @@ GetStockObject
|
|||||||
GetModuleHandle
|
GetModuleHandle
|
||||||
|
|
||||||
GetWindowThreadProcessId
|
GetWindowThreadProcessId
|
||||||
AttachThreadInput
|
AttachThreadInput
|
||||||
|
|
||||||
|
GetWindowPlacement
|
||||||
|
WINDOWPLACEMENT
|
||||||
|
WM_DPICHANGED
|
||||||
Reference in New Issue
Block a user