From 07b05eb19c29b060ac812034889151415c3d16f7 Mon Sep 17 00:00:00 2001 From: Simon Cropp Date: Mon, 23 Mar 2026 13:37:30 +1100 Subject: [PATCH 1/9] MaximizeWindow for excel diff --- src/MsExcelDiff/SpreadsheetCompare.cs | 28 ++++++++++++++++++++++++++- 1 file changed, 27 insertions(+), 1 deletion(-) diff --git a/src/MsExcelDiff/SpreadsheetCompare.cs b/src/MsExcelDiff/SpreadsheetCompare.cs index 92acb5f..94149a7 100644 --- a/src/MsExcelDiff/SpreadsheetCompare.cs +++ b/src/MsExcelDiff/SpreadsheetCompare.cs @@ -1,4 +1,4 @@ -public static class SpreadsheetCompare +public static partial class SpreadsheetCompare { static readonly string[] programFolders = [ @@ -80,6 +80,7 @@ Spreadsheet Compare (SPREADSHEETCOMPARE.EXE) was not found. using var process = await LaunchProcess(exe, tempFile); JobObject.AssignProcess(job, process.Handle); + await MaximizeWindow(process); await process.WaitForExitAsync(); } catch when (TempFiles.TryDelete(tempFile)) @@ -210,4 +211,29 @@ internal static HashSet GetProcessPids(string processName) return null; } + static async Task MaximizeWindow(Process process) + { + // Wait for the main window to appear + for (var i = 0; i < 100; i++) + { + process.Refresh(); + if (process.MainWindowHandle != IntPtr.Zero) + { + // SW_MAXIMIZE = 3 + ShowWindow(process.MainWindowHandle, 3); + SetForegroundWindow(process.MainWindowHandle); + return; + } + + await Task.Delay(100); + } + } + + [LibraryImport("user32.dll")] + [return: MarshalAs(UnmanagedType.Bool)] + private static partial bool ShowWindow(IntPtr hWnd, int nCmdShow); + + [LibraryImport("user32.dll")] + [return: MarshalAs(UnmanagedType.Bool)] + private static partial bool SetForegroundWindow(IntPtr hWnd); } From a1f8c47cc3d07679bf02ee8c008acd6c941819fc Mon Sep 17 00:00:00 2001 From: Simon Cropp Date: Mon, 23 Mar 2026 14:01:34 +1100 Subject: [PATCH 2/9] Update SpreadsheetCompare.cs --- src/MsExcelDiff/SpreadsheetCompare.cs | 176 ++++++++++++++++++++++++++ 1 file changed, 176 insertions(+) diff --git a/src/MsExcelDiff/SpreadsheetCompare.cs b/src/MsExcelDiff/SpreadsheetCompare.cs index 94149a7..6c19e88 100644 --- a/src/MsExcelDiff/SpreadsheetCompare.cs +++ b/src/MsExcelDiff/SpreadsheetCompare.cs @@ -222,6 +222,10 @@ static async Task MaximizeWindow(Process process) // SW_MAXIMIZE = 3 ShowWindow(process.MainWindowHandle, 3); SetForegroundWindow(process.MainWindowHandle); + + // Wait briefly for the window to finish layout after maximize + await Task.Delay(500); + CenterVerticalSplit(process.MainWindowHandle); return; } @@ -229,6 +233,145 @@ static async Task MaximizeWindow(Process process) } } + static void CenterVerticalSplit(IntPtr mainWindow) + { + // Collect all child windows with their parent, class name, and rect + var children = new List<(IntPtr Handle, IntPtr Parent, string ClassName, RECT Rect)>(); + EnumChildWindows(mainWindow, (hwnd, _) => + { + GetWindowRect(hwnd, out var rect); + var className = GetWindowClassName(hwnd); + children.Add((hwnd, GetParent(hwnd), className, rect)); + return true; + }, IntPtr.Zero); + + // Log child window hierarchy for diagnostics + Log.Information("CenterVerticalSplit: found {Count} child windows", children.Count); + foreach (var child in children) + { + var w = child.Rect.Right - child.Rect.Left; + var h = child.Rect.Bottom - child.Rect.Top; + Log.Information( + " hwnd={Handle} parent={Parent} class={ClassName} pos=({Left},{Top}) size={Width}x{Height}", + child.Handle, child.Parent, child.ClassName, + child.Rect.Left, child.Rect.Top, w, h); + } + + // Find the vertical splitter: look for pairs of side-by-side siblings + // with similar height that together span most of their parent's width. + // Pick the pair with the largest combined area. + var bestArea = 0; + var bestLeftRect = default(RECT); + var bestRightRect = default(RECT); + var bestParent = IntPtr.Zero; + + foreach (var group in children.GroupBy(c => c.Parent)) + { + var siblings = group.ToList(); + + for (var i = 0; i < siblings.Count; i++) + { + for (var j = i + 1; j < siblings.Count; j++) + { + var a = siblings[i]; + var b = siblings[j]; + var heightA = a.Rect.Bottom - a.Rect.Top; + var heightB = b.Rect.Bottom - b.Rect.Top; + + var widthA = a.Rect.Right - a.Rect.Left; + var widthB = b.Rect.Right - b.Rect.Left; + + if (heightA < 100 || heightB < 100 || + widthA <= 0 || widthB <= 0) + { + continue; + } + + if (Math.Abs(heightA - heightB) > 20 || + Math.Abs(a.Rect.Top - b.Rect.Top) > 20) + { + continue; + } + + GetClientRect(group.Key, out var parentClient); + var totalSpan = Math.Max(a.Rect.Right, b.Rect.Right) - Math.Min(a.Rect.Left, b.Rect.Left); + if (totalSpan < parentClient.Right * 0.8) + { + continue; + } + + var area = (a.Rect.Right - a.Rect.Left) * heightA + + (b.Rect.Right - b.Rect.Left) * heightB; + if (area <= bestArea) + { + continue; + } + + bestArea = area; + bestParent = group.Key; + if (a.Rect.Left <= b.Rect.Left) + { + bestLeftRect = a.Rect; + bestRightRect = b.Rect; + } + else + { + bestLeftRect = b.Rect; + bestRightRect = a.Rect; + } + } + } + } + + if (bestArea == 0) + { + Log.Information("CenterVerticalSplit: no matching split panel pair found"); + return; + } + + // The splitter bar sits in the gap between the two panels. + // Convert splitter screen position to parent client coordinates and + // send mouse messages directly to the parent (SplitContainer) window. + var splitterScreenX = (bestLeftRect.Right + bestRightRect.Left) / 2; + var splitterScreenY = (bestLeftRect.Top + bestLeftRect.Bottom) / 2; + + var splitterPoint = new POINT { X = splitterScreenX, Y = splitterScreenY }; + ScreenToClient(bestParent, ref splitterPoint); + + GetClientRect(bestParent, out var client); + var targetClientX = client.Right / 2; + + Log.Information( + "CenterVerticalSplit: sending drag from client ({FromX},{FromY}) to ({ToX},{ToY})", + splitterPoint.X, splitterPoint.Y, targetClientX, splitterPoint.Y); + + var downLParam = MakeLParam(splitterPoint.X, splitterPoint.Y); + var moveLParam = MakeLParam(targetClientX, splitterPoint.Y); + + // WM_LBUTTONDOWN = 0x0201, WM_MOUSEMOVE = 0x0200, WM_LBUTTONUP = 0x0202 + // MK_LBUTTON = 0x0001 + SendMessage(bestParent, 0x0201, (IntPtr)0x0001, downLParam); + SendMessage(bestParent, 0x0200, (IntPtr)0x0001, moveLParam); + SendMessage(bestParent, 0x0202, IntPtr.Zero, moveLParam); + } + + static IntPtr MakeLParam(int x, int y) => + (IntPtr)((y << 16) | (x & 0xFFFF)); + + delegate bool EnumWindowsProc(IntPtr hWnd, IntPtr lParam); + + [StructLayout(LayoutKind.Sequential)] + struct RECT + { + public int Left, Top, Right, Bottom; + } + + [StructLayout(LayoutKind.Sequential)] + struct POINT + { + public int X, Y; + } + [LibraryImport("user32.dll")] [return: MarshalAs(UnmanagedType.Bool)] private static partial bool ShowWindow(IntPtr hWnd, int nCmdShow); @@ -236,4 +379,37 @@ static async Task MaximizeWindow(Process process) [LibraryImport("user32.dll")] [return: MarshalAs(UnmanagedType.Bool)] private static partial bool SetForegroundWindow(IntPtr hWnd); + + [LibraryImport("user32.dll")] + [return: MarshalAs(UnmanagedType.Bool)] + private static partial bool EnumChildWindows(IntPtr hWndParent, EnumWindowsProc lpEnumFunc, IntPtr lParam); + + [LibraryImport("user32.dll")] + [return: MarshalAs(UnmanagedType.Bool)] + private static partial bool GetWindowRect(IntPtr hWnd, out RECT lpRect); + + [LibraryImport("user32.dll")] + [return: MarshalAs(UnmanagedType.Bool)] + private static partial bool GetClientRect(IntPtr hWnd, out RECT lpRect); + + [LibraryImport("user32.dll")] + private static partial IntPtr GetParent(IntPtr hWnd); + + [LibraryImport("user32.dll")] + [return: MarshalAs(UnmanagedType.Bool)] + private static partial bool ScreenToClient(IntPtr hWnd, ref POINT lpPoint); + + [LibraryImport("user32.dll", EntryPoint = "SendMessageW")] + private static partial IntPtr SendMessage(IntPtr hWnd, uint msg, IntPtr wParam, IntPtr lParam); + + static string GetWindowClassName(IntPtr hWnd) + { + var buffer = new System.Text.StringBuilder(256); + GetClassName(hWnd, buffer, buffer.Capacity); + return buffer.ToString(); + } + + [DllImport("user32.dll", CharSet = CharSet.Auto)] + private static extern int GetClassName(IntPtr hWnd, System.Text.StringBuilder lpClassName, int nMaxCount); + } From 633fdaafb8d6d98e155195f51ab579a893e047f4 Mon Sep 17 00:00:00 2001 From: Simon Cropp Date: Mon, 23 Mar 2026 14:20:53 +1100 Subject: [PATCH 3/9] . --- src/MsExcelDiff/SpreadsheetCompare.cs | 152 +++++++++++++++++--------- 1 file changed, 98 insertions(+), 54 deletions(-) diff --git a/src/MsExcelDiff/SpreadsheetCompare.cs b/src/MsExcelDiff/SpreadsheetCompare.cs index 6c19e88..ca301e3 100644 --- a/src/MsExcelDiff/SpreadsheetCompare.cs +++ b/src/MsExcelDiff/SpreadsheetCompare.cs @@ -225,7 +225,7 @@ static async Task MaximizeWindow(Process process) // Wait briefly for the window to finish layout after maximize await Task.Delay(500); - CenterVerticalSplit(process.MainWindowHandle); + CenterSplits(process.MainWindowHandle); return; } @@ -233,9 +233,8 @@ static async Task MaximizeWindow(Process process) } } - static void CenterVerticalSplit(IntPtr mainWindow) + static void CenterSplits(IntPtr mainWindow) { - // Collect all child windows with their parent, class name, and rect var children = new List<(IntPtr Handle, IntPtr Parent, string ClassName, RECT Rect)>(); EnumChildWindows(mainWindow, (hwnd, _) => { @@ -245,8 +244,7 @@ static void CenterVerticalSplit(IntPtr mainWindow) return true; }, IntPtr.Zero); - // Log child window hierarchy for diagnostics - Log.Information("CenterVerticalSplit: found {Count} child windows", children.Count); + Log.Information("CenterSplits: found {Count} child windows", children.Count); foreach (var child in children) { var w = child.Rect.Right - child.Rect.Left; @@ -257,12 +255,21 @@ static void CenterVerticalSplit(IntPtr mainWindow) child.Rect.Left, child.Rect.Top, w, h); } - // Find the vertical splitter: look for pairs of side-by-side siblings - // with similar height that together span most of their parent's width. - // Pick the pair with the largest combined area. + CenterSplit(children, SplitOrientation.Vertical); + CenterSplit(children, SplitOrientation.Horizontal); + } + + enum SplitOrientation + { + Vertical, + Horizontal + } + + static void CenterSplit(List<(IntPtr Handle, IntPtr Parent, string ClassName, RECT Rect)> children, SplitOrientation orientation) + { var bestArea = 0; - var bestLeftRect = default(RECT); - var bestRightRect = default(RECT); + var bestFirstRect = default(RECT); + var bestSecondRect = default(RECT); var bestParent = IntPtr.Zero; foreach (var group in children.GroupBy(c => c.Parent)) @@ -275,33 +282,64 @@ static void CenterVerticalSplit(IntPtr mainWindow) { var a = siblings[i]; var b = siblings[j]; - var heightA = a.Rect.Bottom - a.Rect.Top; - var heightB = b.Rect.Bottom - b.Rect.Top; - var widthA = a.Rect.Right - a.Rect.Left; var widthB = b.Rect.Right - b.Rect.Left; + var heightA = a.Rect.Bottom - a.Rect.Top; + var heightB = b.Rect.Bottom - b.Rect.Top; - if (heightA < 100 || heightB < 100 || - widthA <= 0 || widthB <= 0) + if (widthA <= 0 || widthB <= 0 || + heightA <= 0 || heightB <= 0) { continue; } - if (Math.Abs(heightA - heightB) > 20 || - Math.Abs(a.Rect.Top - b.Rect.Top) > 20) + GetClientRect(group.Key, out var parentClient); + + bool isMatch; + if (orientation == SplitOrientation.Vertical) + { + // Side-by-side: same height/top, span parent width + isMatch = heightA >= 100 && heightB >= 100 && + Math.Abs(heightA - heightB) <= 20 && + Math.Abs(a.Rect.Top - b.Rect.Top) <= 20 && + Math.Max(a.Rect.Right, b.Rect.Right) - Math.Min(a.Rect.Left, b.Rect.Left) >= parentClient.Right * 0.8; + } + else + { + // Stacked: same width/left, span parent height + isMatch = widthA >= 100 && widthB >= 100 && + Math.Abs(widthA - widthB) <= 20 && + Math.Abs(a.Rect.Left - b.Rect.Left) <= 20 && + Math.Max(a.Rect.Bottom, b.Rect.Bottom) - Math.Min(a.Rect.Top, b.Rect.Top) >= parentClient.Bottom * 0.8; + } + + if (!isMatch) { continue; } - GetClientRect(group.Key, out var parentClient); - var totalSpan = Math.Max(a.Rect.Right, b.Rect.Right) - Math.Min(a.Rect.Left, b.Rect.Left); - if (totalSpan < parentClient.Right * 0.8) + // Require a gap between the panels (the splitter bar). + // Adjacent windows without a gap (e.g. ribbon/content) are not splits. + int gap; + if (orientation == SplitOrientation.Vertical) + { + var left = a.Rect.Left < b.Rect.Left ? a.Rect : b.Rect; + var right = a.Rect.Left < b.Rect.Left ? b.Rect : a.Rect; + gap = right.Left - left.Right; + } + else + { + var top = a.Rect.Top < b.Rect.Top ? a.Rect : b.Rect; + var bottom = a.Rect.Top < b.Rect.Top ? b.Rect : a.Rect; + gap = bottom.Top - top.Bottom; + } + + if (gap <= 0) { continue; } - var area = (a.Rect.Right - a.Rect.Left) * heightA + - (b.Rect.Right - b.Rect.Left) * heightB; + var area = widthA * heightA + widthB * heightB; if (area <= bestArea) { continue; @@ -309,15 +347,15 @@ static void CenterVerticalSplit(IntPtr mainWindow) bestArea = area; bestParent = group.Key; - if (a.Rect.Left <= b.Rect.Left) + if (orientation == SplitOrientation.Vertical) { - bestLeftRect = a.Rect; - bestRightRect = b.Rect; + bestFirstRect = a.Rect.Left <= b.Rect.Left ? a.Rect : b.Rect; + bestSecondRect = a.Rect.Left <= b.Rect.Left ? b.Rect : a.Rect; } else { - bestLeftRect = b.Rect; - bestRightRect = a.Rect; + bestFirstRect = a.Rect.Top <= b.Rect.Top ? a.Rect : b.Rect; + bestSecondRect = a.Rect.Top <= b.Rect.Top ? b.Rect : a.Rect; } } } @@ -325,38 +363,44 @@ static void CenterVerticalSplit(IntPtr mainWindow) if (bestArea == 0) { - Log.Information("CenterVerticalSplit: no matching split panel pair found"); + Log.Information("CenterSplit({Orientation}): no matching pair found", orientation); return; } - // The splitter bar sits in the gap between the two panels. - // Convert splitter screen position to parent client coordinates and - // send mouse messages directly to the parent (SplitContainer) window. - var splitterScreenX = (bestLeftRect.Right + bestRightRect.Left) / 2; - var splitterScreenY = (bestLeftRect.Top + bestLeftRect.Bottom) / 2; + // Compute splitter position and target in screen coordinates + GetWindowRect(bestParent, out var parentRect); - var splitterPoint = new POINT { X = splitterScreenX, Y = splitterScreenY }; - ScreenToClient(bestParent, ref splitterPoint); - - GetClientRect(bestParent, out var client); - var targetClientX = client.Right / 2; + int fromX, fromY, toX, toY; + if (orientation == SplitOrientation.Vertical) + { + fromX = (bestFirstRect.Right + bestSecondRect.Left) / 2; + fromY = (bestFirstRect.Top + bestFirstRect.Bottom) / 2; + toX = (parentRect.Left + parentRect.Right) / 2; + toY = fromY; + } + else + { + fromX = (bestFirstRect.Left + bestFirstRect.Right) / 2; + fromY = (bestFirstRect.Bottom + bestSecondRect.Top) / 2; + toX = fromX; + toY = (parentRect.Top + parentRect.Bottom) / 2; + } Log.Information( - "CenterVerticalSplit: sending drag from client ({FromX},{FromY}) to ({ToX},{ToY})", - splitterPoint.X, splitterPoint.Y, targetClientX, splitterPoint.Y); - - var downLParam = MakeLParam(splitterPoint.X, splitterPoint.Y); - var moveLParam = MakeLParam(targetClientX, splitterPoint.Y); - - // WM_LBUTTONDOWN = 0x0201, WM_MOUSEMOVE = 0x0200, WM_LBUTTONUP = 0x0202 - // MK_LBUTTON = 0x0001 - SendMessage(bestParent, 0x0201, (IntPtr)0x0001, downLParam); - SendMessage(bestParent, 0x0200, (IntPtr)0x0001, moveLParam); - SendMessage(bestParent, 0x0202, IntPtr.Zero, moveLParam); + "CenterSplit({Orientation}): drag screen ({FromX},{FromY}) to ({ToX},{ToY})", + orientation, fromX, fromY, toX, toY); + + SetCursorPos(fromX, fromY); + Thread.Sleep(100); + mouse_event(MOUSEEVENTF_LEFTDOWN, 0, 0, 0, IntPtr.Zero); + Thread.Sleep(100); + SetCursorPos(toX, toY); + Thread.Sleep(100); + mouse_event(MOUSEEVENTF_LEFTUP, 0, 0, 0, IntPtr.Zero); } - static IntPtr MakeLParam(int x, int y) => - (IntPtr)((y << 16) | (x & 0xFFFF)); + const uint MOUSEEVENTF_LEFTDOWN = 0x0002; + const uint MOUSEEVENTF_LEFTUP = 0x0004; delegate bool EnumWindowsProc(IntPtr hWnd, IntPtr lParam); @@ -397,10 +441,10 @@ struct POINT [LibraryImport("user32.dll")] [return: MarshalAs(UnmanagedType.Bool)] - private static partial bool ScreenToClient(IntPtr hWnd, ref POINT lpPoint); + private static partial bool SetCursorPos(int x, int y); - [LibraryImport("user32.dll", EntryPoint = "SendMessageW")] - private static partial IntPtr SendMessage(IntPtr hWnd, uint msg, IntPtr wParam, IntPtr lParam); + [LibraryImport("user32.dll")] + private static partial void mouse_event(uint dwFlags, int dx, int dy, uint dwData, IntPtr dwExtraInfo); static string GetWindowClassName(IntPtr hWnd) { From 266c38343269ffeb0e7352508a712cfa204d4ae5 Mon Sep 17 00:00:00 2001 From: Simon Cropp Date: Mon, 23 Mar 2026 14:26:26 +1100 Subject: [PATCH 4/9] Update SpreadsheetCompare.cs --- src/MsExcelDiff/SpreadsheetCompare.cs | 106 +++++++++++--------------- 1 file changed, 44 insertions(+), 62 deletions(-) diff --git a/src/MsExcelDiff/SpreadsheetCompare.cs b/src/MsExcelDiff/SpreadsheetCompare.cs index ca301e3..e9fe00f 100644 --- a/src/MsExcelDiff/SpreadsheetCompare.cs +++ b/src/MsExcelDiff/SpreadsheetCompare.cs @@ -267,10 +267,7 @@ enum SplitOrientation static void CenterSplit(List<(IntPtr Handle, IntPtr Parent, string ClassName, RECT Rect)> children, SplitOrientation orientation) { - var bestArea = 0; - var bestFirstRect = default(RECT); - var bestSecondRect = default(RECT); - var bestParent = IntPtr.Zero; + var matches = new List<(RECT First, RECT Second, IntPtr Parent)>(); foreach (var group in children.GroupBy(c => c.Parent)) { @@ -299,16 +296,14 @@ static void CenterSplit(List<(IntPtr Handle, IntPtr Parent, string ClassName, RE if (orientation == SplitOrientation.Vertical) { // Side-by-side: same height/top, span parent width - isMatch = heightA >= 100 && heightB >= 100 && - Math.Abs(heightA - heightB) <= 20 && + isMatch = Math.Abs(heightA - heightB) <= 20 && Math.Abs(a.Rect.Top - b.Rect.Top) <= 20 && Math.Max(a.Rect.Right, b.Rect.Right) - Math.Min(a.Rect.Left, b.Rect.Left) >= parentClient.Right * 0.8; } else { // Stacked: same width/left, span parent height - isMatch = widthA >= 100 && widthB >= 100 && - Math.Abs(widthA - widthB) <= 20 && + isMatch = Math.Abs(widthA - widthB) <= 20 && Math.Abs(a.Rect.Left - b.Rect.Left) <= 20 && Math.Max(a.Rect.Bottom, b.Rect.Bottom) - Math.Min(a.Rect.Top, b.Rect.Top) >= parentClient.Bottom * 0.8; } @@ -321,17 +316,18 @@ static void CenterSplit(List<(IntPtr Handle, IntPtr Parent, string ClassName, RE // Require a gap between the panels (the splitter bar). // Adjacent windows without a gap (e.g. ribbon/content) are not splits. int gap; + RECT first, second; if (orientation == SplitOrientation.Vertical) { - var left = a.Rect.Left < b.Rect.Left ? a.Rect : b.Rect; - var right = a.Rect.Left < b.Rect.Left ? b.Rect : a.Rect; - gap = right.Left - left.Right; + first = a.Rect.Left <= b.Rect.Left ? a.Rect : b.Rect; + second = a.Rect.Left <= b.Rect.Left ? b.Rect : a.Rect; + gap = second.Left - first.Right; } else { - var top = a.Rect.Top < b.Rect.Top ? a.Rect : b.Rect; - var bottom = a.Rect.Top < b.Rect.Top ? b.Rect : a.Rect; - gap = bottom.Top - top.Bottom; + first = a.Rect.Top <= b.Rect.Top ? a.Rect : b.Rect; + second = a.Rect.Top <= b.Rect.Top ? b.Rect : a.Rect; + gap = second.Top - first.Bottom; } if (gap <= 0) @@ -339,64 +335,50 @@ static void CenterSplit(List<(IntPtr Handle, IntPtr Parent, string ClassName, RE continue; } - var area = widthA * heightA + widthB * heightB; - if (area <= bestArea) - { - continue; - } - - bestArea = area; - bestParent = group.Key; - if (orientation == SplitOrientation.Vertical) - { - bestFirstRect = a.Rect.Left <= b.Rect.Left ? a.Rect : b.Rect; - bestSecondRect = a.Rect.Left <= b.Rect.Left ? b.Rect : a.Rect; - } - else - { - bestFirstRect = a.Rect.Top <= b.Rect.Top ? a.Rect : b.Rect; - bestSecondRect = a.Rect.Top <= b.Rect.Top ? b.Rect : a.Rect; - } + matches.Add((first, second, group.Key)); } } } - if (bestArea == 0) + if (matches.Count == 0) { - Log.Information("CenterSplit({Orientation}): no matching pair found", orientation); + Log.Information("CenterSplit({Orientation}): no matching pairs found", orientation); return; } - // Compute splitter position and target in screen coordinates - GetWindowRect(bestParent, out var parentRect); - - int fromX, fromY, toX, toY; - if (orientation == SplitOrientation.Vertical) + foreach (var match in matches) { - fromX = (bestFirstRect.Right + bestSecondRect.Left) / 2; - fromY = (bestFirstRect.Top + bestFirstRect.Bottom) / 2; - toX = (parentRect.Left + parentRect.Right) / 2; - toY = fromY; - } - else - { - fromX = (bestFirstRect.Left + bestFirstRect.Right) / 2; - fromY = (bestFirstRect.Bottom + bestSecondRect.Top) / 2; - toX = fromX; - toY = (parentRect.Top + parentRect.Bottom) / 2; - } + GetWindowRect(match.Parent, out var parentRect); + + int fromX, fromY, toX, toY; + if (orientation == SplitOrientation.Vertical) + { + fromX = (match.First.Right + match.Second.Left) / 2; + fromY = (match.First.Top + match.First.Bottom) / 2; + toX = (parentRect.Left + parentRect.Right) / 2; + toY = fromY; + } + else + { + fromX = (match.First.Left + match.First.Right) / 2; + fromY = (match.First.Bottom + match.Second.Top) / 2; + toX = fromX; + toY = (parentRect.Top + parentRect.Bottom) / 2; + } - Log.Information( - "CenterSplit({Orientation}): drag screen ({FromX},{FromY}) to ({ToX},{ToY})", - orientation, fromX, fromY, toX, toY); - - SetCursorPos(fromX, fromY); - Thread.Sleep(100); - mouse_event(MOUSEEVENTF_LEFTDOWN, 0, 0, 0, IntPtr.Zero); - Thread.Sleep(100); - SetCursorPos(toX, toY); - Thread.Sleep(100); - mouse_event(MOUSEEVENTF_LEFTUP, 0, 0, 0, IntPtr.Zero); + Log.Information( + "CenterSplit({Orientation}): drag screen ({FromX},{FromY}) to ({ToX},{ToY})", + orientation, fromX, fromY, toX, toY); + + SetCursorPos(fromX, fromY); + Thread.Sleep(100); + mouse_event(MOUSEEVENTF_LEFTDOWN, 0, 0, 0, IntPtr.Zero); + Thread.Sleep(100); + SetCursorPos(toX, toY); + Thread.Sleep(100); + mouse_event(MOUSEEVENTF_LEFTUP, 0, 0, 0, IntPtr.Zero); + Thread.Sleep(100); + } } const uint MOUSEEVENTF_LEFTDOWN = 0x0002; From 7d48019630dc443ad9d44aabd0af2f539cc24a5b Mon Sep 17 00:00:00 2001 From: Simon Cropp Date: Mon, 23 Mar 2026 14:30:14 +1100 Subject: [PATCH 5/9] Update SpreadsheetCompare.cs --- src/MsExcelDiff/SpreadsheetCompare.cs | 67 +++++++++++++++++---------- 1 file changed, 42 insertions(+), 25 deletions(-) diff --git a/src/MsExcelDiff/SpreadsheetCompare.cs b/src/MsExcelDiff/SpreadsheetCompare.cs index e9fe00f..cb51d9d 100644 --- a/src/MsExcelDiff/SpreadsheetCompare.cs +++ b/src/MsExcelDiff/SpreadsheetCompare.cs @@ -348,41 +348,57 @@ static void CenterSplit(List<(IntPtr Handle, IntPtr Parent, string ClassName, RE foreach (var match in matches) { - GetWindowRect(match.Parent, out var parentRect); + // Convert screen coordinates to client coordinates of the parent (SplitContainer) + var fromScreen = new POINT(); + var toScreen = new POINT(); + GetClientRect(match.Parent, out var client); - int fromX, fromY, toX, toY; if (orientation == SplitOrientation.Vertical) { - fromX = (match.First.Right + match.Second.Left) / 2; - fromY = (match.First.Top + match.First.Bottom) / 2; - toX = (parentRect.Left + parentRect.Right) / 2; - toY = fromY; + fromScreen.X = (match.First.Right + match.Second.Left) / 2; + fromScreen.Y = (match.First.Top + match.First.Bottom) / 2; + toScreen.X = fromScreen.X; // will be overwritten after conversion + toScreen.Y = fromScreen.Y; } else { - fromX = (match.First.Left + match.First.Right) / 2; - fromY = (match.First.Bottom + match.Second.Top) / 2; - toX = fromX; - toY = (parentRect.Top + parentRect.Bottom) / 2; + fromScreen.X = (match.First.Left + match.First.Right) / 2; + fromScreen.Y = (match.First.Bottom + match.Second.Top) / 2; + toScreen.X = fromScreen.X; + toScreen.Y = fromScreen.Y; } - Log.Information( - "CenterSplit({Orientation}): drag screen ({FromX},{FromY}) to ({ToX},{ToY})", - orientation, fromX, fromY, toX, toY); + ScreenToClient(match.Parent, ref fromScreen); - SetCursorPos(fromX, fromY); - Thread.Sleep(100); - mouse_event(MOUSEEVENTF_LEFTDOWN, 0, 0, 0, IntPtr.Zero); - Thread.Sleep(100); - SetCursorPos(toX, toY); - Thread.Sleep(100); - mouse_event(MOUSEEVENTF_LEFTUP, 0, 0, 0, IntPtr.Zero); + var toClient = new POINT { X = fromScreen.X, Y = fromScreen.Y }; + if (orientation == SplitOrientation.Vertical) + { + toClient.X = client.Right / 2; + } + else + { + toClient.Y = client.Bottom / 2; + } + + Log.Information( + "CenterSplit({Orientation}): PostMessage drag client ({FromX},{FromY}) to ({ToX},{ToY})", + orientation, fromScreen.X, fromScreen.Y, toClient.X, toClient.Y); + + var downLParam = MakeLParam(fromScreen.X, fromScreen.Y); + var moveLParam = MakeLParam(toClient.X, toClient.Y); + + // WM_LBUTTONDOWN=0x0201 WM_MOUSEMOVE=0x0200 WM_LBUTTONUP=0x0202 MK_LBUTTON=0x0001 + PostMessage(match.Parent, 0x0201, (IntPtr)0x0001, downLParam); + Thread.Sleep(50); + PostMessage(match.Parent, 0x0200, (IntPtr)0x0001, moveLParam); + Thread.Sleep(50); + PostMessage(match.Parent, 0x0202, IntPtr.Zero, moveLParam); Thread.Sleep(100); } } - const uint MOUSEEVENTF_LEFTDOWN = 0x0002; - const uint MOUSEEVENTF_LEFTUP = 0x0004; + static IntPtr MakeLParam(int x, int y) => + (IntPtr)((y << 16) | (x & 0xFFFF)); delegate bool EnumWindowsProc(IntPtr hWnd, IntPtr lParam); @@ -423,10 +439,11 @@ struct POINT [LibraryImport("user32.dll")] [return: MarshalAs(UnmanagedType.Bool)] - private static partial bool SetCursorPos(int x, int y); + private static partial bool ScreenToClient(IntPtr hWnd, ref POINT lpPoint); - [LibraryImport("user32.dll")] - private static partial void mouse_event(uint dwFlags, int dx, int dy, uint dwData, IntPtr dwExtraInfo); + [LibraryImport("user32.dll", EntryPoint = "PostMessageW")] + [return: MarshalAs(UnmanagedType.Bool)] + private static partial bool PostMessage(IntPtr hWnd, uint msg, IntPtr wParam, IntPtr lParam); static string GetWindowClassName(IntPtr hWnd) { From 1e11574b3b5ed856302ae6ae7fd48c007c940bd5 Mon Sep 17 00:00:00 2001 From: Simon Cropp Date: Mon, 23 Mar 2026 14:36:57 +1100 Subject: [PATCH 6/9] . --- src/MsExcelDiff/SpreadsheetCompare.cs | 248 +------------------------ src/MsExcelDiff/WindowLayout.cs | 257 ++++++++++++++++++++++++++ 2 files changed, 259 insertions(+), 246 deletions(-) create mode 100644 src/MsExcelDiff/WindowLayout.cs diff --git a/src/MsExcelDiff/SpreadsheetCompare.cs b/src/MsExcelDiff/SpreadsheetCompare.cs index cb51d9d..938ed7d 100644 --- a/src/MsExcelDiff/SpreadsheetCompare.cs +++ b/src/MsExcelDiff/SpreadsheetCompare.cs @@ -1,4 +1,4 @@ -public static partial class SpreadsheetCompare +public static class SpreadsheetCompare { static readonly string[] programFolders = [ @@ -80,7 +80,7 @@ Spreadsheet Compare (SPREADSHEETCOMPARE.EXE) was not found. using var process = await LaunchProcess(exe, tempFile); JobObject.AssignProcess(job, process.Handle); - await MaximizeWindow(process); + await WindowLayout.MaximizeAndCenterSplits(process); await process.WaitForExitAsync(); } catch when (TempFiles.TryDelete(tempFile)) @@ -211,248 +211,4 @@ internal static HashSet GetProcessPids(string processName) return null; } - static async Task MaximizeWindow(Process process) - { - // Wait for the main window to appear - for (var i = 0; i < 100; i++) - { - process.Refresh(); - if (process.MainWindowHandle != IntPtr.Zero) - { - // SW_MAXIMIZE = 3 - ShowWindow(process.MainWindowHandle, 3); - SetForegroundWindow(process.MainWindowHandle); - - // Wait briefly for the window to finish layout after maximize - await Task.Delay(500); - CenterSplits(process.MainWindowHandle); - return; - } - - await Task.Delay(100); - } - } - - static void CenterSplits(IntPtr mainWindow) - { - var children = new List<(IntPtr Handle, IntPtr Parent, string ClassName, RECT Rect)>(); - EnumChildWindows(mainWindow, (hwnd, _) => - { - GetWindowRect(hwnd, out var rect); - var className = GetWindowClassName(hwnd); - children.Add((hwnd, GetParent(hwnd), className, rect)); - return true; - }, IntPtr.Zero); - - Log.Information("CenterSplits: found {Count} child windows", children.Count); - foreach (var child in children) - { - var w = child.Rect.Right - child.Rect.Left; - var h = child.Rect.Bottom - child.Rect.Top; - Log.Information( - " hwnd={Handle} parent={Parent} class={ClassName} pos=({Left},{Top}) size={Width}x{Height}", - child.Handle, child.Parent, child.ClassName, - child.Rect.Left, child.Rect.Top, w, h); - } - - CenterSplit(children, SplitOrientation.Vertical); - CenterSplit(children, SplitOrientation.Horizontal); - } - - enum SplitOrientation - { - Vertical, - Horizontal - } - - static void CenterSplit(List<(IntPtr Handle, IntPtr Parent, string ClassName, RECT Rect)> children, SplitOrientation orientation) - { - var matches = new List<(RECT First, RECT Second, IntPtr Parent)>(); - - foreach (var group in children.GroupBy(c => c.Parent)) - { - var siblings = group.ToList(); - - for (var i = 0; i < siblings.Count; i++) - { - for (var j = i + 1; j < siblings.Count; j++) - { - var a = siblings[i]; - var b = siblings[j]; - var widthA = a.Rect.Right - a.Rect.Left; - var widthB = b.Rect.Right - b.Rect.Left; - var heightA = a.Rect.Bottom - a.Rect.Top; - var heightB = b.Rect.Bottom - b.Rect.Top; - - if (widthA <= 0 || widthB <= 0 || - heightA <= 0 || heightB <= 0) - { - continue; - } - - GetClientRect(group.Key, out var parentClient); - - bool isMatch; - if (orientation == SplitOrientation.Vertical) - { - // Side-by-side: same height/top, span parent width - isMatch = Math.Abs(heightA - heightB) <= 20 && - Math.Abs(a.Rect.Top - b.Rect.Top) <= 20 && - Math.Max(a.Rect.Right, b.Rect.Right) - Math.Min(a.Rect.Left, b.Rect.Left) >= parentClient.Right * 0.8; - } - else - { - // Stacked: same width/left, span parent height - isMatch = Math.Abs(widthA - widthB) <= 20 && - Math.Abs(a.Rect.Left - b.Rect.Left) <= 20 && - Math.Max(a.Rect.Bottom, b.Rect.Bottom) - Math.Min(a.Rect.Top, b.Rect.Top) >= parentClient.Bottom * 0.8; - } - - if (!isMatch) - { - continue; - } - - // Require a gap between the panels (the splitter bar). - // Adjacent windows without a gap (e.g. ribbon/content) are not splits. - int gap; - RECT first, second; - if (orientation == SplitOrientation.Vertical) - { - first = a.Rect.Left <= b.Rect.Left ? a.Rect : b.Rect; - second = a.Rect.Left <= b.Rect.Left ? b.Rect : a.Rect; - gap = second.Left - first.Right; - } - else - { - first = a.Rect.Top <= b.Rect.Top ? a.Rect : b.Rect; - second = a.Rect.Top <= b.Rect.Top ? b.Rect : a.Rect; - gap = second.Top - first.Bottom; - } - - if (gap <= 0) - { - continue; - } - - matches.Add((first, second, group.Key)); - } - } - } - - if (matches.Count == 0) - { - Log.Information("CenterSplit({Orientation}): no matching pairs found", orientation); - return; - } - - foreach (var match in matches) - { - // Convert screen coordinates to client coordinates of the parent (SplitContainer) - var fromScreen = new POINT(); - var toScreen = new POINT(); - GetClientRect(match.Parent, out var client); - - if (orientation == SplitOrientation.Vertical) - { - fromScreen.X = (match.First.Right + match.Second.Left) / 2; - fromScreen.Y = (match.First.Top + match.First.Bottom) / 2; - toScreen.X = fromScreen.X; // will be overwritten after conversion - toScreen.Y = fromScreen.Y; - } - else - { - fromScreen.X = (match.First.Left + match.First.Right) / 2; - fromScreen.Y = (match.First.Bottom + match.Second.Top) / 2; - toScreen.X = fromScreen.X; - toScreen.Y = fromScreen.Y; - } - - ScreenToClient(match.Parent, ref fromScreen); - - var toClient = new POINT { X = fromScreen.X, Y = fromScreen.Y }; - if (orientation == SplitOrientation.Vertical) - { - toClient.X = client.Right / 2; - } - else - { - toClient.Y = client.Bottom / 2; - } - - Log.Information( - "CenterSplit({Orientation}): PostMessage drag client ({FromX},{FromY}) to ({ToX},{ToY})", - orientation, fromScreen.X, fromScreen.Y, toClient.X, toClient.Y); - - var downLParam = MakeLParam(fromScreen.X, fromScreen.Y); - var moveLParam = MakeLParam(toClient.X, toClient.Y); - - // WM_LBUTTONDOWN=0x0201 WM_MOUSEMOVE=0x0200 WM_LBUTTONUP=0x0202 MK_LBUTTON=0x0001 - PostMessage(match.Parent, 0x0201, (IntPtr)0x0001, downLParam); - Thread.Sleep(50); - PostMessage(match.Parent, 0x0200, (IntPtr)0x0001, moveLParam); - Thread.Sleep(50); - PostMessage(match.Parent, 0x0202, IntPtr.Zero, moveLParam); - Thread.Sleep(100); - } - } - - static IntPtr MakeLParam(int x, int y) => - (IntPtr)((y << 16) | (x & 0xFFFF)); - - delegate bool EnumWindowsProc(IntPtr hWnd, IntPtr lParam); - - [StructLayout(LayoutKind.Sequential)] - struct RECT - { - public int Left, Top, Right, Bottom; - } - - [StructLayout(LayoutKind.Sequential)] - struct POINT - { - public int X, Y; - } - - [LibraryImport("user32.dll")] - [return: MarshalAs(UnmanagedType.Bool)] - private static partial bool ShowWindow(IntPtr hWnd, int nCmdShow); - - [LibraryImport("user32.dll")] - [return: MarshalAs(UnmanagedType.Bool)] - private static partial bool SetForegroundWindow(IntPtr hWnd); - - [LibraryImport("user32.dll")] - [return: MarshalAs(UnmanagedType.Bool)] - private static partial bool EnumChildWindows(IntPtr hWndParent, EnumWindowsProc lpEnumFunc, IntPtr lParam); - - [LibraryImport("user32.dll")] - [return: MarshalAs(UnmanagedType.Bool)] - private static partial bool GetWindowRect(IntPtr hWnd, out RECT lpRect); - - [LibraryImport("user32.dll")] - [return: MarshalAs(UnmanagedType.Bool)] - private static partial bool GetClientRect(IntPtr hWnd, out RECT lpRect); - - [LibraryImport("user32.dll")] - private static partial IntPtr GetParent(IntPtr hWnd); - - [LibraryImport("user32.dll")] - [return: MarshalAs(UnmanagedType.Bool)] - private static partial bool ScreenToClient(IntPtr hWnd, ref POINT lpPoint); - - [LibraryImport("user32.dll", EntryPoint = "PostMessageW")] - [return: MarshalAs(UnmanagedType.Bool)] - private static partial bool PostMessage(IntPtr hWnd, uint msg, IntPtr wParam, IntPtr lParam); - - static string GetWindowClassName(IntPtr hWnd) - { - var buffer = new System.Text.StringBuilder(256); - GetClassName(hWnd, buffer, buffer.Capacity); - return buffer.ToString(); - } - - [DllImport("user32.dll", CharSet = CharSet.Auto)] - private static extern int GetClassName(IntPtr hWnd, System.Text.StringBuilder lpClassName, int nMaxCount); - } diff --git a/src/MsExcelDiff/WindowLayout.cs b/src/MsExcelDiff/WindowLayout.cs new file mode 100644 index 0000000..e7dd3a9 --- /dev/null +++ b/src/MsExcelDiff/WindowLayout.cs @@ -0,0 +1,257 @@ +static partial class WindowLayout +{ + /// + /// Maximizes the window and centers all split containers. + /// + internal static async Task MaximizeAndCenterSplits(Process process) + { + for (var i = 0; i < 100; i++) + { + process.Refresh(); + if (process.MainWindowHandle != IntPtr.Zero) + { + // SW_MAXIMIZE = 3 + ShowWindow(process.MainWindowHandle, 3); + SetForegroundWindow(process.MainWindowHandle); + + // Wait for the window to finish layout after maximize + await Task.Delay(500); + CenterSplits(process.MainWindowHandle); + return; + } + + await Task.Delay(100); + } + } + + static void CenterSplits(IntPtr mainWindow) + { + var children = new List<(IntPtr Handle, IntPtr Parent, string ClassName, RECT Rect)>(); + EnumChildWindows( + mainWindow, + (hwnd, _) => + { + GetWindowRect(hwnd, out var rect); + var className = GetWindowClassName(hwnd); + children.Add((hwnd, GetParent(hwnd), className, rect)); + return true; + }, + IntPtr.Zero); + + Log.Information("CenterSplits: found {Count} child windows", children.Count); + foreach (var child in children) + { + var w = child.Rect.Right - child.Rect.Left; + var h = child.Rect.Bottom - child.Rect.Top; + Log.Information( + " hwnd={Handle} parent={Parent} class={ClassName} pos=({Left},{Top}) size={Width}x{Height}", + child.Handle, child.Parent, child.ClassName, + child.Rect.Left, child.Rect.Top, w, h); + } + + CenterSplit(children, SplitOrientation.Vertical); + CenterSplit(children, SplitOrientation.Horizontal); + } + + enum SplitOrientation + { + Vertical, + Horizontal + } + + /// + /// Finds all WinForms SplitContainer pairs in the given orientation and centers each splitter. + /// Identifies split panels by looking for sibling window pairs that: + /// - have matching dimensions on the shared axis (height for vertical, width for horizontal) + /// - together span most of their parent's extent + /// - have a gap between them (the splitter bar) + /// Uses PostMessage to simulate a mouse drag on each splitter, which goes through the + /// target app's message queue so SetCapture works correctly for the drag operation. + /// + static void CenterSplit( + List<(IntPtr Handle, IntPtr Parent, string ClassName, RECT Rect)> children, + SplitOrientation orientation) + { + var matches = new List<(RECT First, RECT Second, IntPtr Parent)>(); + + foreach (var group in children.GroupBy(c => c.Parent)) + { + var siblings = group.ToList(); + + for (var i = 0; i < siblings.Count; i++) + { + for (var j = i + 1; j < siblings.Count; j++) + { + var a = siblings[i]; + var b = siblings[j]; + var widthA = a.Rect.Right - a.Rect.Left; + var widthB = b.Rect.Right - b.Rect.Left; + var heightA = a.Rect.Bottom - a.Rect.Top; + var heightB = b.Rect.Bottom - b.Rect.Top; + + if (widthA <= 0 || widthB <= 0 || + heightA <= 0 || heightB <= 0) + { + continue; + } + + GetClientRect(group.Key, out var parentClient); + + bool isMatch; + if (orientation == SplitOrientation.Vertical) + { + // Side-by-side: same height/top, span parent width + isMatch = Math.Abs(heightA - heightB) <= 20 && + Math.Abs(a.Rect.Top - b.Rect.Top) <= 20 && + Math.Max(a.Rect.Right, b.Rect.Right) - Math.Min(a.Rect.Left, b.Rect.Left) >= parentClient.Right * 0.8; + } + else + { + // Stacked: same width/left, span parent height + isMatch = Math.Abs(widthA - widthB) <= 20 && + Math.Abs(a.Rect.Left - b.Rect.Left) <= 20 && + Math.Max(a.Rect.Bottom, b.Rect.Bottom) - Math.Min(a.Rect.Top, b.Rect.Top) >= parentClient.Bottom * 0.8; + } + + if (!isMatch) + { + continue; + } + + // Require a gap between the panels (the splitter bar). + // Adjacent windows without a gap (e.g. ribbon/content) are not splits. + int gap; + RECT first, second; + if (orientation == SplitOrientation.Vertical) + { + first = a.Rect.Left <= b.Rect.Left ? a.Rect : b.Rect; + second = a.Rect.Left <= b.Rect.Left ? b.Rect : a.Rect; + gap = second.Left - first.Right; + } + else + { + first = a.Rect.Top <= b.Rect.Top ? a.Rect : b.Rect; + second = a.Rect.Top <= b.Rect.Top ? b.Rect : a.Rect; + gap = second.Top - first.Bottom; + } + + if (gap <= 0) + { + continue; + } + + matches.Add((first, second, group.Key)); + } + } + } + + if (matches.Count == 0) + { + Log.Information("CenterSplit({Orientation}): no matching pairs found", orientation); + return; + } + + foreach (var match in matches) + { + // Convert screen coordinates to client coordinates of the parent (SplitContainer) + var fromScreen = new POINT(); + GetClientRect(match.Parent, out var client); + + if (orientation == SplitOrientation.Vertical) + { + fromScreen.X = (match.First.Right + match.Second.Left) / 2; + fromScreen.Y = (match.First.Top + match.First.Bottom) / 2; + } + else + { + fromScreen.X = (match.First.Left + match.First.Right) / 2; + fromScreen.Y = (match.First.Bottom + match.Second.Top) / 2; + } + + ScreenToClient(match.Parent, ref fromScreen); + + var toClient = new POINT { X = fromScreen.X, Y = fromScreen.Y }; + if (orientation == SplitOrientation.Vertical) + { + toClient.X = client.Right / 2; + } + else + { + toClient.Y = client.Bottom / 2; + } + + Log.Information( + "CenterSplit({Orientation}): PostMessage drag client ({FromX},{FromY}) to ({ToX},{ToY})", + orientation, fromScreen.X, fromScreen.Y, toClient.X, toClient.Y); + + var downLParam = MakeLParam(fromScreen.X, fromScreen.Y); + var moveLParam = MakeLParam(toClient.X, toClient.Y); + + // WM_LBUTTONDOWN=0x0201 WM_MOUSEMOVE=0x0200 WM_LBUTTONUP=0x0202 MK_LBUTTON=0x0001 + PostMessage(match.Parent, 0x0201, 0x0001, downLParam); + Thread.Sleep(50); + PostMessage(match.Parent, 0x0200, 0x0001, moveLParam); + Thread.Sleep(50); + PostMessage(match.Parent, 0x0202, IntPtr.Zero, moveLParam); + Thread.Sleep(100); + } + } + + static IntPtr MakeLParam(int x, int y) => + (y << 16) | (x & 0xFFFF); + + static string GetWindowClassName(IntPtr hWnd) + { + var buffer = new StringBuilder(256); + GetClassName(hWnd, buffer, buffer.Capacity); + return buffer.ToString(); + } + + delegate bool EnumWindowsProc(IntPtr hWnd, IntPtr lParam); + + [StructLayout(LayoutKind.Sequential)] + struct RECT + { + public int Left, Top, Right, Bottom; + } + + [StructLayout(LayoutKind.Sequential)] + struct POINT + { + public int X, Y; + } + + [LibraryImport("user32.dll")] + [return: MarshalAs(UnmanagedType.Bool)] + private static partial bool ShowWindow(IntPtr hWnd, int nCmdShow); + + [LibraryImport("user32.dll")] + [return: MarshalAs(UnmanagedType.Bool)] + private static partial bool SetForegroundWindow(IntPtr hWnd); + + [LibraryImport("user32.dll")] + [return: MarshalAs(UnmanagedType.Bool)] + private static partial bool EnumChildWindows(IntPtr hWndParent, EnumWindowsProc lpEnumFunc, IntPtr lParam); + + [LibraryImport("user32.dll")] + [return: MarshalAs(UnmanagedType.Bool)] + private static partial bool GetWindowRect(IntPtr hWnd, out RECT lpRect); + + [LibraryImport("user32.dll")] + [return: MarshalAs(UnmanagedType.Bool)] + private static partial bool GetClientRect(IntPtr hWnd, out RECT lpRect); + + [LibraryImport("user32.dll")] + private static partial IntPtr GetParent(IntPtr hWnd); + + [LibraryImport("user32.dll")] + [return: MarshalAs(UnmanagedType.Bool)] + private static partial bool ScreenToClient(IntPtr hWnd, ref POINT lpPoint); + + [LibraryImport("user32.dll", EntryPoint = "PostMessageW")] + [return: MarshalAs(UnmanagedType.Bool)] + private static partial bool PostMessage(IntPtr hWnd, uint msg, IntPtr wParam, IntPtr lParam); + + [DllImport("user32.dll", CharSet = CharSet.Auto)] + private static extern int GetClassName(IntPtr hWnd, StringBuilder lpClassName, int nMaxCount); +} From 28c50759a1993947aa529535e45abec42bfc7af0 Mon Sep 17 00:00:00 2001 From: Simon Cropp Date: Mon, 23 Mar 2026 14:44:09 +1100 Subject: [PATCH 7/9] Update WindowLayout.cs --- src/MsExcelDiff/WindowLayout.cs | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/src/MsExcelDiff/WindowLayout.cs b/src/MsExcelDiff/WindowLayout.cs index e7dd3a9..589e362 100644 --- a/src/MsExcelDiff/WindowLayout.cs +++ b/src/MsExcelDiff/WindowLayout.cs @@ -11,11 +11,9 @@ internal static async Task MaximizeAndCenterSplits(Process process) if (process.MainWindowHandle != IntPtr.Zero) { // SW_MAXIMIZE = 3 + // ShowWindow is synchronous — WinForms processes WM_SIZE and + // lays out child controls before it returns, so no delay needed. ShowWindow(process.MainWindowHandle, 3); - SetForegroundWindow(process.MainWindowHandle); - - // Wait for the window to finish layout after maximize - await Task.Delay(500); CenterSplits(process.MainWindowHandle); return; } @@ -225,10 +223,6 @@ struct POINT [return: MarshalAs(UnmanagedType.Bool)] private static partial bool ShowWindow(IntPtr hWnd, int nCmdShow); - [LibraryImport("user32.dll")] - [return: MarshalAs(UnmanagedType.Bool)] - private static partial bool SetForegroundWindow(IntPtr hWnd); - [LibraryImport("user32.dll")] [return: MarshalAs(UnmanagedType.Bool)] private static partial bool EnumChildWindows(IntPtr hWndParent, EnumWindowsProc lpEnumFunc, IntPtr lParam); From f3faef7a0409305d4ce3f90deb42fff3107602f7 Mon Sep 17 00:00:00 2001 From: Simon Cropp Date: Mon, 23 Mar 2026 14:49:06 +1100 Subject: [PATCH 8/9] Update WindowLayout.cs --- src/MsExcelDiff/WindowLayout.cs | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/MsExcelDiff/WindowLayout.cs b/src/MsExcelDiff/WindowLayout.cs index 589e362..e5a1355 100644 --- a/src/MsExcelDiff/WindowLayout.cs +++ b/src/MsExcelDiff/WindowLayout.cs @@ -14,6 +14,7 @@ internal static async Task MaximizeAndCenterSplits(Process process) // ShowWindow is synchronous — WinForms processes WM_SIZE and // lays out child controls before it returns, so no delay needed. ShowWindow(process.MainWindowHandle, 3); + await Task.Delay(500); CenterSplits(process.MainWindowHandle); return; } @@ -36,12 +37,12 @@ static void CenterSplits(IntPtr mainWindow) }, IntPtr.Zero); - Log.Information("CenterSplits: found {Count} child windows", children.Count); + Log.Debug("CenterSplits: found {Count} child windows", children.Count); foreach (var child in children) { var w = child.Rect.Right - child.Rect.Left; var h = child.Rect.Bottom - child.Rect.Top; - Log.Information( + Log.Debug( " hwnd={Handle} parent={Parent} class={ClassName} pos=({Left},{Top}) size={Width}x{Height}", child.Handle, child.Parent, child.ClassName, child.Rect.Left, child.Rect.Top, w, h); @@ -145,7 +146,7 @@ static void CenterSplit( if (matches.Count == 0) { - Log.Information("CenterSplit({Orientation}): no matching pairs found", orientation); + Log.Debug("CenterSplit({Orientation}): no matching pairs found", orientation); return; } @@ -178,7 +179,7 @@ static void CenterSplit( toClient.Y = client.Bottom / 2; } - Log.Information( + Log.Debug( "CenterSplit({Orientation}): PostMessage drag client ({FromX},{FromY}) to ({ToX},{ToY})", orientation, fromScreen.X, fromScreen.Y, toClient.X, toClient.Y); From 7384a4f06a91e10f9252b28782c5e0ce40c43f69 Mon Sep 17 00:00:00 2001 From: Simon Cropp Date: Mon, 23 Mar 2026 14:50:52 +1100 Subject: [PATCH 9/9] Update Directory.Build.props --- src/Directory.Build.props | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Directory.Build.props b/src/Directory.Build.props index b12eb47..b19020f 100644 --- a/src/Directory.Build.props +++ b/src/Directory.Build.props @@ -1,7 +1,7 @@ - 0.4.3 + 0.4.4 Word, Diff, Compare A .NET tool that compares two Word/Excel documents using the Microsoft Word/Excel built-in document comparison features. enable