mirror of
https://github.com/microsoft/PowerToys.git
synced 2026-05-18 05:05:25 +02:00
<!-- Enter a brief description/summary of your PR here. What does it fix/what does it change/how was it tested (even manually, if necessary)? --> ## Summary of the Pull Request 1. Re-enable PowerDisplay for PowerToys. 2. Add PowerDisplay back into installer. 3. Use new PowerDisplay icon and logo. 4. Fix some DPI related issue. 5. UI/UX improvement. <!-- Please review the items on the PR checklist before submitting--> ## PR Checklist - [x] Closes: #1052 <!-- - [ ] Closes: #yyy (add separate lines for additional resolved issues) --> - [x] **Communication:** I've discussed this with core contributors already. If the work hasn't been agreed, this work might be rejected - [x] **Tests:** Added/updated and all pass - [x] **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 Pull new code from this branch. Set up PowerDisplay.UI as startup project. Click run in VS. Or, build whole solution, set up runner as startup project. Click run to test full experience. --------- Co-authored-by: Yu Leng (from Dev Box) <yuleng@microsoft.com> Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com> Co-authored-by: Niels Laute <niels.laute@live.nl>
260 lines
10 KiB
C#
260 lines
10 KiB
C#
// Copyright (c) Microsoft Corporation
|
|
// 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.Runtime.InteropServices;
|
|
using Microsoft.UI.Windowing;
|
|
using WinUIEx;
|
|
|
|
namespace PowerDisplay.Helpers
|
|
{
|
|
internal static partial class WindowHelper
|
|
{
|
|
// Cursor position structure for GetCursorPos
|
|
[StructLayout(LayoutKind.Sequential)]
|
|
private struct POINT
|
|
{
|
|
public int X;
|
|
public int Y;
|
|
}
|
|
|
|
// Cursor position for detecting the monitor with the mouse
|
|
[LibraryImport("user32.dll")]
|
|
[return: MarshalAs(UnmanagedType.Bool)]
|
|
private static partial bool GetCursorPos(out POINT lpPoint);
|
|
|
|
[LibraryImport("shcore.dll")]
|
|
private static partial int GetDpiForMonitor(nint hMonitor, uint dpiType, out uint dpiX, out uint dpiY);
|
|
|
|
// Window Styles
|
|
private const int GwlStyle = -16;
|
|
private const int WsCaption = 0x00C00000;
|
|
private const int WsThickframe = 0x00040000;
|
|
private const int WsMinimizebox = 0x00020000;
|
|
private const int WsMaximizebox = 0x00010000;
|
|
private const int WsSysmenu = 0x00080000;
|
|
|
|
// Extended Window Styles
|
|
private const int GwlExstyle = -20;
|
|
private const int WsExDlgmodalframe = 0x00000001;
|
|
private const int WsExWindowedge = 0x00000100;
|
|
private const int WsExClientedge = 0x00000200;
|
|
private const int WsExStaticedge = 0x00020000;
|
|
private const uint SwpNosize = 0x0001;
|
|
private const uint SwpNomove = 0x0002;
|
|
private const uint SwpFramechanged = 0x0020;
|
|
private const uint MdtEffectiveDpi = 0;
|
|
private const int DefaultDpi = 96;
|
|
|
|
// P/Invoke declarations (64-bit only - PowerToys only builds for x64/ARM64)
|
|
[LibraryImport("user32.dll", EntryPoint = "GetWindowLongPtrW")]
|
|
private static partial nint GetWindowLongPtr(nint hWnd, int nIndex);
|
|
|
|
[LibraryImport("user32.dll", EntryPoint = "SetWindowLongPtrW")]
|
|
private static partial nint SetWindowLong(nint hWnd, int nIndex, nint dwNewLong);
|
|
|
|
[LibraryImport("user32.dll")]
|
|
[return: MarshalAs(UnmanagedType.Bool)]
|
|
private static partial bool SetWindowPos(
|
|
nint hWnd,
|
|
nint hWndInsertAfter,
|
|
int x,
|
|
int y,
|
|
int cx,
|
|
int cy,
|
|
uint uFlags);
|
|
|
|
/// <summary>
|
|
/// Disable window moving and resizing functionality
|
|
/// </summary>
|
|
public static void DisableWindowMovingAndResizing(nint hWnd)
|
|
{
|
|
// Get current window style
|
|
nint style = GetWindowLongPtr(hWnd, GwlStyle);
|
|
|
|
// Remove resizable borders, title bar, and system menu
|
|
style &= ~WsThickframe;
|
|
style &= ~WsMaximizebox;
|
|
style &= ~WsMinimizebox;
|
|
style &= ~WsCaption; // Remove entire title bar
|
|
style &= ~WsSysmenu; // Remove system menu
|
|
|
|
// Set new window style
|
|
_ = SetWindowLong(hWnd, GwlStyle, style);
|
|
|
|
// Get extended style and remove related borders
|
|
nint exStyle = GetWindowLongPtr(hWnd, GwlExstyle);
|
|
exStyle &= ~WsExDlgmodalframe;
|
|
exStyle &= ~WsExWindowedge;
|
|
exStyle &= ~WsExClientedge;
|
|
exStyle &= ~WsExStaticedge;
|
|
_ = SetWindowLong(hWnd, GwlExstyle, exStyle);
|
|
|
|
// Refresh window frame
|
|
SetWindowPos(
|
|
hWnd,
|
|
0,
|
|
0,
|
|
0,
|
|
0,
|
|
0,
|
|
SwpNomove | SwpNosize | SwpFramechanged);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Get the DPI scale factor for a window (relative to standard 96 DPI)
|
|
/// </summary>
|
|
/// <param name="window">WinUIEx window</param>
|
|
/// <returns>DPI scale factor (1.0 = 100%, 1.25 = 125%, 1.5 = 150%, 2.0 = 200%)</returns>
|
|
public static double GetDpiScale(WindowEx window)
|
|
{
|
|
return (double)window.GetDpiForWindow() / DefaultDpi;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Get the DPI scale factor for a display area (relative to standard 96 DPI)
|
|
/// </summary>
|
|
/// <param name="displayArea">Target display area</param>
|
|
/// <returns>DPI scale factor (1.0 = 100%, 1.25 = 125%, 1.5 = 150%, 2.0 = 200%)</returns>
|
|
public static double GetDpiScale(DisplayArea displayArea)
|
|
{
|
|
return (double)GetEffectiveDpi(global::Microsoft.UI.Win32Interop.GetMonitorFromDisplayId(displayArea.DisplayId)) / DefaultDpi;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Convert device-independent pixels (DIP) to physical pixels.
|
|
/// </summary>
|
|
/// <param name="dip">Device-independent pixel value</param>
|
|
/// <param name="dpiScale">DPI scale factor</param>
|
|
/// <returns>Physical pixel value</returns>
|
|
public static int ScaleToPhysicalPixels(int dip, double dpiScale)
|
|
{
|
|
return (int)Math.Ceiling(dip * dpiScale);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Convert physical pixels to device-independent pixels (DIP).
|
|
/// </summary>
|
|
/// <param name="physicalPixels">Physical pixel value</param>
|
|
/// <param name="dpiScale">DPI scale factor</param>
|
|
/// <returns>Device-independent pixel value</returns>
|
|
public static int ScaleToDip(int physicalPixels, double dpiScale)
|
|
{
|
|
return (int)Math.Floor(physicalPixels / dpiScale);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Position a window at the bottom-right corner of the monitor where the mouse cursor is located.
|
|
/// Correctly handles all edge cases:
|
|
/// - Multi-monitor setups
|
|
/// - Taskbar at any position (top/bottom/left/right)
|
|
/// - Different DPI settings
|
|
/// </summary>
|
|
/// <param name="window">WinUIEx window to position</param>
|
|
/// <param name="widthDip">Window width in device-independent pixels (DIP)</param>
|
|
/// <param name="heightDip">Window height in device-independent pixels (DIP)</param>
|
|
/// <param name="rightMarginDip">Right margin in device-independent pixels (DIP)</param>
|
|
/// <param name="bottomMarginDip">Bottom margin in device-independent pixels (DIP)</param>
|
|
public static void PositionWindowBottomRight(
|
|
WindowEx window,
|
|
int widthDip,
|
|
int heightDip,
|
|
int rightMarginDip = 0,
|
|
int bottomMarginDip = 0)
|
|
{
|
|
if (!TryGetDisplayAreaAtCursor(out var displayArea) || displayArea is null)
|
|
{
|
|
ManagedCommon.Logger.LogWarning("PositionWindowBottomRight: Unable to determine target display from cursor, skipping positioning");
|
|
return;
|
|
}
|
|
|
|
MoveWindowBottomRight(window, displayArea, widthDip, heightDip, rightMarginDip, bottomMarginDip);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Center a window within the specified display area's work area.
|
|
/// </summary>
|
|
public static void CenterWindowOnDisplay(
|
|
WindowEx window,
|
|
DisplayArea displayArea,
|
|
int widthDip,
|
|
int heightDip)
|
|
{
|
|
double dpiScale = GetDpiScale(displayArea);
|
|
int w = ScaleToPhysicalPixels(widthDip, dpiScale);
|
|
int h = ScaleToPhysicalPixels(heightDip, dpiScale);
|
|
|
|
// WorkArea relative to DisplayArea (accounts for taskbar position)
|
|
var rel = GetWorkAreaRelativeToDisplay(displayArea);
|
|
int x = rel.X + ((rel.Width - w) / 2);
|
|
int y = rel.Y + ((rel.Height - h) / 2);
|
|
|
|
window.AppWindow.MoveAndResize(new Windows.Graphics.RectInt32(x, y, w, h), displayArea);
|
|
}
|
|
|
|
private static void MoveWindowBottomRight(
|
|
WindowEx window,
|
|
DisplayArea displayArea,
|
|
int widthDip,
|
|
int heightDip,
|
|
int rightMarginDip,
|
|
int bottomMarginDip)
|
|
{
|
|
double dpiScale = GetDpiScale(displayArea);
|
|
int w = ScaleToPhysicalPixels(widthDip, dpiScale);
|
|
int h = ScaleToPhysicalPixels(heightDip, dpiScale);
|
|
int marginRight = ScaleToPhysicalPixels(rightMarginDip, dpiScale);
|
|
int marginBottom = ScaleToPhysicalPixels(bottomMarginDip, dpiScale);
|
|
|
|
// WorkArea relative to DisplayArea (accounts for taskbar position)
|
|
var rel = GetWorkAreaRelativeToDisplay(displayArea);
|
|
int x = rel.X + rel.Width - w - marginRight;
|
|
int y = rel.Y + rel.Height - h - marginBottom;
|
|
|
|
window.AppWindow.MoveAndResize(new Windows.Graphics.RectInt32(x, y, w, h), displayArea);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Get the work area rectangle relative to the display area's origin.
|
|
/// MoveAndResize(rect, displayArea) expects coordinates relative to the DisplayArea,
|
|
/// but WorkArea.X/Y are in absolute screen coordinates, so we subtract the DisplayArea origin.
|
|
/// The resulting rect describes where the usable area is within the display (e.g., offset by taskbar).
|
|
/// </summary>
|
|
private static Windows.Graphics.RectInt32 GetWorkAreaRelativeToDisplay(DisplayArea displayArea)
|
|
{
|
|
var outer = displayArea.OuterBounds;
|
|
var work = displayArea.WorkArea;
|
|
return new Windows.Graphics.RectInt32(
|
|
work.X - outer.X,
|
|
work.Y - outer.Y,
|
|
work.Width,
|
|
work.Height);
|
|
}
|
|
|
|
internal static bool TryGetDisplayAreaAtCursor(out DisplayArea? displayArea)
|
|
{
|
|
displayArea = null;
|
|
|
|
if (!GetCursorPos(out var cursorPos))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
displayArea = DisplayArea.GetFromPoint(new Windows.Graphics.PointInt32(cursorPos.X, cursorPos.Y), DisplayAreaFallback.None);
|
|
return displayArea is not null;
|
|
}
|
|
|
|
private static int GetEffectiveDpi(nint hMonitor)
|
|
{
|
|
if (hMonitor == 0)
|
|
{
|
|
return DefaultDpi;
|
|
}
|
|
|
|
var hr = GetDpiForMonitor(hMonitor, MdtEffectiveDpi, out var dpiX, out _);
|
|
return hr >= 0 && dpiX > 0 ? (int)dpiX : DefaultDpi;
|
|
}
|
|
}
|
|
}
|