From 71753a77d5c2d5758d1e6a678f08bdf8e9eea8f1 Mon Sep 17 00:00:00 2001 From: Marco Russo Date: Fri, 4 Sep 2026 11:28:49 +0200 Subject: [PATCH] Size the export dialog to its options and to the monitor it opens on The dialog opened at a fixed height, which left the bottom empty with a short option list and, on a monitor shorter than that height, put the buttons off the screen: the cap it used was the primary monitor's work area, the wrong screen whenever the board is on another. The height now follows the options column, the preview takes the same height rather than driving it, and the cap is the work area of the monitor the board is on, past which the options scroll. When a change of format regrows the window in place, it is moved back inside the screen. Co-Authored-By: Claude Fable 5.1 --- src/SQLBI.Whiteboard/Export/ExportWindow.xaml | 11 ++- .../Export/ExportWindow.xaml.cs | 18 ++++- .../MonitorStartupPlacement.cs | 79 +++++++++++++++++++ 3 files changed, 101 insertions(+), 7 deletions(-) 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,