|
| 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