Skip to content

Commit 7529cd0

Browse files
authored
Fix Excel diff layout (#11)
1 parent ef0afe2 commit 7529cd0

3 files changed

Lines changed: 254 additions & 1 deletion

File tree

src/Directory.Build.props

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
<?xml version="1.0" encoding="utf-8"?>
22
<Project>
33
<PropertyGroup>
4-
<Version>0.4.3</Version>
4+
<Version>0.4.4</Version>
55
<PackageTags>Word, Diff, Compare</PackageTags>
66
<Description>A .NET tool that compares two Word/Excel documents using the Microsoft Word/Excel built-in document comparison features.</Description>
77
<Nullable>enable</Nullable>

src/MsExcelDiff/SpreadsheetCompare.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,7 @@ Spreadsheet Compare (SPREADSHEETCOMPARE.EXE) was not found.
8080
using var process = await LaunchProcess(exe, tempFile);
8181

8282
JobObject.AssignProcess(job, process.Handle);
83+
await WindowLayout.MaximizeAndCenterSplits(process);
8384
await process.WaitForExitAsync();
8485
}
8586
catch when (TempFiles.TryDelete(tempFile))

src/MsExcelDiff/WindowLayout.cs

Lines changed: 252 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,252 @@
1+
static partial class WindowLayout
2+
{
3+
/// <summary>
4+
/// Maximizes the window and centers all split containers.
5+
/// </summary>
6+
internal static async Task MaximizeAndCenterSplits(Process process)
7+
{
8+
for (var i = 0; i < 100; i++)
9+
{
10+
process.Refresh();
11+
if (process.MainWindowHandle != IntPtr.Zero)
12+
{
13+
// SW_MAXIMIZE = 3
14+
// ShowWindow is synchronous — WinForms processes WM_SIZE and
15+
// lays out child controls before it returns, so no delay needed.
16+
ShowWindow(process.MainWindowHandle, 3);
17+
await Task.Delay(500);
18+
CenterSplits(process.MainWindowHandle);
19+
return;
20+
}
21+
22+
await Task.Delay(100);
23+
}
24+
}
25+
26+
static void CenterSplits(IntPtr mainWindow)
27+
{
28+
var children = new List<(IntPtr Handle, IntPtr Parent, string ClassName, RECT Rect)>();
29+
EnumChildWindows(
30+
mainWindow,
31+
(hwnd, _) =>
32+
{
33+
GetWindowRect(hwnd, out var rect);
34+
var className = GetWindowClassName(hwnd);
35+
children.Add((hwnd, GetParent(hwnd), className, rect));
36+
return true;
37+
},
38+
IntPtr.Zero);
39+
40+
Log.Debug("CenterSplits: found {Count} child windows", children.Count);
41+
foreach (var child in children)
42+
{
43+
var w = child.Rect.Right - child.Rect.Left;
44+
var h = child.Rect.Bottom - child.Rect.Top;
45+
Log.Debug(
46+
" hwnd={Handle} parent={Parent} class={ClassName} pos=({Left},{Top}) size={Width}x{Height}",
47+
child.Handle, child.Parent, child.ClassName,
48+
child.Rect.Left, child.Rect.Top, w, h);
49+
}
50+
51+
CenterSplit(children, SplitOrientation.Vertical);
52+
CenterSplit(children, SplitOrientation.Horizontal);
53+
}
54+
55+
enum SplitOrientation
56+
{
57+
Vertical,
58+
Horizontal
59+
}
60+
61+
/// <summary>
62+
/// Finds all WinForms SplitContainer pairs in the given orientation and centers each splitter.
63+
/// Identifies split panels by looking for sibling window pairs that:
64+
/// - have matching dimensions on the shared axis (height for vertical, width for horizontal)
65+
/// - together span most of their parent's extent
66+
/// - have a gap between them (the splitter bar)
67+
/// Uses PostMessage to simulate a mouse drag on each splitter, which goes through the
68+
/// target app's message queue so SetCapture works correctly for the drag operation.
69+
/// </summary>
70+
static void CenterSplit(
71+
List<(IntPtr Handle, IntPtr Parent, string ClassName, RECT Rect)> children,
72+
SplitOrientation orientation)
73+
{
74+
var matches = new List<(RECT First, RECT Second, IntPtr Parent)>();
75+
76+
foreach (var group in children.GroupBy(c => c.Parent))
77+
{
78+
var siblings = group.ToList();
79+
80+
for (var i = 0; i < siblings.Count; i++)
81+
{
82+
for (var j = i + 1; j < siblings.Count; j++)
83+
{
84+
var a = siblings[i];
85+
var b = siblings[j];
86+
var widthA = a.Rect.Right - a.Rect.Left;
87+
var widthB = b.Rect.Right - b.Rect.Left;
88+
var heightA = a.Rect.Bottom - a.Rect.Top;
89+
var heightB = b.Rect.Bottom - b.Rect.Top;
90+
91+
if (widthA <= 0 || widthB <= 0 ||
92+
heightA <= 0 || heightB <= 0)
93+
{
94+
continue;
95+
}
96+
97+
GetClientRect(group.Key, out var parentClient);
98+
99+
bool isMatch;
100+
if (orientation == SplitOrientation.Vertical)
101+
{
102+
// Side-by-side: same height/top, span parent width
103+
isMatch = Math.Abs(heightA - heightB) <= 20 &&
104+
Math.Abs(a.Rect.Top - b.Rect.Top) <= 20 &&
105+
Math.Max(a.Rect.Right, b.Rect.Right) - Math.Min(a.Rect.Left, b.Rect.Left) >= parentClient.Right * 0.8;
106+
}
107+
else
108+
{
109+
// Stacked: same width/left, span parent height
110+
isMatch = Math.Abs(widthA - widthB) <= 20 &&
111+
Math.Abs(a.Rect.Left - b.Rect.Left) <= 20 &&
112+
Math.Max(a.Rect.Bottom, b.Rect.Bottom) - Math.Min(a.Rect.Top, b.Rect.Top) >= parentClient.Bottom * 0.8;
113+
}
114+
115+
if (!isMatch)
116+
{
117+
continue;
118+
}
119+
120+
// Require a gap between the panels (the splitter bar).
121+
// Adjacent windows without a gap (e.g. ribbon/content) are not splits.
122+
int gap;
123+
RECT first, second;
124+
if (orientation == SplitOrientation.Vertical)
125+
{
126+
first = a.Rect.Left <= b.Rect.Left ? a.Rect : b.Rect;
127+
second = a.Rect.Left <= b.Rect.Left ? b.Rect : a.Rect;
128+
gap = second.Left - first.Right;
129+
}
130+
else
131+
{
132+
first = a.Rect.Top <= b.Rect.Top ? a.Rect : b.Rect;
133+
second = a.Rect.Top <= b.Rect.Top ? b.Rect : a.Rect;
134+
gap = second.Top - first.Bottom;
135+
}
136+
137+
if (gap <= 0)
138+
{
139+
continue;
140+
}
141+
142+
matches.Add((first, second, group.Key));
143+
}
144+
}
145+
}
146+
147+
if (matches.Count == 0)
148+
{
149+
Log.Debug("CenterSplit({Orientation}): no matching pairs found", orientation);
150+
return;
151+
}
152+
153+
foreach (var match in matches)
154+
{
155+
// Convert screen coordinates to client coordinates of the parent (SplitContainer)
156+
var fromScreen = new POINT();
157+
GetClientRect(match.Parent, out var client);
158+
159+
if (orientation == SplitOrientation.Vertical)
160+
{
161+
fromScreen.X = (match.First.Right + match.Second.Left) / 2;
162+
fromScreen.Y = (match.First.Top + match.First.Bottom) / 2;
163+
}
164+
else
165+
{
166+
fromScreen.X = (match.First.Left + match.First.Right) / 2;
167+
fromScreen.Y = (match.First.Bottom + match.Second.Top) / 2;
168+
}
169+
170+
ScreenToClient(match.Parent, ref fromScreen);
171+
172+
var toClient = new POINT { X = fromScreen.X, Y = fromScreen.Y };
173+
if (orientation == SplitOrientation.Vertical)
174+
{
175+
toClient.X = client.Right / 2;
176+
}
177+
else
178+
{
179+
toClient.Y = client.Bottom / 2;
180+
}
181+
182+
Log.Debug(
183+
"CenterSplit({Orientation}): PostMessage drag client ({FromX},{FromY}) to ({ToX},{ToY})",
184+
orientation, fromScreen.X, fromScreen.Y, toClient.X, toClient.Y);
185+
186+
var downLParam = MakeLParam(fromScreen.X, fromScreen.Y);
187+
var moveLParam = MakeLParam(toClient.X, toClient.Y);
188+
189+
// WM_LBUTTONDOWN=0x0201 WM_MOUSEMOVE=0x0200 WM_LBUTTONUP=0x0202 MK_LBUTTON=0x0001
190+
PostMessage(match.Parent, 0x0201, 0x0001, downLParam);
191+
Thread.Sleep(50);
192+
PostMessage(match.Parent, 0x0200, 0x0001, moveLParam);
193+
Thread.Sleep(50);
194+
PostMessage(match.Parent, 0x0202, IntPtr.Zero, moveLParam);
195+
Thread.Sleep(100);
196+
}
197+
}
198+
199+
static IntPtr MakeLParam(int x, int y) =>
200+
(y << 16) | (x & 0xFFFF);
201+
202+
static string GetWindowClassName(IntPtr hWnd)
203+
{
204+
var buffer = new StringBuilder(256);
205+
GetClassName(hWnd, buffer, buffer.Capacity);
206+
return buffer.ToString();
207+
}
208+
209+
delegate bool EnumWindowsProc(IntPtr hWnd, IntPtr lParam);
210+
211+
[StructLayout(LayoutKind.Sequential)]
212+
struct RECT
213+
{
214+
public int Left, Top, Right, Bottom;
215+
}
216+
217+
[StructLayout(LayoutKind.Sequential)]
218+
struct POINT
219+
{
220+
public int X, Y;
221+
}
222+
223+
[LibraryImport("user32.dll")]
224+
[return: MarshalAs(UnmanagedType.Bool)]
225+
private static partial bool ShowWindow(IntPtr hWnd, int nCmdShow);
226+
227+
[LibraryImport("user32.dll")]
228+
[return: MarshalAs(UnmanagedType.Bool)]
229+
private static partial bool EnumChildWindows(IntPtr hWndParent, EnumWindowsProc lpEnumFunc, IntPtr lParam);
230+
231+
[LibraryImport("user32.dll")]
232+
[return: MarshalAs(UnmanagedType.Bool)]
233+
private static partial bool GetWindowRect(IntPtr hWnd, out RECT lpRect);
234+
235+
[LibraryImport("user32.dll")]
236+
[return: MarshalAs(UnmanagedType.Bool)]
237+
private static partial bool GetClientRect(IntPtr hWnd, out RECT lpRect);
238+
239+
[LibraryImport("user32.dll")]
240+
private static partial IntPtr GetParent(IntPtr hWnd);
241+
242+
[LibraryImport("user32.dll")]
243+
[return: MarshalAs(UnmanagedType.Bool)]
244+
private static partial bool ScreenToClient(IntPtr hWnd, ref POINT lpPoint);
245+
246+
[LibraryImport("user32.dll", EntryPoint = "PostMessageW")]
247+
[return: MarshalAs(UnmanagedType.Bool)]
248+
private static partial bool PostMessage(IntPtr hWnd, uint msg, IntPtr wParam, IntPtr lParam);
249+
250+
[DllImport("user32.dll", CharSet = CharSet.Auto)]
251+
private static extern int GetClassName(IntPtr hWnd, StringBuilder lpClassName, int nMaxCount);
252+
}

0 commit comments

Comments
 (0)