GPAL - Generally Positive Automation Library v1.0
GPAL The Fluent Automation LIbrary
Loading...
Searching...
No Matches
AutomationElementExtension.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.Collections.ObjectModel;
20using System.Diagnostics;
21using System.Linq;
22using System.Text;
23using System.Threading.Tasks;
24using System.Windows.Automation;
25using System.Windows.Automation.Peers;
26using static GenerallyPositive.Enums;
27
29{
30 internal static class AutomationElementExtension
31 {
32 public static ReadOnlyCollection<GPALAutomationElement> FindElements(
33 this AutomationElement element,
34 string findByString,
35 ConditionPropertyType conditionPropertyType,
36 int idx = 0,
37 bool isRoot = false)
38 {
39 if (element == null)
40 throw new ArgumentNullException(nameof(element));
41
42 PropertyCondition condition = null;
43 ControlType controlType = null;
44
45 switch (conditionPropertyType)
46 {
47 case ConditionPropertyType.AutomationID:
48 condition = new PropertyCondition(AutomationElement.AutomationIdProperty, findByString);
49 break;
50 case ConditionPropertyType.ClassName:
51 condition = new PropertyCondition(AutomationElement.ClassNameProperty, findByString);
52 break;
53 case ConditionPropertyType.ControlType:
54 controlType = GetControlType(findByString);
55 condition = new PropertyCondition(AutomationElement.ControlTypeProperty, controlType);
56 break;
57 case ConditionPropertyType.Name:
58 condition = new PropertyCondition(AutomationElement.NameProperty, findByString);
59 break;
60 case ConditionPropertyType.Value:
61 condition = new PropertyCondition(ValuePattern.ValueProperty, findByString);
62 break;
63 default:
64 throw new ArgumentException("Unsupported ConditionPropertyType");
65 }
66
67 TreeScope treeScope = isRoot ? TreeScope.Subtree : TreeScope.Children;
68
69 List<GPALAutomationElement> tmpElems = new List<GPALAutomationElement>();
70
71 if (isRoot)
72 {
73 AutomationElement found = element.FindFirst(treeScope, condition);
74 if (found != null)
75 tmpElems.Add(new GPALAutomationElement(found));
76 }
77 else
78 {
79 // Your existing custom logic – kept unchanged
80 List<AutomationElement> automationElements = GetAllChildren(element, idx, controlType);
81 foreach (AutomationElement elem in automationElements)
82 tmpElems.Add(new GPALAutomationElement(elem));
83 }
84
85 return new ReadOnlyCollection<GPALAutomationElement>(tmpElems);
86 }
87 /*
88 public static ReadOnlyCollection<GPALAutomationElement> FindElements(this AutomationElement element, string findByString, ConditionPropertyType conditionPropertyType, int idx = 0, bool isRoot = false)
89 {
90 ReadOnlyCollection<GPALAutomationElement> elems = null;
91 List<GPALAutomationElement> tmpElems = new List<GPALAutomationElement>();
92 PropertyCondition condition = null;
93 ControlType controlType = null;
94
95 switch (conditionPropertyType)
96 {
97 case ConditionPropertyType.AutomationID:
98 condition = new PropertyCondition(AutomationElement.AutomationIdProperty, findByString);
99 break;
100 case ConditionPropertyType.ClassName:
101 condition = new PropertyCondition(AutomationElement.ClassNameProperty, findByString);
102 break;
103 case ConditionPropertyType.ControlType:
104 controlType = GetControlType(findByString);
105 condition = new PropertyCondition(AutomationElement.ControlTypeProperty, controlType);
106 break;
107 case ConditionPropertyType.Name:
108 condition = new PropertyCondition(AutomationElement.NameProperty, findByString);
109 break;
110 case ConditionPropertyType.Value:
111 condition = new PropertyCondition(System.Windows.Automation.ValuePattern.ValueProperty, findByString);
112 break;
113 }
114 TreeScope treeScope = System.Windows.Automation.TreeScope.Children;
115 if (true == isRoot)
116 treeScope = System.Windows.Automation.TreeScope.Subtree;
117 List<AutomationElement> automationElements = new List<AutomationElement>();
118 AutomationElement automationElement;
119 if (true == isRoot)
120 {
121 automationElement = element.FindFirst(treeScope, condition);
122 tmpElems.Add(new GPALAutomationElement(automationElement));
123 }
124 else
125 {
126 //AutomationElementCollection automationElems;
127 // automationElems = element.FindAll(treeScope, condition);
128 // foreach (AutomationElement elem in automationElems)
129 // tmpElems.Add(new GPALAutomationElement(elem));
130 //automationElement = element.FindFirst(treeScope, condition);
131
132 automationElements = GetAllChildren(element, idx, controlType);
133 foreach (AutomationElement elem in automationElements)
134 tmpElems.Add(new GPALAutomationElement(elem));
135
136
137 //if (0 == automationElems.Count)
138 //{
139 // treeScope = System.Windows.Automation.TreeScope.Subtree;
140 // automationElems = element.FindAll(treeScope, condition);
141 // if (0 == automationElems.Count)
142 // {
143 // treeScope = System.Windows.Automation.TreeScope.Descendants;
144 // automationElems = element.FindAll(treeScope, condition);
145 // }
146 //}
147 //foreach (AutomationElement elem in automationElems)
148 // tmpElems.Add(new GPALAutomationElement(elem));
149 }
150
151 elems = new ReadOnlyCollection<GPALAutomationElement>(tmpElems);
152 return elems;
153 }
154 */
155 private static List<AutomationElement> GetAllChildren(AutomationElement rootElement, int idx, ControlType controlType)
156 {
157 Condition condition = new PropertyCondition(AutomationElement.IsControlElementProperty, true);
158 TreeWalker walker = new TreeWalker(condition);
159 AutomationElement elementNode = walker.GetFirstChild(rootElement);
160 int cnt = 0;
161
162 if (null == elementNode) return null;
163
164 List<AutomationElement> automationElements = new List<AutomationElement>();
165
166 while (elementNode != null && cnt <= idx) // only get as many children as we require
167 {
168 if (controlType == elementNode.Current.ControlType)
169 {
170 automationElements.Add(elementNode);
171 cnt++;
172 }
173
174 elementNode = walker.GetNextSibling(elementNode);
175 }
176
177 return automationElements;
178 }
186 public static string GetValue(this AutomationElement element)
187 {
188 string retVal = null;
189
190 if (element.TryGetCurrentPattern(ValuePattern.Pattern, out object patternObj))
191 retVal = ((ValuePattern)patternObj).Current.Value;
192
193 return retVal;
194 }
195
196 public static string GetText(this AutomationElement element)
197 {
198 object patternObj;
199 // LegacyIAccessiblePattern only accessible in UIACommWrapperX
200 //if (element.TryGetCurrentPattern(LegacyIAccessiblePattern.Pattern, out patternObj))
201 //{
202 // var legacyPattern = (LegacyIAccessiblePattern)patternObj;
203 // return legacyPattern.Current.Value;
204 //}
205 //else
206 // TextPattern first, because it is the richer of the two and the only one that answers with a
207 // whole document. ValuePattern is meant for a short value, an edit field or a combo entry, and a
208 // provider truncates long content in it: a Scintilla editor holding 500 lines answered with
209 // exactly 4096 characters, cut mid word. DocumentRange.GetText(-1) is unlimited and gives the lot.
210 //
211 // Nothing is lost by the swap. A control that genuinely is just a value does not offer
212 // TextPattern, so it still falls through to exactly where it always did
213 string retVal;
214 string answeredBy;
215
216 if (element.TryGetCurrentPattern(TextPattern.Pattern, out patternObj))
217 {
218 var textPattern = (TextPattern)patternObj;
219 retVal = textPattern.DocumentRange.GetText(-1).TrimEnd('\r'); // often there is an extra '\r' hanging off the end.
220 answeredBy = "TextPattern";
221 }
222 else if (element.TryGetCurrentPattern(ValuePattern.Pattern, out patternObj))
223 {
224 var valuePattern = (ValuePattern)patternObj;
225 retVal = valuePattern.Current.Value;
226 answeredBy = "ValuePattern, which a provider may truncate";
227 }
228 else
229 {
230 retVal = element.Current.Name;
231 answeredBy = "Name, because the control offers neither TextPattern nor ValuePattern";
232 }
233
234 // which pattern answered decides whether a long control comes back whole or cut, and there is no
235 // way to tell from the string itself, so it is said out loud
236 GPAL.PublishSimpleEvent(GPALEventType.DEBUG, $"[{element.Current.LocalizedControlType}] text of [{retVal?.Length ?? 0}] characters came from {answeredBy}.", null, GPALObjectType.Application);
237
238 return retVal;
239 }
240
241 internal static ControlType GetControlType(string controlTypeString)
242 {
243 if (null == controlTypeString) return null;
244
245 AutomationControlType typeEnum;
246 bool result = Enum.TryParse(controlTypeString, true, out typeEnum);
247 if (result) typeEnum = (AutomationControlType)Enum.Parse(typeof(AutomationControlType), controlTypeString);
248 int enumId = (int)typeEnum + 50000;
249 ControlType controlType = ControlType.LookupById(enumId);
250
251 return controlType;
252 }
253 }
254}
255
256