├── .gitignore ├── Examples ├── Example01_Notepad.ahk ├── Example02_StartingPointElements.ahk ├── Example03_FindElements.ahk ├── Example04_TreeWalking.ahk ├── Example05_Notepad.ahk ├── Example06_Calculator.ahk ├── Example07_FocusChangedEvent.ahk ├── Example08_SelectionEventHandler.ahk ├── Example09_InvokePatternTogglePattern.ahk ├── Example10_ExpandCollapsePattern.ahk ├── Example11_ScrollPatternScrollItemPattern.ahk ├── Example12_GridPatternGridItemPattern.ahk ├── Example13_TablePatternTableItemPattern.ahk ├── Example14_MultipleViewPattern.ahk ├── Example15_SelectionPatternSelectionItemPattern.ahk ├── Example16_WindowPatternTransformPattern.ahk ├── Example17_TextPatternTextRange.ahk ├── Example18_TextRangeTextChangedEvent.ahk ├── Example19_PropertyChangedEvent.ahk ├── Example20_StructureChangedEvent.ahk ├── Example21_NotificationEvent.ahk ├── Example22_EventHandlerGroup.ahk ├── Example23_Caching.ahk ├── Example24_CachingNotepad.ahk ├── Example25_ElementVisibility.ahk ├── Spotify.ahk ├── UIA_Browser_Example01_Chrome.ahk ├── UIA_Browser_Example02_EdgeScrolling.ahk └── UIA_Browser_Example03_Login.ahk ├── LICENSE ├── Lib ├── UIA.ahk └── UIA_Browser.ahk ├── README.md └── UIATreeInspector.ahk /.gitignore: -------------------------------------------------------------------------------- 1 | Test/ 2 | .gitignore 3 | .vscode/ -------------------------------------------------------------------------------- /Examples/Example01_Notepad.ahk: -------------------------------------------------------------------------------- 1 | #Requires AutoHotkey v2 2 | ;#include ; Uncomment if you have moved UIA.ahk to your main Lib folder 3 | #include ..\Lib\UIA.ahk 4 | 5 | /* 6 | Simple example to demonstrate some basic methods of the UIA class. 7 | */ 8 | 9 | Run "notepad.exe" 10 | WinWaitActive "ahk_exe notepad.exe" 11 | try { 12 | ; Get the element for the Notepad window 13 | npEl := UIA.ElementFromHandle("ahk_exe notepad.exe") 14 | ; Find the first Document or Edit control (in Notepad there is only one). 15 | documentEl := npEl.FindElement([{Type:"Document"}, {Type:"Edit"}]) 16 | } catch { 17 | ; Windows 11 has broken Notepad so that the Document element isn't findable; instead get it by the ClassNN 18 | Sleep 40 19 | documentEl := UIA.ElementFromHandle(ControlGetHwnd("RichEditD2DPT1")) 20 | } 21 | ; Highlight the found element 22 | documentEl.Highlight() 23 | ; Set the value for the document control. 24 | documentEl.Value := "Lorem ipsum" 25 | 26 | ; This could also be chained together as: 27 | ; UIA.ElementFromHandle("ahk_exe notepad.exe").FindFirst([{Type:"Document"}, {Type:"Edit"}]).Highlight().Value := "Lorem ipsum" 28 | ExitApp -------------------------------------------------------------------------------- /Examples/Example02_StartingPointElements.ahk: -------------------------------------------------------------------------------- 1 | #Requires AutoHotkey v2 2 | ;#include ; Uncomment if you have moved UIA.ahk to your main Lib folder 3 | #include ..\Lib\UIA.ahk 4 | 5 | /* 6 | One of the best ways of accessing UI elements (buttons, text etc) is by first getting the window 7 | element for the starting element with ElementFromHandle. For that we can use the same notation 8 | as for any other AHK function. 9 | */ 10 | Run "notepad.exe" 11 | WinWaitActive "ahk_exe notepad.exe" 12 | npEl := UIA.ElementFromHandle("ahk_exe notepad.exe") ; Get the element for the Notepad window 13 | MsgBox "Notepad window element with all descendants: `n`n" npEl.DumpAll() ; Display all the sub-elements for the Notepad window. 14 | 15 | /* 16 | ElementFromHandle doesn't only access windows, but can use any handle: control handles can be 17 | used to get a part of the window. This is sometimes necessary when for some reason UIAutomation 18 | hasn't been implemented properly and not all elements are displayed in the UIA tree. 19 | */ 20 | try editHandle := ControlGetHwnd("Edit1", "ahk_exe notepad.exe") 21 | catch 22 | editHandle := ControlGetHwnd("RichEditD2DPT1") 23 | 24 | editEl := UIA.ElementFromHandle(editHandle) 25 | MsgBox "Edit control element with all descendants: `n`n" editEl.DumpAll() 26 | 27 | /* 28 | A special case for ElementFromHandle using a control is ElementFromChromium, which gets the 29 | element for the renderer control. This special case exists because Chromium-based (browser-based) 30 | applications frequently have the problem of not being UI-accessible from the main window. 31 | 32 | For this example you need to have Chrome open. 33 | */ 34 | if WinExist("ahk_exe chrome.exe") { 35 | WinActivate("ahk_exe chrome.exe") 36 | WinWaitActive("ahk_exe chrome.exe") 37 | } 38 | chromiumEl := UIA.ElementFromChromium("ahk_exe chrome.exe") 39 | MsgBox "Chromium control element without descendants: `n`n" chromiumEl.Dump() 40 | 41 | /* 42 | Elements can also be gotten from any point on the screen with ElementFromPoint. This usually won't 43 | return the window element under the cursor, but a sub-element of the window that the mouse 44 | is hovering over. Omitting x and y arguments from ElementFromPoint will use the current mouse position. 45 | */ 46 | 47 | MsgBox "Starting ElementFromPoint example. `nPress OK to start, F5 to exit." 48 | Loop 49 | try ToolTip UIA.ElementFromPoint().Dump() 50 | 51 | ExitApp 52 | F5::ExitApp -------------------------------------------------------------------------------- /Examples/Example03_FindElements.ahk: -------------------------------------------------------------------------------- 1 | #Requires AutoHotkey v2 2 | ;#include ; Uncomment if you have moved UIA.ahk to your main Lib folder 3 | #include ..\Lib\UIA.ahk 4 | 5 | /* To find elements we have a few methods available: FindElement, FindElements, WaitElement, ElementFromPath, and TreeWalkers. 6 | This file will demonstrate use of FindElement, FindElements, WaitElement, and using conditions. 7 | To see examples on ElementFromPath and TreeWalkers, see Example4. 8 | 9 | A "condition" is a set of conditions that the found elements must match. 10 | For example, we could only look for elements of certain Type, with certain Name, AutomationId etc. 11 | */ 12 | 13 | if VerCompare(A_OSVersion, ">=10.0.22000") { 14 | MsgBox "This example works only in Windows 10. Press OK to Exit." 15 | ExitApp 16 | } 17 | 18 | Run "notepad.exe" 19 | WinWaitActive "ahk_exe notepad.exe" 20 | ; Get the element for the Notepad window 21 | npEl := UIA.ElementFromHandle("ahk_exe notepad.exe") 22 | 23 | ; A single property condition consists of an object where the key is the property name, and value is the property value: 24 | MsgBox "The first MenuItem element: " npEl.FindElement({Type:"MenuItem"}).Highlight().Dump() 25 | 26 | ; Everything inside curly brackets creates an "and" condition, which means the element has to match all the conditions at once: 27 | MsgBox "The first MenuItem element with Name 'File': " npEl.FindElement({Type:"MenuItem", Name:"File"}).Highlight().Dump() 28 | 29 | ; Everything inside square brackets creates an "or" condition, which means the element has to match at least one of the conditions: 30 | ; Note that we put two single conditions inside the square brackets: [{}, {}] 31 | MsgBox "The first element with type Document or type Edit: " npEl.FindElement([{Type:"Document"}, {Type:"Edit"}]).Highlight().Dump() 32 | 33 | ; To find an nth element, supply either "index" or "i" property: 34 | MsgBox "The third MenuItem element: " npEl.FindElement({Type:"MenuItem", index:3}).Highlight().Dump() 35 | 36 | ; By default, FindElement(s) and WaitElement looks for exact matches. To look for partial matches or by 37 | ; RegEx, then supply "matchmode" or "mm" property with the UIA.MatchMode value (same as AHK-s TitleMatchMode). 38 | MsgBox "The first element with Name containing 'Bar': " npEl.FindElement({Name:"Bar", matchmode:"Substring"}).Highlight().Dump() ; Short form for this matchmode is mm:2 39 | 40 | ; Search case-sensitivity can be changed with "casesense" or "c" property, which by default is case-sensitive: 41 | MsgBox "The first element with Name 'file', case-insensitive: " npEl.FindElement({Name:"file", casesense:0}).Highlight().Dump() 42 | 43 | ; A "not" condition can be created by having the property key as "not", or supplying an "operator" or "op" property with value "not": 44 | MsgBox "The first MenuItem element with Name not 'System': " npEl.FindElement({Type:"MenuItem", not:{Name:"System"}}).Highlight().Dump() 45 | 46 | ; FindElement can traverse the tree in reverse, starting the search from the end: 47 | MsgBox "The first MenuItem element from the end: " npEl.FindElement({Type:"MenuItem", order:"LastToFirstOrder"}).Highlight().Dump() 48 | 49 | 50 | ; FindElements works like FindElement, but returns all the matches: 51 | matches := "" 52 | for el in npEl.FindElements({Type:"MenuItem"}) 53 | matches .= el.Dump() "`n" 54 | MsgBox "All elements with type MenuItem: `n`n" matches 55 | 56 | ; WaitElement will wait for the element to exist. It works by calling FindElement until the element 57 | ; is found or the timeout is reached. This is useful when after an action (such as clicking) the 58 | ; user interface changes (such as a web page changing after clicking on a link) to wait for the 59 | ; element to load properly. If we used instead FindElement, we might not find it because for example 60 | ; a webpage might still be loading. 61 | ; It works exactly like FindElement, only the second argument is the timeout. 62 | MsgBox "Waited for the first MenuItem element (which might have been useful if Notepad were slow to load): " npEl.WaitElement({Type:"MenuItem"}).Highlight().Dump() 63 | 64 | ExitApp -------------------------------------------------------------------------------- /Examples/Example04_TreeWalking.ahk: -------------------------------------------------------------------------------- 1 | #Requires AutoHotkey v2 2 | ;#include ; Uncomment if you have moved UIA.ahk to your main Lib folder 3 | #include ..\Lib\UIA.ahk 4 | 5 | /* 6 | FindElement can only search through the children of the starting point element, 7 | to get to parent or sibling elements we need to use either ElementFromPath (easiest) or TreeWalkers (more difficult). 8 | */ 9 | 10 | Run "notepad.exe" 11 | WinWaitActive "ahk_exe notepad.exe" 12 | ; Get the element for the Notepad window 13 | npEl := UIA.ElementFromHandle("ahk_exe notepad.exe") 14 | 15 | /* 16 | With ElementFromPath(path1[, path2, path3...]), we need to supply a comma-separated path that defines the route of tree traversal. 17 | This can be either an UIA path from UIAViewer, comma-separated integers (integer n chooses nth child), 18 | or TypeN (chooses Nth element of Type), or conditions. 19 | 20 | For traversing the tree in any direction we can use WalkTree(path, filterCondition?) 21 | n: gets the nth child 22 | +n: gets the nth next sibling 23 | -n: gets the nth previous sibling 24 | pn: gets the nth parent 25 | Optionally we can also supply a condition for tree traversal that selects only elements that match the condition. 26 | */ 27 | 28 | ; UIA path for the "Edit" MenuItem: 29 | if VerCompare(A_OSVersion, ">=10.0.22000") ; Windows 11 30 | npEl["YABq"].Highlight() 31 | else 32 | npEl["ABq"].Highlight() 33 | 34 | ; Condition path for the "Edit" MenuItem 35 | if VerCompare(A_OSVersion, ">=10.0.22000") ; Windows 11 36 | npEl[{T:33,CN:"Windows.UI.Input.InputSite.WindowClass"}, {T:10,A:"MenuBar"}, {T:11,CN:"Microsoft.UI.Xaml.Controls.MenuBarItem", i:2}].Highlight() 37 | else 38 | npEl[{T:10,A:"MenuBar"}, {T:11,N:"Edit"}].Highlight() ; Equivalent: npEl.ElementFromPath({T:10,A:"MenuBar"}, {T:11,N:"Edit"}).Highlight() 39 | 40 | ; This should also get us to the "Edit" MenuItem in Windows 10, but to the Minimize button in Windows 11 41 | editMenuItem := npEl.ElementFromPath("4,2").Highlight() 42 | ; Moving two sibling over, we should get to the "View" MenuItem, or in Windows 11 to "Close" 43 | editMenuItem.WalkTree("+2").Highlight() 44 | 45 | ; We can also use the array notation, which accepts ElementFromPath paths and also conditions 46 | npEl[4,1].Highlight() 47 | if VerCompare(A_OSVersion, ">=10.0.22000") ; Windows 11 48 | npEl["Pane", "MenuBar",{Name:"File"}].HighLight() 49 | else 50 | npEl["MenuBar",{Name:"File"}].HighLight() -------------------------------------------------------------------------------- /Examples/Example05_Notepad.ahk: -------------------------------------------------------------------------------- 1 | #Requires AutoHotkey v2 2 | ;#include ; Uncomment if you have moved UIA.ahk to your main Lib folder 3 | #include ..\Lib\UIA.ahk 4 | 5 | Run "notepad.exe" 6 | WinWaitActive "ahk_exe notepad.exe" 7 | ; Get the element for the Notepad window 8 | npEl := UIA.ElementFromHandle("ahk_exe notepad.exe") 9 | ; Display all the sub-elements for the Notepad window. Press OK to continue 10 | MsgBox npEl.DumpAll() 11 | ; Find the first Document or Edit control (in Notepad there is only one). In older versions of Windows this was an Edit type control, in newer ones it's Document. 12 | try documentEl := npEl.FindElement([{Type:"Document"}, {Type:"Edit"}]) 13 | catch { 14 | ; Windows 11 has broken Notepad so that the Document element isn't findable; instead get it by the ClassNN 15 | Sleep 40 16 | documentEl := UIA.ElementFromHandle(ControlGetHwnd("RichEditD2DPT1")) 17 | } 18 | ; Set the value of the document control, same as documentEl.SetValue("Lorem ipsum") 19 | documentEl.Value := "Lorem ipsum" 20 | MsgBox "Press OK to test saving." ; Wait for the user to press OK 21 | ; Find the "File" menu item 22 | fileEl := npEl.FindElement({Type:"MenuItem", Name:"File"}) 23 | fileEl.Highlight() 24 | fileEl.Click() 25 | ; The last three lines could be combined into: 26 | ; fileEl.FindElement({Type:"MenuItem", Name:"File"}).Highlight(2000).Click() 27 | saveEl := npEl.WaitElement({Name:"Save", mm:2}) ; Wait for the "Save" menu item to exist 28 | saveEl.Highlight() 29 | ; And now click Save 30 | saveEl.Click() 31 | ExitApp 32 | 33 | -------------------------------------------------------------------------------- /Examples/Example06_Calculator.ahk: -------------------------------------------------------------------------------- 1 | #Requires AutoHotkey v2 2 | ;#include ; Uncomment if you have moved UIA_Interface.ahk to your main Lib folder 3 | #include ..\Lib\UIA.ahk 4 | SetTitleMatchMode 3 5 | 6 | Run "calc.exe" 7 | WinWaitActive "Calculator" 8 | ; Get the element for the Calculator window 9 | cEl := UIA.ElementFromHandle("Calculator") 10 | ; All the calculator buttons are of "Button" ControlType, and if the system language is English then the Name 11 | ; of the elements are the English words for the buttons (eg button 5 is named "Five", 12 | ; = sign is named "Equals") 13 | 14 | ; Wait for the "Six" button by name and click it 15 | cEl.WaitElement({Name:"Six"}).Click() 16 | ; Specify both name "Five" and control type "Button" 17 | cEl.FindElement({Name:"Five", Type:"Button"}).Click() 18 | cEl.FindElement({Type:"Button", Name:"Plus"}).Click() 19 | ; The type can be specified as "Button", UIA.Type.Button, or 50000 (which is the value of UIA.Type.Button) 20 | cEl.FindElement({Name:"Four", Type:UIA.Type.Button}).Click() 21 | cEl.FindElement({Name:"Equals", Type:"Button"}).Click() 22 | ExitApp 23 | -------------------------------------------------------------------------------- /Examples/Example07_FocusChangedEvent.ahk: -------------------------------------------------------------------------------- 1 | #Requires AutoHotkey v2 2 | ;#include ; Uncomment if you have moved UIA.ahk to your main Lib folder 3 | #include ..\Lib\UIA.ahk 4 | 5 | EventHandler(el) { 6 | try { 7 | ToolTip "Caught event!`nElement name: " el.Name 8 | } 9 | } 10 | 11 | ExitFunc(*) { 12 | global h 13 | UIA.RemoveFocusChangedEventHandler(h) ; Remove the event handler. Alternatively use UIA.RemoveAllEventHandlers() to remove all handlers 14 | } 15 | 16 | browserExe := "chrome.exe" 17 | Run browserExe " -incognito" 18 | WinWaitActive "ahk_exe " browserExe 19 | 20 | global h := UIA.CreateFocusChangedEventHandler(EventHandler) ; Create a new FocusChanged event handler that calls the function EventHandler (required arguments: element) 21 | UIA.AddFocusChangedEventHandler(h) ; Add a new FocusChangedEventHandler 22 | OnExit(ExitFunc) ; Set up an OnExit call to clean up the handler when exiting the script 23 | return 24 | 25 | F5::ExitApp 26 | -------------------------------------------------------------------------------- /Examples/Example08_SelectionEventHandler.ahk: -------------------------------------------------------------------------------- 1 | #Requires AutoHotkey v2 2 | ;#include ; Uncomment if you have moved UIA.ahk to your main Lib folder 3 | #include ..\Lib\UIA.ahk 4 | 5 | TextSelectionChangedEventHandler(el, eventId) { 6 | textPattern := el.TextPattern 7 | selectionArray := textPattern.GetSelection() ; Gets the currently selected text in Notepad as an array of TextRanges (some elements support selecting multiple pieces of text at the same time, thats why an array is returned) 8 | selectedRange := selectionArray[1] ; Our range of interest is the first selection (TextRange) 9 | wholeRange := textPattern.DocumentRange ; For comparison, get the whole range (TextRange) of the document 10 | selectionStart := selectedRange.CompareEndpoints(UIA.TextPatternRangeEndpoint.Start, wholeRange, UIA.TextPatternRangeEndpoint.Start) ; Compare the start point of the selection to the start point of the whole document 11 | selectionEnd := selectedRange.CompareEndpoints(UIA.TextPatternRangeEndpoint.End, wholeRange, UIA.TextPatternRangeEndpoint.Start) ; Compare the end point of the selection to the start point of the whole document 12 | 13 | ToolTip "Selected text: " selectedRange.GetText() "`nSelection start location: " selectionStart "`nSelection end location: " selectionEnd ; Display the selected text and locations of selection 14 | } 15 | 16 | ExitFunc(*) { 17 | global handler, NotepadEl 18 | try UIA.RemoveAutomationEventHandler(UIA.Event.Text_TextSelectionChanged, NotepadEl, handler) ; Remove the event handler. Alternatively use UIA.RemoveAllEventHandlers() to remove all handlers. If the Notepad window doesn't exist any more, this throws an error. 19 | } 20 | 21 | ; Some sample text to play around with 22 | lorem := "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum." 23 | 24 | Run "notepad.exe" 25 | WinWaitActive "ahk_exe notepad.exe" 26 | 27 | try { 28 | NotepadEl := UIA.ElementFromHandle("ahk_exe notepad.exe") 29 | DocumentControl := NotepadEl.FindElement([{Type:"Document"}, {Type:"Edit"}]) ; If UIA Interface version is 1, then the ControlType is Edit instead of Document! 30 | } catch { 31 | ; Windows 11 has broken Notepad, so the Document element isn't findable; instead get the focused element 32 | Sleep 40 33 | DocumentControl := UIA.ElementFromHandle(ControlGetHwnd("RichEditD2DPT1")) 34 | } 35 | DocumentControl.Value := lorem ; Set the value to our sample text 36 | 37 | handler := UIA.CreateAutomationEventHandler(TextSelectionChangedEventHandler) ; Create a new event handler that points to the function TextSelectionChangedEventHandler, which must accept two arguments: element and eventId. 38 | UIA.AddAutomationEventHandler(handler, NotepadEl, UIA.Event.Text_TextSelectionChanged) ; Add a new automation handler for the TextSelectionChanged event 39 | OnExit(ExitFunc) ; Set up an OnExit call to clean up the handler when exiting the script 40 | return 41 | 42 | F5::ExitApp 43 | -------------------------------------------------------------------------------- /Examples/Example09_InvokePatternTogglePattern.ahk: -------------------------------------------------------------------------------- 1 | #Requires AutoHotkey v2 2 | ;#include ; Uncomment if you have moved UIA.ahk to your main Lib folder 3 | #include ..\Lib\UIA.ahk 4 | 5 | if VerCompare(A_OSVersion, ">=10.0.22000") { 6 | MsgBox "This example works only in Windows 10. Press OK to Exit." 7 | ExitApp 8 | } 9 | 10 | Run "explore C:\" 11 | WinWaitActive DriveGetLabel("C:") " (C:)",, 1 12 | WinMove(100, 200, 1000, , "A") 13 | explorerEl := UIA.ElementFromHandle("A") 14 | if !explorerEl { 15 | MsgBox "Drive C: element not found! Exiting app..." 16 | ExitApp 17 | } 18 | fileEl := explorerEl.FindElement({Type:"Button", Name:"File tab"}) 19 | MsgBox "Invoke pattern doesn't have any properties. Press OK to call Invoke on the `"File`" button..." 20 | fileEl.Invoke() 21 | 22 | Sleep 1000 23 | MsgBox "Press OK to navigate to the View tab to test TogglePattern..." ; Not part of this demonstration 24 | if !explorerEl.FindElement({Name:"Lower ribbon", cs:0}) ; Not part of this demonstration 25 | try explorerEl.FindElement({T:0,N:"Minimize the Ribbon"}).Invoke() ; Not part of this demonstration 26 | explorerEl.FindElement({Type:"TabItem", Name:"View"}).Select() ; Not part of this demonstration 27 | 28 | hiddenItemsCB := explorerEl.FindElement({Type:"CheckBox", Name:"Hidden items"}) 29 | Sleep 500 30 | MsgBox "TogglePattern properties for `"Hidden items`" checkbox: " 31 | . "`nCurrentToggleState: " hiddenItemsCB.ToggleState 32 | 33 | MsgBox "Press OK to toggle" 34 | hiddenItemsCB.Toggle() 35 | Sleep 500 36 | MsgBox "Press OK to toggle again" 37 | hiddenItemsCB.TogglePattern.Toggle() ; This way Toggle() will be called specifically from TogglePattern 38 | 39 | ; hiddenItemsCB.ToggleState := 1 ; ToggleState can also be used to set the state 40 | 41 | ExitApp 42 | -------------------------------------------------------------------------------- /Examples/Example10_ExpandCollapsePattern.ahk: -------------------------------------------------------------------------------- 1 | #Requires AutoHotkey v2 2 | ;#include ; Uncomment if you have moved UIA.ahk to your main Lib folder 3 | #include ..\Lib\UIA.ahk 4 | 5 | Run "explore C:\" 6 | CDriveName := DriveGetLabel("C:") " (C:)" 7 | WinWaitActive(CDriveName,,1) 8 | WinMove(100, 200, 1000, , "A") 9 | explorerEl := UIA.ElementFromHandle("A") 10 | if !explorerEl { 11 | MsgBox "Drive C: element not found! Exiting app..." 12 | ExitApp 13 | } 14 | CDriveEl := explorerEl.FindElement({Type:"TreeItem", Name:CDriveName, matchmode:"Substring"}) 15 | 16 | Sleep 500 17 | MsgBox "ExpandCollapsePattern properties: " 18 | . "`nCurrentExpandCollapseState: " (state := CDriveEl.ExpandCollapseState) " (" UIA.ExpandCollapseState[state] ")" 19 | Sleep 500 20 | MsgBox "Press OK to expand drive C: element" 21 | CDriveEl.Expand() 22 | Sleep 500 23 | MsgBox "Press OK to collapse drive C: element" 24 | CDriveEl.Collapse() 25 | 26 | ExitApp 27 | -------------------------------------------------------------------------------- /Examples/Example11_ScrollPatternScrollItemPattern.ahk: -------------------------------------------------------------------------------- 1 | #Requires AutoHotkey v2 2 | ;#include ; Uncomment if you have moved UIA.ahk to your main Lib folder 3 | #include ..\Lib\UIA.ahk 4 | 5 | Run "explore C:\" 6 | CDriveName := DriveGetLabel("C:") " (C:)" 7 | WinWaitActive(CDriveName,,1) 8 | WinMove(100, 200, 1000, , "A") 9 | explorerEl := UIA.ElementFromHandle("A") 10 | if !explorerEl { 11 | MsgBox "Drive C: element not found! Exiting app..." 12 | ExitApp 13 | } 14 | treeEl := explorerEl.FindElement({Type:"Tree"}) 15 | 16 | MsgBox "For this example, make sure that the folder tree on the left side in File Explorer has some scrollable elements (make the window small enough)." 17 | Sleep 500 18 | MsgBox "ScrollPattern properties: " 19 | . "`nCurrentHorizontalScrollPercent: " treeEl.HorizontalScrollPercent 20 | . "`nCurrentVerticalScrollPercent: " treeEl.VerticalScrollPercent 21 | . "`nCurrentHorizontalViewSize: " treeEl.HorizontalViewSize 22 | . "`nCurrentHorizontallyScrollable: " treeEl.HorizontallyScrollable 23 | . "`nCurrentVerticallyScrollable: " treeEl.VerticallyScrollable 24 | Sleep 50 25 | MsgBox "Press OK to set scroll percent to 50% vertically and 0% horizontally." 26 | treeEl.SetScrollPercent(50) ; Equivalent to treeEl.VerticalScrollPercent := 50 27 | Sleep 500 28 | MsgBox "Press OK to scroll a Page Up equivalent upwards vertically." 29 | treeEl.Scroll(UIA.ScrollAmount.LargeDecrement) ; LargeDecrement is equivalent to pressing the PAGE UP key or clicking on a blank part of a scroll bar. SmallDecrement is equivalent to pressing an arrow key or clicking the arrow button on a scroll bar. 30 | 31 | Sleep 500 32 | MsgBox "Press OK to scroll drive C: into view." 33 | CDriveEl := explorerEl.FindElement({Type:"TreeItem", Name:CDriveName, matchmode:"Substring"}) 34 | if !CDriveEl { 35 | MsgBox "C: drive element not found! Exiting app..." 36 | ExitApp 37 | } 38 | CDriveEl.ScrollIntoView() 39 | 40 | ExitApp -------------------------------------------------------------------------------- /Examples/Example12_GridPatternGridItemPattern.ahk: -------------------------------------------------------------------------------- 1 | #Requires AutoHotkey v2 2 | ;#include ; Uncomment if you have moved UIA.ahk to your main Lib folder 3 | #include ..\Lib\UIA.ahk 4 | 5 | Run "explore C:\" 6 | CDriveName := DriveGetLabel("C:") " (C:)" 7 | WinWaitActive(CDriveName,,1) 8 | WinMove(100, 200, 1000, , "A") 9 | explorerEl := UIA.ElementFromHandle("A") 10 | if !explorerEl { 11 | MsgBox "Drive C: element not found! Exiting app..." 12 | ExitApp 13 | } 14 | listEl := explorerEl.FindElement({Type:"List"}) 15 | MsgBox "GridPattern properties: " 16 | . "`nCurrentRowCount: " listEl.RowCount 17 | . "`nCurrentColumnCount: " listEl.ColumnCount 18 | Sleep 500 19 | MsgBox "Getting grid item from row 4, column 1 (0-based indexing)" 20 | editEl := listEl.GetItem(3,0).Highlight() 21 | MsgBox "Got this element: `n" editEl.Dump() 22 | 23 | MsgBox "GridItemPattern properties: " 24 | . "`nCurrentRow: " editEl.Row 25 | . "`nCurrentColumn: " editEl.Column 26 | . "`nCurrentRowSpan: " editEl.RowSpan 27 | . "`nCurrentColumnSpan: " editEl.ColumnSpan 28 | ; editEl.ContainingGrid should return listEl 29 | 30 | ExitApp 31 | -------------------------------------------------------------------------------- /Examples/Example13_TablePatternTableItemPattern.ahk: -------------------------------------------------------------------------------- 1 | #Requires AutoHotkey v2 2 | ;#include ; Uncomment if you have moved UIA.ahk to your main Lib folder 3 | #include ..\Lib\UIA.ahk 4 | 5 | Run "explore C:\Windows" 6 | WinWaitActive("Windows",,1) 7 | WinMove(100, 200, 1000, , "A") 8 | explorerEl := UIA.ElementFromHandle("A") 9 | if !explorerEl { 10 | MsgBox "Windows folder window not found! Exiting app..." 11 | ExitApp 12 | } 13 | Sleep 500 14 | listEl := explorerEl.FindElement({Type:"List"}) 15 | 16 | MsgBox "TablePattern properties: " 17 | . "`nCurrentRowOrColumnMajor: " listEl.RowOrColumnMajor 18 | 19 | rowHeaders := listEl.GetRowHeaders() 20 | rowHeadersDump := "" 21 | for header in rowHeaders 22 | rowHeadersDump .= header.Dump() "`n" 23 | MsgBox "TablePattern elements from GetRowHeaders:`n" rowHeadersDump ; Should be empty, there aren't any row headers 24 | columnHeaders := listEl.GetColumnHeaders() 25 | columnHeadersDump := "" 26 | for header in columnHeaders 27 | columnHeadersDump .= header.Dump() "`n" 28 | MsgBox "TablePattern elements from GetColumnHeaders:`n" columnHeadersDump 29 | 30 | editEl := listEl.GetItem(3,0) ; To test the TableItem pattern, we need to get an element supporting that using Grid pattern... 31 | rowHeaderItems := editEl.GetRowHeaderItems() 32 | rowHeaderItemsDump := "" 33 | for headerItem in rowHeaderItems 34 | rowHeaderItemsDump .= headerItem.Dump() "`n" 35 | MsgBox "TableItemPattern elements from GetRowHeaderItems:`n" rowHeaderItemsDump ; Should be empty, there aren't any row headers 36 | columnHeaderItems := editEl.GetColumnHeaderItems() 37 | columnHeaderItemsDump := "" 38 | for headerItem in columnHeaderItems 39 | columnHeaderItemsDump .= headerItem.Dump() "`n" 40 | MsgBox "TableItemPattern elements from GetCurrentColumnHeaderItems:`n" columnHeaderItemsDump 41 | ExitApp 42 | -------------------------------------------------------------------------------- /Examples/Example14_MultipleViewPattern.ahk: -------------------------------------------------------------------------------- 1 | #Requires AutoHotkey v2 2 | ;#include ; Uncomment if you have moved UIA.ahk to your main Lib folder 3 | #include ..\Lib\UIA.ahk 4 | 5 | Run "explore C:\Windows" 6 | WinWaitActive("Windows",,1) 7 | WinMove(100, 200, 1000, , "A") 8 | explorerEl := UIA.ElementFromHandle("A") 9 | if !explorerEl { 10 | MsgBox "Drive C: element not found! Exiting app..." 11 | ExitApp 12 | } 13 | listEl := explorerEl.FindElement({Type:"List"}) 14 | 15 | MsgBox "MultipleView properties: " 16 | . "`nCurrentCurrentView: " (currentView := listEl.CurrentView) 17 | 18 | supportedViews := listEl.GetSupportedViews() 19 | viewNames := "" 20 | for view in supportedViews { 21 | viewNames .= listEl.GetViewName(view) " (" view ")`n" 22 | } 23 | MsgBox "This MultipleView supported views:`n" viewNames 24 | MsgBox "Press OK to set MultipleView to view 4." 25 | listEl.SetCurrentView(4) 26 | 27 | Sleep 500 28 | MsgBox "Press OK to reset back to view " currentView "." 29 | listEl.SetCurrentView(currentView) 30 | 31 | ExitApp 32 | -------------------------------------------------------------------------------- /Examples/Example15_SelectionPatternSelectionItemPattern.ahk: -------------------------------------------------------------------------------- 1 | #Requires AutoHotkey v2 2 | ;#include ; Uncomment if you have moved UIA.ahk to your main Lib folder 3 | #include ..\Lib\UIA.ahk 4 | 5 | Run "explore C:\" 6 | CDriveName := DriveGetLabel("C:") " (C:)" 7 | WinWaitActive(CDriveName,,1) 8 | WinMove(100, 200, 1000, , "A") 9 | explorerEl := UIA.ElementFromHandle("A") 10 | if !explorerEl { 11 | MsgBox "Drive C: element not found! Exiting app..." 12 | ExitApp 13 | } 14 | listEl := explorerEl.FindElement({Type:"List"}) 15 | 16 | MsgBox "SelectionPattern properties: " 17 | . "`nCurrentCanSelectMultiple: " listEl.CanSelectMultiple 18 | . "`nCurrentIsSelectionRequired: " listEl.IsSelectionRequired 19 | 20 | currentSelectionEls := listEl.GetCurrentSelection() 21 | currentSelections := "" 22 | for index, selection in currentSelectionEls 23 | currentSelections .= index ": " selection.Dump() "`n" 24 | 25 | windowsListItem := explorerEl.FindElement({Name:"Windows", Type:"ListItem"}) 26 | MsgBox "ListItemPattern properties for Windows folder list item:" 27 | . "`nCurrentIsSelected: " windowsListItem.IsSelected 28 | . "`nCurrentSelectionContainer: " windowsListItem.SelectionContainer.Dump() 29 | 30 | MsgBox "Press OK to select `"Windows`" folder list item." 31 | windowsListItem.Select() 32 | MsgBox "Press OK to add to selection `"Program Files`" folder list item." 33 | explorerEl.FindElement({Name:"Program Files", Type:"ListItem"}).AddToSelection() 34 | MsgBox "Press OK to remove selection from `"Windows`" folder list item." 35 | windowsListItem.RemoveFromSelection() 36 | 37 | ExitApp 38 | -------------------------------------------------------------------------------- /Examples/Example16_WindowPatternTransformPattern.ahk: -------------------------------------------------------------------------------- 1 | #Requires AutoHotkey v2 2 | ;#include ; Uncomment if you have moved UIA.ahk to your main Lib folder 3 | #include ..\Lib\UIA.ahk 4 | 5 | Run "explore C:\" 6 | CDriveName := DriveGetLabel("C:") " (C:)" 7 | WinWaitActive(CDriveName,,1) 8 | WinMove(200, 100, 1000, 800, CDriveName) 9 | explorerEl := UIA.ElementFromHandle(CDriveName) 10 | if !explorerEl { 11 | MsgBox "Drive C: element not found! Exiting app..." 12 | ExitApp 13 | } 14 | ; WindowPattern 15 | Sleep 500 16 | MsgBox "WindowPattern properties: " 17 | . "`nCurrentCanMaximize: " explorerEl.CanMaximize 18 | . "`nCurrentCanMinimize: " explorerEl.CanMinimize 19 | . "`nCurrentIsModal: " explorerEl.IsModal 20 | . "`nCurrentIsTopmost: " explorerEl.IsTopmost 21 | . "`nCurrentWindowVisualState: " (visualState := explorerEl.WindowVisualState) " (" UIA.WindowVisualState[visualState] ")" 22 | . "`nCurrentWindowInteractionState: " (interactionState := explorerEl.WindowInteractionState) " (" UIA.WindowInteractionState[interactionState] ")" 23 | Sleep 50 24 | MsgBox "Press OK to try minimizing" 25 | explorerEl.SetWindowVisualState(UIA.WindowVisualState.Minimized) 26 | 27 | Sleep 500 28 | MsgBox "Press OK to bring window back to normal" 29 | explorerEl.SetWindowVisualState(UIA.WindowVisualState.Normal) 30 | 31 | ; TransformPattern 32 | Sleep 500 33 | MsgBox "TransformPattern properties: " 34 | . "`nCurrentCanMove: " explorerEl.CanMove 35 | . "`nCurrentCanResize: " explorerEl.CanResize 36 | . "`nCurrentCanRotate: " explorerEl.CanRotate 37 | 38 | MsgBox "Press OK to move to coordinates x100 y200" 39 | explorerEl.Move(100,200) 40 | 41 | Sleep 500 42 | MsgBox "Press OK to resize to w600 h400" 43 | explorerEl.Resize(600,400) 44 | 45 | Sleep 500 46 | MsgBox "Press OK to close window" 47 | WinMove(100, 200, 1000, 800, CDriveName) 48 | explorerEl.Close() 49 | ExitApp 50 | -------------------------------------------------------------------------------- /Examples/Example17_TextPatternTextRange.ahk: -------------------------------------------------------------------------------- 1 | #Requires AutoHotkey v2 2 | ;#include ; Uncomment if you have moved UIA_Interface.ahk to your main Lib folder 3 | #include ..\Lib\UIA.ahk 4 | 5 | lorem := "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum." 6 | 7 | Run "notepad.exe" 8 | ;WinActivate, "ahk_exe notepad.exe" 9 | WinWaitActive "ahk_exe notepad.exe" 10 | NotepadEl := UIA.ElementFromHandle("ahk_exe notepad.exe") 11 | editEl := NotepadEl.FindElement([{Type:"Document"}, {Type:"Edit"}]) ; Get the Edit or Document element (differs between UIAutomation versions) 12 | editEl.Value := lorem ; Set the text to our sample text 13 | textPattern := editEl.TextPattern 14 | 15 | MsgBox "TextPattern properties:" 16 | . "`nDocumentRange: returns the TextRange for all the text inside the element" 17 | . "`nSupportedTextSelection: " editEl.SupportedTextSelection 18 | . "`n`nTextPattern methods:" 19 | . "`nRangeFromPoint(x,y): retrieves an empty TextRange nearest to the specified screen coordinates" 20 | . "`nRangeFromChild(child): retrieves a text range enclosing a child element such as an image, hyperlink, Microsoft Excel spreadsheet, or other embedded object." 21 | . "`nGetSelection(): returns the currently selected text" 22 | . "`nGetVisibleRanges(): retrieves an array of disjoint text ranges from a text-based control where each text range represents a contiguous span of visible text" 23 | 24 | wholeRange := editEl.DocumentRange ; Get the TextRange for all the text inside the Edit element 25 | 26 | MsgBox "To select a certain phrase inside the text, use FindText() method to get the corresponding TextRange, then Select() to select it.`n`nPress OK to select the text `"dolor sit amet`"" 27 | WinActivate "ahk_exe notepad.exe" 28 | wholeRange.FindText("dolor sit amet").Select() 29 | Sleep 1000 30 | 31 | ; For the next example we need to clone the TextRange, because some methods change the supplied TextRange directly (here we don't want to change our original wholeRange TextRange). An alternative would be to use wholeRange, and after moving the endpoints and selecting the new range, we could call ExpandToEnclosingUnit() to reset the endpoints and get the whole TextRange back 32 | textSpan := wholeRange.Clone() 33 | 34 | MsgBox "To select a span of text, we need to move the endpoints of the TextRange. This can be done with MoveEndpointByUnit.`n`nPress OK to select the text with startpoint of 28 characters from start`nand 390 characters from the end of the sample text" 35 | WinActivate "ahk_exe notepad.exe" 36 | textSpan.MoveEndpointByUnit(UIA.TextPatternRangeEndpoint.Start, UIA.TextUnit.Character, 28) ; Move 28 characters from the start of the sample text 37 | textSpan.MoveEndpointByUnit(UIA.TextPatternRangeEndpoint.End, UIA.TextUnit.Character, -390) ; Move 390 characters backwards from the end of the sample text 38 | textSpan.Select() 39 | Sleep 1000 40 | 41 | MsgBox "We can also get the location of texts. Press OK to test it" 42 | br := wholeRange.GetBoundingRectangles() 43 | for k, v in br { 44 | RangeTip(v.x, v.y, v.w, v.h) 45 | Sleep 1000 46 | } 47 | RangeTip() 48 | 49 | ExitApp 50 | 51 | RangeTip(x:="", y:="", w:="", h:="", color:="Red", d:=2) { ; from the FindText library, credit goes to feiyue 52 | static HighlightGui := [] 53 | if x="" { 54 | for r in HighlightGui 55 | r.Destroy() 56 | HighlightGui := [] 57 | return 58 | } 59 | Loop 4 60 | HighlightGui.Push(Gui("+AlwaysOnTop -Caption +ToolWindow -DPIScale +E0x08000000")) 61 | Loop 4 { 62 | i:=A_Index 63 | , x1:=(i=2 ? x+w : x-d) 64 | , y1:=(i=3 ? y+h : y-d) 65 | , w1:=(i=1 or i=3 ? w+2*d : d) 66 | , h1:=(i=2 or i=4 ? h+2*d : d) 67 | HighlightGui[i].BackColor := color 68 | HighlightGui[i].Show("NA x" . x1 . " y" . y1 . " w" . w1 . " h" . h1) 69 | } 70 | } -------------------------------------------------------------------------------- /Examples/Example18_TextRangeTextChangedEvent.ahk: -------------------------------------------------------------------------------- 1 | #Requires AutoHotkey v2 2 | ;#include ; Uncomment if you have moved UIA_Interface.ahk to your main Lib folder 3 | #include ..\Lib\UIA.ahk 4 | 5 | MsgBox "To test this file, create a new Word document (with the title `"Document1 - Word`") and write some sample text in it." 6 | 7 | program := "Document1 - Word" 8 | WinActivate program 9 | WinWaitActive program 10 | wordEl := UIA.ElementFromHandle(program) 11 | bodyEl := wordEl.FindElement({AutomationId:"Body"}) ; First get the body element of Word 12 | document := bodyEl.DocumentRange ; Get the TextRange for the whole document 13 | 14 | MsgBox "Current text inside Word body element:`n" document.GetText() ; Display the text from the TextRange 15 | WinActivate program 16 | 17 | MsgBox "We can get text from a specific attribute, such as text within a `"bullet list`".`nTo test this, create a bullet list (with filled bullets) in Word and press OK." 18 | MsgBox "Found the following text in bullet list:`n" document.FindAttribute(UIA.TextAttribute.BulletStyle, UIA.BulletStyle.FilledRoundBullet).GetText() 19 | 20 | Loop { 21 | out := InputBox("Search text in Word by font. Type some example text in Word.`nThen write a font (such as `"Calibri`") and press OK`n`nNote that this is case-sensitive, and fonts start with a capital letter`n(`"calibri`" is not the same as `"Calibri`")", "Find") 22 | if out.Result != "OK" 23 | break 24 | else if !out.value 25 | MsgBox "You need to type a font to search!" 26 | else if (found := document.FindAttribute(UIA.TextAttribute.FontName, out.value)) 27 | MsgBox "Found the following text:`n" found.GetText() 28 | else 29 | MsgBox "No text with the font " out.value " found!" 30 | } 31 | 32 | MsgBox "Press OK to create a new EventHandler for the TextChangedEvent.`nTo test this, type some new text inside Word, and a tooltip should pop up.`n`nTo exit the script, press F5." 33 | handler := UIA.CreateAutomationEventHandler(TextChangedEventHandler) ; Create a new event handler that points to the function TextChangedEventHandler, which must accept two arguments: element and eventId. 34 | UIA.AddAutomationEventHandler(handler, UIA.Event.Text_TextChanged, wordEl) ; Add a new automation handler for the TextChanged event. Note that we can only use wordEl here, not bodyEd, because the event is handled for the whole window. 35 | OnExit(ExitFunc) ; Set up an OnExit call to clean up the handler when exiting the script 36 | 37 | return 38 | 39 | TextChangedEventHandler(el, eventId) { 40 | try { 41 | ToolTip "You changed text in Word:`n`n" el.DocumentRange.GetText() 42 | SetTimer RemoveToolTip, -2000 43 | } 44 | } 45 | 46 | ExitFunc(*) { 47 | global handler, wordEl 48 | UIA.RemoveAutomationEventHandler(handler, UIA.Event.Text_TextChanged, wordEl) ; Remove the event handler. Alternatively use UIA.RemoveAllEventHandlers() to remove all handlers 49 | } 50 | 51 | RemoveToolTip() { 52 | ToolTip 53 | } 54 | 55 | F5::ExitApp 56 | -------------------------------------------------------------------------------- /Examples/Example19_PropertyChangedEvent.ahk: -------------------------------------------------------------------------------- 1 | #Requires AutoHotkey v2 2 | ;#include ; Uncomment if you have moved UIA.ahk to your main Lib folder 3 | #include ..\Lib\UIA.ahk 4 | 5 | Run "explore C:\" 6 | CDriveName := DriveGetLabel("C:") " (C:)" 7 | WinWaitActive(CDriveName,,1) 8 | explorerEl := UIA.ElementFromHandle("A") 9 | if !explorerEl { 10 | MsgBox "Drive C: element not found! Exiting app..." 11 | ExitApp 12 | } 13 | MsgBox "Press OK to create a new EventHandler for the PropertyChanged event (property UIA_NamePropertyId).`nTo test this, click on any file/folder, and a tooltip should pop up.`n`nTo exit the script, press F5." 14 | handler := UIA.CreatePropertyChangedEventHandler(PropertyChangedEventHandler) 15 | UIA.AddPropertyChangedEventHandler(handler, explorerEl, UIA.Property.Name) ; Multiple properties can be specified in an array 16 | OnExit(ExitFunc) ; Set up an OnExit call to clean up the handler when exiting the script 17 | return 18 | 19 | PropertyChangedEventHandler(sender, propertyId, newValue) { 20 | ToolTip "Sender: " sender.Dump() 21 | . "`nPropertyId: " propertyId 22 | . "`nNew value: " newValue 23 | SetTimer ToolTip, -3000 24 | } 25 | 26 | ExitFunc(*) { 27 | UIA.RemoveAllEventHandlers() 28 | } 29 | 30 | F5::ExitApp 31 | -------------------------------------------------------------------------------- /Examples/Example20_StructureChangedEvent.ahk: -------------------------------------------------------------------------------- 1 | #Requires AutoHotkey v2 2 | ;#include ; Uncomment if you have moved UIA.ahk to your main Lib folder 3 | #include ..\Lib\UIA.ahk 4 | 5 | Run "explore C:\" 6 | CDriveName := DriveGetLabel("C:") " (C:)" 7 | WinWaitActive(CDriveName,,1) 8 | explorerEl := UIA.ElementFromHandle("A") 9 | if !explorerEl { 10 | MsgBox "Drive C: element not found! Exiting app..." 11 | ExitApp 12 | } 13 | MsgBox "Press OK to create a new EventHandler for the StructureChanged event.`nTo test this, interact with the Explorer window, and a tooltip should pop up.`n`nTo exit the script, press F5." 14 | handler := UIA.CreateStructureChangedEventHandler(StructureChangedEventHandler) 15 | UIA.AddStructureChangedEventHandler(handler, explorerEl) 16 | OnExit(ExitFunc) ; Set up an OnExit call to clean up the handler when exiting the script 17 | return 18 | 19 | StructureChangedEventHandler(sender, changeType, runtimeId) { 20 | ToolTip "Sender: " sender.Dump() 21 | . "`nChange type: " changeType 22 | . "`nRuntime Id: " UIA.RuntimeIdToString(runtimeId) 23 | SetTimer ToolTip, -3000 24 | } 25 | 26 | ExitFunc(*) { 27 | UIA.RemoveAllEventHandlers() 28 | } 29 | 30 | F5::ExitApp 31 | -------------------------------------------------------------------------------- /Examples/Example21_NotificationEvent.ahk: -------------------------------------------------------------------------------- 1 | #Requires AutoHotkey v2 2 | ;#include ; Uncomment if you have moved UIA.ahk to your main Lib folder 3 | #include ..\Lib\UIA.ahk 4 | 5 | Run "calc.exe" 6 | Sleep 1000 7 | cEl := UIA.ElementFromHandle("A") 8 | MsgBox "Press OK to create a new EventHandler for the Notification event.`nTo test this, interact with the Calculator window, and a tooltip should pop up.`n`nTo exit the script, press F5." 9 | handler := UIA.CreateNotificationEventHandler(NotificationEventHandler) 10 | UIA.AddNotificationEventHandler(handler, cEl) 11 | OnExit(ExitFunc) ; Set up an OnExit call to clean up the handler when exiting the script 12 | return 13 | 14 | NotificationEventHandler(sender, notificationKind, notificationProcessing, displayString, activityId) { 15 | ToolTip "Sender: " sender.Dump() 16 | . "`nNotification kind: " notificationKind " (" UIA.NotificationKind[notificationKind] ")" 17 | . "`nNotification processing: " notificationProcessing " (" UIA.NotificationProcessing[notificationProcessing] ")" 18 | . "`nDisplay string: " displayString 19 | . "`nActivity Id: " activityId 20 | SetTimer ToolTip, -3000 21 | } 22 | 23 | ExitFunc(*) { 24 | UIA.RemoveAllEventHandlers() 25 | } 26 | 27 | F5::ExitApp 28 | -------------------------------------------------------------------------------- /Examples/Example22_EventHandlerGroup.ahk: -------------------------------------------------------------------------------- 1 | #Requires AutoHotkey v2 2 | ;#include ; Uncomment if you have moved UIA.ahk to your main Lib folder 3 | #include ..\Lib\UIA.ahk 4 | 5 | Run "calc.exe" 6 | Sleep 1000 7 | cEl := UIA.ElementFromHandle("A") 8 | 9 | ehGroup := UIA.CreateEventHandlerGroup() 10 | h1 := UIA.CreateAutomationEventHandler(AutomationEventHandler) 11 | h2 := UIA.CreateNotificationEventHandler(NotificationEventHandler) 12 | ehGroup.AddAutomationEventHandler(h1, UIA.Event.AutomationFocusChanged) 13 | ehGroup.AddNotificationEventHandler(h2) 14 | UIA.AddEventHandlerGroup(ehGroup, cEl) 15 | 16 | OnExit(ExitFunc) ; Set up an OnExit call to clean up the handler when exiting the script 17 | return 18 | 19 | AutomationEventHandler(sender, eventId) { 20 | ToolTip "Sender: " sender.Dump() 21 | . "`nEvent Id: " eventId 22 | Sleep 500 23 | SetTimer ToolTip, -3000 24 | } 25 | 26 | NotificationEventHandler(sender, notificationKind, notificationProcessing, displayString, activityId) { 27 | ToolTip "Sender: " sender.Dump() 28 | . "`nNotification kind: " notificationKind " (" UIA.NotificationKind[notificationKind] ")" 29 | . "`nNotification processing: " notificationProcessing " (" UIA.NotificationProcessing[notificationProcessing] ")" 30 | . "`nDisplay string: " displayString 31 | . "`nActivity Id: " activityId 32 | Sleep 500 33 | SetTimer ToolTip, -3000 34 | } 35 | 36 | ExitFunc(*) { 37 | UIA.RemoveAllEventHandlers() 38 | } 39 | 40 | F5::ExitApp 41 | -------------------------------------------------------------------------------- /Examples/Example23_Caching.ahk: -------------------------------------------------------------------------------- 1 | #Requires AutoHotkey v2 2 | ;#include ; Uncomment if you have moved UIA.ahk to your main Lib folder 3 | #include ..\Lib\UIA.ahk 4 | 5 | cacheRequest := UIA.CreateCacheRequest() 6 | ; Set TreeScope to include the starting element and all descendants as well 7 | cacheRequest.TreeScope := 5 8 | ; Add all the necessary properties that DumpAll uses: ControlType, LocalizedControlType, AutomationId, Name, Value, ClassName, AcceleratorKey 9 | cacheRequest.AddProperty("Type") 10 | cacheRequest.AddProperty("LocalizedType") 11 | cacheRequest.AddProperty("AutomationId") 12 | cacheRequest.AddProperty("Name") 13 | cacheRequest.AddProperty("Value") 14 | cacheRequest.AddProperty("ClassName") 15 | cacheRequest.AddProperty("AcceleratorKey") 16 | 17 | ; To use cached patterns, first add the pattern 18 | cacheRequest.AddPattern("Window") 19 | ; Also need to add any pattern properties we wish to use 20 | cacheRequest.AddProperty("WindowCanMaximize") 21 | 22 | ; This all can be done in one line as well: 23 | ; cacheRequest := UIA.CreateCacheRequest(["Type", "LocalizedType", "AutomationId", "Name", "Value", "ClassName", "AcceleratorKey", "WindowCanMaximize"], ["Window"], "Subtree") 24 | 25 | Run "notepad.exe" 26 | WinWaitActive "ahk_exe notepad.exe" 27 | 28 | ; Get element and also build the cache 29 | npEl:= UIA.ElementFromHandle("ahk_exe notepad.exe", cacheRequest) 30 | ; We now have a cached "snapshot" of the window from which we can access our desired elements faster. 31 | MsgBox npEl.CachedDump() 32 | MsgBox npEl.CachedWindowPattern.CachedCanMaximize 33 | 34 | ExitApp -------------------------------------------------------------------------------- /Examples/Example24_CachingNotepad.ahk: -------------------------------------------------------------------------------- 1 | #Requires AutoHotkey v2 2 | ;#include ; Uncomment if you have moved UIA.ahk to your main Lib folder 3 | #include ..\Lib\UIA.ahk 4 | 5 | cacheRequest := UIA.CreateCacheRequest(["Type", "Name", "Value"],, "Subtree") 6 | /* 7 | ; Instead we could also define a cacherequest like this: 8 | cacheRequest := UIA.CreateCacheRequest() 9 | ; Set TreeScope to include the starting element and all descendants as well 10 | cacheRequest.TreeScope := 5 11 | ; Add some properties to be cached 12 | cacheRequest.AddProperty("Type") 13 | cacheRequest.AddProperty("Name") 14 | cacheRequest.AddProperty("Value") 15 | 16 | ; Or like this: 17 | cacheRequest := UIA.CreateCacheRequest({properties:["Type", "Name", "Value"], scope:"Subtree"}) 18 | */ 19 | 20 | Run "notepad.exe" 21 | WinWaitActive "ahk_exe notepad.exe" 22 | 23 | MsgBox("Type something in Notepad: note that the document content won't change in the tooltip.`nPress F5 to refresh the cache - then the document content will also update in the tooltip.") 24 | 25 | ; Get element and also build the cache 26 | npEl:= UIA.ElementFromHandle("ahk_exe notepad.exe", cacheRequest) 27 | docEl := npEl.FindElement([{Type:"Document"}, {Type:"Edit"}],,,,,cacheRequest) 28 | ; We now have a cached "snapshot" of the window from which we can access our desired elements faster. 29 | Loop { 30 | ToolTip "Cached window name: " npEl.CachedName "`nCached document content: " docEl.CachedValue 31 | } 32 | 33 | F5:: 34 | { 35 | global npEl, docEl, cacheRequest 36 | npEl := npEl.BuildUpdatedCache(cacheRequest) 37 | docEl := docEl.BuildUpdatedCache(cacheRequest) 38 | } 39 | Esc::ExitApp -------------------------------------------------------------------------------- /Examples/Example25_ElementVisibility.ahk: -------------------------------------------------------------------------------- 1 | #Requires AutoHotkey v2 2 | ;#include ; Uncomment if you have moved UIA.ahk to your main Lib folder 3 | #include ..\Lib\UIA.ahk 4 | 5 | /* 6 | UIAutomation usually cannot "see" elements if they are not visible on the screen. 7 | Different programs handle this differently. Some just set Element.IsOffscreen property to 0, 8 | others don't display the element at all in the UIA tree. 9 | 10 | This example shows one way how an element (in this case the System32 folder) can be 11 | searched for by interacting with the window. 12 | 13 | // Credit for this example goes to user neogna2. 14 | */ 15 | 16 | SetTitleMatchMode 2 17 | Run "explorer C:\Windows" 18 | if !explorerHwnd := WinWaitActive("Windows",,1) 19 | ExitApp 20 | ; Decrease window height so that folder "System32" is out of view 21 | WinMove( , , 1000, 800, explorerHwnd) 22 | explorerEl := UIA.ElementFromHandle("A") 23 | if !explorerEl 24 | ExitApp 25 | listEl := explorerEl.FindElement({Type:"List"}) 26 | if !listItem := explorerEl.ElementExist({Name:"System32", Type:"ListItem"}) { 27 | if "OK" != MsgBox("Press OK to scroll until 'System32' is in view and then select it.") 28 | ExitApp 29 | Loop { 30 | if listItem := explorerEl.ElementExist({Name:"System32", Type:"ListItem"}) { 31 | listItem.AddToSelection() 32 | break 33 | } 34 | listEl.Scroll("LargeIncrement") 35 | } Until Round(listEl.VerticalScrollPercent) = 100 36 | } 37 | ExitApp 38 | 39 | F5::ExitApp -------------------------------------------------------------------------------- /Examples/Spotify.ahk: -------------------------------------------------------------------------------- 1 | #include ..\Lib\UIA.ahk 2 | /** 3 | * This is a small library to demonstrate how UIA could be used to automate the Spotify app. 4 | * It has not been tested enough to be actually used. 5 | */ 6 | 7 | ; Don't bring Spotify to focus automatically 8 | UIA.AutoSetFocus := False 9 | 10 | F1::Spotify.TogglePlay() 11 | F11::Spotify.ToggleFullscreen() 12 | ^d::Spotify.Toast("Playing song: " (song := Spotify.CurrentSong).Name "`nArtist: " song.Artist "`nPlay time: " song.Time " / " song.Length) 13 | ^l:: 14 | { 15 | song := Spotify.CurrentSong 16 | Spotify.Toast("You " (!Spotify.LikeState ? "liked " : "removed a like from ") song.Name " by " song.Artist) 17 | Spotify.ToggleLike() 18 | } 19 | ^Left::Spotify.NextSong() 20 | ^Right::Spotify.PreviousSong() 21 | ^+::Spotify.Volume += 10 22 | ^-::Spotify.Volume -= 10 23 | ^m::Spotify.ToggleMute() 24 | 25 | class Spotify { 26 | static winExe := "ahk_exe Spotify.exe" 27 | static winTitle => WinGetTitle(this.winExe) 28 | static exePath := A_AppData "\Spotify\Spotify.exe" 29 | 30 | ; Internal method to show a Toast message, but before that remove the previous one 31 | static Toast(message) { 32 | TrayTip 33 | A_IconHidden := true 34 | Sleep 200 35 | A_IconHidden := false 36 | TrayTip(message, "Spotify info") 37 | } 38 | ; Internal methods to get some commonly used Spotify UIA elements 39 | static GetSpotifyElement() => UIA.ElementFromHandle(Spotify.winExe)[1] 40 | static GetLikeElement() => Spotify.GetSpotifyElement().ElementFromPath({T:26, i:3}, {T:26,N:"Now playing: ",mm:2}, {T:0,N:"Library", mm:2}) 41 | static GetCurrentSongElement() => Spotify.FullscreenState ? Spotify.GetSpotifyElement() : Spotify.GetSpotifyElement()[{T:26, i:3}] 42 | 43 | static LikeState { 44 | get => Spotify.GetLikeElement().ToggleState 45 | set => Spotify.GetLikeElement().ToggleState := value 46 | } 47 | static Like() => Spotify.LikeState := 1 48 | static RemoveLike() => Spotify.LikeState := 0 49 | static ToggleLike() => Spotify.LikeState := !Spotify.LikeState 50 | 51 | static CurrentSong { 52 | get { 53 | contentEl := Spotify.GetCurrentSongElement() 54 | return { 55 | Name:contentEl[{Type:"Group"},{Type:"Link",i:2}].Name, 56 | Artist:contentEl[{Type:"Group"},{Type:"Link",i:3}].Name, 57 | Time:contentEl[{Type:"Text", i:1}].Name, 58 | Length:contentEl[{Type:"Text", i:3}].Name 59 | } 60 | } 61 | } 62 | static CurrentSongState { 63 | get { 64 | try return Spotify.GetCurrentSongElement()[[{Name:"Play"}, {Name:"Pause"}]].Name == "Play" 65 | catch 66 | throw Error("Play/Pause button not found!", -1) 67 | } 68 | set { 69 | if value != Spotify.CurrentSongState 70 | try Spotify.GetCurrentSongElement()[[{Name:"Play"}, {Name:"Pause"}]].Click() 71 | } 72 | } 73 | static Play() => Spotify.CurrentSongState := 1 74 | static Pause() => Spotify.CurrentSongState := 0 75 | static TogglePlay() => Spotify.CurrentSongState := !Spotify.CurrentSongState 76 | 77 | static NextSong() => Spotify.GetCurrentSongElement()[{Name:"Next"}].Click() 78 | static PreviousSong() => Spotify.GetCurrentSongElement()[{Name:"Previous"}].Click() 79 | 80 | static FullscreenState { 81 | get => Spotify.GetSpotifyElement()[-1].Type == UIA.Type.Button 82 | set { 83 | WinActivate(Spotify.winExe) 84 | WinWaitActive(Spotify.winExe,,1) 85 | if Spotify.FullscreenState 86 | Spotify.GetSpotifyElement()[-1].Click() 87 | else 88 | Spotify.GetCurrentSongElement()[-1].Click() 89 | } 90 | } 91 | static ToggleFullscreen() => Spotify.FullscreenState := !Spotify.FullscreenState 92 | 93 | static MuteState { 94 | get => Spotify.GetCurrentSongElement()[{Type:"Button", i:-2}] == "Mute" 95 | set { 96 | currentState := Spotify.MuteState 97 | if Value && !currentState 98 | Spotify.GetCurrentSongElement()[{Type:"Button", i:-2}].Click() 99 | if !Value && currentState 100 | Spotify.GetCurrentSongElement()[{Type:"Button", i:-2}].Click() 101 | 102 | } 103 | } 104 | static ToggleMute() => Spotify.MuteState := !Spotify.MuteState 105 | static Mute() => Spotify.MuteState := 1 106 | static Unmute() => Spotify.MuteState := 0 107 | 108 | static Volume { 109 | get => Spotify.GetCurrentSongElement()[{Type:"Slider",i:-1}].Value 110 | set => Spotify.GetCurrentSongElement()[{Type:"Slider",i:-1}].Value := value 111 | } 112 | } -------------------------------------------------------------------------------- /Examples/UIA_Browser_Example01_Chrome.ahk: -------------------------------------------------------------------------------- 1 | #Requires AutoHotkey v2 2 | ;#include ; Uncomment if you have moved UIA.ahk to your main Lib folder 3 | #include ..\Lib\UIA.ahk 4 | ;#include ; Uncomment if you have moved UIA_Browser.ahk to your main Lib folder 5 | #include ..\Lib\UIA_Browser.ahk 6 | 7 | /** 8 | * This example starts a new Chrome window and then gets the Document element content into the Clipboard. 9 | */ 10 | 11 | ; Run in Incognito mode to avoid any extensions interfering. 12 | Run "chrome.exe -incognito" 13 | WinWaitActive "ahk_exe chrome.exe" 14 | Sleep 500 15 | ; Initialize UIA_Browser, use Last Found Window (returned by WinWaitActive) 16 | cUIA := UIA_Browser() 17 | A_Clipboard := "" 18 | ; Get the current document element (this excludes the URL bar, navigation buttons etc) 19 | ; and dump all the information about it in the clipboard. 20 | ; Use Ctrl+V to paste it somewhere, such as in Notepad. 21 | A_Clipboard := cUIA.GetCurrentDocumentElement().DumpAll() 22 | ClipWait 1 23 | if A_Clipboard 24 | MsgBox "Page information successfully dumped. Use Ctrl+V to paste the info somewhere, such as in Notepad." 25 | else 26 | MsgBox "Something went wrong and nothing was dumped in the clipboard!" 27 | ExitApp 28 | -------------------------------------------------------------------------------- /Examples/UIA_Browser_Example02_EdgeScrolling.ahk: -------------------------------------------------------------------------------- 1 | #Requires AutoHotkey v2 2 | ;#include ; Uncomment if you have moved UIA_Interface.ahk to your main Lib folder 3 | #include ..\Lib\UIA.ahk 4 | ;#include ; Uncomment if you have moved UIA_Browser.ahk to your main Lib folder 5 | #include ..\Lib\UIA_Browser.ahk 6 | 7 | /** 8 | * A small example for Edge, demostrating form filling (the search box) and scrolling the page. 9 | */ 10 | 11 | ; Run in Incognito mode to avoid any extensions interfering. 12 | Run "msedge.exe -inprivate" 13 | WinWaitActive "ahk_exe msedge.exe" 14 | Sleep 500 15 | ; Initialize UIA_Browser, use Last Found Window 16 | cUIA := UIA_Browser() 17 | ; Wait the "New inprivate tab" (case insensitive) page to load with a timeout of 5 seconds 18 | cUIA.WaitPageLoad("New inprivate tab", 5000) 19 | ; Set the URL to google and navigate 20 | cUIA.Navigate("google.com") 21 | 22 | ; First lets make sure the selected language is correct. 23 | ; This waits an element to exist where ClassName is "neDYw tHlp8d" and ControlType is Button OR an element with a MenuItem type. 24 | if (langBut := cUIA.WaitElement([{ClassName:"neDYw tHlp8d", Type:"Button"}, {Type:"MenuItem"}],1000)) { 25 | ; Check that it is collapsed 26 | if (langBut.ExpandCollapseState == UIA.ExpandCollapseState.Collapsed) 27 | langBut.Expand() 28 | ; Select the English language 29 | cUIA.WaitElement({Name:"English", Type:"MenuItem"}).Click() 30 | ; If the "I agree" or "Accept all" button exists, then click it to get rid of the consent form 31 | cUIA.WaitElement({Type:"Button", or:[{Name:"Accept all"}, {Name:"I agree"}]}).Click() 32 | } 33 | ; Looking for a partial name match "Searc" OR the ClassName for the search box (found using UIAViewer), using matchMode=Substring. 34 | ; WaitElement instead of FindElement is used here, because if the "I agree" button was clicked then this element might not exist right away, so lets first wait for it to exist. 35 | searchBox := cUIA.WaitElement({or:[{Name:"Searc"}, {ClassName:"gLFyf"}], Type:"ComboBox", matchmode:"Substring"}) 36 | ; Set the search box text to "autohotkey forums" 37 | searchBox.Value := "autohotkey forums" 38 | ; Click the search button to search (either Name "Google Search" OR the ClassName for it) 39 | cUIA.FindElement([{Name:"Google Search"}, {ClassName:"gNO89b"}]).Click() 40 | cUIA.WaitPageLoad() 41 | 42 | ; Now that the Google search results have loaded, lets scroll the page to the end. 43 | ; First get the document element 44 | docEl := cUIA.GetCurrentDocumentElement() 45 | ToolTip "Current scroll percent: " docEl.VerticalScrollPercent 46 | ; Lets scroll down in steps of 10% 47 | for percent in [10, 20, 30, 40, 50, 60, 70, 80, 90, 100] { 48 | docEl.VerticalScrollPercent := percent 49 | Sleep 500 50 | ToolTip "Current scroll percent: " docEl.VerticalScrollPercent 51 | } 52 | Sleep 3000 53 | ToolTip 54 | ExitApp 55 | -------------------------------------------------------------------------------- /Examples/UIA_Browser_Example03_Login.ahk: -------------------------------------------------------------------------------- 1 | #Requires AutoHotkey v2 2 | ;#include ; Uncomment if you have moved UIA.ahk to your main Lib folder 3 | #include ..\Lib\UIA.ahk 4 | ;#include ; Uncomment if you have moved UIA_Browser.ahk to your main Lib folder 5 | #include ..\Lib\UIA_Browser.ahk 6 | 7 | /** 8 | * This example demonstrates automating a login page. 9 | * 10 | * CAUTION: DO NOT use this with Google or other big companies login pages. UIAutomation may be 11 | * detected as botting and your browser will get banned from logging in. Gmail detects this 12 | * kind of automation for example. 13 | */ 14 | 15 | Run "chrome.exe https://www.w3schools.com/howto/howto_css_login_form.asp -incognito" 16 | WinWaitActive "ahk_exe chrome.exe" 17 | Sleep 3000 ; Give enough time to load the page 18 | cUIA := UIA_Browser() 19 | 20 | try { 21 | ; Might ask for permission to store cookies 22 | cUIA.FindElement({Name:"Accept all & visit the site"}).Click() 23 | Sleep 500 24 | } 25 | 26 | ; Click the Login button 27 | cUIA.FindElement({Name:"Login", Type:"Button"}).Click() 28 | ; Enter username and password 29 | cUIA.WaitElement({Name:"Enter Username", Type:"Edit"}).Value := "MyUsername" 30 | passwordEdit := cUIA.FindElement({Name:"Enter Password", Type:"Edit"}) 31 | passwordEdit.Value := "MyPassword" 32 | ; Uncheck the "Remember me" option 33 | cUIA.FindElement({Name:"Remember me", Type:"Checkbox"}).Toggle() 34 | ; Find the first Login button, starting the search from the passwordEdit element. 35 | ; If we did the search without the startingElement argument, then instead the first login button would be pressed. 36 | ; (Try removing the startingElement part: ", startingElement:passwordEdit") 37 | cUIA.FindElement({Name:"Login", Type:"Button", startingElement:passwordEdit}).Highlight().Click() 38 | ExitApp -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Descolada 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /Lib/UIA_Browser.ahk: -------------------------------------------------------------------------------- 1 | /* 2 | Introduction: 3 | UIA_Browser implements some methods to help automate browsers with UIAutomation framework. 4 | 5 | Initiate new instance of UIA_Browser with 6 | cUIA := UIA_Browser(wTitle="") 7 | wTitle: the title of the browser 8 | Example: cUIA := UIA_Browser("ahk_exe chrome.exe") 9 | 10 | Instances for specific browsers may be initiated with UIA_Chrome, UIA_Edge, UIA_Mozilla (arguments are the same as for UIA_Browser). 11 | These are usually auto-detected by UIA_Browser, so do not have to be used. 12 | 13 | Available properties for UIA_Browser: 14 | BrowserId 15 | ahk_id of the browser window 16 | BrowserType 17 | "Chrome", "Edge", "Mozilla", "Vivaldi", "Brave" or "Unknown" 18 | BrowserElement 19 | The browser window element, which can also be accessed by just calling an element method from UIA_Browser (cUIA.FindFirst would call FindFirst method on the BrowserElement, is equal to cUIA.BrowserElement.FindFirst) 20 | MainPaneElement 21 | Element for the upper part of the browser containing the URL bar, tabs, extensions etc 22 | URLEditElement 23 | Element for the address bar 24 | 25 | UIA_Browser methods: 26 | GetCurrentMainPaneElement() 27 | Refreshes UIA_Browser.MainPaneElement and also returns it 28 | GetCurrentDocumentElement() 29 | Returns the current document/content element of the browser. For Mozilla, the tab name which content to get can be specified. 30 | GetAllText() 31 | Gets all text from the browser element (Name properties for all child elements) 32 | GetAllLinks() 33 | Gets all link elements from the browser (returns an array of elements) 34 | WaitTitleChange(targetTitle:="", timeOut:=-1) 35 | Waits the browser title to change to targetTitle (by default just waits for the title to change), timeOut is in milliseconds (default is indefinite waiting) 36 | WaitPageLoad(targetTitle:="", timeOut:=-1, sleepAfter:=500, titleMatchMode:=3, titleCaseSensitive:=True) 37 | Waits the browser page to load to targetTitle, default timeOut is indefinite waiting, sleepAfter additionally sleeps for 200ms after the page has loaded. 38 | Back() 39 | Presses the Back button 40 | Forward() 41 | Presses the Forward button 42 | Reload() 43 | Presses the Reload button 44 | Home() 45 | Presses the Home button if it exists. 46 | GetCurrentURL(fromAddressBar:=False) 47 | Gets the current URL. fromAddressBar=True gets it straight from the URL bar element, which is not a very good method, because the text might be changed by the user and doesn't start with "http(s)://". Default of fromAddressBar=False will cause the real URL to be fetched, but the browser must be visible for it to work (if is not visible, it will be automatically activated). 48 | SetURL(newUrl, navigateToNewUrl:=False) 49 | Sets the URL bar to newUrl, optionally also navigates to it if navigateToNewUrl=True 50 | Navigate(url, targetTitle:="", waitLoadTimeOut:=-1, sleepAfter:=500) 51 | Navigates to URL and waits page to load 52 | NewTab() 53 | Opens a new tab. 54 | GetTab(searchPhrase:="", matchMode:=3, caseSense:=True) 55 | Returns a tab element with text of searchPhrase, or if empty then the currently selected tab. matchMode follows SetTitleMatchMode scheme: 1=tab name must must start with tabName; 2=can contain anywhere; 3=exact match; RegEx 56 | TabExist(searchPhrase:="", matchMode:=3, caseSense:=True) 57 | Checks whether a tab element with text of searchPhrase exists: if a matching tab is found then the element is returned, otherwise 0 is returned. matchMode follows SetTitleMatchMode scheme: 1=tab name must must start with tabName; 2=can contain anywhere; 3=exact match; RegEx 58 | GetTabs(searchPhrase:="", matchMode:=3, caseSense:=True) 59 | Returns all tab elements with text of searchPhrase, or if empty then all tabs. matchMode follows SetTitleMatchMode scheme: 1=tab name must must start with tabName; 2=can contain anywhere; 3=exact match; RegEx 60 | GetAllTabNames() 61 | Gets all the titles of tabs 62 | SelectTab(tabName, matchMode:=3, caseSense:=True) 63 | Selects a tab with the text of tabName. matchMode follows SetTitleMatchMode scheme: 1=tab name must must start with tabName; 2=can contain anywhere; 3=exact match; RegEx 64 | CloseTab(tabElementOrName:="", matchMode:=3, caseSense:=True) 65 | Close tab by either providing the tab element or the name of the tab. If tabElementOrName is left empty, the current tab will be closed. 66 | IsBrowserVisible() 67 | Returns True if any of the 4 corners of the browser are visible. 68 | Send(text) 69 | Uses ControlSend to send text to the browser. 70 | GetAlertText() 71 | Gets the text from an alert box 72 | CloseAlert() 73 | Closes an alert box 74 | JSExecute(js) 75 | Executes Javascript code using the address bar 76 | NOTE: In Firefox this is done by default through the console, which is a slow and inefficient method. 77 | A better way is to create a new bookmark with URL "javascript:%s" and keyword "javascript". This 78 | allows executing javascript through the address bar with "javascript alert("hello")". 79 | To make JSExecute use this method, either create UIA_Mozilla with JavascriptExecutionMethod set to "Bookmark", 80 | or set cUIA.JavascriptExecutionMethod := "Bookmark". Default value is "Console". 81 | JSReturnThroughClipboard(js) 82 | Executes Javascript code using the address bar and returns the return value of the code using the clipboard (resetting it back afterwards) 83 | JSReturnThroughTitle(js, timeOut:=500) 84 | Executes Javascript code using the address bar and returns the return value of the code using the browsers title (resetting it back afterwards). This might be unreliable, so the clipboard method is recommended instead. 85 | JSSetTitle(newTitle) 86 | Uses Javascript through the address bar to change the title of the browser 87 | JSGetElementPos(selector, useRenderWidgetPos:=False) 88 | Uses Javascript's querySelector to get a Javascript element and then its position. useRenderWidgetPos=True uses position of the Chrome_RenderWidgetHostHWND1 control to locate the position element relative to the window, otherwise it uses UIA_Browsers CurrentDocumentElement position. 89 | JSClickElement(selector) 90 | Uses Javascript's querySelector to get and click a Javascript element 91 | ControlClickJSElement(selector, WhichButton:="", ClickCount:="", Options:="", useRenderWidgetPos:=False) 92 | Uses Javascript's querySelector to get a Javascript element and then ControlClicks that position. useRenderWidgetPos=True uses position of the Chrome_RenderWidgetHostHWND1 control to locate the position element relative to the window, otherwise it uses UIA_Browsers CurrentDocumentElement position. 93 | ClickJSElement(selector, WhichButton:="", ClickCount:=1, DownOrUp:="", Relative:="", useRenderWidgetPos:=False) 94 | Uses Javascript's querySelector to get a Javascript element and then Clicks that position. useRenderWidgetPos=True uses position of the Chrome_RenderWidgetHostHWND1 control to locate the position element relative to the window, otherwise it uses UIA_Browsers CurrentDocumentElement position. 95 | */ 96 | 97 | 98 | /* 99 | If implementing new browser classes, then necessary methods/properties for main browser functions are: 100 | 101 | this.GetCurrentMainPaneElement() -- fetches MainPaneElement, NavigationBarElement, TabBarElement, URLEditElement 102 | // this might be necessary to implement for speed reasons, and is automatically called by InitiateUIA method 103 | this.GetCurrentDocumentElement() -- fetches Document element for the current page // might be necessary to implement 104 | this.GetCurrentReloadButton() 105 | 106 | this.MainPaneElement -- element that doesn't contain page content: this element includes URL bar, navigation buttons, setting buttons etc 107 | this.NavigationBarElement -- smallest element (usually a Toolbar element) that contains the URL bar and navigation buttons 108 | this.TabBarElement -- contains only tabs 109 | this.URLEditElement -- the URL bar element 110 | this.ReloadButton 111 | */ 112 | 113 | class UIA_Vivaldi extends UIA_Browser { 114 | __New(wTitle:="") { 115 | this.BrowserType := "Vivaldi" 116 | this.InitiateUIA(wTitle) 117 | } 118 | GetCurrentMainPaneElement() { 119 | this.GetCurrentDocumentElement() 120 | this.DialogTreeWalker := UIA.CreateTreeWalker(UIA.CreateAndCondition(UIA.CreatePropertyCondition(UIA.Property.Type, UIA.Type.Group), UIA.CreatePropertyCondition(UIA.Property.AutomationId, "modal-bg"))) 121 | if !this.HasOwnProp("DocumentElement") && !(this.DocumentElement := this.MainPaneElement) 122 | throw TargetError("UIA_Browser was unable to find the Document element for browser. Make sure the browser is at least partially visible or active before calling UIA_Browser()", -2) 123 | Loop 2 { 124 | this.URLEditElement := this.BrowserElement.WaitElement({AutomationId:"urlFieldInput"}, 3000) 125 | TabElement := this.BrowserElement.FindElement({AutomationId:"tab-", matchmode:"Substring"}) 126 | NewTabButton := this.BrowserElement.FindElement({Type:"Button", startingElement:TabElement}) 127 | try { 128 | this.TabBarElement := TabElement.Parent 129 | this.NavigationBarElement := this.TabBarElement.Parent 130 | this.ReloadButton := "", this.ReloadButtonDescription := "", this.ReloadButtonFullDescription := "", this.ReloadButtonName := "" 131 | this.ReloadButton := this.URLEditElement.WalkTree("-3", {Type:"Button"}) 132 | this.ReloadButtonDescription := this.ReloadButton.LegacyIAccessiblePattern.Description 133 | this.ReloadButtonName := this.ReloadButton.Name 134 | if !this.ReloadButtonDescription && !this.ReloadButtonName 135 | this.ReloadButtonName := "Reload" 136 | return this.MainPaneElement 137 | } catch { 138 | WinActivate "ahk_id " this.BrowserId 139 | WinWaitActive "ahk_id " this.BrowserId,,1 140 | } 141 | } 142 | ; If all goes well, this part is not reached 143 | } 144 | 145 | GetCurrentDocumentElement() { 146 | Loop 2 { 147 | try { 148 | this.MainPaneElement := this.BrowserElement.FindElement({Type:"Document"}) 149 | return this.DocumentElement := this.BrowserElement.FindElement({Type:"Document", not:{Value:""}, startingElement:this.MainPaneElement}) 150 | } catch TargetError { 151 | WinActivate this.BrowserId 152 | WinWaitActive this.BrowserId,,1 153 | } 154 | } 155 | } 156 | 157 | GetAllTabs() { 158 | return this.TabBarElement.FindElements({AutomationId:"tab-", matchmode:"Substring"}, 2) 159 | } 160 | 161 | GetTabs(searchPhrase:="", matchMode:=3, caseSense:=True) { 162 | local allTabs := this.GetAllTabs() 163 | matchMode := UIA.TypeValidation.MatchMode(matchMode) 164 | if !searchPhrase 165 | return allTabs 166 | return UIA.Filter(allTabs, (element) => element.ElementExist({Name:searchPhrase, matchMode:matchMode, caseSense:caseSense})) 167 | } 168 | 169 | GetTab(searchPhrase:="", matchMode:=3, caseSense:=True) { 170 | local match, els 171 | if searchPhrase is Integer 172 | return this.TabBarElement.FindElement({AutomationId:"tab-", matchmode:"Substring", i:searchPhrase}, 2) 173 | if !searchPhrase { 174 | RegExMatch(WinGetTitle(this.BrowserId), "(.*) - Vivaldi$", &match:="") 175 | searchPhrase := match[1], matchMode := 3, caseSense := True 176 | } 177 | if !(tabs := this.GetAllTabs()).Length 178 | throw Error("Unable to get tab elements", -1, "Please file a bug report") 179 | if !(els := UIA.Filter(tabs, (element) => element.ElementExist({Type:"Text", Name:searchPhrase, matchMode:matchMode, caseSense:caseSense}))).Length 180 | throw Error("No search phrase matches found", -1) 181 | return els[els.Length] 182 | } 183 | 184 | GetAllTabNames() { 185 | local names := [], k, v 186 | for k, v in this.GetTabs() { 187 | names.Push(v.FindElement({Type:"Text"}).Name) 188 | } 189 | return names 190 | } 191 | 192 | SetURL(newUrl, navigateToNewUrl := False) => UIA_Mozilla.Prototype.GetMethod("SetURL")(this, newUrl, navigateToNewUrl) 193 | 194 | CloseTab(tabElementOrName:="", matchMode:=3, caseSense:=True) { 195 | this.SelectTab(tabElementOrName) 196 | Sleep 40 197 | this.ControlSend("{ctrl down}w{ctrl up}") 198 | } 199 | 200 | Reload() { 201 | this.GetCurrentReloadButton().ControlClick() 202 | } 203 | 204 | Back() { 205 | this.ReloadButton.WalkTree("-2", this.ButtonControlCondition).Click() 206 | } 207 | 208 | Forward() { 209 | this.ReloadButton.WalkTree("-1", this.ButtonControlCondition).Click() 210 | } 211 | 212 | Home() { 213 | throw Error("Method not implemented", -1) 214 | } 215 | } 216 | 217 | class UIA_Chrome extends UIA_Browser { 218 | __New(wTitle:="") { 219 | this.BrowserType := "Chrome" 220 | this.InitiateUIA(wTitle) 221 | } 222 | ; Refreshes UIA_Browser.MainPaneElement and returns it 223 | GetCurrentMainPaneElement() { 224 | this.GetCurrentDocumentElement() 225 | if !this.HasOwnProp("DocumentElement") 226 | throw TargetError("UIA_Browser was unable to find the Document element for browser. Make sure the browser is at least partially visible or active before calling UIA_Browser()", -2) 227 | Loop 2 { 228 | try this.URLEditElement := this.BrowserElement[4,1,2,1].FindFirstWithOptions(this.EditControlCondition, 2, this.BrowserElement) 229 | catch 230 | this.URLEditElement := this.BrowserElement.FindFirstWithOptions(this.EditControlCondition, 2, this.BrowserElement) 231 | try { 232 | if !this.URLEditElement 233 | this.URLEditElement := UIA.CreateTreeWalker(this.EditControlCondition).GetLastChildElement(this.BrowserElement) 234 | this.NavigationBarElement := UIA.CreateTreeWalker(this.ToolbarControlCondition).GetParentElement(this.URLEditElement) 235 | this.MainPaneElement := UIA.TreeWalkerTrue.GetParentElement(this.NavigationBarElement) 236 | if !this.NavigationBarElement 237 | this.NavigationBarElement := this.BrowserElement 238 | if !this.MainPaneElement 239 | this.MainPaneElement := this.BrowserElement 240 | if !(this.TabBarElement := UIA.CreateTreeWalker(this.TabControlCondition).GetPreviousSiblingElement(this.NavigationBarElement)) 241 | this.TabBarElement := this.MainPaneElement 242 | this.ReloadButton := "", this.ReloadButtonDescription := "", this.ReloadButtonFullDescription := "", this.ReloadButtonName := "" 243 | Loop 2 { 244 | try { 245 | this.ReloadButton := UIA.TreeWalkerTrue.GetNextSiblingElement(UIA.TreeWalkerTrue.GetNextSiblingElement(this.ButtonTreeWalker.GetFirstChildElement(this.NavigationBarElement))) 246 | this.ReloadButtonDescription := this.ReloadButton.LegacyIAccessiblePattern.Description 247 | this.ReloadButtonName := this.ReloadButton.Name 248 | } 249 | if (this.ReloadButtonDescription || this.ReloadButtonName) 250 | break 251 | Sleep 200 252 | } 253 | return this.MainPaneElement 254 | } catch { 255 | WinActivate "ahk_id " this.BrowserId 256 | WinWaitActive "ahk_id " this.BrowserId,,1 257 | } 258 | } 259 | ; If all goes well, this part is not reached 260 | } 261 | } 262 | 263 | class UIA_Brave extends UIA_Chrome { 264 | } 265 | 266 | class UIA_Edge extends UIA_Browser { 267 | __New(wTitle:="") { 268 | this.BrowserType := "Edge" 269 | this.InitiateUIA(wTitle) 270 | } 271 | 272 | ; Refreshes UIA_Browser.MainPaneElement and returns it 273 | GetCurrentMainPaneElement() { 274 | local k, v, el, topCoord, bt 275 | this.GetCurrentDocumentElement() 276 | if !this.HasOwnProp("DocumentElement") 277 | throw TargetError("UIA_Browser was unable to find the Document element for browser. Make sure the browser is at least partially visible or active before calling UIA_Browser()", -2) 278 | Loop 2 { 279 | try { 280 | if !(this.URLEditElement := this.BrowserElement.ElementExist({Type:"Edit"})) { 281 | this.ToolbarElements := this.BrowserElement.FindAll(this.ToolbarControlCondition), topCoord := 10000000 282 | for k, v in this.ToolbarElements { 283 | if ((bT := v.BoundingRectangle.t) && (bt < topCoord)) 284 | topCoord := bT, this.NavigationBarElement := v 285 | } 286 | this.URLEditElement := this.NavigationBarElement.FindFirst(this.EditControlCondition) 287 | if this.URLEditElement.GetChildren().Length 288 | this.URLEditElement := (el := this.URLEditElement.FindFirst(this.EditControlCondition)) ? el : this.URLEditElement 289 | } Else { 290 | this.NavigationBarElement := UIA.CreateTreeWalker(this.ToolbarControlCondition).GetParentElement(this.URLEditElement) 291 | } 292 | this.MainPaneElement := UIA.TreeWalkerTrue.GetParentElement(this.NavigationBarElement) 293 | if !this.NavigationBarElement 294 | this.NavigationBarElement := this.BrowserElement 295 | if !this.MainPaneElement 296 | this.MainPaneElement := this.BrowserElement 297 | if !(this.TabBarElement := UIA.CreateTreeWalker(this.TabControlCondition).GetPreviousSiblingElement(this.NavigationBarElement)) 298 | this.TabBarElement := this.MainPaneElement 299 | this.ReloadButton := "", this.ReloadButtonDescription := "", this.ReloadButtonFullDescription := "", this.ReloadButtonName := "" 300 | Loop 2 { 301 | try { 302 | this.ReloadButton := this.ButtonTreeWalker.GetNextSiblingElement(this.ButtonTreeWalker.GetNextSiblingElement(this.ButtonTreeWalker.GetFirstChildElement(this.NavigationBarElement))) 303 | this.ReloadButtonFullDescription := this.ReloadButton.FullDescription 304 | this.ReloadButtonName := this.ReloadButton.Name 305 | } 306 | if (this.ReloadButtonDescription || this.ReloadButtonName) 307 | break 308 | Sleep 200 309 | } 310 | return this.MainPaneElement 311 | } catch { 312 | WinActivate "ahk_id " this.BrowserId 313 | WinWaitActive "ahk_id " this.BrowserId,,1 314 | } 315 | } 316 | ; If all goes well, this part is not reached 317 | } 318 | 319 | GetCurrentDocumentElement() { 320 | local endtime := A_TickCount+3000 321 | While A_TickCount < endtime 322 | try return this.DocumentElement := this.CurrentDocumentElement := UIA.ElementFromHandle(this.BrowserId).FindFirst(this.DocumentControlCondition,4) ; ElementFromChromium works unreliably 323 | throw Error("Unable to get the current Document element", -1) 324 | } 325 | } 326 | 327 | class UIA_Mozilla extends UIA_Browser { 328 | __New(wTitle:="", javascriptExecutionMethod:="Console") { 329 | this.JavascriptExecutionMethod := javascriptExecutionMethod 330 | this.BrowserType := "Mozilla" 331 | this.InitiateUIA(wTitle) 332 | } 333 | ; Refreshes UIA_Browser.MainPaneElement and returns it 334 | GetCurrentMainPaneElement() { 335 | try this.BrowserElement.FindElement({AutomationId:"panel", mm:2},2) 336 | catch { 337 | WinActivate this.BrowserId 338 | WinWaitActive this.BrowserId,,1 339 | } 340 | Loop 2 { 341 | try { 342 | this.TabBarElement := this.ToolbarTreeWalker.GetNextSiblingElement(this.ToolbarTreeWalker.GetFirstChildElement(this.BrowserElement)) 343 | this.NavigationBarElement := this.ToolbarTreeWalker.GetNextSiblingElement(this.TabBarElement) 344 | this.URLEditElement := this.NavigationBarElement.FindFirst(this.EditControlCondition) 345 | this.MainPaneElement := UIA.TreeWalkerTrue.GetParentElement(this.NavigationBarElement) 346 | if !this.NavigationBarElement 347 | this.NavigationBarElement := this.BrowserElement 348 | if !this.MainPaneElement 349 | this.MainPaneElement := this.BrowserElement 350 | this.ReloadButton := "", this.ReloadButtonDescription := "", this.ReloadButtonFullDescription := "", this.ReloadButtonName := "" 351 | Loop 2 { 352 | try { 353 | this.ReloadButton := UIA.TreeWalkerTrue.GetNextSiblingElement(UIA.TreeWalkerTrue.GetNextSiblingElement(UIA.TreeWalkerTrue.GetFirstChildElement(this.NavigationBarElement))) 354 | this.ReloadButtonFullDescription := this.ReloadButton.FullDescription 355 | this.ReloadButtonName := this.ReloadButton.Name 356 | } 357 | if (this.ReloadButtonDescription || this.ReloadButtonName) 358 | break 359 | Sleep 200 360 | } 361 | return this.MainPaneElement 362 | } catch { 363 | WinActivate this.BrowserId 364 | WinWaitActive this.BrowserId,,1 365 | } 366 | } 367 | ; If all goes well, this part is not reached 368 | } 369 | 370 | ; Returns the current document/content element of the browser 371 | GetCurrentDocumentElement() { 372 | Loop 2 { 373 | try { 374 | this.DocumentPanelElement := this.BrowserElement.FindElement({Type:"Custom", IsOffscreen:0},2) 375 | return UIA.TreeWalkerTrue.GetFirstChildElement(UIA.TreeWalkerTrue.GetFirstChildElement(this.DocumentPanelElement)) 376 | } catch TargetError { 377 | WinActivate this.BrowserId 378 | WinWaitActive this.BrowserId,,1 379 | } 380 | } 381 | } 382 | 383 | ; Sets the URL bar to newUrl, optionally also navigates to it if navigateToNewUrl=True 384 | SetURL(newUrl, navigateToNewUrl := False) { 385 | local endTime 386 | this.URLEditElement.SetFocus() 387 | this.URLEditElement.Value := newUrl " " 388 | endTime := A_TickCount+200 389 | if navigateToNewUrl { 390 | while !InStr(this.URLEditElement.Value, newUrl) && (A_TickCount < endTime) 391 | Sleep 40 392 | if A_TickCount < endTime { 393 | this.ControlSend("{LCtrl down}{Enter}{LCtrl up}") 394 | } 395 | } 396 | } 397 | 398 | JSExecute(js) { 399 | if this.JavascriptExecutionMethod = "Bookmark" { 400 | this.SetURL("javascript " js, True) 401 | return 402 | } 403 | this.ControlSend("{ctrl down}{shift down}k{ctrl up}{shift up}") 404 | if !this.BrowserElement.WaitElement({Name:"Switch to multi-line editor mode (Ctrl + B)", Type:"Button"},5000) 405 | return 406 | ClipSave := ClipboardAll() 407 | A_Clipboard := js 408 | this.ControlSend("allow pasting{ctrl down}z{ctrl up}{ctrl down}v{ctrl up}") 409 | Sleep 20 410 | this.ControlSend("{ctrl down}{enter}{ctrl up}") 411 | sleep 40 412 | this.ControlSend("{ctrl down}{shift down}i{ctrl up}{shift up}") 413 | A_Clipboard := ClipSave 414 | } 415 | 416 | ; Gets text from an alert-box 417 | GetAlertText(closeAlert:=True, timeOut:=3000) { 418 | this.GetCurrentDocumentElement() 419 | local startTime := A_TickCount, text := "" 420 | if !(alertEl := UIA.TreeWalkerTrue.GetNextSiblingElement(UIA.TreeWalkerTrue.GetFirstChildElement(this.DocumentPanelElement))) 421 | return 422 | 423 | while ((A_tickCount - startTime) < timeOut) { 424 | try { 425 | dialogEl := alertEl.FindElement({AutomationId:"commonDialogWindow"}) 426 | OKBut := dialogEl.FindFirst(this.ButtonControlCondition) 427 | break 428 | } catch 429 | Sleep 100 430 | } 431 | try text := dialogEl.FindFirst(this.TextControlCondition).Name 432 | if closeAlert 433 | try OKBut.Click() 434 | return text 435 | } 436 | 437 | CloseAlert() { 438 | this.GetCurrentDocumentElement() 439 | try UIA.TreeWalkerTrue.GetNextSiblingElement(UIA.TreeWalkerTrue.GetFirstChildElement(this.DocumentPanelElement)).FindElement({AutomationId:"commonDialogWindow"}).FindFirst(this.ButtonControlCondition).Click() 440 | } 441 | 442 | ; Close tab by either providing the tab element or the name of the tab. If tabElementOrName is left empty, the current tab will be closed. 443 | CloseTab(tabElementOrName:="", matchMode:=3, caseSense:=True) { 444 | if (tabElementOrName != "") { 445 | if IsObject(tabElementOrName) { 446 | if (tabElementOrName.Type == UIA.Type.TabItem) 447 | tabElementOrName.Click() 448 | } else { 449 | try this.TabBarElement.FindElement({Name:tabElementOrName, Type:"TabItem", mm:matchMode, cs:caseSense}).Click() 450 | } 451 | } 452 | this.ControlSend("{Ctrl down}w{Ctrl up}") 453 | } 454 | } 455 | 456 | class UIA_Browser { 457 | InitiateUIA(wTitle:="") { 458 | this.BrowserId := WinExist(wTitle) 459 | if !this.BrowserId 460 | throw TargetError("UIA_Browser: failed to find the browser!", -1) 461 | this.TextControlCondition := UIA.CreatePropertyCondition(UIA.Property.Type, UIA.Type.Text) 462 | this.DocumentControlCondition := UIA.CreatePropertyCondition(UIA.Property.Type, UIA.Type.Document) 463 | this.ButtonControlCondition := UIA.CreatePropertyCondition(UIA.Property.Type, UIA.Type.Button) 464 | this.EditControlCondition := UIA.CreatePropertyCondition(UIA.Property.Type, UIA.Type.Edit) 465 | this.ToolbarControlCondition := UIA.CreatePropertyCondition(UIA.Property.Type, UIA.Type.ToolBar) 466 | this.TabControlCondition := UIA.CreatePropertyCondition(UIA.Property.Type, UIA.Type.Tab) 467 | this.ToolbarTreeWalker := UIA.CreateTreeWalker(this.ToolbarControlCondition) 468 | this.ButtonTreeWalker := UIA.CreateTreeWalker(this.ButtonControlCondition) 469 | this.BrowserElement := UIA.ElementFromHandle(this.BrowserId) 470 | this.DialogTreeWalker := UIA.CreateTreeWalker(UIA.CreateOrCondition(UIA.CreatePropertyCondition(UIA.Property.Type, UIA.Type.Custom), UIA.CreatePropertyCondition(UIA.Property.Type, UIA.Type.Window))) 471 | this.GetCurrentMainPaneElement() 472 | } 473 | ; Initiates UIA and hooks to the browser window specified with wTitle. 474 | __New(wTitle:="") { 475 | this.BrowserId := WinExist(wTitle) 476 | if !this.BrowserId 477 | throw TargetError("UIA_Browser: failed to find the browser!", -1) 478 | wExe := WinGetProcessName("ahk_id" this.BrowserId) 479 | wClass := WinGetClass("ahk_id" this.BrowserId) 480 | this.BrowserType := (wExe = "chrome.exe") ? "Chrome" : (wExe = "msedge.exe") ? "Edge" : (wExe = "vivaldi.exe") ? "Vivaldi" : InStr(wClass, "Mozilla") ? "Mozilla" : (wExe = "brave.exe") ? "Brave" : "Unknown" 481 | if (this.BrowserType != "Unknown") { 482 | this.base := UIA_%(this.BrowserType)%.Prototype 483 | this.__New(wTitle) 484 | } else 485 | this.InitiateUIA(wTitle) 486 | } 487 | 488 | __Get(member, params) { 489 | local err 490 | if this.HasOwnProp("BrowserElement") { 491 | try return this.BrowserElement.%member% 492 | catch PropertyError { 493 | } catch Any as err 494 | throw %Type(err)%(err.Message, -1, err.Extra) 495 | } 496 | try return UIA.%member% 497 | catch PropertyError 498 | throw PropertyError("This class does not contain property `"" member "`"", -1) 499 | catch Any as err 500 | throw %Type(err)%(err.Message, -1, err.Extra) 501 | } 502 | 503 | __Call(member, params) { 504 | local err 505 | if this.HasOwnProp("BrowserElement") { 506 | try return this.BrowserElement.%member%(params*) 507 | catch MethodError { 508 | } catch Any as err 509 | throw %Type(err)%(err.Message, -1, err.Extra) 510 | } 511 | try return UIA.%member%(params*) 512 | catch MethodError 513 | throw MethodError("This class does not contain method `"" member "`"", -1) 514 | catch Any as err 515 | throw %Type(err)%(err.Message, -1, err.Extra) 516 | } 517 | 518 | __Set(member, params, value) { 519 | if this.HasOwnProp("BrowserElement") 520 | if this.BrowserElement.HasOwnProp(member) 521 | this.BrowserElement.%member% := Value 522 | if UIA.HasOwnProp(member) 523 | UIA.%member% := Value 524 | this.DefineProp(member, {Value:value}) 525 | } 526 | 527 | __Item[params*] { 528 | get => this.BrowserElement[params*] 529 | } 530 | 531 | ; Refreshes UIA_Browser.MainPaneElement and returns it 532 | GetCurrentMainPaneElement() { 533 | this.GetCurrentDocumentElement() 534 | if !this.HasOwnProp("DocumentElement") 535 | throw TargetError("UIA_Browser was unable to find the Document element for browser. Make sure the browser is at least partially visible or active before calling UIA_Browser()", -2) 536 | ; Finding the correct Toolbar ends up to be quite tricky. 537 | ; In Chrome the toolbar element is located in the tree after the content element, 538 | ; so if the content contains a toolbar then that will be returned. 539 | ; Two workarounds I can think of: either look for the Toolbar by name ("Address and search bar" 540 | ; both in Chrome and edge), or by location (it must be the topmost toolbar). I opted for a 541 | ; combination of two, so if finding by name fails, all toolbar elements are evaluated. 542 | Loop 2 { 543 | try this.URLEditElement := (this.BrowserType = "Chrome" && this.BrowserElement[1].Type = UIA.Property.Document) ? this.BrowserElement.FindFirstWithOptions(this.EditControlCondition, 2, this.BrowserElement) : this.BrowserElement.FindFirst(this.EditControlCondition) 544 | try { 545 | if (this.BrowserType = "Chrome") && !this.URLEditElement 546 | this.URLEditElement := UIA.CreateTreeWalker(this.EditControlCondition).GetLastChildElement(this.BrowserElement) 547 | this.NavigationBarElement := UIA.CreateTreeWalker(this.ToolbarControlCondition).GetParentElement(this.URLEditElement) 548 | this.MainPaneElement := UIA.TreeWalkerTrue.GetParentElement(this.NavigationBarElement) 549 | if !this.NavigationBarElement 550 | this.NavigationBarElement := this.BrowserElement 551 | if !this.MainPaneElement 552 | this.MainPaneElement := this.BrowserElement 553 | if !(this.TabBarElement := UIA.CreateTreeWalker(this.TabControlCondition).GetPreviousSiblingElement(this.NavigationBarElement)) 554 | this.TabBarElement := this.MainPaneElement 555 | this.GetCurrentReloadButton() 556 | this.ReloadButton := "", this.ReloadButtonDescription := "", this.ReloadButtonFullDescription := "", this.ReloadButtonName := "" 557 | Loop 2 { 558 | try { 559 | this.ReloadButtonDescription := this.ReloadButton.LegacyIAccessiblePattern.Description 560 | this.ReloadButtonFullDescription := this.ReloadButton.FullDescription 561 | this.ReloadButtonName := this.ReloadButton.Name 562 | } 563 | if (this.ReloadButtonDescription || this.ReloadButtonName) 564 | break 565 | Sleep 200 566 | } 567 | 568 | return this.MainPaneElement 569 | } catch { 570 | WinActivate "ahk_id " this.BrowserId 571 | WinWaitActive "ahk_id " this.BrowserId,,1 572 | } 573 | } 574 | ; If all goes well, this part is not reached 575 | } 576 | 577 | ; Returns the current document/content element of the browser 578 | GetCurrentDocumentElement() { 579 | return (this.DocumentElement := this.CurrentDocumentElement := this.BrowserElement.WaitElement(this.DocumentControlCondition, 3000)) 580 | } 581 | 582 | GetCurrentReloadButton() { 583 | try { 584 | if this.ReloadButton && this.ReloadButton.Name 585 | return this.ReloadButton 586 | } 587 | this.ReloadButton := this.ButtonTreeWalker.GetNextSiblingElement(this.ButtonTreeWalker.GetNextSiblingElement(this.ButtonTreeWalker.GetFirstChildElement(this.NavigationBarElement))) 588 | return this.ReloadButton 589 | } 590 | 591 | ; Uses Javascript to set the title of the browser. 592 | JSSetTitle(newTitle) { 593 | this.JSExecute("document.title=`"" newTitle "`"; void(0);") 594 | } 595 | 596 | JSExecute(js) { 597 | this.SetURL("javascript:" js, True) 598 | } 599 | 600 | JSAlert(js, closeAlert:=True, timeOut:=3000) { 601 | this.JSExecute("alert(" js ");") 602 | return this.GetAlertText(closeAlert, timeOut) 603 | } 604 | 605 | ; Executes Javascript code through the address bar and returns the return value through the clipboard. 606 | JSReturnThroughClipboard(js) { 607 | saveClip := ClipboardAll() 608 | A_Clipboard := "" 609 | this.JSExecute("copyToClipboard(" js ");function copyToClipboard(text) {const elem = document.createElement('textarea');elem.value = text;document.body.appendChild(elem);elem.select();document.execCommand('copy');document.body.removeChild(elem);}") 610 | ClipWait 2 611 | returnText := A_Clipboard 612 | A_Clipboard := saveClip 613 | return returnText 614 | } 615 | 616 | ; Executes Javascript code through the address bar and returns the return value through the browser windows title. 617 | JSReturnThroughTitle(js, timeOut:=500) { 618 | this.JSExecute("origTitle=document.title;document.title=(" js ");void(0);setTimeout(function() {document.title=origTitle;void(0);}, " timeOut ")") 619 | local startTime := A_TickCount, origTitle := WinGetTitle("ahk_id " this.BrowserId), newTitle 620 | Loop { 621 | newTitle := WinGetTitle("ahk_id " this.BrowserId) 622 | Sleep 40 623 | } Until ((origTitle != newTitle) || (A_TickCount - startTime > timeOut)) 624 | return (origTitle == newTitle) ? "" : RegexReplace(newTitle, "(?: - Personal)? - [^-]+$") 625 | } 626 | 627 | ; Uses Javascript's querySelector to get a Javascript element and then its position. useRenderWidgetPos=True uses position of the Chrome_RenderWidgetHostHWND1 control to locate the position element relative to the window, otherwise it uses UIA_Browsers CurrentDocumentElement position. 628 | JSGetElementPos(selector, useRenderWidgetPos:=False) { ; based on code by AHK Forums user william_ahk 629 | local js := Format(" 630 | (LTrim Join 631 | (() => { 632 | let bounds = document.querySelector("{1}").getBoundingClientRect().toJSON(); 633 | let zoom = window.devicePixelRatio.toFixed(2); 634 | for (const key in bounds) { 635 | bounds[key] = bounds[key] * zoom; 636 | } 637 | return JSON.stringify(bounds); 638 | })() 639 | )", selector) 640 | local bounds_str := this.JSReturnThroughClipboard(js) 641 | RegexMatch(bounds_str, "`"x`":(\d+).?\d*?,`"y`":(\d+).?\d*?,`"width`":(\d+).?\d*?,`"height`":(\d+).?\d*?", &size) 642 | if useRenderWidgetPos { 643 | ControlGetPos &win_x, &win_y, &win_w, &win_h, "Chrome_RenderWidgetHostHWND1", this.BrowserId 644 | return {x:size[1]+win_x,y:size[2]+win_y,w:size[3],h:size[4]} 645 | } else { 646 | br := this.GetCurrentDocumentElement().GetPos("window") 647 | return {x:size[1]+br.x,y:size[2]+br.y,w:size[3],h:size[4]} 648 | } 649 | } 650 | 651 | ; Uses Javascript's querySelector to get and click a Javascript element. Compared with ClickJSElement method, this method has the advantage of skipping the need to wait for a return value from the clipboard, but it might be more unreliable (some elements might not support Javascript's "click()" properly). 652 | JSClickElement(selector) { 653 | this.JSExecute("document.querySelector(`"" selector "`").click();") 654 | } 655 | 656 | ; Uses Javascript's querySelector to get a Javascript element and then ControlClicks that position. useRenderWidgetPos=True uses position of the Chrome_RenderWidgetHostHWND1 control to locate the position element relative to the window, otherwise it uses UIA_Browsers CurrentDocumentElement position. 657 | ControlClickJSElement(selector, WhichButton?, ClickCount?, Options?, useRenderWidgetPos:=False) { 658 | bounds := this.JSGetElementPos(selector, useRenderWidgetPos) 659 | ControlClick("X" (bounds.x + bounds.w // 2) " Y" (bounds.y + bounds.h // 2), this.browserId,, WhichButton?, ClickCount?, Options?) 660 | } 661 | 662 | ; Uses Javascript's querySelector to get a Javascript element and then Clicks that position. useRenderWidgetPos=True uses position of the Chrome_RenderWidgetHostHWND1 control to locate the position element relative to the window, otherwise it uses UIA_Browsers CurrentDocumentElement position. 663 | ClickJSElement(selector, WhichButton:="", ClickCount:=1, DownOrUp:="", Relative:="", useRenderWidgetPos:=False) { 664 | bounds := this.JSGetElementPos(selector, useRenderWidgetPos) 665 | Click((bounds.x + bounds.w / 2) " " (bounds.y + bounds.h / 2) " " WhichButton (ClickCount ? " " ClickCount : "") (DownOrUp ? " " DownOrUp : "") (Relative ? " " Relative : "")) 666 | } 667 | 668 | ; Gets text from an alert-box created with for example javascript:alert('message') 669 | GetAlertText(closeAlert:=True, timeOut:=3000) { 670 | local startTime := A_TickCount, text := "" 671 | startTime := A_TickCount 672 | while ((A_tickCount - startTime) < timeOut) { 673 | try { 674 | if IsObject(dialogEl := this.DialogTreeWalker.GetLastChildElement(this.BrowserElement)) && IsObject(OKBut := dialogEl.FindFirst(this.ButtonControlCondition)) 675 | break 676 | } 677 | Sleep 100 678 | } 679 | try 680 | text := this.BrowserType = "Edge" ? dialogEl.FindFirstWithOptions(this.TextControlCondition, 2, dialogEl).Name : dialogEl.FindFirst(this.TextControlCondition).Name 681 | if closeAlert { 682 | Sleep 500 683 | try OKBut.Click() 684 | } 685 | return text 686 | } 687 | 688 | CloseAlert() { 689 | try { 690 | dialogEl := this.DialogTreeWalker.GetLastChildElement(this.BrowserElement) 691 | OKBut := dialogEl.FindFirst(this.ButtonControlCondition) 692 | OKBut.Click() 693 | } 694 | } 695 | 696 | ; Gets all text from the browser element (Name properties for all Text elements) 697 | GetAllText() { 698 | local TextArray, Text, k, v 699 | if !this.IsBrowserVisible() 700 | WinActivate this.BrowserId 701 | 702 | TextArray := this.BrowserElement.FindAll(this.TextControlCondition) 703 | Text := "" 704 | for k, v in TextArray 705 | Text .= v.Name "`n" 706 | return Text 707 | } 708 | ; Gets all link elements from the browser 709 | GetAllLinks() { 710 | static LinkCondition := UIA.CreatePropertyCondition(UIA.Property.Type, UIA.Type.Hyperlink) 711 | if !this.IsBrowserVisible() 712 | WinActivate this.BrowserId 713 | return this.BrowserElement.FindAll(LinkCondition) 714 | } 715 | 716 | ; Waits the browser title to change to targetTitle (by default just waits for the title to change), timeOut is in milliseconds (default is indefinite waiting) 717 | WaitTitleChange(targetTitle:="", timeOut:=-1) { 718 | local origTitle := WinGetTitle("ahk_id" this.BrowserId), startTime := A_TickCount, newTitle := origTitle 719 | while ((((A_TickCount - startTime) < timeOut) || (timeOut = -1)) && (targetTitle ? !UIA_Browser.CompareTitles(targetTitle, newTitle) : (origTitle == newTitle))) { 720 | Sleep 200 721 | newTitle := WinGetTitle("A") 722 | } 723 | if (((A_TickCount - startTime) < timeOut) || (timeOut = -1)) 724 | return newTitle 725 | return false 726 | } 727 | 728 | ; Waits the browser page to load to targetTitle, default timeOut is indefinite waiting, sleepAfter additionally sleeps for 200ms after the page has loaded. 729 | WaitPageLoad(targetTitle:="", timeOut:=-1, sleepAfter:=500, titleMatchMode:="", titleCaseSensitive:=False) { 730 | local legacyPattern := "", startTime := A_TickCount, wTitle := "", ReloadButtonName := "", ReloadButtonDescription := "", ReloadButtonFullDescription := "" 731 | Sleep 200 ; Give some time for the Reload button to change after navigating 732 | if this.ReloadButtonDescription 733 | try legacyPattern := this.ReloadButton.LegacyIAccessiblePattern 734 | while ((A_TickCount - startTime) < timeOut) || (timeOut = -1) { 735 | if this.BrowserType = "Mozilla" 736 | this.GetCurrentReloadButton() 737 | try ReloadButtonName := this.ReloadButton.Name 738 | try ReloadButtonDescription := legacyPattern.Description 739 | try ReloadButtonFullDescription := this.ReloadButton.FullDescription 740 | if (((this.ReloadButtonName ? InStr(ReloadButtonName, this.ReloadButtonName) : 1) 741 | && (this.ReloadButtonDescription && legacyPattern ? InStr(ReloadButtonDescription, this.ReloadButtonDescription) : 1) 742 | && (this.ReloadButtonFullDescription ? InStr(ReloadButtonFullDescription, this.ReloadButtonFullDescription) : 1))) 743 | || !this.ReloadButton.IsEnabled { 744 | if targetTitle != "" { 745 | wTitle := WinGetTitle(this.BrowserId) 746 | if UIA_Browser.CompareTitles(targetTitle, wTitle, titleMatchMode, titleCaseSensitive) 747 | break 748 | } else 749 | break 750 | } 751 | Sleep 40 752 | } 753 | if ((A_TickCount - startTime) < timeOut) || (timeOut = -1) 754 | Sleep sleepAfter 755 | else 756 | return false 757 | return targetTitle = "" ? true : wTitle 758 | } 759 | 760 | ; Presses the Back button 761 | Back() { 762 | this.ButtonTreeWalker.GetFirstChildElement(this.NavigationBarElement).Invoke() 763 | } 764 | 765 | ; Presses the Forward button 766 | Forward() { 767 | this.ButtonTreeWalker.GetNextSiblingElement(this.ButtonTreeWalker.GetFirstChildElement(this.NavigationBarElement)).Click() 768 | } 769 | 770 | ; Presses the Reload button 771 | Reload() { 772 | this.GetCurrentReloadButton().Click() 773 | } 774 | 775 | ; Presses the Home button if it exists. 776 | Home() { 777 | if homeBut := this.ButtonTreeWalker.GetNextSiblingElement(this.ReloadButton) 778 | return homeBut.Click() 779 | ;NameCondition := UIA.CreatePropertyCondition(UIA.NamePropertyId, this.CustomNames.HomeButtonName ? this.CustomNames.HomeButtonName : butName) 780 | ;this.NavigationBarElement.FindFirst(UIA.CreateAndCondition(NameCondition, this.ButtonControlCondition)).Click() 781 | } 782 | 783 | ; Gets the current URL. fromAddressBar=True gets it straight from the URL bar element, which is not a very good method, because the text might be changed by the user and doesn't start with "http(s)://". Default of fromAddressBar=False will cause the real URL to be fetched, but the browser must be visible for it to work (if is not visible, it will be automatically activated). 784 | GetCurrentURL(fromAddressBar:=False) { 785 | if fromAddressBar { 786 | URL := this.URLEditElement.Value 787 | return URL ? (RegexMatch(URL, "^https?:\/\/") ? URL : "https://" URL) : "" 788 | } else { 789 | ; This can be used in Chrome and Edge, but works only if the window is active 790 | if (!this.IsBrowserVisible() && (this.BrowserType != "Mozilla")) 791 | WinActivate this.BrowserId 792 | return this.GetCurrentDocumentElement().Value 793 | } 794 | } 795 | 796 | ; Sets the URL bar to newUrl, optionally also navigates to it if navigateToNewUrl=True 797 | SetURL(newUrl, navigateToNewUrl := False) { 798 | this.URLEditElement.ValuePattern.SetValue(newUrl " ") 799 | if !InStr(this.URLEditElement.Value, newUrl) { 800 | legacyPattern := this.URLEditElement.LegacyIAccessiblePattern 801 | legacyPattern.SetValue(newUrl " ") 802 | } 803 | if (navigateToNewUrl&&InStr(this.URLEditElement.Value, newUrl)) { 804 | this.ControlSend("{Ctrl down}l{Ctrl up}{Enter}") 805 | } 806 | } 807 | 808 | ; Navigates to URL and waits page to load 809 | Navigate(url, targetTitle:="", waitLoadTimeOut:=-1, sleepAfter:=500) { 810 | this.SetURL(url, True) 811 | this.WaitPageLoad(targetTitle,waitLoadTimeOut,sleepAfter) 812 | } 813 | 814 | ; Opens a new tab by sending Ctrl+T 815 | NewTab() { 816 | this.ControlSend("{LCtrl down}t{LCtrl up}") 817 | } 818 | 819 | GetAllTabs() { 820 | return this.TabBarElement.FindAll(UIA.CreatePropertyCondition(UIA.Property.Type, UIA.Type.TabItem)) 821 | } 822 | ; Gets all tab elements matching searchPhrase, matchMode and caseSense 823 | ; If searchPhrase is omitted then all tabs will be returned 824 | GetTabs(searchPhrase:="", matchMode:=3, caseSense:=True) { 825 | return (searchPhrase == "") ? this.TabBarElement.FindAll(UIA.CreatePropertyCondition(UIA.Property.Type, UIA.Type.TabItem)) : this.TabBarElement.FindElements({Name:searchPhrase, Type:"TabItem", mm:matchMode, cs:caseSense}) 826 | } 827 | 828 | ; Gets all the titles of tabs 829 | GetAllTabNames() { 830 | local names := [], k, v 831 | for k, v in this.GetTabs() { 832 | names.Push(v.Name) 833 | } 834 | return names 835 | } 836 | 837 | ; Returns a tab element with text of searchPhrase, or if empty then the currently selected tab. matchMode follows SetTitleMatchMode scheme: 1=tab name must must start with tabName; 2=can contain anywhere; 3=exact match; RegEx 838 | GetTab(searchPhrase:="", matchMode:=3, caseSense:=True) { 839 | return (searchPhrase == "") ? this.TabBarElement.FindElement({Type:"TabItem", SelectionItemIsSelected:1}) : this.TabBarElement.FindElement(searchPhrase is Integer ? {Type:"TabItem", i:searchPhrase} : {Name:searchPhrase, Type:"TabItem", mm:matchMode, cs:caseSense}) 840 | } 841 | ; Checks whether a tab element with text of searchPhrase exists: if a matching tab is found then the element is returned, otherwise 0 is returned. matchMode follows SetTitleMatchMode scheme: 1=tab name must must start with tabName; 2=can contain anywhere; 3=exact match; RegEx 842 | TabExist(searchPhrase:="", matchMode:=3, caseSense:=True) { 843 | try return this.GetTab(searchPhrase, matchMode, caseSense) 844 | return 0 845 | } 846 | 847 | ; Selects a tab with the text of tabName. matchMode follows SetTitleMatchMode scheme: 1=tab name must must start with tabName; 2=can contain anywhere; 3=exact match; RegEx 848 | SelectTab(tabName, matchMode:=3, caseSense:=True) { 849 | local selectedTab 850 | try { 851 | selectedTab := IsObject(tabName) ? tabName : this.GetTab(tabName, matchMode, caseSense) 852 | if this.BrowserType = "Vivaldi" 853 | selectedTab.ControlClick(,, "NA", this.BrowserId) 854 | else 855 | selectedTab.Click() 856 | } catch TargetError 857 | throw TargetError("Tab with name " tabName " was not found (MatchMode: " matchMode ", CaseSense: " caseSense ")") 858 | return selectedTab 859 | } 860 | 861 | ; Close tab by either providing the tab element or the name of the tab. If tabElementOrName is left empty, the current tab will be closed. 862 | CloseTab(tabElementOrName:="", matchMode:=3, caseSense:=True) { 863 | if IsObject(tabElementOrName) { 864 | if (tabElementOrName.Type == UIA.Type.TabItem) 865 | try UIA.TreeWalkerTrue.GetLastChildElement(tabElementOrName).Click() 866 | } else { 867 | if (tabElementOrName == "") { 868 | try UIA.TreeWalkerTrue.GetLastChildElement(this.GetTab()).Click() 869 | } else { 870 | try { 871 | targetTab := this.GetTab(tabElementOrName, matchMode, caseSense) 872 | UIA.TreeWalkerTrue.GetLastChildElement().Click(targetTab) 873 | } catch 874 | throw TargetError("Tab with name " tabElementOrName " was not found (MatchMode: " matchMode ", CaseSense: " caseSense ")") 875 | } 876 | } 877 | } 878 | 879 | ; Returns True if any of window 4 corners are visible 880 | IsBrowserVisible() { 881 | local X, Y, W, H 882 | WinGetPos &X, &Y, &W, &H, "ahk_id" this.BrowserId 883 | if ((this.BrowserId == this.WindowFromPoint(X, Y)) || (this.BrowserId == this.WindowFromPoint(X, Y+H-1)) || (this.BrowserId == this.WindowFromPoint(X+W-1, Y)) || (this.BrowserId == this.WindowFromPoint(X+W-1, Y+H-1))) 884 | return True 885 | return False 886 | } 887 | 888 | Send(text) { 889 | SendMessage(0x0006, 1, this.BrowserId,, this.BrowserId) 890 | ControlSend text, , this.BrowserId 891 | } 892 | 893 | ControlSend(text, releaseModifiers:=true) { 894 | SendMessage(0x0006, 1, this.BrowserId,, this.BrowserId) 895 | PrevKeyDelay := A_KeyDelay, PrevKeyDuration := A_KeyDuration 896 | SetKeyDelay -1, 1 897 | if releaseModifiers { 898 | released := [] 899 | for key in ["LCtrl", "RCtrl", "LAlt", "RAlt", "LShift", "RShift"] 900 | if GetKeyState(key) 901 | released.Push(key), ControlSend("{" key " up}", , this.BrowserId) 902 | } 903 | ControlSend text, , this.BrowserId 904 | if releaseModifiers { 905 | for key in released 906 | ControlSend "{" key " down}", , this.BrowserId 907 | } 908 | SetKeyDelay PrevKeyDelay, PrevKeyDuration 909 | } 910 | 911 | WindowFromPoint(X, Y) { ; by SKAN and Linear Spoon 912 | return DllCall( "GetAncestor", "UInt" 913 | , DllCall( "WindowFromPoint", "UInt64", (X & 0xFFFFFFFF) | (Y << 32)) 914 | , "UInt", 2 ) ; GA_ROOT 915 | } 916 | 917 | PrintArray(arr) { 918 | local ret := "", k, v 919 | for k, v in arr 920 | ret .= "Key: " k " Value: " (HasMethod(v)? v.name:IsObject(v)?this.PrintArray(v):v) "`n" 921 | return ret 922 | } 923 | 924 | static CompareTitles(compareTitle, winTitle, matchMode:="", caseSense:=True) => UIA_Browser.StrCompare(winTitle, compareTitle, matchMode ? matchMode : A_TitleMatchMode, caseSense) 925 | 926 | static StrCompare(str1, str2, matchMode:=3, caseSense:=True) { 927 | local str3, len3 928 | matchMode := UIA.TypeValidation.MatchMode(matchMode) 929 | if matchMode != "RegEx" && (len1 := StrLen(str1)) < (len2 := StrLen(str2)) 930 | str3 := str1, str1 := str2, str2 := str3, len3 := len1, len1 := len2, len2 := len3 931 | if matchMode = 1 932 | return caseSense ? SubStr(str1, 1, len2) == str2 : SubStr(str1, 1, len2) = str2 933 | else if matchMode = 2 934 | return InStr(str1, str2, caseSense) 935 | else if matchMode = 3 936 | return caseSense ? str1 == str2 : str1 = str2 937 | else if matchMode = "Regex" 938 | return RegExMatch(str1, str2) 939 | return 0 940 | } 941 | } 942 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # UIA-v2 2 | 3 | This library is a wrapper for the UIAutomation framework, which can be used to automate windows that normally might be difficult or impossible to automate with AHK. 4 | 5 | The main library file is UIA.ahk, which is based on [thqby's UIAutomation](https://github.com/thqby/ahk2_lib/tree/master/UIAutomation) library, but additionally contains custom helper functions for easier use of UIA. [Read here]() how this library differs from thqby's and the original UIAutomation framework by Microsoft. 6 | Additionally there is UIA_Browser.ahk, which contains helper functions for browser automation (fetching URLs, switching tabs etc). Currently it supports Chrome, Edge, and Firefox. 7 | 8 | ## For more information and tutorials, [check out the Wiki](https://github.com/Descolada/UIA-v2/wiki). 9 | 10 | ### For instructional videos, check out [the Tutorial Series](https://www.youtube.com/playlist?list=PLg-VAp_I6_oTfdL9sUqcC7s61jcQW9K38), [Joe the Automator](https://www.the-automator.com/automate-any-program-with-ui-automation/) and [Axlefublr](https://www.youtube.com/watch?v=o2E6sRxFoB0) 11 | 12 | Multiple examples are included in the Examples folder. 13 | 14 | If you have questions or issues, either post it in the [AutoHotkey forums' main thread](https://www.autohotkey.com/boards/viewtopic.php?f=83&t=113065) or create a [new issue](https://github.com/Descolada/UIA-v2/issues). 15 | 16 | If you wish to support me in this and other projects: 17 | [!["Buy Me A Coffee"](https://www.buymeacoffee.com/assets/img/custom_images/orange_img.png)](https://www.buymeacoffee.com/descolada) -------------------------------------------------------------------------------- /UIATreeInspector.ahk: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | TreeInspector() 4 | 5 | class TreeInspector { 6 | static SettingsFolderPath := A_AppData "\UIATreeInspector" 7 | static SettingsFilePath := A_AppData "\UIATreeInspector\settings.ini" 8 | __New() { 9 | local v, pattern, value 10 | OnError this.ErrorHandler.Bind(this) 11 | CoordMode "Mouse", "Screen" 12 | DetectHiddenWindows True 13 | this.Stored := {hWnd:0, WinList:Map(), FilteredTreeView:Map(), TreeView:Map(), Controls:Map(), HighlightedElement:0} 14 | this.Capturing := False, this.MacroSidebarVisible := False, this.MacroSidebarWidth := 350, this.MacroSidebarMinWidth := 290, this.GuiMinWidth := 840, this.GuiMinHeight := 400, this.Focused := 1 15 | this.LoadSettings() 16 | this.cacheRequest := UIA.CreateCacheRequest() 17 | ; Don't even get the live element, because we don't need it. Gives a significant speed improvement. 18 | this.cacheRequest.AutomationElementMode := UIA.AutomationElementMode.None 19 | ; Set TreeScope to include the starting element and all descendants as well 20 | this.cacheRequest.TreeScope := 5 21 | 22 | this.gViewer := Gui((this.AlwaysOnTop ? "AlwaysOnTop " : "") "Resize +MinSize" this.GuiMinWidth "x" this.GuiMinHeight, "UIATreeInspector") 23 | this.gViewer.OnEvent("Close", this.gViewer_Close.Bind(this)) 24 | this.gViewer.OnEvent("Size", this.gViewer_Size.Bind(this)) 25 | 26 | this.gViewer.Add("Text", "w140", "Windows and Controls").SetFont("bold") 27 | this.TVWins := this.gViewer.Add("TreeView", "w300 h465") 28 | this.TVWins.OnEvent("ItemSelect", this.TVWins_ItemSelect.Bind(this)) 29 | this.ButRefreshTVWins := this.gViewer.Add("Button", "xm+0 y500 w100", "&Refresh list") 30 | this.ButRefreshTVWins.OnEvent("Click", this.RefreshTVWins.Bind(this)) 31 | this.CBWinVisible := this.gViewer.Add("Checkbox", "xm+110 y505 w50" (this.WinVisible ? " Checked" : ""), "Visible") 32 | this.CBWinVisible.OnEvent("Click", (Ctrl, *) => (this.WinVisible := Ctrl.Value, this.RefreshTVWins())) 33 | this.CBWinTitle := this.gViewer.Add("Checkbox", "xm+160 y505 w40" (this.WinTitle ? " Checked" : ""), "Title") 34 | this.CBWinTitle.OnEvent("Click", (Ctrl, *) => (this.WinTitle := Ctrl.Value, this.RefreshTVWins())) 35 | this.CBWinActivate := this.gViewer.Add("Checkbox", "xm+200 y505 w60" (this.WinActivate ? " Checked" : ""), "Activate") 36 | this.CBWinActivate.OnEvent("Click", (Ctrl, *) => (this.WinActivate := Ctrl.Value)) 37 | 38 | this.gViewer.Add("Text", "x320 y5 w100", "Window Info").SetFont("bold") 39 | this.LVWin := this.gViewer.Add("ListView", "x320 y25 h135 w250", ["Property", "Value"]) 40 | this.LVWin.OnEvent("ContextMenu", LV_CopyTextMethod := this.LV_CopyText.Bind(this)) 41 | this.LVWin.ModifyCol(1,60) 42 | this.LVWin.ModifyCol(2,180) 43 | for v in ["Title", "Text", "Id", "Location", "Class(NN)", "Process", "PID"] 44 | this.LVWin.Add(,v,"") 45 | this.gViewer.Add("Text", "w100", "Properties").SetFont("bold") 46 | this.LVProps := this.gViewer.Add("ListView", "h200 w250", ["Property", "Value"]) 47 | this.LVProps.OnEvent("ContextMenu", LV_CopyTextMethod) 48 | this.LVProps.ModifyCol(1,100) 49 | this.LVProps.ModifyCol(2,140) 50 | this.DisplayedProps := ["Type", "LocalizedType", "Name", "Value", "AutomationId", "BoundingRectangle", "ClassName", "FullDescription", "HelpText", "AccessKey", "AcceleratorKey", "HasKeyboardFocus", "IsKeyboardFocusable", "ItemType", "ProcessId", "IsEnabled", "IsPassword", "IsOffscreen", "FrameworkId", "IsRequiredForForm", "ItemStatus", "RuntimeId"] 51 | Loop DisplayedPropsLength := this.DisplayedProps.Length { 52 | v := this.DisplayedProps[i := DisplayedPropsLength-A_Index+1] 53 | try this.cacheRequest.AddProperty(v) ; Throws if not available, 54 | catch 55 | this.DisplayedProps.RemoveAt(i) ; Remove property if it is not available 56 | } 57 | for v in this.DisplayedProps 58 | this.LVProps.Add(,v = "BoundingRectangle" ? "Location" : v,"") 59 | for pattern in [UIA.Property.OwnProps()*] { 60 | if pattern ~= "Is([\w]+Pattern.?)Available" 61 | try this.cacheRequest.AddProperty(UIA.Property.%pattern%) 62 | catch 63 | UIA.Property.DeleteProp(pattern) ; Remove pattern if it is not available 64 | } 65 | 66 | (this.TextTVPatterns := this.gViewer.Add("Text", "w100", "Patterns")).SetFont("bold") 67 | this.TVPatterns := this.gViewer.Add("TreeView", "h85 w250") 68 | this.TVPatterns.OnEvent("DoubleClick", this.TVPatterns_DoubleClick.Bind(this)) 69 | 70 | this.SBMain := this.gViewer.Add("StatusBar",, " Right-click to change additional settings") 71 | this.SBMain.OnEvent("Click", this.SBMain_Click.Bind(this)) 72 | this.SBMain.OnEvent("ContextMenu", this.SBMain_ContextMenu.Bind(this)) 73 | 74 | this.gViewer.Add("Text", "x580 y5 w100", "UIA Tree").SetFont("bold") 75 | this.TVUIA := this.gViewer.Add("TreeView", "x580 y25 w300 h465") 76 | this.TVUIA.OnEvent("Click", this.TVUIA_Click.Bind(this)) 77 | this.TVUIA.OnEvent("ContextMenu", this.TVUIA_ContextMenu.Bind(this)) 78 | this.TVUIA.Add("Select window or control") 79 | this.TextFilterTVUIA := this.gViewer.Add("Text", "x275 y503", "&Filter:") 80 | this.EditFilterTVUIA := this.gViewer.Add("Edit", "x305 y500 w100") 81 | this.EditFilterTVUIA.OnEvent("Change", this.EditFilterTVUIA_Change.Bind(this)) 82 | 83 | this.GroupBoxMacro := this.gViewer.Add("GroupBox", "x1200 y20 w" (this.MacroSidebarWidth-20), "Macro creator") 84 | (this.TextMacroAction := this.gViewer.Add("Text", "x1200 y40 w40", "Action:")).SetFont("bold") 85 | this.DDLMacroAction := this.gViewer.Add("DDL", "Choose1 x1200 y38 w120", ["No element selected"]) 86 | (this.ButMacroAddElement := this.gViewer.Add("Button","x1200 y37 w90 h20", "&Add element")).SetFont("bold") 87 | this.ButMacroAddElement.OnEvent("Click", this.ButMacroAddElement_Click.Bind(this)) 88 | (this.EditMacroScript := this.gViewer.Add("Edit", "-Wrap HScroll x1200 y65 h410 w" (this.MacroSidebarWidth-40), "#include `n`n")).SetFont("s10") ; Setting a font here disables UTF-8-BOM 89 | (this.ButMacroScriptRun := this.gViewer.Add("Button", "x1180 y120 w70", "&Test script")).SetFont("bold") 90 | this.ButMacroScriptRun.OnEvent("Click", this.ButMacroScriptRun_Click.Bind(this)) 91 | this.ButMacroScriptCopy := this.gViewer.Add("Button", "x1220 y120 w70", "&Copy") 92 | this.ButMacroScriptCopy.OnEvent("Click", (*) => (A_Clipboard := this.EditMacroScript.Text, ToolTip("Macro code copied to Clipboard!"), SetTimer(ToolTip, -3000))) 93 | this.ButToggleMacroSidebar := this.gViewer.Add("Button", "x490 y500 w120", "Show macro &sidebar =>") 94 | this.ButToggleMacroSidebar.OnEvent("Click", this.ButToggleMacroSidebar_Click.Bind(this)) 95 | xy := "" 96 | if this.RememberGuiPosition { 97 | xy := StrSplit(this.RememberGuiPosition, ","), monitor := 0 98 | Loop MonitorGetCount() { 99 | MonitorGetWorkArea(A_Index, &Left, &Top, &Right, &Bottom) 100 | if xy[1] > (Left-50) && xy[2] > (Top-50) && xy[1] < (Right-50) && xy[2] < (Bottom-30) { 101 | monitor := A_Index 102 | break 103 | } 104 | } 105 | xy := monitor ? "x" xy[1] " y" xy[2] " " : "" 106 | } 107 | this.gViewer.Show(xy "w900 h550") 108 | this.gViewer_Size(this.gViewer,0,900,550) 109 | this.RefreshTVWins() 110 | this.FocusHook := DllCall("SetWinEventHook", "UInt", 0x8005, "UInt", 0x8005, "Ptr",0,"Ptr", CallbackCreate(this.HandleFocusChangedEvent.Bind(this), "F", 7),"UInt", 0, "UInt",0, "UInt",0) 111 | } 112 | __Delete() { 113 | DllCall("UnhookWinEvent", "Ptr", this.FocusHook) 114 | } 115 | HandleFocusChangedEvent(hWinEventHook, Event, hWnd, idObject, idChild, dwEventThread, dwmsEventTime) { 116 | winHwnd := DllCall("GetAncestor", "UInt", hWnd, "UInt", 2) 117 | try winTitle := WinGetTitle(winHwnd) 118 | catch 119 | winTitle := "" 120 | if winHwnd = this.gViewer.Hwnd || winTitle == "UIATreeInspector" { 121 | if !this.Focused { 122 | this.Focused := 1 123 | if IsObject(this.Stored.HighlightedElement) 124 | this.Stored.HighlightedElement.Highlight(0, "Blue", 4) 125 | } 126 | } else { 127 | if this.Focused { 128 | this.Focused := 0 129 | if IsObject(this.Stored.HighlightedElement) 130 | this.Stored.HighlightedElement.Highlight("clear") 131 | } 132 | } 133 | return 0 134 | } 135 | SaveSettings() { 136 | if !FileExist(A_AppData "\UIATreeInspector") 137 | DirCreate(A_AppData "\UIATreeInspector") 138 | IniWrite(this.PathIgnoreNames, TreeInspector.SettingsFilePath, "Path", "IgnoreNames") 139 | IniWrite(this.PathType, TreeInspector.SettingsFilePath, "Path", "Type") 140 | IniWrite(this.AlwaysOnTop, TreeInspector.SettingsFilePath, "General", "AlwaysOnTop") 141 | IniWrite(this.DPIAwareness, UIA.Viewer.SettingsFilePath, "General", "DPIAwareness") 142 | IniWrite(this.RememberGuiPosition, UIA.Viewer.SettingsFilePath, "General", "RememberGuiPosition") 143 | IniWrite(this.WinVisible, TreeInspector.SettingsFilePath, "WinTree", "Visible") 144 | IniWrite(this.WinTitle, TreeInspector.SettingsFilePath, "WinTree", "Title") 145 | IniWrite(this.WinActivate, TreeInspector.SettingsFilePath, "WinTree", "Activate") 146 | } 147 | LoadSettings() { 148 | this.PathIgnoreNames := IniRead(TreeInspector.SettingsFilePath, "Path", "IgnoreNames", 1) 149 | this.PathType := IniRead(TreeInspector.SettingsFilePath, "Path", "Type", "") 150 | this.AlwaysOnTop := IniRead(TreeInspector.SettingsFilePath, "General", "AlwaysOnTop", 1) 151 | this.DPIAwareness := IniRead(UIA.Viewer.SettingsFilePath, "General", "DPIAwareness", 0) 152 | this.RememberGuiPosition := IniRead(UIA.Viewer.SettingsFilePath, "General", "RememberGuiPosition", "") 153 | this.WinVisible := IniRead(TreeInspector.SettingsFilePath, "WinTree", "Visible", 1) 154 | this.WinTitle := IniRead(TreeInspector.SettingsFilePath, "WinTree", "Title", 1) 155 | this.WinActivate := IniRead(TreeInspector.SettingsFilePath, "WinTree", "Activate", 1) 156 | } 157 | ErrorHandler(Exception, Mode) => (OutputDebug(Format("{1} ({2}) : ({3}) {4}`n", Exception.File, Exception.Line, Exception.What, Exception.Message) (HasProp(Exception, "Extra") ? " Specifically: " Exception.Extra "`n" : "") "Stack:`n" Exception.Stack "`n`n"), 1) 158 | gViewer_Close(GuiObj, *) { 159 | if this.RememberGuiPosition 160 | WinGetPos(&X, &Y,,,GuiObj.Hwnd), this.RememberGuiPosition := X "," Y, this.SaveSettings() 161 | ExitApp() 162 | } 163 | ; Resizes window controls when window is resized 164 | gViewer_Size(GuiObj, MinMax, Width, Height) { 165 | static RedrawFunc := WinRedraw.Bind(GuiObj.Hwnd) 166 | this.TVUIA.GetPos(&TV_Pos_X, &TV_Pos_Y, &TV_Pos_W, &TV_Pos_H) 167 | this.MoveControls(this.MacroSidebarVisible ? {Control:this.TVUIA,h:(TV_Pos_H:=Height-TV_Pos_Y-60)} : {Control:this.TVUIA,w:(TV_Pos_W:=Width-TV_Pos_X-10),h:(TV_Pos_H:=Height-TV_Pos_Y-60)}) 168 | TV_Pos_R := TV_Pos_X+TV_Pos_W 169 | this.LVProps.GetPos(&LVPropsX, &LVPropsY, &LVPropsWidth, &LVPropsHeight) 170 | this.ButToggleMacroSidebar.GetPos(,,&ButToggleMacroSidebarW) 171 | this.MoveControls( 172 | {Control:this.TextFilterTVUIA, x:TV_Pos_X, y:Height-47}, 173 | {Control:this.ButToggleMacroSidebar, x:TV_Pos_X+TV_Pos_W-ButToggleMacroSidebarW, y:Height-50}, 174 | {Control:this.EditFilterTVUIA, x:TV_Pos_X+30, y:Height-50}, 175 | {Control:this.LVProps,h:Height-LVPropsY-170}, 176 | {Control:this.TextTVPatterns,y:Height-165}, 177 | {Control:this.TVPatterns,y:Height-145}, 178 | {Control:this.ButRefreshTVWins,y:Height-50}, 179 | {Control:this.CBWinVisible,y:Height-45}, 180 | {Control:this.CBWinTitle,y:Height-45}, 181 | {Control:this.CBWinActivate,y:Height-45}, 182 | {Control:this.TVWins,h:(TV_Pos_H:=Height-TV_Pos_Y-60)}) 183 | if this.MacroSidebarVisible 184 | this.MacroSidebarWidth := Width-(TV_Pos_X+TV_Pos_W)-10 185 | this.MoveControls( 186 | {Control:this.GroupBoxMacro,x:TV_Pos_R+15, w:Width-TV_Pos_R-30, h:TV_Pos_H+35}, 187 | {Control:this.TextMacroAction,x:TV_Pos_R+25}, 188 | {Control:this.DDLMacroAction,x:TV_Pos_R+70}, 189 | {Control:this.ButMacroAddElement,x:TV_Pos_R+this.MacroSidebarWidth-105}, 190 | {Control:this.EditMacroScript,x:TV_Pos_R+25,w:Width-TV_Pos_R-50,h:TV_Pos_H-50}, 191 | {Control:this.ButMacroScriptRun,x:TV_Pos_R+85,y:TV_Pos_Y+TV_Pos_H-2}, 192 | {Control:this.ButMacroScriptCopy,x:TV_Pos_R+this.MacroSidebarWidth-145,y:TV_Pos_Y+TV_Pos_H-2}) 193 | DllCall("RedrawWindow", "ptr", GuiObj.Hwnd, "ptr", 0, "ptr", 0, "uint", 0x0081) ; Reduces flicker compared to RedrawFunc 194 | } 195 | MoveControls(ctrls*) { 196 | for ctrl in ctrls 197 | ctrl.Control.Move(ctrl.HasOwnProp("x") ? ctrl.x : unset, ctrl.HasOwnProp("y") ? ctrl.y : unset, ctrl.HasOwnProp("w") ? ctrl.w : unset, ctrl.HasOwnProp("h") ? ctrl.h : unset) 198 | } 199 | RefreshTVWins(*) { 200 | DetectHiddenWindows(!this.WinVisible) 201 | this.TVWins.Delete() 202 | for hWnd in WinGetList() { 203 | wTitle := WinGetTitle(hWnd) 204 | if (this.WinTitle && (wTitle == "")) 205 | continue 206 | try wExe := WinGetProcessName(hWnd) 207 | catch 208 | wExe := "ERROR" 209 | parent := this.TVWins.Add(wTitle " (" wExe ")") 210 | this.Stored.WinList[parent] := hWnd 211 | for ctrl in WinGetControlsHwnd(hWnd) { 212 | try { 213 | classNN := ControlGetClassNN(ctrl, hWnd) 214 | item := this.TVWins.Add(classNN, parent) 215 | this.Stored.WinList[item] := ctrl 216 | this.Stored.Controls[ctrl] := hWnd 217 | } 218 | } 219 | } 220 | } 221 | ; Show/hide macros sidebar 222 | ButToggleMacroSidebar_Click(GuiCtrlObj?, Info?) { 223 | local w 224 | this.MacroSidebarVisible := !this.MacroSidebarVisible 225 | GuiCtrlObj.Text := this.MacroSidebarVisible ? "Hide macro &sidebar <=" : "Show macro &sidebar =>" 226 | this.gViewer.GetPos(,, &w) 227 | this.gViewer.Opt("+MinSize" (this.MacroSidebarVisible ? w + this.MacroSidebarMinWidth : this.GuiMinWidth) "x" this.GuiMinHeight) 228 | this.gViewer.Move(,,w+(this.MacroSidebarVisible ? this.MacroSidebarWidth : -this.MacroSidebarWidth)) 229 | } 230 | ; Handles adding elements with actions to the macro Edit 231 | ButMacroAddElement_Click(GuiCtrlObj?, Info?) { 232 | local match 233 | if !this.Stored.HasOwnProp("CapturedElement") 234 | return 235 | processName := WinGetProcessName(this.Stored.hWnd) 236 | winElVariable := RegExMatch(processName, "^[^ .\d]+", &match:="") ? RegExReplace(match[], "[^\p{L}0-9_#@$]") "El" : "winEl" ; Leaves only letters, numbers, and symbols _#@$ (allowed AHK characters) 237 | winTitle := "`"" WinGetTitle(this.Stored.hWnd) " ahk_exe " processName "`"" 238 | winElText := winElVariable " := UIA.ElementFromHandle(" (this.Stored.Controls.Has(ctrl := this.Stored.hWnd) ? "ControlGetHwnd(`"" ControlGetClassNN(ctrl, this.Stored.Controls[ctrl]) "`", " winTitle ")" : winTitle) ")" 239 | if !InStr(this.EditMacroScript.Text, winElText) || RegExMatch(this.EditMacroScript.Text, "\Q" winElText "\E(?=[\w\W]*\QwinEl := UIA.ElementFromHandle(`"ahk_exe\E)") 240 | this.EditMacroScript.Text := RTrim(this.EditMacroScript.Text, "`r`n`t ") "`r`n`r`n" winElText 241 | else 242 | this.EditMacroScript.Text := RTrim(this.EditMacroScript.Text, "`r`n`t ") 243 | winElVariable := winElVariable (SubStr(this.SBMain.Text, 9) ? ".ElementFromPath(" SubStr(this.SBMain.Text, 9) ")" : "") (this.DDLMacroAction.Text ? "." this.DDLMacroAction.Text : "") 244 | if InStr(this.DDLMacroAction.Text, "Dump") 245 | winElVariable := "MsgBox(" winElVariable ")" 246 | this.EditMacroScript.Text := this.EditMacroScript.Text "`r`n" RegExReplace(winElVariable, "(? GuiCtrlObj.GetCount() 272 | ? ListViewGetContent("", GuiCtrlObj) 273 | : ListViewGetContent("Selected", GuiCtrlObj) 274 | for LVData in StrSplit(LVData, "`n") { 275 | LVData := StrSplit(LVData, "`t",,2) 276 | if LVData.Length < 2 277 | continue 278 | switch LVData[1], 0 { 279 | case "Type": 280 | LVData[2] := "`"" RTrim(SubStr(LVData[2],8), ")") "`"" 281 | case "Location": 282 | LVData[2] := "{" RegExReplace(LVData[2], "(\w:) (\d+)(?= )", "$1$2,") "}" 283 | } 284 | Property := -1 285 | try Property := UIA.Property.%LVData[1]% 286 | out .= ", " (GuiCtrlObj.Hwnd = this.LVWin.Hwnd ? "" : LVData[1] ":") (UIA.PropertyVariantTypeBSTR.Has(Property) ? "`"" StrReplace(StrReplace(LVData[2], "``", "````"), "`"", "```"") "`"" : LVData[2]) 287 | } 288 | ToolTip("Copied: " (A_Clipboard := SubStr(out, 3))) 289 | SetTimer(ToolTip, -3000) 290 | } 291 | ; Handles running pattern methods, first trying to find the live element by RuntimeId 292 | TVPatterns_DoubleClick(GuiCtrlObj, Info) { 293 | if !Info 294 | return 295 | Item := GuiCtrlObj.GetText(Info) 296 | if !InStr(Item, "()") 297 | return 298 | Item := SubStr(Item, 1, -2) 299 | if !(CurrentEl := UIA.ElementFromHandle(this.Stored.hWnd).ElementExist({RuntimeId:this.Stored.CapturedElement.CachedRuntimeId})) 300 | return MsgBox("Live element not found!",,"4096") 301 | if Item ~= "Value|Scroll(?!Into)" { 302 | this.gViewer.Opt("-AlwaysOnTop") 303 | Ret := InputBox("Insert value", Item, "W200 H120") 304 | this.gViewer.Opt("+AlwaysOnTop") 305 | if Ret.Result != "OK" 306 | return 307 | } 308 | parent := DllCall("GetAncestor", "UInt", this.Stored.hWnd, "UInt", 2) 309 | WinActivate(parent) 310 | WinWaitActive(parent,1) 311 | try CurrentEl.%GuiCtrlObj.GetText(GuiCtrlObj.GetParent(Info)) "Pattern"%.%Item%(IsSet(Ret) ? Ret.Value : unset) 312 | } 313 | ; Copies the UIA path to clipboard when statusbar is clicked 314 | SBMain_Click(GuiCtrlObj, Info, *) { 315 | if InStr(this.SBMain.Text, "Path:") { 316 | ToolTip("Copied: " (A_Clipboard := SubStr(this.SBMain.Text, 9))) 317 | SetTimer(ToolTip, -3000) 318 | } 319 | } 320 | ; StatusBar context menu creation 321 | SBMain_ContextMenu(GuiCtrlObj, Item, IsRightClick, X, Y) { 322 | SBMain_Menu := Menu() 323 | if InStr(this.SBMain.Text, "Path:") { 324 | SBMain_Menu.Add("Copy UIA path", (*) => (ToolTip("Copied: " (A_Clipboard := this.Stored.CapturedElement.Path)), SetTimer(ToolTip, -3000))) 325 | SBMain_Menu.Add("Copy condition path", (*) => (ToolTip("Copied: " (A_Clipboard := this.Stored.CapturedElement.ConditionPath)), SetTimer(ToolTip, -3000))) 326 | SBMain_Menu.Add("Copy numeric path", (*) => (ToolTip("Copied: " (A_Clipboard := this.Stored.CapturedElement.NumericPath)), SetTimer(ToolTip, -3000))) 327 | SBMain_Menu.Add() 328 | } 329 | SBMain_Menu.Add("Display UIA path (relatively reliable, shortest)", (*) => (this.PathType := this.PathType = "" ? "" : "", this.Stored.HasOwnProp("CapturedElement") && this.Stored.CapturedElement.HasOwnProp("Path") ? this.SBMain.SetText(" Path: " (this.PathType = "Numeric" ? this.Stored.CapturedElement.NumericPath : this.Stored.CapturedElement.Path)) : 1)) 330 | SBMain_Menu.Add("Display numeric path (least reliable, short)", (*) => (this.PathType := this.PathType = "Numeric" ? "" : "Numeric", this.Stored.HasOwnProp("CapturedElement") && this.Stored.CapturedElement.HasOwnProp("Path") ? this.SBMain.SetText(" Path: " (this.PathType = "Numeric" ? this.Stored.CapturedElement.NumericPath : this.Stored.CapturedElement.Path)) : 1)) 331 | SBMain_Menu.Add("Display condition path (most reliable, longest)", (*) => (this.PathType := this.PathType = "Condition" ? "" : "Condition", this.Stored.HasOwnProp("CapturedElement") && this.Stored.CapturedElement.HasOwnProp("Path") ? this.SBMain.SetText(" Path: " (this.PathType = "Condition" ? this.Stored.CapturedElement.ConditionPath : this.Stored.CapturedElement.Path)) : 1)) 332 | SBMain_Menu.Add("Ignore Name properties in condition path", (*) => (this.PathIgnoreNames := !this.PathIgnoreNames)) 333 | if this.PathIgnoreNames 334 | SBMain_Menu.Check("Ignore Name properties in condition path") 335 | if this.PathType = "" 336 | SBMain_Menu.Check("Display UIA path (relatively reliable, shortest)") 337 | if this.PathType = "Numeric" 338 | SBMain_Menu.Check("Display numeric path (least reliable, short)") 339 | if this.PathType = "Condition" 340 | SBMain_Menu.Check("Display condition path (most reliable, longest)") 341 | SBMain_Menu.Add() 342 | SBMain_Menu.Add("UIATreeInspector always on top", (*) => (this.AlwaysOnTop := !this.AlwaysOnTop, this.gViewer.Opt((this.AlwaysOnTop ? "+" : "-") "AlwaysOnTop"))) 343 | if this.AlwaysOnTop 344 | SBMain_Menu.Check("UIATreeInspector always on top") 345 | SBMain_Menu.Add("Remember UIATreeInspector position", (*) => (this.RememberGuiPosition := !this.RememberGuiPosition, this.SaveSettings())) 346 | if this.RememberGuiPosition 347 | SBMain_Menu.Check("Remember UIATreeInspector position") 348 | SBMain_Menu.Add("Enable DPI awareness", (*) => (this.DPIAwareness := !this.DPIAwareness, this.DPIAwareness ? UIA.SetMaximumDPIAwareness() : UIA.DPIAwareness := -2)) 349 | if this.DPIAwareness 350 | SBMain_Menu.Check("Enable DPI awareness") 351 | SBMain_Menu.Add() 352 | SBMain_Menu.Add("Save settings", (*) => (this.SaveSettings(), ToolTip("Settings saved!"), SetTimer(ToolTip, -2000))) 353 | SBMain_Menu.Show() 354 | } 355 | ; Updates the GUI with the selected item 356 | TVWins_ItemSelect(GuiCtrlObj, Info) { 357 | local hWnd := this.Stored.WinList[Info], parent := DllCall("GetAncestor", "UInt", hWnd, "UInt", 2) 358 | this.Stored.hWnd := hWnd 359 | if UIA.ProcessIsElevated(WinGetPID(parent)) > 0 && !A_IsAdmin { 360 | if MsgBox("The inspected window is running with elevated privileges.`nUIATreeInspector must be running in UIAccess mode or as administrator to inspect it.`n`nRun UIATreeInspector as administrator to inspect it?",, 0x1000 | 0x30 | 0x4) = "Yes" { 361 | try { 362 | Run('*RunAs "' (A_IsCompiled ? A_ScriptFullPath '" /restart' : A_AhkPath '" /restart "' A_ScriptFullPath '"')) 363 | ExitApp 364 | } 365 | } 366 | } 367 | this.cacheRequest.TreeScope := 1 368 | try this.Stored.CapturedElement := UIA.ElementFromHandle(hWnd, this.cacheRequest) 369 | this.cacheRequest.TreeScope := 5 370 | propsOrder := ["Title", "Text", "Id", "Location", "Class(NN)", "Process", "PID"] 371 | if this.WinActivate { 372 | WinActivate(parent) 373 | WinWaitActive(parent, 1) 374 | WinActivate(this.gViewer.Hwnd) 375 | } 376 | WinGetPos(&wX, &wY, &wW, &wH, hWnd) 377 | props := Map("Title", WinGetTitle(hWnd), "Text", WinGetText(hWnd), "Id", hWnd, "Location", "x: " wX " y: " wY " w: " wW " h: " wH, "Class(NN)", WinGetClass(hWnd), "Process", WinGetProcessName(hWnd), "PID", WinGetPID(hWnd)) 378 | this.LVWin.Delete() 379 | for propName in propsOrder 380 | this.LVWin.Add(,propName,props[propName]) 381 | this.PopulatePropsPatterns(this.Stored.CapturedElement) 382 | this.ConstructTreeView() 383 | } 384 | ; Populates the listview with UIA element properties 385 | PopulatePropsPatterns(Element) { 386 | local v, value, pattern, parent, proto, match, X, Y, W, H 387 | if IsObject(this.Stored.HighlightedElement) 388 | this.Stored.HighlightedElement.Highlight("clear") 389 | this.Stored.HighlightedElement := Element 390 | try { ; Show the Highlight only if the window is visible and 391 | WinGetPos(&X, &Y, &W, &H, this.Stored.hWnd) 392 | if IsObject(this.Stored.HighlightedElement) && (elBR := this.Stored.HighlightedElement.CachedBoundingRectangle) && UIA.IntersectRect(X, Y, X+W, Y+H, elBR.l, elBR.t, elBR.r, elBR.b) 393 | Element.Highlight(0, "Blue", 4) ; Indefinite show 394 | } 395 | this.LVProps.Delete() 396 | this.TVPatterns.Delete() 397 | for v in this.DisplayedProps { 398 | try prop := Element.Cached%v% 399 | switch v, 0 { 400 | case "Type": 401 | try name := UIA.Type[prop] 402 | catch 403 | name := "Unknown" 404 | try this.LVProps.Add(, v, prop " (" name ")") 405 | case "BoundingRectangle": 406 | prop := prop ? prop : {l:0,t:0,r:0,b:0} 407 | try this.LVProps.Add(, "Location", "x: " prop.l " y: " prop.t " w: " (prop.r - prop.l) " h: " (prop.b - prop.t)) 408 | case "RuntimeId": 409 | continue ; Don't display this for now, since it might confuse users into using it as a search property. 410 | ; try this.LVProps.Add(, v, UIA.RuntimeIdToString(prop)) ; Uncomment for debugging purposes 411 | default: 412 | try this.LVProps.Add(, v, prop) 413 | } 414 | prop := "" 415 | } 416 | lastAction := this.DDLMacroAction.Text 417 | this.DDLMacroAction.Delete() 418 | this.DDLMacroAction.Add(['', 'Click()', 'Click("left")', 'ControlClick()', 'SetFocus()', 'ShowContextMenu()', 'Highlight()', 'Dump()','DumpAll()']) 419 | for pattern, value in UIA.Property.OwnProps() { 420 | if RegExMatch(pattern, "Is([\w]+)Pattern(\d?)Available", &match:=0) && Element.GetCachedPropertyValue(value) { 421 | parent := this.TVPatterns.Add(match[1] (match.Count > 1 ? match[2] : "")) 422 | if !IsObject(UIA.IUIAutomation%match[1]%Pattern) 423 | continue 424 | proto := UIA.IUIAutomation%match[1]%Pattern.Prototype 425 | switch match[1], 0 { 426 | case "Invoke": 427 | this.DDLMacroAction.Add(['Invoke()']) 428 | case "ExpandCollapse": 429 | this.DDLMacroAction.Add(['Expand()', 'Collapse()']) 430 | case "Value": 431 | this.DDLMacroAction.Add(['Value := "value"']) 432 | case "Toggle": 433 | this.DDLMacroAction.Add(['Toggle()']) 434 | case "SelectionItem": 435 | this.DDLMacroAction.Add(['Select()', 'AddToSelection()', 'RemoveFromSelection()']) 436 | case "ScrollItem": 437 | this.DDLMacroAction.Add(['ScrollIntoView()']) 438 | } 439 | for name in proto.OwnProps() { 440 | if name ~= "i)^(_|Cached)" 441 | continue 442 | this.TVPatterns.Add(name (proto.GetOwnPropDesc(name).HasOwnProp("call") ? "()" : ""), parent) 443 | } 444 | } 445 | } 446 | try this.DDLMacroAction.Choose(lastAction) 447 | catch 448 | this.DDLMacroAction.Choose(7) 449 | } 450 | ; Handles selecting elements in the UIA tree, highlights the selected element 451 | TVUIA_Click(GuiCtrlObj, Info) { 452 | if this.Capturing 453 | return 454 | try Element := this.EditFilterTVUIA.Value ? this.Stored.FilteredTreeView[Info] : this.Stored.TreeView[Info] 455 | if IsSet(Element) && Element { 456 | if IsObject(this.Stored.HighlightedElement) { 457 | if this.SafeCompareElements(Element, this.Stored.HighlightedElement) 458 | return (this.Stored.HighlightedElement.Highlight("clear"), this.Stored.HighlightedElement := 0) 459 | } 460 | this.Stored.CapturedElement := Element 461 | try this.SBMain.SetText(" Path: " (this.PathType = "Numeric" ? Element.NumericPath : this.PathType = "Condition" ? Element.ConditionPath : Element.Path)) 462 | this.PopulatePropsPatterns(Element) 463 | } 464 | } 465 | ; Permits copying the Dump of UIA element(s) to clipboard 466 | TVUIA_ContextMenu(GuiCtrlObj, Item, IsRightClick, X, Y) { 467 | TVUIA_Menu := Menu() 468 | try Element := this.EditFilterTVUIA.Value ? this.Stored.FilteredTreeView[Item] : this.Stored.TreeView[Item] 469 | if IsSet(Element) 470 | TVUIA_Menu.Add("Copy to Clipboard", (*) => (ToolTip("Copied Dump() output to Clipboard!"), A_Clipboard := Element.CachedDump(), SetTimer((*) => ToolTip(), -3000))) 471 | TVUIA_Menu.Add("Copy Tree to Clipboard", (*) => (ToolTip("Copied DumpAll() output to Clipboard!"), A_Clipboard := UIA.ElementFromHandle(this.Stored.hWnd, this.cacheRequest).DumpAll(), SetTimer((*) => ToolTip(), -3000))) 472 | TVUIA_Menu.Show() 473 | } 474 | ; Handles filtering the UIA elements inside the TreeView when the text hasn't been changed in 500ms. 475 | ; Sorts the results by UIA properties. 476 | EditFilterTVUIA_Change(GuiCtrlObj, Info, *) { 477 | static TimeoutFunc := "", ChangeActive := False 478 | if !this.Stored.TreeView.Count 479 | return 480 | if (Info != "DoAction") || ChangeActive { 481 | if !TimeoutFunc 482 | TimeoutFunc := this.EditFilterTVUIA_Change.Bind(this, GuiCtrlObj, "DoAction") 483 | SetTimer(TimeoutFunc, -500) 484 | return 485 | } 486 | ChangeActive := True 487 | this.Stored.FilteredTreeView := Map(), parents := Map() 488 | if !(searchPhrase := this.EditFilterTVUIA.Value) { 489 | this.ConstructTreeView() 490 | ChangeActive := False 491 | return 492 | } 493 | this.TVUIA.Delete() 494 | temp := this.TVUIA.Add("Searching...") 495 | Sleep -1 496 | this.TVUIA.Opt("-Redraw") 497 | this.TVUIA.Delete() 498 | for index, Element in this.Stored.TreeView { 499 | for prop in this.DisplayedProps { 500 | try { 501 | if InStr(Element.Cached%Prop%, searchPhrase) { 502 | if !parents.Has(prop) 503 | parents[prop] := this.TVUIA.Add(prop,, "Expand") 504 | this.Stored.FilteredTreeView[this.TVUIA.Add(this.GetShortDescription(Element), parents[prop], "Expand")] := Element 505 | } 506 | } 507 | } 508 | } 509 | if !this.Stored.FilteredTreeView.Count 510 | this.TVUIA.Add("No results found matching `"" searchPhrase "`"") 511 | this.TVUIA.Opt("+Redraw") 512 | TimeoutFunc := "", ChangeActive := False 513 | } 514 | ; Populates the TreeView with the UIA tree when capturing and the mouse is held still 515 | ConstructTreeView() { 516 | local k, v 517 | this.TVUIA.Delete() 518 | this.TVUIA.Add("Constructing Tree, please wait...") 519 | Sleep -1 520 | this.TVUIA.Opt("-Redraw") 521 | this.TVUIA.Delete() 522 | this.Stored.TreeView := Map() 523 | try this.RecurseTreeView(UIA.ElementFromHandle(this.Stored.hWnd, this.cacheRequest)) 524 | catch { 525 | this.Stored.TreeView := [] 526 | this.TVUIA.Add("Error: unspecified error (window not found?)") 527 | } 528 | 529 | this.TVUIA.Opt("+Redraw") 530 | this.SBMain.SetText(" Path: ") 531 | if !this.Stored.CapturedElement.HasOwnProp("Path") { 532 | this.Stored.CapturedElement.DefineProp("Path", {Value:""}) 533 | this.Stored.CapturedElement.DefineProp("NumericPath", {Value:""}) 534 | this.Stored.CapturedElement.DefineProp("ConditionPath", {Value:""}) 535 | } 536 | for k, v in this.Stored.TreeView { 537 | if this.SafeCompareElements(this.Stored.CapturedElement, v) { 538 | this.TVUIA.Modify(k, "Vis Select"), this.SBMain.SetText(" Path: " (this.PathType = "Numeric" ? v.NumericPath : this.PathType = "Condition" ? v.ConditionPath : v.Path)) 539 | , this.Stored.CapturedElement.Path := v.Path 540 | , this.Stored.CapturedElement.NumericPath := v.NumericPath 541 | , this.Stored.CapturedElement.ConditionPath := v.ConditionPath 542 | } 543 | } 544 | } 545 | ; Stores the UIA tree with corresponding path values for each element 546 | RecurseTreeView(Element, parent:=0, path:="", conditionpath := "", numpath:="") { 547 | local info, child, type, k, paths := Map(), childInfo := [], children := Element.CachedChildren 548 | Element.DefineProp("Path", {value:"`"" path "`""}) 549 | Element.DefineProp("ConditionPath", {value:conditionpath}) 550 | Element.DefineProp("NumericPath", {value:numpath}) 551 | this.Stored.TreeView[TWEl := this.TVUIA.Add(this.GetShortDescription(Element), parent, "Expand")] := Element 552 | ; First count up all multiple-condition-index conditions and type-index conditions 553 | ; This is to know whether the condition is the last of the matching ones, so we can use index -1 554 | ; This gives an important speed difference over regular indexing 555 | for child in children { 556 | compactCondition := this.GetCompactCondition(child, &paths, &typeCondition := "", &type:="") 557 | childInfo.Push([compactCondition, paths[compactCondition], typeCondition, paths[typeCondition], type]) 558 | } 559 | ; Now create the final conditions and recurse the tree 560 | for k, child in children { 561 | info := childInfo[k], compactCondition := info[1], conditionIndex := info[2] 562 | if conditionIndex > 1 && conditionIndex = paths[compactCondition] 563 | conditionIndex := -1 564 | compactCondition .= conditionIndex = 1 ? "}" : ", i:" conditionIndex "}" 565 | typeIndex := info[4] 566 | if typeIndex > 1 && typeIndex = paths[info[3]] 567 | typeIndex := -1 568 | this.RecurseTreeView(child, TWEl, path UIA.EncodePath([typeIndex = 1 ? {Type:info[5]} : {Type:info[5], i:typeIndex}]), conditionpath (conditionpath?", ":"") compactCondition, numpath (numpath?",":"") k) 569 | } 570 | } 571 | ; CompareElements sometimes fails to match elements, so this compares some properties instead 572 | SafeCompareElements(e1, e2) { 573 | if e1.CachedDump() == e2.CachedDump() { 574 | try { 575 | if UIA.RuntimeIdToString(e1.CachedRuntimeId) == UIA.RuntimeIdToString(e2.CachedRuntimeId) 576 | return 1 577 | } 578 | br_e1 := e1.CachedBoundingRectangle, br_e2 := e2.CachedBoundingRectangle 579 | return br_e1.l = br_e2.l && br_e1.t = br_e2.t && br_e1.r = br_e2.r && br_e1.b = br_e2.b 580 | } 581 | return 0 582 | } 583 | ; Creates a short description string for the UIA tree elements 584 | GetShortDescription(Element) { 585 | local elDesc := " `"`"" 586 | try elDesc := " `"" Element.CachedName "`"" 587 | try elDesc := Element.CachedLocalizedType elDesc 588 | catch 589 | elDesc := "`"`"" elDesc 590 | return elDesc 591 | } 592 | GetCompactCondition(Element, &pathsMap, &t := "", &type := "", &automationId := "", &className := "", &name := "") { 593 | local n := "", c := "", a := "" 594 | type := Element.CachedType 595 | t := "{T:" (type-50000) 596 | pathsMap[t] := pathsMap.Has(t) ? pathsMap[t] + 1 : 1 597 | try a := StrReplace(automationId := Element.CachedAutomationId, "`"", "```"") 598 | if a != "" && !IsInteger(a) { ; Ignore Integer AutomationIds, since they seem to be auto-generated in Chromium apps 599 | a := t ",A:`"" a "`"" 600 | pathsMap[a] := pathsMap.Has(a) ? pathsMap[a] + 1 : 1 ; This actually shouldn't be needed, if AutomationId's are unique 601 | } 602 | try c := StrReplace(className := Element.CachedClassName, "`"", "```"") 603 | if c != "" { 604 | c := t ",CN:`"" c "`"" 605 | pathsMap[c] := pathsMap.Has(c) ? pathsMap[c] + 1 : 1 606 | } 607 | try n := StrReplace(name := Element.CachedName, "`"", "```"") 608 | if !this.PathIgnoreNames && n != "" { ; Consider Name last, because it can change (eg. window title) 609 | n := t ",N:`"" n "`"" 610 | pathsMap[n] := pathsMap.Has(n) ? pathsMap[n] + 1 : 1 611 | } 612 | if pathsMap[t] = 1 613 | return t 614 | if a != "" && !IsInteger(a) { 615 | return c != "" ? (pathsMap[a] <= pathsMap[c] ? a : c) : a 616 | } else if c != "" 617 | return c 618 | else if !this.PathIgnoreNames && n != "" && (pathsMap[n] < pathsMap[t]) 619 | return n 620 | return t 621 | } 622 | } --------------------------------------------------------------------------------