diff --git a/src/Core/src/Platform/Windows/MauiWinUIWindow.cs b/src/Core/src/Platform/Windows/MauiWinUIWindow.cs index db91429bb4c7..7d1d6333d5a2 100644 --- a/src/Core/src/Platform/Windows/MauiWinUIWindow.cs +++ b/src/Core/src/Platform/Windows/MauiWinUIWindow.cs @@ -86,6 +86,7 @@ IntPtr NewWindowProc(IntPtr hWnd, uint msg, IntPtr wParam, IntPtr lParam) NativeMessage?.Invoke(this, args); + Essentials.Platform.NewWindowProc(hWnd, msg, wParam, lParam); return CallWindowProc(oldWndProc, hWnd, msg, wParam, lParam); } diff --git a/src/Essentials/samples/Samples/Platforms/Windows/app.manifest b/src/Essentials/samples/Samples/Platforms/Windows/app.manifest new file mode 100644 index 000000000000..4c44e1f9c8e2 --- /dev/null +++ b/src/Essentials/samples/Samples/Platforms/Windows/app.manifest @@ -0,0 +1,15 @@ + + + + + + + + true/PM + PerMonitorV2, PerMonitor + + + diff --git a/src/Essentials/src/DeviceDisplay/DeviceDisplay.uwp.cs b/src/Essentials/src/DeviceDisplay/DeviceDisplay.uwp.cs index 4876b1f54222..f610cdfa5615 100644 --- a/src/Essentials/src/DeviceDisplay/DeviceDisplay.uwp.cs +++ b/src/Essentials/src/DeviceDisplay/DeviceDisplay.uwp.cs @@ -1,5 +1,7 @@ #nullable enable using System; +using System.Runtime.InteropServices; +using Microsoft.UI.Windowing; using Windows.Graphics.Display; using Windows.Graphics.Display.Core; using Windows.System.Display; @@ -47,6 +49,289 @@ public bool KeepScreenOn } } + static DisplayRotation CalculateRotation(DisplayOrientations native, DisplayOrientations current) + { + if (native == DisplayOrientations.Portrait) + { + switch (current) + { + case DisplayOrientations.Landscape: + return DisplayRotation.Rotation90; + case DisplayOrientations.Portrait: + return DisplayRotation.Rotation0; + case DisplayOrientations.LandscapeFlipped: + return DisplayRotation.Rotation270; + case DisplayOrientations.PortraitFlipped: + return DisplayRotation.Rotation180; + } + } + else if (native == DisplayOrientations.Landscape) + { + switch (current) + { + case DisplayOrientations.Landscape: + return DisplayRotation.Rotation0; + case DisplayOrientations.Portrait: + return DisplayRotation.Rotation270; + case DisplayOrientations.LandscapeFlipped: + return DisplayRotation.Rotation180; + case DisplayOrientations.PortraitFlipped: + return DisplayRotation.Rotation90; + } + } + + return DisplayRotation.Unknown; + } + +#if WINDOWS + + AppWindow? _currentAppWindowListeningTo; + public DisplayInfo GetMainDisplayInfo() + { + var appWindow = Platform.CurrentAppWindow; + var windowHandler = Platform.CurrentWindowHandle; + var mi = GetDisplay(windowHandler); + + if (mi == null) + return new DisplayInfo(); + + DEVMODE vDevMode = new DEVMODE(); + EnumDisplaySettings(mi.Value.DeviceNameToLPTStr(), -1, ref vDevMode); + + var rotation = CalculateRotation(vDevMode, appWindow); + var perpendicular = + rotation == DisplayRotation.Rotation90 || + rotation == DisplayRotation.Rotation270; + + var w = vDevMode.dmPelsWidth; + var h = vDevMode.dmPelsHeight; + var dpi = GetDpiForWindow(windowHandler) / 96.0; + + return new DisplayInfo( + width: perpendicular ? h : w, + height: perpendicular ? w : h, + density: dpi, + orientation: GetWindowOrientationWin32(appWindow) == DisplayOrientations.Landscape ? DisplayOrientation.Landscape : DisplayOrientation.Portrait, + rotation: rotation, + rate: vDevMode.dmDisplayFrequency); + } + + static MONITORINFOEX? GetDisplay(IntPtr hwnd) + { + IntPtr hMonitor; + RECT rc; + GetWindowRect(hwnd, out rc); + hMonitor = MonitorFromRect(ref rc, MonitorOptions.MONITOR_DEFAULTTONEAREST); + + MONITORINFOEX mi = new MONITORINFOEX(); + mi.Size = Marshal.SizeOf(mi); + bool success = GetMonitorInfo(hMonitor, ref mi); + if (success) + { + return mi; + } + return null; + } + + public void StartScreenMetricsListeners() + { + MainThread.BeginInvokeOnMainThread(() => + { + Platform.CurrentWindowDisplayChanged += OnWindowDisplayChanged; + Platform.CurrentWindowChanged += OnCurrentWindowChanged; + _currentAppWindowListeningTo = Platform.CurrentAppWindow; + _currentAppWindowListeningTo.Changed += OnAppWindowChanged; + }); + } + + public void StopScreenMetricsListeners() + { + MainThread.BeginInvokeOnMainThread(() => + { + Platform.CurrentWindowChanged -= OnCurrentWindowChanged; + Platform.CurrentWindowDisplayChanged -= OnWindowDisplayChanged; + + if (_currentAppWindowListeningTo != null) + _currentAppWindowListeningTo.Changed -= OnAppWindowChanged; + + _currentAppWindowListeningTo = null; + }); + } + + void OnCurrentWindowChanged(object? sender, EventArgs e) + { + if (_currentAppWindowListeningTo != null) + { + _currentAppWindowListeningTo.Changed -= OnAppWindowChanged; + } + + _currentAppWindowListeningTo = Platform.CurrentAppWindow; + _currentAppWindowListeningTo.Changed += OnAppWindowChanged; + } + + void OnWindowDisplayChanged(object? sender, EventArgs e) + { + WindowDisplayPropertiesChanged(); + } + + void WindowDisplayPropertiesChanged() + { + var metrics = GetMainDisplayInfo(); + MainDisplayInfoChanged?.Invoke(this, new DisplayInfoChangedEventArgs(metrics)); + } + + void OnAppWindowChanged(AppWindow sender, AppWindowChangedEventArgs args) => + WindowDisplayPropertiesChanged(); + + static DisplayRotation CalculateRotation(DEVMODE devMode, AppWindow appWindow) + { + DisplayOrientations native = DisplayOrientations.Portrait; + switch (devMode.dmDisplayOrientation) + { + case 0: + native = DisplayOrientations.Landscape; + break; + case 1: + native = DisplayOrientations.Portrait; + break; + case 2: + native = DisplayOrientations.LandscapeFlipped; + break; + case 3: + native = DisplayOrientations.PortraitFlipped; + break; + } + + var current = GetWindowOrientationWin32(appWindow); + return CalculateRotation(native, current); + } + + // https://github.com/marb2000/DesktopWindow/blob/abb21b797767bb24da09c066514117d5f1aabd75/WindowExtensions/DesktopWindow.cs#L407 + static DisplayOrientations GetWindowOrientationWin32(AppWindow appWindow) + { + DisplayOrientations orientationEnum; + int theScreenWidth = appWindow.Size.Width; + int theScreenHeight = appWindow.Size.Height; + if (theScreenWidth > theScreenHeight) + orientationEnum = DisplayOrientations.Landscape; + else + orientationEnum = DisplayOrientations.Portrait; + + return orientationEnum; + } + + [DllImport("User32", CharSet = CharSet.Unicode)] + static extern int GetDpiForWindow(IntPtr hwnd); + + [DllImport("User32", CharSet = CharSet.Unicode, SetLastError = true)] + static extern IntPtr MonitorFromRect(ref RECT lprc, MonitorOptions dwFlags); + + [DllImport("User32", CharSet = CharSet.Unicode, SetLastError = true)] + [return: MarshalAs(UnmanagedType.Bool)] + static extern bool GetWindowRect(IntPtr hWnd, out RECT lpRect); + + [DllImport("User32.dll")] + [return: MarshalAs(UnmanagedType.Bool)] + static extern Boolean EnumDisplaySettings( + byte[] lpszDeviceName, + [param: MarshalAs(UnmanagedType.U4)] int iModeNum, + [In, Out] ref DEVMODE lpDevMode); + + [DllImport("user32.dll", CharSet = CharSet.Unicode)] + static extern bool GetMonitorInfo(IntPtr hMonitor, ref MONITORINFOEX lpmi); + + [DllImport("user32.dll")] + static extern bool EnumDisplayMonitors(IntPtr hdc, IntPtr lprcClip, EnumMonitorsDelegate lpfnEnum, IntPtr dwData); + delegate bool EnumMonitorsDelegate(IntPtr hMonitor, IntPtr hdcMonitor, ref RECT lprcMonitor, IntPtr dwData); + + static bool MonitorEnumProc(IntPtr hMonitor, IntPtr hdcMonitor, ref RECT lprcMonitor, IntPtr dwData) + { + MONITORINFOEX mi = new MONITORINFOEX(); + mi.Size = Marshal.SizeOf(typeof(MONITORINFOEX)); + if (GetMonitorInfo(hMonitor, ref mi)) + Console.WriteLine(mi.DeviceName); + + return true; + } + + enum MonitorOptions : uint + { + MONITOR_DEFAULTTONULL, + MONITOR_DEFAULTTOPRIMARY, + MONITOR_DEFAULTTONEAREST + } + + [StructLayout(LayoutKind.Sequential)] + struct RECT + { + public int Left; + public int Top; + public int Right; + public int Bottom; + } + + [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)] + struct MONITORINFOEX + { + public int Size; + public RECT Monitor; + public RECT WorkArea; + public uint Flags; + [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 0x20)] + public string DeviceName; + + public byte[] DeviceNameToLPTStr() + { + var lptArray = new byte[DeviceName.Length + 1]; + + var index = 0; + foreach (char c in DeviceName.ToCharArray()) + lptArray[index++] = Convert.ToByte(c); + + lptArray[index] = Convert.ToByte('\0'); + + return lptArray; + } + } + + struct DEVMODE + { + private const int CCHDEVICENAME = 0x20; + private const int CCHFORMNAME = 0x20; + [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 0x20)] + public string dmDeviceName; + public short dmSpecVersion; + public short dmDriverVersion; + public short dmSize; + public short dmDriverExtra; + public int dmFields; + public int dmPositionX; + public int dmPositionY; + public int dmDisplayOrientation; + public int dmDisplayFixedOutput; + public short dmColor; + public short dmDuplex; + public short dmYResolution; + public short dmTTOption; + public short dmCollate; + [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 0x20)] + public string dmFormName; + public short dmLogPixels; + public int dmBitsPerPel; + public int dmPelsWidth; + public int dmPelsHeight; + public int dmDisplayFlags; + public int dmDisplayFrequency; + public int dmICMMethod; + public int dmICMIntent; + public int dmMediaType; + public int dmDitherType; + public int dmReserved1; + public int dmReserved2; + public int dmPanningWidth; + public int dmPanningHeight; + } +#elif WINDOWS_UWP public DisplayInfo GetMainDisplayInfo() => GetMainDisplayInfo(null); @@ -122,36 +407,9 @@ static DisplayRotation CalculateRotation(DisplayInformation di) var native = di.NativeOrientation; var current = di.CurrentOrientation; - if (native == DisplayOrientations.Portrait) - { - switch (current) - { - case DisplayOrientations.Landscape: - return DisplayRotation.Rotation90; - case DisplayOrientations.Portrait: - return DisplayRotation.Rotation0; - case DisplayOrientations.LandscapeFlipped: - return DisplayRotation.Rotation270; - case DisplayOrientations.PortraitFlipped: - return DisplayRotation.Rotation180; - } - } - else if (native == DisplayOrientations.Landscape) - { - switch (current) - { - case DisplayOrientations.Landscape: - return DisplayRotation.Rotation0; - case DisplayOrientations.Portrait: - return DisplayRotation.Rotation270; - case DisplayOrientations.LandscapeFlipped: - return DisplayRotation.Rotation180; - case DisplayOrientations.PortraitFlipped: - return DisplayRotation.Rotation90; - } - } - - return DisplayRotation.Unknown; + return CalculateRotation(native, current); } } +#endif + } } diff --git a/src/Essentials/src/DeviceInfo/DeviceInfo.uwp.cs b/src/Essentials/src/DeviceInfo/DeviceInfo.uwp.cs index 22d8e7a114c5..b86a0a556bd9 100644 --- a/src/Essentials/src/DeviceInfo/DeviceInfo.uwp.cs +++ b/src/Essentials/src/DeviceInfo/DeviceInfo.uwp.cs @@ -63,7 +63,9 @@ static DeviceIdiom GetIdiom() { try { - var uiMode = UIViewSettings.GetForCurrentView().UserInteractionMode; + var currentHandle = Essentials.Platform.CurrentWindowHandle; + var settings = UIViewSettingsInterop.GetForWindow(currentHandle); + var uiMode = settings.UserInteractionMode; currentIdiom = uiMode == UserInteractionMode.Mouse ? DeviceIdiom.Desktop : DeviceIdiom.Tablet; } catch (Exception ex) diff --git a/src/Essentials/src/Platform/Platform.uwp.cs b/src/Essentials/src/Platform/Platform.uwp.cs index 940e42b22120..659f9514de21 100644 --- a/src/Essentials/src/Platform/Platform.uwp.cs +++ b/src/Essentials/src/Platform/Platform.uwp.cs @@ -5,6 +5,7 @@ using WindowActivationState = Windows.UI.Core.CoreWindowActivationState; #elif WINDOWS using System; +using Microsoft.UI.Windowing; using Microsoft.UI.Xaml; #endif @@ -12,15 +13,54 @@ namespace Microsoft.Maui.Essentials { public static partial class Platform { + const uint DISPLAY_CHANGED = 126; + const uint DPI_CHANGED = 736; + + static internal event EventHandler CurrentWindowChanged; + static internal event EventHandler CurrentWindowDisplayChanged; + internal static Window CurrentWindow { get => _currentWindow ?? Window.Current; - set => _currentWindow = value; + set + { + bool changed = _currentWindow != value; + _currentWindow = value; + + if(changed) + CurrentWindowChanged?.Invoke(value, EventArgs.Empty); + } } internal static IntPtr CurrentWindowHandle => WinRT.Interop.WindowNative.GetWindowHandle(CurrentWindow); - +#if WINDOWS + internal static UI.WindowId CurrentWindowId + => UI.Win32Interop.GetWindowIdFromWindow(CurrentWindowHandle); + + internal static AppWindow CurrentAppWindow + => AppWindow.GetFromWindowId(CurrentWindowId); + + // Currently there isn't a way to detect Orientation Changes unless you subclass the WinUI.Window and watch the messages + // Maui.Core forwards these messages to here so that WinUI can react accordingly. + // This is the "subtlest" way to currently wire this together. + // Hopefully there will be a more public API for this down the road so we can just use that directly from Essentials + internal static void NewWindowProc(IntPtr hWnd, uint msg, IntPtr wParam, IntPtr lParam) + { + if (CurrentWindowDisplayChanged == null) + return; + + // We only care about orientation or dpi changes + if (DISPLAY_CHANGED != msg && DPI_CHANGED != msg) + return; + + if (_currentWindow != null && hWnd == CurrentWindowHandle) + { + CurrentWindowDisplayChanged?.Invoke(CurrentWindow, EventArgs.Empty); + } + } +#endif + internal const string AppManifestFilename = "AppxManifest.xml"; internal const string AppManifestXmlns = "http://schemas.microsoft.com/appx/manifest/foundation/windows10"; internal const string AppManifestUapXmlns = "http://schemas.microsoft.com/appx/manifest/uap/windows10"; @@ -34,7 +74,9 @@ public static async void OnLaunched(LaunchActivatedEventArgs e) public static void OnActivated(Window window, WindowActivatedEventArgs args) { if (args.WindowActivationState != WindowActivationState.Deactivated) + { CurrentWindow = window; + } } } } diff --git a/src/Essentials/src/Properties/AssemblyInfo.uwp.cs b/src/Essentials/src/Properties/AssemblyInfo.uwp.cs new file mode 100644 index 000000000000..c254f190c6c2 --- /dev/null +++ b/src/Essentials/src/Properties/AssemblyInfo.uwp.cs @@ -0,0 +1,3 @@ +using System.Runtime.CompilerServices; + +[assembly: InternalsVisibleTo("Microsoft.Maui")]