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 {
34 private static int lastRowCnt = 0;
35
41 public static bool GotoNextPage(Application.Application application)
42 {
43 UnitOfWork uow = application.CurrentUOW;
44
45 if (null != uow.NextPageButton)
46 {
47 bool matchedAll; List<GPALAutomationElement> matched;
48 var found = ElementHelper.FindElements(application, uow, uow.NextPageButton, out matchedAll, out matched);
49 if (null == found || 0 == found.Count)
50 {
51 GPAL.PublishSimpleEvent(GPALEventType.INFO, "GetGrid: next-page control not found; stopping paging.", application, GPALObjectType.Application);
52 return false;
53 }
54 ElementHelper.Click(application, uow.NextPageButton, found[0], ClickType.LeftClick, ModifierKeys.NONE);
55 return true;
56 }
57
58 if (uow.InfiniteScroll)
59 GPAL.PublishSimpleEvent(GPALEventType.WARNING, "GetGrid WithInfiniteScroll is not supported for desktop applications; retrieving a single page.", application, GPALObjectType.Application);
60
61 return false;
62 }
63
68 public static void TopProcess(Process process)
69 {
70 try
71 {
72 if (process.MainWindowHandle == GetForegroundWindow()) return;
73 }
74 catch
75 {
76 // can't determine who is the top window, so go ahead and top it
77 }
78
79 try
80 {
81 // Show window https://docs.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-showwindow
82 ShowWindow(process.MainWindowHandle, 1); // 1= SW_NORMAL - show normal, 3 = max, 2 = min
83
85 //keybd_event((byte)VK_ALT, 0x45, EXTENDEDKEY | 0, 0);
86
88 //keybd_event((byte)VK_ALT, 0x45, EXTENDEDKEY | KEYUP, 0);
89
90 // Show window in forground.
91 SetForegroundWindow(process.MainWindowHandle);
92 }
93 catch (Exception ex)
94 {
95
96 try
97 {
98 GPAL.PublishSimpleEvent(GPALEventType.EXCEPTION, $"Unable to top process [{process.ProcessName}], attempting a hardware click to top.", null, GPALObjectType.None, ex);
99 Rectangle rect;
100 GetWindowRect(process.MainWindowHandle, out rect);
101 HardwareHelper.HardwareClick(rect.X + 10, rect.Y + 10, ClickType.LeftClick);
102 }
103 catch
104 {
105 GPAL.PublishSimpleEvent(GPALEventType.EXCEPTION, $"Unable to top process, unable to get process information using GetWindowRect.", null, GPALObjectType.None, ex);
106 }
107 }
108 }
109
117 public static void FillInWithTokens<T>(Application.Application application, IGPALGrid<T> tokens, WriteMode writeMode)
118 {
119 while (true)
120 {
121 int tokenIdx = 0;
122 List<ReadOnlyCollection<GPALAutomationElement>> rowsOfColumns = new List<ReadOnlyCollection<GPALAutomationElement>>();
123 List<ReadOnlyCollection<GPALAutomationElement>> rowsOfElements = new List<ReadOnlyCollection<GPALAutomationElement>>();
124 // 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
125 // of course selectors are always a 'column'
126 List<SmallSelectorNode> interactionInfo = new List<SmallSelectorNode>();
127
128 // waitfor did not match if returns false
129 int rowCnt = 0;
130 int elementIdx = 0;
131
132 rowsOfElements.Clear();
133 rowsOfColumns.Clear();
134 application.CurrentUOW.MatchedRowIndexes.Clear();
135 application.CurrentUOW.ColCount = 0;
136 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
137 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
138
139 foreach (Selector sel in application.CurrentUOW.WithSelectorList)
140 {
141 // retrieve one column, which can span multiple rows if it repeats
142 List<GPALAutomationElement> matchedElems;
143 ReadOnlyCollection<GPALAutomationElement> tmpElems = ElementHelper.FindElements(application, application.CurrentUOW, sel, out bool matchedAll, out matchedElems);
144 rowsOfElements.Add(tmpElems);
145 application.CurrentUOW.ColCount++;
146
147 if (false == matchedAll)
148 application.CurrentUOW.PartialMatch |= true;
149
150 if (0 < tmpElems?.Count && true == matchedAll)
151 {
152 // RowCount is the total count of elements found or just the WithAllThatMatch row count
153 application.CurrentUOW.RowCount = Math.Max(application.CurrentUOW.RowCount, (int.MaxValue == application.CurrentUOW.WithAllThatMatch ? tmpElems.Count : application.CurrentUOW.WithAllThatMatch));
154 }
155 else if (0 < matchedElems?.Count && false == matchedAll)
156 {
157 // partial match
158 int tmpIdx = 0;
159 // add the indices of the matched items
160 // when we build the grid, we will only get rows that matched
161 // CAVEAT: NO 'AND' on selector MATCH
162 // only 'OR' operator, all those that match on different selectors
163 foreach (GPALAutomationElement webElement in matchedElems)
164 if (-1 != (tmpIdx = tmpElems.IndexOf(webElement)))
165 if (false == application.CurrentUOW.MatchedRowIndexes.Contains(new KeyValuePair<int, string>(tmpIdx, sel.AttributeName)))
166 application.CurrentUOW.MatchedRowIndexes.Add(tmpIdx, sel.AttributeName);
167
168 // 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
169 // up to WithAllThatMatch row count if not int.MaxValue
170 application.CurrentUOW.RowCount = (int.MaxValue == application.CurrentUOW.WithAllThatMatch ? application.CurrentUOW.MatchedRowIndexes.Count : application.CurrentUOW.WithAllThatMatch);
171 }
172
173 // if we returned elements, add an entry for this selector in interactionInfo, we consume it below
174 if (0 < matchedElems?.Count || 0 < tmpElems?.Count)
175 interactionInfo.Add(new SmallSelectorNode() { InteractionType = sel.InteractionType, OffsetX = sel.DeltaX, OffsetY = sel.DeltaY, DeltaX = sel.DeltaY, DeltaY = sel.DeltaY });
176 }
177
178 // we have partial matches from one or more selectors, so construct the grid only from matched rows
179 if (true == application.CurrentUOW.PartialMatch)
180 {
181 List<GPALAutomationElement> newColumn = new List<GPALAutomationElement>();
182
183 application.CurrentUOW.RowCount = application.CurrentUOW.MatchedRowIndexes.Count;
184 // get a list of all the elements in one column
185 // iterate thru each column pulling out matched rows
186 // create the return grid as only matched elements
187 foreach (ReadOnlyCollection<GPALAutomationElement> rowOfElements in rowsOfElements)
188 {
189 // create a newCOlumn from all elements via indexes where rows 'matched' on some selector value
190 // we have multiple selectors contributing to matched rows based upon that selectors 'match' criteria
191 foreach (KeyValuePair<int, string> idx in application.CurrentUOW.MatchedRowIndexes)
192 newColumn.Add(rowOfElements[idx.Key]);
193 rowsOfColumns.Add(new ReadOnlyCollection<GPALAutomationElement>(newColumn));
194 newColumn = new List<GPALAutomationElement>();
195 }
196 }
197 else
198 {
199 foreach (ReadOnlyCollection<GPALAutomationElement> rowOfElements in rowsOfElements)
200 rowsOfColumns.Add(rowOfElements);
201 }
202
203 // if we are looping, we have a new 'page' and new elements to interact with, our old list is stale
204 application.CurrentUOW.ElementGrid.Clear();
205
206 // we should have one column for each selector (that found elements)
207 if (0 != application.CurrentUOW.ColCount)
208 {
209 for (int cnt = 0; cnt < application.CurrentUOW.RowCount; cnt++)
210 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
211
212 int columnIdx = 0; // start with the first column in the row
213
214 // grab a row of columns
215 // construct our grid by pivoting the data from columns, to rows
216 // iterate each row of columns [elements], add a column (element) to each row of the element grid
217 foreach (ReadOnlyCollection<GPALAutomationElement> rowOfColumns in rowsOfColumns)
218 {
219 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)
220 // add this column to all rows
221 foreach (GPALAutomationElement elem in rowOfColumns)
222 {
223 switch (elem.Ae.Current.LocalizedControlType.ToLower())
224 {
225 case "pane":
226 case "text":
227 case "edit":
228 // add the element we will work with as a column to the current row list
229 application.CurrentUOW.ElementGrid[rowCnt++].Add(
231 {
232 AutomationElement = elem.Ae,
233 InteractionType = interactionInfo[columnIdx].InteractionType,
234 OffsetX = interactionInfo[columnIdx].OffsetX,
235 OffsetY = interactionInfo[columnIdx].OffsetY
236 });
237 break;
238
239 default:
240 break;
241 }
242
243 // only add as many rows as was asked for
244 // all or only save up to withallthatmatch row count
245 if (rowCnt >= application.CurrentUOW.WithAllThatMatch)
246 break;
247 }
248 }
249 }
250
251 // 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
252 // BUG: IF WE consume all out tokens, why would we loop again (while true)
253
254 // keep looping while we have tokens
255 // but after dealing with the first round of tokens, with more to go
256 if (tokenIdx < tokens.Count())
257 {
258 // we now have our elements in rows and columns
259 // iterate over the tokens, in rows and columns to fill in the text
260 foreach (List<UnitOfWork.ElementNode> elementNode in application.CurrentUOW.ElementGrid) // get one row of elements
261 {
262 List<T> currentRow = tokens[tokenIdx++];
263
264 foreach (T token in currentRow) // iterate over tokens, filling in the corresponding input/textarea webelement
265 {
266 if (elementIdx < elementNode.Count && null != elementNode[elementIdx])
267 {
268 if (InteractionType.Hardware == elementNode[elementIdx].InteractionType)
269 ElementHelper.HardwareFillInFrom(application, new GPALAutomationElement(elementNode[elementIdx].AutomationElement), elementNode[elementIdx].OffsetX, elementNode[elementIdx].OffsetY, token.ToString(), writeMode);
270 else
271 ElementHelper.FillInFrom(application, elementNode[elementIdx].AutomationElement, elementNode[elementIdx], token.ToString(), writeMode);
272 }
273 else
274 // detect if we have too many tokens to consume and publish an information event [entirely possible this scenario is fine]
275 GPAL.PublishSimpleEvent(GPALEventType.WARNING, $"Unused token [{token}] from tokens [{string.Join(",", currentRow)}]", application, GPALObjectType.Application);
276 elementIdx++; // increment the column [element] count for each token. if we have a token, we are expecting elements for it.
277 }
278
279 // detect if we do not have enough tokens for all the form fields [entirely possible this scenario is fine]
280 // we got here and finished our tokens but have not exhausted our columns [elements]
281 while (elementIdx < elementNode.Count && null != elementNode[elementIdx++])
282 GPAL.PublishSimpleEvent(GPALEventType.WARNING, $"No token for control: [{elementNode[elementIdx++].AutomationElement.Current.LocalizedControlType}]", application, GPALObjectType.Application);
283 }
284
285 UnitOfWork safeUOW = application.CurrentUOW;
286 CallIfStatus handled = CallIfStatus.NotHandled;
287
288 // CAVEAT: there is no concept of 'handled (1)' vs 'not handled (0)' but definitely can request to exit
289 // now that we have a list of grids, maybe we have 2/3, next token/grid ?
290 if (null != application.CurrentUOW.AppCallAfterFillIn)
291 handled = application.CurrentUOW.AppCallAfterFillIn(application, (IGPALGrid<string>)tokens, tokenIdx);
292
293 // CAVEAT: kludge - the call after handler can and probably will set a new current unit of work, but we need our old current unit
294 // the call after handler UOW is no longer in scope, so restore our UOW
295 application.CurrentUOW = safeUOW;
296
297 if (CallIfStatus.Terminate == handled)
298 {
299 GPAL.PublishSimpleEvent(GPALEventType.INFO, $"CallAfterFillIn handler requested program termination.", application, GPALObjectType.Application);
300 throw new GPALException($"{GPAL.MyMethodName()}: CallAfterFillIn handler requested program termination.");
301 }
302 }
303 else
304 break; // we consumed all tokens move on
305
306 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
307
308 // we are out of tokens, break out
309 // if we have more tokens, shouldn't we go to another page?
310 // 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?
311 // or do we break because we have more tokens then elements? or should we loop?
312 // why did i put this in a while true loop?
313 if (tokenIdx == tokens.Count())
314 break; // continue, loop into us again, we have more tokens to consume
315 }
316 return;
317 }
318
324 public static void FillInWithTokens(Application.Application application, List<IGPALGrid<string>> tokenList, WriteMode writeMode)
325 {
326 foreach (IGPALGrid<string> tokens in tokenList)
327 FillInWithTokens(application, tokens, writeMode);
328 }
329
330
331 [DllImport("user32.dll")]
332 private static extern IntPtr GetForegroundWindow();
333
334 [DllImport("user32.dll")]
335 private static extern void keybd_event(byte bVk, byte bScan, uint dwFlags, int dwExtraInfo);
336 [DllImport("user32.dll")]
337 private static extern bool SetForegroundWindow(IntPtr hWnd);
338 [DllImport("user32.dll")]
339 internal static extern bool ShowWindow(IntPtr hWnd, int nCmdShow);
340 [DllImport("user32.dll")]
341 private static extern int GetWindowRect(IntPtr hwnd, out Rectangle rect);
342 [DllImport("user32.dll")]
343 private static extern bool SetWindowPos(IntPtr hWnd, IntPtr hWndInsertAfter, int X, int Y, int cx, int cy, uint uFlags);
344
351 internal static void ResizeWindow(IntPtr hwnd, int width, int height)
352 {
353 if (hwnd == IntPtr.Zero) return;
354 const uint SWP_NOMOVE = 0x0002;
355 const uint SWP_NOZORDER = 0x0004;
356 const uint SWP_NOACTIVATE = 0x0010;
357 SetWindowPos(hwnd, IntPtr.Zero, 0, 0, width, height, SWP_NOMOVE | SWP_NOZORDER | SWP_NOACTIVATE);
358 }
359
366 internal static void MoveWindow(IntPtr hwnd, int x, int y)
367 {
368 if (hwnd == IntPtr.Zero) return;
369 const uint SWP_NOSIZE = 0x0001;
370 const uint SWP_NOZORDER = 0x0004;
371 const uint SWP_NOACTIVATE = 0x0010;
372 SetWindowPos(hwnd, IntPtr.Zero, x, y, 0, 0, SWP_NOSIZE | SWP_NOZORDER | SWP_NOACTIVATE);
373 }
374 }
375}
376
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:2406
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:826
int DeltaY
Drag-and-drop deltaY to move from the elemenet.Y + OffsetY.
Definition Selector.cs:841
InteractionType InteractionType
Defined method to interact with this element. NOTE: This can be overridden in the workflow.
Definition Selector.cs:731
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