GPAL - Generally Positive Automation Library v1.0
GPAL The Fluent Automation LIbrary
Loading...
Searching...
No Matches
FormHelper.cs
1// =============================================================================
2// GPAL - Generally Positive Automation Library
3// Copyright © 2026 Software Decisions, Inc. All rights reserved.
4//
5// This file is part of GPAL.
6// Licensed under the Business Source License 1.1
7//
8// Primary development, architecture, and vision by Michael B. Vederman,
9// CEO of Software Decisions, Inc., Texas.
10//
11// Internal development maintained privately.
12// Public releases appear on GitHub: https://github.com/SoftwareDecisionsInc/GPAL.
13//
14// See LICENSE for full terms, including Additional Use Grant.
15// =============================================================================
16
17using System;
18using System.Collections.Generic;
19using System.Diagnostics;
20using System.Linq;
21using System.Runtime.InteropServices;
22using System.Text;
23using System.Text.RegularExpressions;
24using System.Threading;
25using System.Threading.Tasks;
26using System.Windows.Forms;
27using static GenerallyPositive.Enums;
29using HWND = System.IntPtr;
30
32{
33 internal class FormHelper
34 {
41 public static void FillInFrom(GPALControl controlEntry, string useText, WriteMode writeMode)
42 {
43 Control control = controlEntry.WindowsControl as Control;
44
45 if (null == control)
46 GPAL.PublishSimpleEvent(GPALEventType.WARNING,
47 $"Control [{controlEntry.Name}] is not on a form yet, so nothing was written to it",
48 null, GPALObjectType.GPALForm);
49 else if (FormControlType.Input != controlEntry.ControlType
50 && FormControlType.TextArea != controlEntry.ControlType)
51 GPAL.PublishSimpleEvent(GPALEventType.WARNING,
52 $"A [{controlEntry.ControlType}] does not take text, so nothing was written to [{controlEntry.Name}]",
53 null, GPALObjectType.GPALForm);
54 else
55 GPALControl.OnUi(control, () => control.Text = TextFor(control, useText, writeMode));
56 }
57
68 private static string TextFor(Control control, string useText, WriteMode writeMode)
69 {
70 string current = control.Text ?? string.Empty;
71 string retVal = useText;
72
73 if (WriteMode.Append == writeMode)
74 retVal = current + useText;
75 else if (WriteMode.Insert == writeMode)
76 {
77 // a text box knows where the caret is, anything else has no caret to speak of and takes it at the end
78 int at = control is TextBoxBase box ? box.SelectionStart : current.Length;
79
80 retVal = current.Insert(Math.Min(Math.Max(at, 0), current.Length), useText);
81 }
82
83 return retVal;
84 }
85 public static void FillInWithTokens(GPALForm myForm, List<IGPALGrid<string>> tokenList, WriteMode writeMode)
86 {
87 foreach (IGPALGrid<string> tokens in tokenList)
88 {
89 // iterate over every row of tokens, filling in the form's editable controls a row at a time
90 for (int tokenIdx = 0; tokenIdx < tokens.Count(); tokenIdx++)
91 {
92 int elementIdx = 0;
93 List<string> currentRow = tokens[tokenIdx];
94
95 foreach (string token in currentRow) // iterate over tokens, filling in the corresponding input/textarea webelement
96 {
97 bool textFilled = false;
98
99 while (false == textFilled && elementIdx < myForm.ControlList.Count)
100 {
101 switch (myForm.ControlList[elementIdx].ControlType)
102 {
103 case FormControlType.TextArea:
104 case FormControlType.Input:
105 myForm.Form.SwitchToTab(myForm.ControlList[elementIdx]); // bring the control's tab into view so the change is visible as it happens
106 FillInFrom(myForm.ControlList[elementIdx], token, writeMode);
107 textFilled = true;
108 break;
109
110 default:
111 break;
112 }
113 elementIdx++;
114 }
115
116 // detect if we have too many tokens to consume and publish an information event [entirely possible this scenario is fine]
117 if (elementIdx == myForm.ControlList.Count && false == textFilled)
118 GPAL.PublishSimpleEvent(GPALEventType.WARNING, $"Unused token: [{token}]", myForm, GPALObjectType.GPALForm);
119 }
120
121 // detect if we do not have enough tokens for all the form fields [entirely possible this scenario is fine]
122 while (elementIdx < myForm.ControlList.Count)
123 {
124 switch (myForm.ControlList[elementIdx].ControlType)
125 {
126 case FormControlType.TextArea:
127 case FormControlType.Input:
128 GPAL.PublishSimpleEvent(GPALEventType.WARNING, $"No token for control: [{myForm.ControlList[elementIdx].Name}]", myForm, GPALObjectType.GPALForm);
129 break;
130
131 default:
132 break;
133 }
134 elementIdx++;
135 }
136
137 // CAVEAT: Handled/NotHandled are both ignored here, the handler can request to exit by returning CallIfStatus.Terminate
138 if (null != myForm.Form.CallAfterFillIn)
139 {
140 CallIfStatus handled = myForm.Form.CallAfterFillIn(myForm, tokens, tokenIdx);
141
142 if (CallIfStatus.Terminate == handled)
143 {
144 GPAL.PublishSimpleEvent(GPALEventType.INFO, $"CallAfterFillIn handler [{myForm.Form.CallAfterFillIn.GetInvocationList()[0].Method.Name}] requested program termination.", myForm, GPALObjectType.GPALForm);
145 throw new GPALException($"{GPAL.MyMethodName()}: CallAfterFillIn handler [{myForm.Form.CallAfterFillIn.GetInvocationList()[0].Method.Name}] requested program termination.");
146 }
147 }
148 }
149 }
150 }
151 public static HWND WaitForWindow(string windowTitle, int waitTimeInSeconds = 10)
152 {
153 return WaitForWindowGeneric(windowTitle, false, waitTimeInSeconds);
154 }
155
156 public static HWND WaitForWindowRegex(string windowTitleRegex, int waitTimeInSeconds)
157 {
158 return WaitForWindowGeneric(windowTitleRegex, true, waitTimeInSeconds);
159 }
160
161 public static HWND WaitForWindowGeneric(string waitForTitle, bool useRegex, int waitTimeInSeconds)
162 {
163 bool found = false;
164 HWND retHWND = IntPtr.Zero;
165
166 for (int cnt = 0; cnt < waitTimeInSeconds && false == found; cnt++)
167 {
168 Thread.Sleep(1000); // best to sleep first and give a second more for the window to appear
169 IDictionary<HWND, string> windows = GetOpenWindows();
170 foreach (var entry in windows)
171 {
172 if (true == useRegex)
173 {
174 MatchCollection mc = Regex.Matches(entry.Value, waitForTitle);
175 if (0 < mc.Count)
176 {
177 found = true;
178 retHWND = entry.Key;
179 break;
180 }
181 }
182 else
183 {
184 if (true == waitForTitle.Equals(entry.Value))
185 {
186 found = true;
187 retHWND = entry.Key;
188 break;
189 }
190 }
191 }
192 }
193 return retHWND;
194 }
195
209 public static HWND WaitForCommonDialog(int processId, int waitTimeInSeconds = 10)
210 {
211 HWND retHWND = IntPtr.Zero;
212 Stopwatch waited = Stopwatch.StartNew();
213
214 // what was already on screen before the waiting started. A dialog that was there before anything
215 // was asked for is not the answer to what was asked for: a reminder, a prompt from another program,
216 // whatever the user left open. #32770 is the class of every one of them, so the class alone cannot
217 // tell them apart and the moment it appeared is what does
218 var before = new HashSet<HWND>(CommonDialogsOf(0));
219
220 while (IntPtr.Zero == retHWND && waited.ElapsedMilliseconds < waitTimeInSeconds * 1000)
221 {
222 List<HWND> appeared = new List<HWND>();
223
224 // a dialog qualifies two ways: it turned up while we were waiting, or it was already there and
225 // is shaped like a file dialog. The second is what catches one that opened between the click
226 // and the first look, which is every download after the first once the browser is warm
227 foreach (HWND window in CommonDialogsOf(0))
228 if (false == before.Contains(window) || true == LooksLikeAFileDialog(window))
229 appeared.Add(window);
230
231 // among the new ones, the one belonging to the process that was asked about
232 foreach (HWND window in appeared)
233 {
234 GetWindowThreadProcessId(window, out int owner);
235
236 if (IntPtr.Zero == retHWND && owner == processId)
237 retHWND = window;
238 }
239
240 // and failing that, whichever new one turned up. The owning process is a preference because a
241 // browser launched through a driver, or one that relaunched itself, owns its windows from a
242 // process nobody here was ever handed
243 if (IntPtr.Zero == retHWND && 0 < appeared.Count)
244 {
245 GetWindowThreadProcessId(appeared[0], out int owner);
246
247 GPAL.PublishSimpleEvent(GPALEventType.DEBUG, $"A common dialog appeared belonging to process [{owner}] rather than [{processId}], and is taken as the one that was waited for.", null, GPALObjectType.None);
248
249 retHWND = appeared[0];
250 }
251
252 if (IntPtr.Zero == retHWND)
253 Thread.Sleep(250);
254 }
255
256 if (IntPtr.Zero == retHWND)
257 GPAL.PublishSimpleEvent(GPALEventType.DEBUG, $"No common dialog for process [{processId}] in [{waitTimeInSeconds}]s. {VisibleWindows()}", null, GPALObjectType.None);
258
259 return retHWND;
260 }
261
271 public static string TitleOf(HWND window)
272 {
273 var title = new StringBuilder(256);
274
275 GetWindowText(window, title, title.Capacity);
276
277 return title.ToString();
278 }
279
288 public static string VisibleWindows()
289 {
290 var seen = new List<string>();
291
292 EnumWindows(delegate (HWND hWnd, int lParam)
293 {
294 if (true == IsWindowVisible(hWnd))
295 {
296 var className = new StringBuilder(256);
297 var title = new StringBuilder(256);
298
299 GetClassName(hWnd, className, className.Capacity);
300 GetWindowText(hWnd, title, title.Capacity);
301 GetWindowThreadProcessId(hWnd, out int owner);
302
303 if (0 < className.Length)
304 seen.Add($"[{className}] '{title}' (process {owner})");
305 }
306
307 return true;
308
309 }, 0);
310
311 return 0 == seen.Count ? "No visible windows were found at all." : $"What is on screen: {string.Join(" | ", seen)}";
312 }
313
319 public static List<HWND> CommonDialogsOf(int processId)
320 {
321 var found = new List<HWND>();
322
323 EnumWindows(delegate (HWND hWnd, int lParam)
324 {
325 if (true == IsWindowVisible(hWnd))
326 {
327 var className = new StringBuilder(64);
328
329 GetClassName(hWnd, className, className.Capacity);
330
331 if (COMMON_DIALOG_CLASS == className.ToString())
332 {
333 GetWindowThreadProcessId(hWnd, out int owner);
334
335 if (0 == processId || owner == processId)
336 found.Add(hWnd);
337 }
338 }
339
340 return true;
341
342 }, 0);
343
344 return found;
345 }
346
358 public static bool LooksLikeAFileDialog(HWND dialog)
359 {
360 bool accept = false;
361 bool typing = false;
362
363 EnumChildWindows(dialog, delegate (HWND child, int lParam)
364 {
365 if (ACCEPT_CONTROL_ID == GetDlgCtrlID(child))
366 accept = true;
367
368 var className = new StringBuilder(64);
369
370 GetClassName(child, className, className.Capacity);
371
372 string name = className.ToString();
373
374 if ("Edit" == name || true == name.StartsWith("ComboBox"))
375 typing = true;
376
377 return true;
378
379 }, 0);
380
381 return true == accept && true == typing;
382 }
383
384 // the class every Open, Save As and folder picker has carried since Windows 3
385 private const string COMMON_DIALOG_CLASS = "#32770";
386
387 // IDOK, the accept button on any dialog that has one
388 private const int ACCEPT_CONTROL_ID = 1;
389
390 [DllImport("USER32.DLL")]
391 private static extern bool EnumChildWindows(HWND parent, EnumWindowsProc enumFunc, int lParam);
392
393 [DllImport("USER32.DLL")]
394 private static extern int GetDlgCtrlID(HWND hWnd);
395
396 [DllImport("USER32.DLL")]
397 private static extern int GetClassName(HWND hWnd, StringBuilder lpClassName, int nMaxCount);
398
399 [DllImport("USER32.DLL")]
400 private static extern int GetWindowThreadProcessId(HWND hWnd, out int processId);
401
402 // https://stackoverflow.com/questions/7268302/get-the-titles-of-all-open-windows
406 public static IDictionary<HWND, string> GetOpenWindows()
407 {
408 HWND shellWindow = GetShellWindow();
409 Dictionary<HWND, string> windows = new Dictionary<HWND, string>();
410
411 EnumWindows(delegate (HWND hWnd, int lParam)
412 {
413 if (hWnd == shellWindow) return true;
414 if (!IsWindowVisible(hWnd)) return true;
415
416 int length = GetWindowTextLength(hWnd);
417 if (length == 0) return true;
418
419 StringBuilder builder = new StringBuilder(length);
420 GetWindowText(hWnd, builder, length + 1);
421
422 windows[hWnd] = builder.ToString();
423 return true;
424
425 }, 0);
426
427 return windows;
428 }
429
430 private delegate bool EnumWindowsProc(HWND hWnd, int lParam);
431
432 [DllImport("USER32.DLL")]
433 private static extern bool EnumWindows(EnumWindowsProc enumFunc, int lParam);
434
435 [DllImport("USER32.DLL")]
436 private static extern int GetWindowText(HWND hWnd, StringBuilder lpString, int nMaxCount);
437
438 [DllImport("USER32.DLL")]
439 private static extern int GetWindowTextLength(HWND hWnd);
440
441 [DllImport("USER32.DLL")]
442 private static extern bool IsWindowVisible(HWND hWnd);
443
444 [DllImport("USER32.DLL")]
445 private static extern IntPtr GetShellWindow();
446 }
447}
448
Thrown where GPAL deliberately ends the workflow, such as a CallIf handler returning CallIfStatus....
Base class for all GPAL form controls.
FormControlType ControlType
The control type of WindowsControl. Set when the control is instantiated with GPAL.
Form object that contains the fluent methods to create interactive forms. Instatiated using GPAL....
Definition GPALForm.cs:64
Everything starts here, all the GPAL controls and global settings using fluent syntax are here....
Definition GPAL.cs:49
static void PublishSimpleEvent(GPALEventType gPALEventType, string msg, dynamic gPALObject=null, Enums.GPALObjectType gPALObjectType=GPALObjectType.None, Exception ex=null)
Publish a message to either the information channel or exception channel (if exception passed in) Pub...
Definition GPAL.cs:2568