diff --git a/src/SQLBI.Whiteboard/Export/ExportWindow.xaml b/src/SQLBI.Whiteboard/Export/ExportWindow.xaml index 95adce8..b51524c 100644 --- a/src/SQLBI.Whiteboard/Export/ExportWindow.xaml +++ b/src/SQLBI.Whiteboard/Export/ExportWindow.xaml @@ -3,15 +3,17 @@ xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" Title="Export" Width="1000" - Height="720" + SizeToContent="Height" MinWidth="760" - MinHeight="560" + MinHeight="420" WindowStartupLocation="CenterOwner" ResizeMode="CanResize" ShowInTaskbar="False" Background="{DynamicResource ToolbarBackgroundBrush}" Stylus.IsPressAndHoldEnabled="False" PreviewKeyDown="Window_PreviewKeyDown" + SourceInitialized="Window_SourceInitialized" + SizeChanged="Window_SizeChanged" Loaded="Window_Loaded" Closing="Window_Closing"> @@ -70,9 +72,12 @@ + + BorderThickness="1" + MaxHeight="{Binding ActualHeight, ElementName=Options}"> + MaxHeight = Math.Max(MinHeight, MonitorStartupPlacement.WorkAreaHeight(this) - 24); + + private void Window_SizeChanged(object sender, SizeChangedEventArgs e) + { + if (e.HeightChanged && IsLoaded) + { + MonitorStartupPlacement.KeepWithinWorkArea(this); + } + } + private void Window_Loaded(object sender, RoutedEventArgs e) { // Rendered once the window is on screen, so the dialog appears before a diff --git a/src/SQLBI.Whiteboard/MonitorStartupPlacement.cs b/src/SQLBI.Whiteboard/MonitorStartupPlacement.cs index dd52b83..95a0277 100644 --- a/src/SQLBI.Whiteboard/MonitorStartupPlacement.cs +++ b/src/SQLBI.Whiteboard/MonitorStartupPlacement.cs @@ -3,6 +3,7 @@ using System.Text; using System.Windows; using System.Windows.Interop; +using System.Windows.Media; using Microsoft.Win32; using SQLBI.Whiteboard.Core.Settings; @@ -22,6 +23,7 @@ internal static class MonitorStartupPlacement private const uint SwpNoZOrder = 0x0004; private const uint SwpNoActivate = 0x0010; private const uint SwpFrameChanged = 0x0020; + private const uint SwpNoSize = 0x0001; public static IReadOnlyList Enumerate() => EnumerateMonitors() @@ -117,6 +119,79 @@ public static void FillCurrentMonitor(Window window) SwpNoZOrder | SwpNoActivate | SwpFrameChanged); } + /// + /// The height of the working area, in this window's device-independent + /// units, of the monitor the window (or, before it has a handle, its owner) + /// is on. A dialog sizes itself against this rather than the primary + /// monitor's, which is the wrong screen whenever the board is on another. + /// + public static double WorkAreaHeight(Window window) + { + ArgumentNullException.ThrowIfNull(window); + + var anchor = new WindowInteropHelper(window).Handle == 0 && window.Owner is { } owner ? owner : window; + if (!TryGetWorkArea(anchor, out var area)) + { + return SystemParameters.WorkArea.Height; + } + + var scale = VisualTreeHelper.GetDpi(anchor).DpiScaleY; + return (area.Bottom - area.Top) / Math.Max(scale, 0.01); + } + + /// + /// Moves the window up or left as far as needed for the whole of it to sit + /// inside its monitor's working area. Called after a window grows in place, + /// which WPF does downward from wherever it was centred. + /// + public static void KeepWithinWorkArea(Window window) + { + ArgumentNullException.ThrowIfNull(window); + + var windowHandle = new WindowInteropHelper(window).Handle; + if (windowHandle == 0 || + !TryGetWorkArea(window, out var area) || + !GetWindowRect(windowHandle, out var bounds)) + { + return; + } + + var left = Math.Max(area.Left, Math.Min(bounds.Left, area.Right - (bounds.Right - bounds.Left))); + var top = Math.Max(area.Top, Math.Min(bounds.Top, area.Bottom - (bounds.Bottom - bounds.Top))); + if (left != bounds.Left || top != bounds.Top) + { + SetWindowPos(windowHandle, 0, left, top, 0, 0, SwpNoZOrder | SwpNoActivate | SwpNoSize); + } + } + + private static bool TryGetWorkArea(Window window, out NativeRectangle area) + { + area = default; + var windowHandle = new WindowInteropHelper(window).Handle; + if (windowHandle == 0) + { + return false; + } + + var monitorHandle = MonitorFromWindow(windowHandle, MonitorDefaultToNearest); + if (monitorHandle == 0) + { + return false; + } + + var monitorInfo = new NativeMonitorInfo + { + Size = (uint)Marshal.SizeOf(), + }; + if (!GetMonitorInfo(monitorHandle, ref monitorInfo)) + { + return false; + } + + area = monitorInfo.WorkingArea; + return true; + } + private static IReadOnlyList EnumerateMonitors() { var monitors = new List(); @@ -327,6 +402,10 @@ private static extern bool EnumDisplayDevices( ref NativeDisplayDevice displayDevice, uint flags); + [DllImport("user32.dll")] + [return: MarshalAs(UnmanagedType.Bool)] + private static extern bool GetWindowRect(nint windowHandle, out NativeRectangle rectangle); + [DllImport("user32.dll")] private static extern nint MonitorFromWindow( nint windowHandle,