GPAL - Generally Positive Automation Library v1.0
GPAL The Fluent Automation LIbrary
Loading...
Searching...
No Matches
ApplicationHelper.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
18using System;
19using System.Collections.Generic;
20using System.Diagnostics;
21using System.Drawing;
22using System.Linq;
23using System.Runtime.InteropServices;
24using System.Text;
25using System.Threading.Tasks;
26using static GenerallyPositive.Enums;
27using System.Windows.Automation;
28using System.Collections.ObjectModel;
29
30namespace GenerallyPositive
31{
33 {
39 public static bool GotoNextPage(Application.Application application)
40 {
41 UnitOfWork uow = application.CurrentUOW;
42
43 if (null != uow.NextPageButton)
44 {
45 bool matchedAll; List<GPALAutomationElement> matched;
46 var found = ElementHelper.FindElements(application, uow, uow.NextPageButton, out matchedAll, out matched);
47 if (null == found || 0 == found.Count)
48 {
49 GPAL.PublishSimpleEvent(GPALEventType.INFO, "GetGrid: next-page control not found; stopping paging.", application, GPALObjectType.Application);
50 return false;
51 }
52 ElementHelper.Click(application, uow.NextPageButton, found[0], ClickType.LeftClick, ModifierKeys.NONE);
53 return true;
54 }
55
56 if (uow.InfiniteScroll)
57 GPAL.PublishSimpleEvent(GPALEventType.WARNING, "GetGrid WithInfiniteScroll is not supported for desktop applications; retrieving a single page.", application, GPALObjectType.Application);
58
59 return false;
60 }
61
66 public static void TopProcess(Process process)
67 {
68 // a launcher hands its work to something already running and exits, so it owns no window and there
69 // is nothing here to bring forward. That is not a failure, and saying so on every action buries the
70 // log in exceptions about a process that was never meant to stay
71 if (null == process || true == process.HasExited || IntPtr.Zero == process.MainWindowHandle)
72 return;
73
74 try
75 {
76 IntPtr foreground = GetForegroundWindow();
77
78 if (process.MainWindowHandle == foreground) return;
79
80 // a menu, a dropdown or a modal dialog is its own window, so the foreground handle is not the
81 // main one even though the application is already in front. Topping the main window here closes
82 // whatever popup is open, and a context menu is gone before the selector that wants it runs
83 GetWindowThreadProcessId(foreground, out int owner);
84
85 if (owner == process.Id) return;
86 }
87 catch
88 {
89 // can't determine who is the top window, so go ahead and top it
90 }
91
92 try
93 {
94 // Show window https://docs.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-showwindow
95 ShowWindow(process.MainWindowHandle, 1); // 1= SW_NORMAL - show normal, 3 = max, 2 = min
96
98 //keybd_event((byte)VK_ALT, 0x45, EXTENDEDKEY | 0, 0);
99
101 //keybd_event((byte)VK_ALT, 0x45, EXTENDEDKEY | KEYUP, 0);
102
103 // Show window in forground.
104 SetForegroundWindow(process.MainWindowHandle);
105 }
106 catch (Exception ex)
107 {
108
109 try
110 {
111 GPAL.PublishSimpleEvent(GPALEventType.EXCEPTION, $"Unable to top process [{process.ProcessName}], attempting a hardware click to top.", null, GPALObjectType.None, ex);
112 Rectangle rect;
113 GetWindowRect(process.MainWindowHandle, out rect);
114 HardwareHelper.HardwareClick(rect.X + 10, rect.Y + 10, ClickType.LeftClick);
115 }
116 catch
117 {
118 GPAL.PublishSimpleEvent(GPALEventType.EXCEPTION, $"Unable to top process, unable to get process information using GetWindowRect.", null, GPALObjectType.None, ex);
119 }
120 }
121 }
122
130 public static void FillInWithTokens<T>(Application.Application application, IGPALGrid<T> tokens, WriteMode writeMode)
131 {
132 while (true)
133 {
134 int tokenIdx = 0;
135 List<ReadOnlyCollection<GPALAutomationElement>> rowsOfColumns = new List<ReadOnlyCollection<GPALAutomationElement>>();
136 List<ReadOnlyCollection<GPALAutomationElement>> rowsOfElements = new List<ReadOnlyCollection<GPALAutomationElement>>();
137 // a column-wise list of the selector info to match with the column of matched/returned elements to know how to interact with that column of elemnts
138 // of course selectors are always a 'column'
139 List<SmallSelectorNode> interactionInfo = new List<SmallSelectorNode>();
140
141 // waitfor did not match if returns false
142 int rowCnt = 0;
143 // a local, the way the browser twin declares it. As a private static it was never reset, so the
144 // second grid fill in a process, or a second Application, started writing past the end of the grid
145 int lastRowCnt = 0;
146 int elementIdx = 0;
147
148 rowsOfElements.Clear();
149 rowsOfColumns.Clear();
150 application.CurrentUOW.MatchedRowIndexes.Clear();
151 application.CurrentUOW.ColCount = 0;
152 application.CurrentUOW.RowCount = 0; // Math.Max below only ever raises it, so a pass that finds fewer elements than the last would size the grid for rows it has no elements to fill
153 application.CurrentUOW.PartialMatch = false; // only ever |= true below, so one pass that did not match everything would put every later pass down the partial-match path
154
155 foreach (Selector sel in application.CurrentUOW.WithSelectorList)
156 {
157 // retrieve one column, which can span multiple rows if it repeats
158 List<GPALAutomationElement> matchedElems;
159 ReadOnlyCollection<GPALAutomationElement> tmpElems = ElementHelper.FindElements(application, application.CurrentUOW, sel, out bool matchedAll, out matchedElems);
160 rowsOfElements.Add(tmpElems);
161 application.CurrentUOW.ColCount++;
162
163 if (false == matchedAll)
164 application.CurrentUOW.PartialMatch |= true;
165
166 if (0 < tmpElems?.Count && true == matchedAll)
167 {
168 // RowCount is the total count of elements found or just the WithAllThatMatch row count
169 application.CurrentUOW.RowCount = Math.Max(application.CurrentUOW.RowCount, (int.MaxValue == application.CurrentUOW.WithAllThatMatch ? tmpElems.Count : application.CurrentUOW.WithAllThatMatch));
170 }
171 else if (0 < matchedElems?.Count && false == matchedAll)
172 {
173 // partial match
174 int tmpIdx = 0;
175 // add the indices of the matched items
176 // when we build the grid, we will only get rows that matched
177 // CAVEAT: NO 'AND' on selector MATCH
178 // only 'OR' operator, all those that match on different selectors
179 foreach (GPALAutomationElement webElement in matchedElems)
180 if (-1 != (tmpIdx = tmpElems.IndexOf(webElement)))
181 if (false == application.CurrentUOW.MatchedRowIndexes.Contains(new KeyValuePair<int, string>(tmpIdx, sel.AttributeName)))
182 application.CurrentUOW.MatchedRowIndexes.Add(tmpIdx, sel.AttributeName);
183
184 // matched row indexes is the row count we will extract data from on partial matches, so however many indices we have, that is our MAX row count
185 // up to WithAllThatMatch row count if not int.MaxValue
186 application.CurrentUOW.RowCount = (int.MaxValue == application.CurrentUOW.WithAllThatMatch ? application.CurrentUOW.MatchedRowIndexes.Count : application.CurrentUOW.WithAllThatMatch);
187 }
188
189 // if we returned elements, add an entry for this selector in interactionInfo, we consume it below
190 if (0 < matchedElems?.Count || 0 < tmpElems?.Count)
191 interactionInfo.Add(new SmallSelectorNode() { InteractionType = sel.InteractionType, OffsetX = sel.DeltaX, OffsetY = sel.DeltaY, DeltaX = sel.DeltaY, DeltaY = sel.DeltaY });
192 }
193
194 // we have partial matches from one or more selectors, so construct the grid only from matched rows
195 if (true == application.CurrentUOW.PartialMatch)
196 {
197 List<GPALAutomationElement> newColumn = new List<GPALAutomationElement>();
198
199 application.CurrentUOW.RowCount = application.CurrentUOW.MatchedRowIndexes.Count;
200 // get a list of all the elements in one column
201 // iterate thru each column pulling out matched rows
202 // create the return grid as only matched elements
203 foreach (ReadOnlyCollection<GPALAutomationElement> rowOfElements in rowsOfElements)
204 {
205 // create a newCOlumn from all elements via indexes where rows 'matched' on some selector value
206 // we have multiple selectors contributing to matched rows based upon that selectors 'match' criteria
207 foreach (KeyValuePair<int, string> idx in application.CurrentUOW.MatchedRowIndexes)
208 newColumn.Add(rowOfElements[idx.Key]);
209 rowsOfColumns.Add(new ReadOnlyCollection<GPALAutomationElement>(newColumn));
210 newColumn = new List<GPALAutomationElement>();
211 }
212 }
213 else
214 {
215 foreach (ReadOnlyCollection<GPALAutomationElement> rowOfElements in rowsOfElements)
216 rowsOfColumns.Add(rowOfElements);
217 }
218
219 // if we are looping, we have a new 'page' and new elements to interact with, our old list is stale
220 application.CurrentUOW.ElementGrid.Clear();
221
222 // we should have one column for each selector (that found elements)
223 if (0 != application.CurrentUOW.ColCount)
224 {
225 for (int cnt = 0; cnt < application.CurrentUOW.RowCount; cnt++)
226 application.CurrentUOW.ElementGrid.AddRow(new List<UnitOfWork.ElementNode>(application.CurrentUOW.ColCount)); // create rows in the grid for each row we will return with capacity for colCount columns
227
228 int columnIdx = 0; // start with the first column in the row
229
230 // grab a row of columns
231 // construct our grid by pivoting the data from columns, to rows
232 // iterate each row of columns [elements], add a column (element) to each row of the element grid
233 foreach (ReadOnlyCollection<GPALAutomationElement> rowOfColumns in rowsOfColumns)
234 {
235 rowCnt = lastRowCnt; // if we are on subsequent pages, we have to add at the end of the list, up to WithAllThatMatch row count (or all rows)
236 // add this column to all rows
237 foreach (GPALAutomationElement elem in rowOfColumns)
238 {
239 switch (elem.Ae.Current.LocalizedControlType.ToLower())
240 {
241 case "pane":
242 case "text":
243 case "edit":
244 // add the element we will work with as a column to the current row list
245 application.CurrentUOW.ElementGrid[rowCnt++].Add(
247 {
248 AutomationElement = elem.Ae,
249 InteractionType = interactionInfo[columnIdx].InteractionType,
250 OffsetX = interactionInfo[columnIdx].OffsetX,
251 OffsetY = interactionInfo[columnIdx].OffsetY
252 });
253 break;
254
255 default:
256 break;
257 }
258
259 // only add as many rows as was asked for
260 // all or only save up to withallthatmatch row count
261 if (rowCnt >= application.CurrentUOW.WithAllThatMatch)
262 break;
263 }
264 }
265 }
266
267 // TODO: CAVEAT: do we have concept of next page in apps? prolly? if we still have tokens and are done, shouldn't we get a next page or bail
268 // BUG: IF WE consume all out tokens, why would we loop again (while true)
269
270 // keep looping while we have tokens
271 // but after dealing with the first round of tokens, with more to go
272 if (tokenIdx < tokens.Count())
273 {
274 // we now have our elements in rows and columns
275 // iterate over the tokens, in rows and columns to fill in the text
276 foreach (List<UnitOfWork.ElementNode> elementNode in application.CurrentUOW.ElementGrid) // get one row of elements
277 {
278 elementIdx = 0; // each row starts at its first column. Carried over, row two onwards had no columns left and called every token unused
279 List<T> currentRow = tokens[tokenIdx++];
280
281 foreach (T token in currentRow) // iterate over tokens, filling in the corresponding input/textarea webelement
282 {
283 if (elementIdx < elementNode.Count && null != elementNode[elementIdx])
284 {
285 if (InteractionType.Hardware == elementNode[elementIdx].InteractionType)
286 ElementHelper.HardwareFillInFrom(application, new GPALAutomationElement(elementNode[elementIdx].AutomationElement), elementNode[elementIdx].OffsetX, elementNode[elementIdx].OffsetY, token.ToString(), writeMode);
287 else
288 ElementHelper.FillInFrom(application, elementNode[elementIdx].AutomationElement, elementNode[elementIdx], token.ToString(), writeMode);
289 }
290 else
291 // detect if we have too many tokens to consume and publish an information event [entirely possible this scenario is fine]
292 GPAL.PublishSimpleEvent(GPALEventType.WARNING, $"Unused token [{token}] from tokens [{string.Join(",", currentRow)}]", application, GPALObjectType.Application);
293 elementIdx++; // increment the column [element] count for each token. if we have a token, we are expecting elements for it.
294 }
295
296 // detect if we do not have enough tokens for all the form fields [entirely possible this scenario is fine]
297 // we got here and finished our tokens but have not exhausted our columns [elements]
298 while (elementIdx < elementNode.Count && null != elementNode[elementIdx])
299 {
300 GPAL.PublishSimpleEvent(GPALEventType.WARNING, $"No token for control: [{elementNode[elementIdx].AutomationElement.Current.LocalizedControlType}]", application, GPALObjectType.Application);
301 elementIdx++; // once per control. Incrementing in the guard and again in the body skipped every other one and read past the end
302 }
303 }
304
305 UnitOfWork safeUOW = application.CurrentUOW;
306 CallIfStatus handled = CallIfStatus.NotHandled;
307
308 // CAVEAT: there is no concept of 'handled (1)' vs 'not handled (0)' but definitely can request to exit
309 // now that we have a list of grids, maybe we have 2/3, next token/grid ?
310 if (null != application.CurrentUOW.AppCallAfterFillIn)
311 handled = application.CurrentUOW.AppCallAfterFillIn(application, (IGPALGrid<string>)tokens, tokenIdx);
312
313 // CAVEAT: kludge - the call after handler can and probably will set a new current unit of work, but we need our old current unit
314 // the call after handler UOW is no longer in scope, so restore our UOW
315 application.CurrentUOW = safeUOW;
316
317 if (CallIfStatus.Terminate == handled)
318 {
319 GPAL.PublishSimpleEvent(GPALEventType.INFO, $"CallAfterFillIn handler requested program termination.", application, GPALObjectType.Application);
320 throw new GPALException($"{GPAL.MyMethodName()}: CallAfterFillIn handler requested program termination.");
321 }
322 }
323 else
324 break; // we consumed all tokens move on
325
326 lastRowCnt += rowCnt; // the total number of rows we are returning, also when looping thru after the first time, add to the end after the last row
327
328 // we are out of tokens, break out
329 // if we have more tokens, shouldn't we go to another page?
330 // CAVEAT: BUG: we are just looping back thru the same selectors and consuming more tokens, so does that mean CallAfterFillIn will do something and this is the behavior we want?
331 // or do we break because we have more tokens then elements? or should we loop?
332 // why did i put this in a while true loop?
333 if (tokenIdx == tokens.Count())
334 break; // continue, loop into us again, we have more tokens to consume
335 }
336 return;
337 }
338
344 public static void FillInWithTokens(Application.Application application, List<IGPALGrid<string>> tokenList, WriteMode writeMode)
345 {
346 foreach (IGPALGrid<string> tokens in tokenList)
347 FillInWithTokens(application, tokens, writeMode);
348 }
349
350
351 [DllImport("user32.dll")]
352 private static extern IntPtr GetForegroundWindow();
353 [DllImport("user32.dll")]
354 private static extern int GetWindowThreadProcessId(IntPtr hWnd, out int processId);
355
356 [DllImport("user32.dll")]
357 private static extern void keybd_event(byte bVk, byte bScan, uint dwFlags, int dwExtraInfo);
358 [DllImport("user32.dll")]
359 private static extern bool SetForegroundWindow(IntPtr hWnd);
360 [DllImport("user32.dll")]
361 internal static extern bool ShowWindow(IntPtr hWnd, int nCmdShow);
362 [DllImport("user32.dll")]
363 private static extern int GetWindowRect(IntPtr hwnd, out Rectangle rect);
364 [DllImport("user32.dll")]
365 private static extern bool SetWindowPos(IntPtr hWnd, IntPtr hWndInsertAfter, int X, int Y, int cx, int cy, uint uFlags);
366
373 internal static void ResizeWindow(IntPtr hwnd, int width, int height)
374 {
375 if (hwnd == IntPtr.Zero) return;
376 const uint SWP_NOMOVE = 0x0002;
377 const uint SWP_NOZORDER = 0x0004;
378 const uint SWP_NOACTIVATE = 0x0010;
379 SetWindowPos(hwnd, IntPtr.Zero, 0, 0, width, height, SWP_NOMOVE | SWP_NOZORDER | SWP_NOACTIVATE);
380 }
381
388 internal static void MoveWindow(IntPtr hwnd, int x, int y)
389 {
390 if (hwnd == IntPtr.Zero) return;
391 const uint SWP_NOSIZE = 0x0001;
392 const uint SWP_NOZORDER = 0x0004;
393 const uint SWP_NOACTIVATE = 0x0010;
394 SetWindowPos(hwnd, IntPtr.Zero, x, y, 0, 0, SWP_NOSIZE | SWP_NOZORDER | SWP_NOACTIVATE);
395 }
396 }
397}
398
Application object that contains the fluent methods to create your Application workflow....
Application workflow container for automation elements.
static bool GotoNextPage(Application.Application application)
Advance to the next page of results during GetGrid paging. Clicks the configured NextPageButton contr...
static void FillInWithTokens< T >(Application.Application application, IGPALGrid< T > tokens, WriteMode writeMode)
Enter tokens into form controls.
static void TopProcess(Process process)
Brings the given process's main window to the foreground/top of the z-order, if it is not already.
static void FillInWithTokens(Application.Application application, List< IGPALGrid< string > > tokenList, WriteMode writeMode)
Enter tokens into form controls for each grid of tokens in the list.
Thrown where GPAL deliberately ends the workflow, such as a CallIf handler returning CallIfStatus....
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
GPAL Selector used to locate Application and Browser elements. Instantiated with GPAL....
Definition Selector.cs:56
int DeltaX
Drag-and-drop deltaX to move from the elemenet.X + OffsetX.
Definition Selector.cs:838
int DeltaY
Drag-and-drop deltaY to move from the elemenet.Y + OffsetY.
Definition Selector.cs:853
InteractionType InteractionType
Defined method to interact with this element. NOTE: This can be overridden in the workflow.
Definition Selector.cs:743
Everything revolves around the Unit of Work. A Unit of Work is defined as one or more selectors betw...
Definition UnitOfWork.cs:39
Selector NextPageButton
The next page button. When scraping multiple pages of results, this element is pressed for a new page...
Definition UnitOfWork.cs:66
bool InfiniteScroll
Go to page end to get the next page of results. Use with .WithPages() to retrieve multiple pages of ...
Definition UnitOfWork.cs:74