From 9134fbe1f515c6e6d5b1d2e9c1a67a2d961cc217 Mon Sep 17 00:00:00 2001 From: Marcel Wagner Date: Wed, 17 Jan 2024 19:45:03 +0100 Subject: [PATCH] Add minimum window sizes (#1404) ## Description Adding a minimum size for the apps window and also the TabView tearout sample for better user experience. ## Motivation and Context Closes #1181 Co-authored-by: Niels Laute --- WinUIGallery/App.xaml.cs | 24 +++-- WinUIGallery/Common/Win32.cs | 25 ++++++ WinUIGallery/ControlPages/TabViewPage.xaml.cs | 10 ++- WinUIGallery/Helper/Win32WindowHelper.cs | 88 +++++++++++++++++++ .../TabViewWindowingSamplePage.xaml.cs | 19 ++-- 5 files changed, 145 insertions(+), 21 deletions(-) create mode 100644 WinUIGallery/Helper/Win32WindowHelper.cs diff --git a/WinUIGallery/App.xaml.cs b/WinUIGallery/App.xaml.cs index 2562dc805..99d333223 100644 --- a/WinUIGallery/App.xaml.cs +++ b/WinUIGallery/App.xaml.cs @@ -21,7 +21,10 @@ using Windows.ApplicationModel.Activation; using WinUIGallery.DesktopWap.DataModel; using WASDK = Microsoft.WindowsAppSDK; - +using Microsoft.Windows.AppLifecycle; +using System.IO; +using WinUIGallery.Helper; + namespace AppUIBasics { /// @@ -30,23 +33,24 @@ namespace AppUIBasics sealed partial class App : Application { private static Window startupWindow; + private static Win32WindowHelper win32WindowHelper; public static string WinAppSdkDetails { // TODO: restore patch number and version tag when WinAppSDK supports them both get => string.Format("Windows App SDK {0}.{1}", WASDK.Release.Major, WASDK.Release.Minor); - } - + } + public static string WinAppSdkRuntimeDetails { get - { + { // Retrieve Windows App Runtime version info dynamically - var windowsAppRuntimeVersion = - from module in Process.GetCurrentProcess().Modules.OfType() - where module.FileName.EndsWith("Microsoft.WindowsAppRuntime.Insights.Resource.dll") - select FileVersionInfo.GetVersionInfo(module.FileName); + var windowsAppRuntimeVersion = + from module in Process.GetCurrentProcess().Modules.OfType() + where module.FileName.EndsWith("Microsoft.WindowsAppRuntime.Insights.Resource.dll") + select FileVersionInfo.GetVersionInfo(module.FileName); return WinAppSdkDetails + ", Windows App Runtime " + windowsAppRuntimeVersion.First().FileVersion; } } @@ -107,6 +111,10 @@ protected override void OnLaunched(Microsoft.UI.Xaml.LaunchActivatedEventArgs ar startupWindow = WindowHelper.CreateWindow(); startupWindow.ExtendsContentIntoTitleBar = true; + + win32WindowHelper = new Win32WindowHelper(startupWindow); + win32WindowHelper.SetWindowMinMaxSize(new Win32WindowHelper.POINT() { x = 500, y = 500 }); + #if DEBUG if (System.Diagnostics.Debugger.IsAttached) { diff --git a/WinUIGallery/Common/Win32.cs b/WinUIGallery/Common/Win32.cs index 1146c78a1..ba43a9afc 100644 --- a/WinUIGallery/Common/Win32.cs +++ b/WinUIGallery/Common/Win32.cs @@ -17,6 +17,18 @@ internal static class Win32 [DllImport("kernel32.dll", CharSet = CharSet.Auto)] public static extern IntPtr GetModuleHandle(IntPtr moduleName); + [DllImport("User32.dll")] + internal static extern int GetDpiForWindow(IntPtr hwnd); + + [DllImport("user32.dll", EntryPoint = "SetWindowLong")] + internal static extern int SetWindowLong32(IntPtr hWnd, WindowLongIndexFlags nIndex, WinProc newProc); + + [DllImport("user32.dll", EntryPoint = "SetWindowLongPtr")] + internal static extern IntPtr SetWindowLongPtr64(IntPtr hWnd, WindowLongIndexFlags nIndex, WinProc newProc); + + [DllImport("user32.dll")] + internal static extern IntPtr CallWindowProc(IntPtr lpPrevWndFunc, IntPtr hWnd, WindowMessage Msg, IntPtr wParam, IntPtr lParam); + public const int WM_ACTIVATE = 0x0006; public const int WA_ACTIVE = 0x01; public const int WA_INACTIVE = 0x00; @@ -24,5 +36,18 @@ internal static class Win32 public const int WM_SETICON = 0x0080; public const int ICON_SMALL = 0; public const int ICON_BIG = 1; + + internal delegate IntPtr WinProc(IntPtr hWnd, WindowMessage Msg, IntPtr wParam, IntPtr lParam); + + [Flags] + internal enum WindowLongIndexFlags : int + { + GWL_WNDPROC = -4, + } + + internal enum WindowMessage : int + { + WM_GETMINMAXINFO = 0x0024, + } } } diff --git a/WinUIGallery/ControlPages/TabViewPage.xaml.cs b/WinUIGallery/ControlPages/TabViewPage.xaml.cs index 56966a981..2b86a61fb 100644 --- a/WinUIGallery/ControlPages/TabViewPage.xaml.cs +++ b/WinUIGallery/ControlPages/TabViewPage.xaml.cs @@ -245,12 +245,14 @@ private void TabCloseButtonOverlayModeComboBox_SelectionChanged(object sender, S private void TabViewWindowingButton_Click(object sender, Microsoft.UI.Xaml.RoutedEventArgs e) { + var tabViewSample = new TabViewWindowingSamplePage(); + var newWindow = WindowHelper.CreateWindow(); + newWindow.ExtendsContentIntoTitleBar = true; + newWindow.Content = tabViewSample; + tabViewSample.LoadDemoData(); + tabViewSample.SetupWindowMinSize(newWindow); - Frame frame = new Frame(); - frame.RequestedTheme = ThemeHelper.RootTheme; - frame.Navigate(typeof(TabViewWindowingSamplePage), null); - newWindow.Content = frame; newWindow.Activate(); } } diff --git a/WinUIGallery/Helper/Win32WindowHelper.cs b/WinUIGallery/Helper/Win32WindowHelper.cs new file mode 100644 index 000000000..66fde3338 --- /dev/null +++ b/WinUIGallery/Helper/Win32WindowHelper.cs @@ -0,0 +1,88 @@ +using System; +using System.Runtime.InteropServices; +using Microsoft.UI.Xaml; +using static AppUIBasics.Win32; + +namespace WinUIGallery.Helper +{ + internal class Win32WindowHelper + { + private static WinProc newWndProc = null; + private static nint oldWndProc = nint.Zero; + + private POINT? minWindowSize = null; + private POINT? maxWindowSize = null; + + private readonly Window window; + + public Win32WindowHelper(Window window) + { + this.window = window; + } + + public void SetWindowMinMaxSize(POINT? minWindowSize = null, POINT? maxWindowSize = null) + { + this.minWindowSize = minWindowSize; + this.maxWindowSize = maxWindowSize; + + var hwnd = GetWindowHandleForCurrentWindow(window); + + newWndProc = new WinProc(WndProc); + oldWndProc = SetWindowLongPtr(hwnd, WindowLongIndexFlags.GWL_WNDPROC, newWndProc); + } + + private static nint GetWindowHandleForCurrentWindow(object target) => + WinRT.Interop.WindowNative.GetWindowHandle(target); + + private nint WndProc(nint hWnd, WindowMessage Msg, nint wParam, nint lParam) + { + switch (Msg) + { + case WindowMessage.WM_GETMINMAXINFO: + var dpi = GetDpiForWindow(hWnd); + var scalingFactor = (float)dpi / 96; + + var minMaxInfo = Marshal.PtrToStructure(lParam); + if (minWindowSize != null) + { + minMaxInfo.ptMinTrackSize.x = (int)(minWindowSize.Value.x * scalingFactor); + minMaxInfo.ptMinTrackSize.y = (int)(minWindowSize.Value.y * scalingFactor); + } + if (maxWindowSize != null) + { + minMaxInfo.ptMaxTrackSize.x = (int)(maxWindowSize.Value.x * scalingFactor); + minMaxInfo.ptMaxTrackSize.y = (int)(minWindowSize.Value.y * scalingFactor); + } + + Marshal.StructureToPtr(minMaxInfo, lParam, true); + break; + + } + return CallWindowProc(oldWndProc, hWnd, Msg, wParam, lParam); + } + + private nint SetWindowLongPtr(nint hWnd, WindowLongIndexFlags nIndex, WinProc newProc) + { + if (nint.Size == 8) + return SetWindowLongPtr64(hWnd, nIndex, newProc); + else + return new nint(SetWindowLong32(hWnd, nIndex, newProc)); + } + + internal struct POINT + { + public int x; + public int y; + } + + [StructLayout(LayoutKind.Sequential)] + private struct MINMAXINFO + { + public POINT ptReserved; + public POINT ptMaxSize; + public POINT ptMaxPosition; + public POINT ptMinTrackSize; + public POINT ptMaxTrackSize; + } + } +} diff --git a/WinUIGallery/TabViewPages/TabViewWindowingSamplePage.xaml.cs b/WinUIGallery/TabViewPages/TabViewWindowingSamplePage.xaml.cs index 9d53b846f..1ce2dc353 100644 --- a/WinUIGallery/TabViewPages/TabViewWindowingSamplePage.xaml.cs +++ b/WinUIGallery/TabViewPages/TabViewWindowingSamplePage.xaml.cs @@ -14,12 +14,14 @@ using System.Threading.Tasks; using Windows.System; using DispatcherQueueHandler = Microsoft.UI.Dispatching.DispatcherQueueHandler; +using WinUIGallery.Helper; namespace AppUIBasics.TabViewPages { public sealed partial class TabViewWindowingSamplePage : Page { private const string DataIdentifier = "MyTabItem"; + private Win32WindowHelper win32WindowHelper; public TabViewWindowingSamplePage() { this.InitializeComponent(); @@ -29,6 +31,12 @@ public TabViewWindowingSamplePage() Loaded += TabViewWindowingSamplePage_Loaded; } + public void SetupWindowMinSize(Window window) + { + win32WindowHelper = new Win32WindowHelper(window); + win32WindowHelper.SetWindowMinMaxSize(new Win32WindowHelper.POINT() { x = 500, y = 300 }); + } + private void TabViewWindowingSamplePage_Loaded(object sender, RoutedEventArgs e) { var currentWindow = WindowHelper.GetWindowForElement(this); @@ -57,16 +65,8 @@ private void Tabs_TabItemsChanged(TabView sender, Windows.Foundation.Collections } } - protected override void OnNavigatedTo(NavigationEventArgs e) - { - base.OnNavigatedTo(e); - - SetupWindow(); - } - - void SetupWindow() + public void LoadDemoData() { - // Main Window -- add some default items for (int i = 0; i < 3; i++) { @@ -92,6 +92,7 @@ private void Tabs_TabDroppedOutside(TabView sender, TabViewTabDroppedOutsideEven var newWindow = WindowHelper.CreateWindow(); newWindow.ExtendsContentIntoTitleBar = true; newWindow.Content = newPage; + newPage.SetupWindowMinSize(newWindow); newWindow.Activate(); }