Version 2 of some Accessibility code.

Post your working scripts, libraries and tools.
ludamo
Posts: 44
Joined: 25 Mar 2015, 02:21

Version 2 of some Accessibility code.

Post by ludamo » 16 Aug 2021, 00:13

Thank you to Lexikos for v2 and to jeeswg, Sean, jethrow & Sancarn for their v1 Accessibility code. While converting my v1 to v2 code I was unable to find examples of v2 Accessibility code. So after many days of trial and error, reading the error messages and the help file I have these working in version 2. I am hoping they may be helpful as a starter for others looking for the same thing. Please feel free to improve or point out errors.

Code: Select all

#Requires AutoHotkey v2.0-a

Acc_ObjectFromPoint(&idChild := "") {
	DllCall("GetCursorPos", "int64P", &pt64:=0)
	pvarChild := Buffer(8 + 2 * A_PtrSize)				; 24 
	if DllCall("oleacc\AccessibleObjectFromPoint", "int64",pt64, "ptr*",&ppAcc := 0, "ptr",pvarChild) = 0
	{	; returns a pointer from which we get a Com Object - see next function for a variation
		idChild:=NumGet(pvarChild,8,"UInt")
		Return ComValue(9, ppAcc)
	}
}

Acc_ObjectFromWindow(hWnd, idObject := -4) {
	IID := Buffer(16)
	if DllCall("oleacc\AccessibleObjectFromWindow", "ptr",hWnd, "uint",idObject &= 0xFFFFFFFF
			, "ptr",-16 + NumPut("int64", idObject == 0xFFFFFFF0 ? 0x46000000000000C0 : 0x719B3800AA000C81, NumPut("int64", idObject == 0xFFFFFFF0 ? 0x0000000000020400 : 0x11CF3C3D618736E0, IID))
			, "ptr*", ComObj := ComValue(9,0)) = 0
		; returns the Com Object directly - see previous function for a variation
		Return ComObj
}

Acc_ObjectFromPath(ChildPath, hWnd) {
	accObj := Acc_ObjectFromWindow(hWnd, 0)
	if ComObjType(accObj, "Name") != "IAccessible"
		MsgBox "Could not access an IAccessible Object"
	else
	{	
		ObjAddRef(ComObjValue(accObj))
		Loop Parse ChildPath, "."
		{
			if IsInteger(A_LoopField)
				Children:=Acc_Children(accObj), m2:=A_LoopField
			else
				MsgBox "Cannot access ChildPath Item"

			if not Children.Has(m2)
				MsgBox "No children found"
			else
				accObj := Children[m2]
		}
	}
	Return accObj
}

Acc_Children(Acc) {

	if ComObjType(Acc,"Name") != "IAccessible"
		MsgBox "Invalid IAccessible Object"
	else
	{
		cChildren := Acc.accChildCount, Children := Array()
		
		varChildren := Buffer(cChildren * (8+2*A_PtrSize))
		if DllCall("oleacc\AccessibleChildren", "ptr",ComObjValue(Acc), "int",0, "int",cChildren, "ptr",varChildren, "int*",cChildren) = 0
		{
			Loop cChildren {
				i := (A_Index-1) * (A_PtrSize * 2 + 8) + 8
				child := NumGet(varChildren, i, "ptr64")
				Children.Push(NumGet(varChildren, i-8, "ptr64") = 9 ? Acc_Query(child) : child)
				NumGet(varChildren, i-8, "ptr64") = 9 ? ObjRelease(child) : ""
			}
			Return Children.Length ? Children : ""
		}
		else
			MsgBox "AccessibleChildren DllCall Failed"
	}
}

Acc_Query(Acc) {
	Try Return ComValue(9, ComObjQuery(Acc, "{618736e0-3c3d-11cf-810c-00aa00389b71}"), 1)
}

Acc_Location(Acc, ChildId:=0, &Position:="") {
	x:=Buffer(4), y:=Buffer(4), w:=Buffer(4), h:=Buffer(4)
	Try Acc.accLocation(ComValue(0x4003, x.ptr, 1), ComValue(0x4003, y.ptr, 1), ComValue(0x4003, w.ptr, 1), ComValue(0x4003, h.ptr, 1), ChildId)
	Catch
		Return
	Position := "x" NumGet(x,0,"int") " y" NumGet(y,0,"int") " w" NumGet(w,0,"int") " h" NumGet(h,0,"int")
	Return	{x:NumGet(x,0,"int"), y:NumGet(y,0,"int"), w:NumGet(w,0,"int"), h:NumGet(h,0,"int")}
}

User avatar
JoeSchmoe
Posts: 129
Joined: 08 Dec 2014, 08:58

Re: Version 2 of some Accessibility code.

Post by JoeSchmoe » 17 Aug 2021, 17:09

Hey Ludamo, thanks for this. Could it be used to identify the URL from Chrome?

ludamo
Posts: 44
Joined: 25 Mar 2015, 02:21

Re: Version 2 of some Accessibility code.

Post by ludamo » 18 Aug 2021, 22:54

Well I don't have Chrome, I use Firefox instead. But what I do is to use the version 1 Accessible Info Viewer (https://autohotkey.com/boards/viewtopic.php?f=7&t=40590&p=215803&hilit=accessibility#p215803) which I have compiled into an executable with version 1 (e.g. AutoHotkeyU64.exe, so I can run it separately even though I run v2 as my main instance) to find the path of the URL control, which in Firefox v91.0.1 is "4.19.16.1". Then run for example the following code.

Code: Select all

#Requires AutoHotkey v2.0-a

h := DllCall("LoadLibrary","Str","oleacc.dll","ptr")

#HotIf WinActive("Mozilla Firefox ahk_class MozillaWindowClass")

q:: {
	global h
	hWnd := WinGetID("A")
	g_oAcc := Acc_ObjectFromPath("4.19.16.1", hWnd)
	Sleep 50
	Try _AccName := g_oAcc.accName(0)						; ChildId = 0 
	Sleep 50
	; ToolTip  _AccName 
	If IsSet(_AccName)
		MsgBox _AccName "`n" g_oAcc.accValue(0)
	g_oAcc := ""
	if IsSet(_AccName)
		_AccName := ""
}

Acc_ObjectFromPath(ChildPath, hWnd) {
	accObj := Acc_ObjectFromWindow(hWnd, 0)
	if ComObjType(accObj, "Name") != "IAccessible"
		MsgBox "Could not access an IAccessible Object"
	else
	{	
		ObjAddRef(ComObjValue(accObj))
		Loop Parse ChildPath, "."
		{
			if IsInteger(A_LoopField)
				Children:=Acc_Children(accObj), m2:=A_LoopField
			else
				MsgBox "Cannot access ChildPath Item"

			if not Children.Has(m2)
				MsgBox "No children found"
			else
			{	accObj := Children[m2]
				; MsgBox ComObjType(accObj, "Name") . " | " . m2
			
			}
		}
	}
	Return accObj
}

Acc_ObjectFromWindow(hWnd, idObject := -4) {
	IID := Buffer(16)
	if DllCall("oleacc\AccessibleObjectFromWindow", "ptr",hWnd, "uint",idObject &= 0xFFFFFFFF
			, "ptr",-16 + NumPut("int64", idObject == 0xFFFFFFF0 ? 0x46000000000000C0 : 0x719B3800AA000C81, NumPut("int64", idObject == 0xFFFFFFF0 ? 0x0000000000020400 : 0x11CF3C3D618736E0, IID))
			, "ptr*", ComObj := ComValue(9,0)) = 0
		Return ComObj
}

Acc_Children(Acc) {

	if ComObjType(Acc,"Name") != "IAccessible"
		MsgBox "Invalid IAccessible Object"
	else
	{
		cChildren := Acc.accChildCount, Children := Array()
		
		varChildren := Buffer(cChildren * (8+2*A_PtrSize))
		if DllCall("oleacc\AccessibleChildren", "ptr",ComObjValue(Acc), "int",0, "int",cChildren, "ptr",varChildren, "int*",cChildren) = 0
		{
			Loop cChildren {
				i := (A_Index-1) * (A_PtrSize * 2 + 8) + 8
				child := NumGet(varChildren, i, "ptr64")
				Children.Push(NumGet(varChildren, i-8, "ptr64") = 9 ? Acc_Query(child) : child)
				
				; Try MsgBox Children.Length . " | " . A_Index . " | " . Children[A_Index].AccName
				
				NumGet(varChildren, i-8, "ptr64") = 9 ? ObjRelease(child) : ""
			}
			Return Children.Length ? Children : ""
		}
		else
			MsgBox "AccessibleChildren DllCall Failed"
	}
	
}

Acc_Query(Acc) { ; thanks Lexikos - www.autohotkey.com/forum/viewtopic.php?t=81731&p=509530#509530
	Try Return ComValue(9, ComObjQuery(Acc, "{618736e0-3c3d-11cf-810c-00aa00389b71}"), 1)
}

neogna2
Posts: 586
Joined: 15 Sep 2016, 15:44

Re: Version 2 of some Accessibility code.

Post by neogna2 » 19 Aug 2021, 12:09

ludamo wrote:
16 Aug 2021, 00:13
v2 Accessibility code
I got your code working with numerical Acc paths like 4.2.1.3 . That is quite useful, thank you.

Sometimes string based Acc paths are easy to work with so having that feature from AHK 1.1 Acc libraries would also be nice. Here are some notes on that. I think the parts needed from AHK v1.1. Acc.ahk are Acc_ChildrenByRole(), Acc_Role() and Acc_GetRoleText() . I tried converting them but the code is not yet working. There are probably several bugs here but one appears to be that the DllCall to GetRoleText never receives any text. Perhaps someone else can spot and correct the errors?

Code: Select all

;ludamo's function but with two lines added
Acc_ObjectFromPath(ChildPath, hWnd) {
	accObj := Acc_ObjectFromWindow(hWnd, 0)
	if ComObjType(accObj, "Name") != "IAccessible"
		MsgBox "Could not access an IAccessible Object"
	else
	{
		ObjAddRef(ComObjValue(accObj))
		Loop Parse ChildPath, "."
		{
			if IsInteger(A_LoopField)
				Children:=Acc_Children(accObj), m2:=A_LoopField
			;these two lines are added
			else if RegExMatch(A_LoopField, "(\D*)(\d*)", &m)
				Children:=Acc_ChildrenByRole(AccObj, m[1]), m2:=(m[2]?m[2]:1)
			else
				MsgBox "Cannot access ChildPath Item"

			if not Children.Has(m2)
				MsgBox "No children found"
			else
				accObj := Children[m2]
		}
	}
	Return accObj
}

;attempted ports of https://github.com/Ixiko/AHK-libs-and-classes-collection/blob/master/libs/a-f/ACC.ahk

;Cf https://github.com/Ixiko/AHK-libs-and-classes-collection/blob/master/libs/a-f/ACC.ahk#L116
Acc_ChildrenByRole(Acc, Role) {
	if ComObjType(Acc,"Name") != "IAccessible"
		MsgBox "Invalid IAccessible Object"
	else {
		cChildren:=Acc.accChildCount, Children := Array()
		varChildren := Buffer(cChildren * (8+2*A_PtrSize))
		if DllCall("oleacc\AccessibleChildren", "ptr",ComObjValue(Acc), "int",0, "int",cChildren, "ptr",varChildren, "int*",cChildren) = 0
		{
			Loop cChildren {
				i := (A_Index-1) * (A_PtrSize * 2 + 8) + 8
				child := NumGet(varChildren, i, "ptr64")
				if NumGet(varChildren, i-8, "ptr64") = 9
					AccChild := Acc_Query(child), ObjRelease(child), Acc_Role(AccChild) = Role ? Children.Push(AccChild) : ""
				else
					Acc_Role(Acc, child) = Role ? Children.Push(child) : ""
			}
			return Children.Length ? Children : ""
		} 
		else
			MsgBox "AccessibleChildren DllCall Failed"
	}
}

;Cf https://github.com/Ixiko/AHK-libs-and-classes-collection/blob/master/libs/a-f/ACC.ahk#L68
Acc_Role(Acc, ChildId := 0) {
	try return ComObjType(Acc,"Name")="IAccessible" ? Acc_GetRoleText(Acc.accRole(ChildId)) : "invalid object"
}

;Cf https://github.com/Ixiko/AHK-libs-and-classes-collection/blob/master/libs/a-f/ACC.ahk#L37
Acc_GetRoleText(nRole)
{
	;https://docs.microsoft.com/en-us/windows/win32/api/oleacc/nf-oleacc-getroletextw
	;"Retrieves the localized string that describes the object's role for the specified role value."
	;"If lpszRole is NULL, the return value represents the string's length, not including the null character"
	;I think string length unit is in UTF16 characters (2 bytes)?
	;"UINT GetRoleTextW(DWORD  lRole, LPWSTR lpszRole, UINT   cchRoleMax)"
	;LPWSTR lpszRole: "Address of a buffer that receives the role text string."
	;compare https://lexikos.github.io/v2/docs/objects/Buffer.htm#ExString
	;	"#1: Use a Buffer to receive a string from an external function via DllCall."
	;	bufW := Buffer(max_chars*2)
	;	;Print a UTF-16 string into the buffer with wsprintfW().
	;	DllCall("wsprintfW", "Ptr", bufW, "Str", "0x%08x", "UInt", 4919, "CDecl")
	nSize := DllCall("oleacc\GetRoleText", "Uint", nRole, "Ptr", 0, "Uint", 0)
	sRole := Buffer(nSize*2, 0)
	DllCall("oleacc\GetRoleText", "Uint", nRole, "Ptr", sRole, "Uint", nSize+1)
	Return sRole
}

ludamo
Posts: 44
Joined: 25 Mar 2015, 02:21

Re: Version 2 of some Accessibility code.

Post by ludamo » 19 Aug 2021, 19:59

Thanks neogna2 for interacting. I think the last line in your code should be

Code: Select all

Return StrGet(sRole)
sRole is the pointer to the string, nRole is an Integer key from 0x1 = "titlebar" to 0x40 = "outlinebutton" which indicates which Role the control is playing (search for accv2.ahk by Sancarn on GitHub for the full list). I tested this and it works.

Your Acc_ChildrenByRole(Acc, Role) looks correct but I am still working out how to test it.

ludamo
Posts: 44
Joined: 25 Mar 2015, 02:21

Re: Version 2 of some Accessibility code.

Post by ludamo » 19 Aug 2021, 20:12

Looking at it a bit more, Acc_ChildrenByRole() seems to be a subsidiary function that is called from Acc_Get() in v1. My function Acc_ObjectFromPath() is a new smaller function adapted out of Acc_Get(). It seems that Acc_ChildrenByRole() is called when Acc_Get() is passed the command "Role" or "State" and the ChildPath is not a dot separated string of integers but perhaps a string representing the Role or State of the control. Might I suggest that you break out new functions Acc_ObjectFromRole() and Acc_ObjectFromState() to simplify matters? V2 seems to have done this with the inbuilt functions quite a bit e.g. the many Control... functions.

neogna2
Posts: 586
Joined: 15 Sep 2016, 15:44

Re: Version 2 of some Accessibility code.

Post by neogna2 » 20 Aug 2021, 05:05

ludamo wrote:
19 Aug 2021, 19:59

Code: Select all

Return StrGet(sRole)
Oops, thanks. With that fix plus a few more I got the ported code for Acc path role string segments working.
ludamo wrote:
19 Aug 2021, 20:12
Looking at it a bit more, Acc_ChildrenByRole() seems to be a subsidiary function that is called from Acc_Get() in v1. My function Acc_ObjectFromPath() is a new smaller function adapted out of Acc_Get(). It seems that Acc_ChildrenByRole() is called when Acc_Get() is passed the command "Role" or "State" and the ChildPath is not a dot separated string of integers but perhaps a string representing the Role or State of the control. Might I suggest that you break out new functions Acc_ObjectFromRole() and Acc_ObjectFromState() to simplify matters? V2 seems to have done this with the inbuilt functions quite a bit e.g. the many Control... functions.
Counterpoint: In v1 Acc.ahk accepts mixed paths where each path segment can be integer or role string (with index suffix).
For example these all represent the URL editbox in my currently open Firefox window.
"4.15.6.1"
"application.tool_bar3.combo_box1.editable_text"
"4.tool_bar3.combo_box1.1"
Using role text for a segment is sometimes more robust. For example imagine we have path 4.13.12 to a control, but opening a side pane in the GUI changes the segment 13 to 14. In some such cases(*) the equivalent role (and its index) is constant e.g. tool_bar2 so we can more reliably use 4.tool_bar2.12 regardless of side pane state.

(* In other more tricky cases the role index changes too e.g. tool_bar2 to tool_bar3 which means we have to make the code loop over alternatives to determine the correct segment value given the current state of the GUI)

I think it is convenient to have one Acc_ObjectFromPath() function that accepts all such path variants as input.

Here's the fixed Acc code with an hotkey example to get the Firefox active tab URL. You may have to adjust the Acc paths to match your Firefox GUI settings.

Code: Select all

#Requires AutoHotkey v2.0-a

#HotIf WinActive("ahk_exe Firefox.exe")
t::
{
	h := DllCall("LoadLibrary","Str","oleacc.dll","ptr")
	AccPath := "application.tool_bar3.combo_box1.editable_text"
	;AccPath := "4.15.6.1"
	;AccPath := "4.tool_bar3.combo_box1.1"
	oAcc := Acc_ObjectFromPath(AccPath, WinExist("A"))
	try Url := oAcc.accValue(0)
	MsgBox(Url)
	oAcc := ""
}
#HotIf

Esc:: ExitApp

;ludamo's V2 Acc
Acc_ObjectFromPoint(&idChild := "") {
	DllCall("GetCursorPos", "int64P", &pt64:=0)
	pvarChild := Buffer(8 + 2 * A_PtrSize)				; 24 
	if DllCall("oleacc\AccessibleObjectFromPoint", "int64",pt64, "ptr*",&ppAcc := 0, "ptr",pvarChild) = 0
	{	; returns a pointer from which we get a Com Object - see next function for a variation
		idChild:=NumGet(pvarChild,8,"UInt")
		Return ComValue(9, ppAcc)
	}
}

Acc_ObjectFromWindow(hWnd, idObject := -4) {
	IID := Buffer(16)
	if DllCall("oleacc\AccessibleObjectFromWindow", "ptr",hWnd, "uint",idObject &= 0xFFFFFFFF
			, "ptr",-16 + NumPut("int64", idObject == 0xFFFFFFF0 ? 0x46000000000000C0 : 0x719B3800AA000C81, NumPut("int64", idObject == 0xFFFFFFF0 ? 0x0000000000020400 : 0x11CF3C3D618736E0, IID))
			, "ptr*", ComObj := ComValue(9,0)) = 0
		; returns the Com Object directly - see previous function for a variation
		Return ComObj
}

;modified Acc_ObjectFromPath at script end
/*
Acc_ObjectFromPath(ChildPath, hWnd) {
	accObj := Acc_ObjectFromWindow(hWnd, 0)
	if ComObjType(accObj, "Name") != "IAccessible"
		MsgBox "Could not access an IAccessible Object"
	else
	{	
		ObjAddRef(ComObjValue(accObj))
		Loop Parse ChildPath, "."
		{
			if IsInteger(A_LoopField)
				Children:=Acc_Children(accObj), m2:=A_LoopField
			else
				MsgBox "Cannot access ChildPath Item"

			if not Children.Has(m2)
				MsgBox "No children found"
			else
				accObj := Children[m2]
		}
	}
	Return accObj
}
*/

Acc_Children(Acc) {

	if ComObjType(Acc,"Name") != "IAccessible"
		MsgBox "Invalid IAccessible Object"
	else
	{
		cChildren := Acc.accChildCount, Children := Array()
		
		varChildren := Buffer(cChildren * (8+2*A_PtrSize))
		if DllCall("oleacc\AccessibleChildren", "ptr",ComObjValue(Acc), "int",0, "int",cChildren, "ptr",varChildren, "int*",cChildren) = 0
		{
			Loop cChildren {
				i := (A_Index-1) * (A_PtrSize * 2 + 8) + 8
				child := NumGet(varChildren, i, "ptr64")
				Children.Push(NumGet(varChildren, i-8, "ptr64") = 9 ? Acc_Query(child) : child)
				NumGet(varChildren, i-8, "ptr64") = 9 ? ObjRelease(child) : ""
			}
			Return Children.Length ? Children : ""
		}
		else
			MsgBox "AccessibleChildren DllCall Failed"
	}
}

Acc_Query(Acc) {
	Try Return ComValue(9, ComObjQuery(Acc, "{618736e0-3c3d-11cf-810c-00aa00389b71}"), 1)
}

Acc_Location(Acc, ChildId:=0, &Position:="") {
	x:=Buffer(4), y:=Buffer(4), w:=Buffer(4), h:=Buffer(4)
	Try Acc.accLocation(ComValue(0x4003, x.ptr, 1), ComValue(0x4003, y.ptr, 1), ComValue(0x4003, w.ptr, 1), ComValue(0x4003, h.ptr, 1), ChildId)
	Catch
		Return
	Position := "x" NumGet(x,0,"int") " y" NumGet(y,0,"int") " w" NumGet(w,0,"int") " h" NumGet(h,0,"int")
	Return	{x:NumGet(x,0,"int"), y:NumGet(y,0,"int"), w:NumGet(w,0,"int"), h:NumGet(h,0,"int")}
}


;ludamo's function but with modifications at ;###
Acc_ObjectFromPath(ChildPath, hWnd) {
	accObj := Acc_ObjectFromWindow(hWnd, 0)
	if ComObjType(accObj, "Name") != "IAccessible"
		MsgBox "Could not access an IAccessible Object"
	else
	{
		;###
		ChildPath := StrReplace(ChildPath, "_", " ")
		ObjAddRef(ComObjValue(accObj))
		Loop Parse ChildPath, "."
		{
			if IsInteger(A_LoopField)
				Children:=Acc_Children(accObj), m2:=A_LoopField
			;###
			else if RegExMatch(A_LoopField, "(\D*)(\d*)", &m)
				Children:=Acc_ChildrenByRole(AccObj, m[1]), m2:=(m[2]?m[2]:1)
			else
				MsgBox "Cannot access ChildPath Item"

			if not Children.Has(m2)
				MsgBox "No children found"
			else
				accObj := Children[m2]
		}
	}
	Return accObj
}

;attempted ports of https://github.com/Ixiko/AHK-libs-and-classes-collection/blob/master/libs/a-f/ACC.ahk
;to handle role strings in paths

;Cf https://github.com/Ixiko/AHK-libs-and-classes-collection/blob/master/libs/a-f/ACC.ahk#L116
Acc_ChildrenByRole(Acc, Role) {
	if ComObjType(Acc,"Name") != "IAccessible"
		MsgBox "Invalid IAccessible Object"
	else {
		cChildren:=Acc.accChildCount, Children := Array()
		varChildren := Buffer(cChildren * (8+2*A_PtrSize))
		if DllCall("oleacc\AccessibleChildren", "ptr",ComObjValue(Acc), "int",0, "int",cChildren, "ptr",varChildren, "int*",cChildren) = 0
		{
			Loop cChildren {
				i := (A_Index-1) * (A_PtrSize * 2 + 8) + 8
				child := NumGet(varChildren, i, "ptr64")
				if NumGet(varChildren, i-8, "ptr64") = 9
					AccChild := Acc_Query(child), ObjRelease(child), Acc_Role(AccChild) = Role ? Children.Push(AccChild) : ""
				else
					Acc_Role(Acc, child) = Role ? Children.Push(child) : ""
			}
			return Children.Length ? Children : ""
		} 
		else
			MsgBox "AccessibleChildren DllCall Failed"
	}
}

;Cf https://github.com/Ixiko/AHK-libs-and-classes-collection/blob/master/libs/a-f/ACC.ahk#L68
Acc_Role(Acc, ChildId := 0) {
	try return ComObjType(Acc,"Name")="IAccessible" ? Acc_GetRoleText(Acc.accRole(ChildId)) : "invalid object"
}

;Cf https://github.com/Ixiko/AHK-libs-and-classes-collection/blob/master/libs/a-f/ACC.ahk#L37
Acc_GetRoleText(nRole)
{
	;https://docs.microsoft.com/en-us/windows/win32/api/oleacc/nf-oleacc-getroletextw
	nSize := DllCall("oleacc\GetRoleText", "Uint", nRole, "Ptr", 0, "Uint", 0)
	sRole := Buffer(nSize*2, 0)
	DllCall("oleacc\GetRoleText", "Uint", nRole, "Ptr", sRole, "Uint", nSize+1)
	Return StrGet(sRole)
}
As for sticking with the even more general Acc_Get() versus breaking out separate functions like Acc_ObjectFromPath() I suppose there is a trade-off between keeping things as similar as possible to v1, which may ease things for users porting their v1 scripts to v2, versus improving the Acc library.

As a side note v1 Acc uses the convention of underscores instead of spaces in role strings in Acc paths and I retained that in the ported code. The underscores are later converted to spaces in ChildPath := StrReplace(ChildPath, "_", " ") before comparison with the string from GetRoleText. But I don't know if there was/is a good reason for that underscore convention in the first place. Is it only for making the path more readable or is there something else to it, I wonder.

ludamo
Posts: 44
Joined: 25 Mar 2015, 02:21

Re: Version 2 of some Accessibility code.

Post by ludamo » 20 Aug 2021, 23:48

Thanks neogna2 for all that work and information. I appreciate learning something new about the text-role way of submitting the path. It should help to simplify and make more robust some scripts where the numbers can vary, as you say. I was trying to modify a script of my own where I have a snap-to-default-button in Firefox but unfortunately I couldn't get it to work and also my script sometimes eventually crashed without any final error message.
So I copied and pasted your script for Firefox and tried it on its own. It works the first 3 times with the same Firefox tab but then crashes and if I change the Firefox tab the script quietly crashes after working one time. Ughhh.

ludamo
Posts: 44
Joined: 25 Mar 2015, 02:21

Re: Version 2 of some Accessibility code.

Post by ludamo » 21 Aug 2021, 04:03

After working with this a bit more I have found that altering the Acc_GetRoleText(nRole) function slightly by enlarging the buffer as shown has made the script more reliable with no crashes so far. nSize is the length of the string (in letters) without the terminal null character so we have to add 1 for it and then for Unicode we multiply x 2. The last parameter nSize+1 in the 2nd DllCall remains the same as I think the function compensates depending on whether the Ansi or W version is called.

Code: Select all

Acc_GetRoleText(nRole)
{
	;https://docs.microsoft.com/en-us/windows/win32/api/oleacc/nf-oleacc-getroletextw
	nSize := DllCall("oleacc\GetRoleText", "Uint", nRole, "Ptr", 0, "Uint", 0)
	sRole := Buffer(nSize*2 +2, 0)										; altered line
	DllCall("oleacc\GetRoleText", "Uint", nRole, "Ptr", sRole, "Uint", nSize+1)	; altered line
	Return StrGet(sRole)
}

neogna2
Posts: 586
Joined: 15 Sep 2016, 15:44

Re: Version 2 of some Accessibility code.

Post by neogna2 » 21 Aug 2021, 16:42

ludamo wrote:
21 Aug 2021, 04:03
After working with this a bit more I have found that altering the Acc_GetRoleText(nRole) function slightly by enlarging the buffer as shown has made the script more reliable with no crashes so far. nSize is the length of the string (in letters) without the terminal null character so we have to add 1 for it and then for Unicode we multiply x 2.
I think you're right.

This is not the first time a null-termination issue trips me up, and probably not the last. Let me try to make the problem explicit for myself, to trip on it less. I appreciate if whoever reads this points out any mistakes.

I will use NUL as abbreviation for the 2-byte UTF-16 null-termination character.

GetRoleTextW expects as second argument "Type: LPTSTR Address of a buffer that receives the role text string."
LPTSTR is "An LPWSTR if UNICODE is defined" and
LPWSTR is "A pointer to a null-terminated string of 16-bit Unicode characters."

In v1 Acc.ahk we have

Code: Select all

Acc_GetRoleText(nRole)
{
	nSize := DllCall("oleacc\GetRoleText", "Uint", nRole, "Ptr", 0, "Uint", 0)
	VarSetCapacity(sRole, (A_IsUnicode?2:1)*nSize)
	DllCall("oleacc\GetRoleText", "Uint", nRole, "str", sRole, "Uint", nSize+1)
	Return	sRole
}
That works because in v1 U32/U64 VarSetCapacity automatically adds NUL, so there is no need to manually increase sRole size with +2
Specify for RequestedCapacity the number of bytes that the variable should be able to hold after the adjustment. For Unicode strings, this should be the length times two. RequestedCapacity does not include the internal zero terminator. For example, specifying 1 would allow the variable to hold up to one byte in addition to its internal terminator.

In contrast in v2 Buffer allocates only the number of bytes we specify, without automatically adding NUL. So to allocate enough memory for an UTF-16 string and NUL we must manually increase sRole size with +2 bytes. Like you showed.
ludamo wrote:
21 Aug 2021, 04:03

Code: Select all

Acc_GetRoleText(nRole)
{
	;https://docs.microsoft.com/en-us/windows/win32/api/oleacc/nf-oleacc-getroletextw
	nSize := DllCall("oleacc\GetRoleText", "Uint", nRole, "Ptr", 0, "Uint", 0)
	sRole := Buffer(nSize*2 +2, 0)										; altered line
	DllCall("oleacc\GetRoleText", "Uint", nRole, "Ptr", sRole, "Uint", nSize+1)	; altered line
	Return StrGet(sRole)
}
Even without adding the +2 bytes my earlier v2 attempt at Acc_GetRoleText() did return the role string and did work without issue sometimes. But the lurking problem is that the DllCall to GetRoleText wrote the string, which filled up all memory allocated by Buffer, and then continued to write NUL. If the 2 bytes of memory (over)written with NUL were already in use for something else the script could crash.


It is worth to also mention that in v2 there is an alternative to Buffer when dealing with UTF-16 strings: VarSetStrCapacity, which does automatically add NUL.
Specify for RequestedCapacity the number of characters that the variable should be able to hold after the adjustment. RequestedCapacity does not include the internal zero terminator. For example, specifying 1 would allow the variable to hold up to one character in addition to its internal terminator.
(Note a difference: v1 VarSetCapacity's size unit is bytes, v2 VarSetStrCapacity's size unit is (2-byte UTF-16) characters)

So we could if we wanted alternatively do

Code: Select all

Acc_GetRoleText(nRole)
{
	nSize := DllCall("oleacc\GetRoleText", "Uint", nRole, "Ptr", 0, "Uint", 0)
	VarSetStrCapacity(&sRole, nSize)
	DllCall("oleacc\GetRoleText", "Uint", nRole, "Str", sRole, "Uint", nSize+1)
	Return sRole
}

swagfag
Posts: 6222
Joined: 11 Jan 2017, 17:59

Re: Version 2 of some Accessibility code.

Post by swagfag » 28 Aug 2021, 19:58

i havent come across a single online sample that actually retrieves the required size to heap allocate the exact amount of memory needed. instead they allocate 500/1000 wchars on the stack and use that
which mean u could probably get away with dropping 1 DllCall, caching the Buffer and keep reusing it(the function will always write a null terminator in for u)

neogna2
Posts: 586
Joined: 15 Sep 2016, 15:44

Re: Version 2 of some Accessibility code.

Post by neogna2 » 01 Sep 2021, 08:06

swagfag wrote:
28 Aug 2021, 19:58
i havent come across a single online sample that actually retrieves the required size to heap allocate the exact amount of memory needed. instead they allocate 500/1000 wchars on the stack and use that
which mean u could probably get away with dropping 1 DllCall, caching the Buffer and keep reusing it(the function will always write a null terminator in for u)
Not sure I understand. Do you mean that
A. Buffer() automatically adds a null-terminator. (Its documentation doesn't mention that.)
B. Buffer() with a very small ByteCount value will in practice anyway allocate a bigger chunk of memory which means that the DllCall GetRoleText call will always be able to fit the string and a null-termination character, which means that even if we don't add +2 in the Buffer() call there is no practical risk that GetRoleText will overwrite memory used for something else and cause a crash.
C. Something else

swagfag
Posts: 6222
Joined: 11 Jan 2017, 17:59

Re: Version 2 of some Accessibility code.

Post by swagfag » 01 Sep 2021, 10:35

i mean i didnt see any examples doing this:

Code: Select all

auto nSize{ GetRoleText(nRole, nullptr, 0) };
auto sRole = new wchar_t[nSize + 1];
GetRoleText(nRole, sRole, nSize + 1);
delete sRole;
but i saw many doing this:

Code: Select all

wchar_t sRole[500];
GetRoleText(nRole, sRole, 500);
meaning u could do this:

Code: Select all

Acc_GetRoleText(nRole) {
	static sRole := Buffer(500)
	DllCall("oleacc\GetRoleText", "Uint", nRole, "Ptr", sRole, "Uint", 500)
	Return StrGet(sRole)
}
and this would be fine*, since StrGet() will return everything up to the null-terminator and GetRoleText() will always write one in for u(apparently)
no, Buffer() doesnt add null-terminator. it leaves the memory intact(which may ultimately end up containing a null-terminator somewhere), or blanket sets it to the specified byte.
oh no, the risk is there and its as real as anything. it is also *proportional to the likelihood of microsoft introducing role constants exceeding 500 chars. so far the ones present average about 20.

neogna2
Posts: 586
Joined: 15 Sep 2016, 15:44

Re: Version 2 of some Accessibility code.

Post by neogna2 » 02 Sep 2021, 15:13

swagfag wrote:
01 Sep 2021, 10:35
u could do this:

Code: Select all

Acc_GetRoleText(nRole) {
	static sRole := Buffer(500)
	DllCall("oleacc\GetRoleText", "Uint", nRole, "Ptr", sRole, "Uint", 500)
	Return StrGet(sRole)
}
and this would be fine*, since StrGet() will return everything up to the null-terminator and GetRoleText() will always write one in for u(apparently)
Ok I got it now, thank you for the elaboration. That might yield a speed gain for scripts with a lot of Acc_GetRoleText calls? Like jeeswg's JEE_AccGetTextAll().

swagfag
Posts: 6222
Joined: 11 Jan 2017, 17:59

Re: Version 2 of some Accessibility code.

Post by swagfag » 02 Sep 2021, 16:54

possibly, idk, ud have to benchmark. the differences are probably more apparent in native code

eugenesv
Posts: 171
Joined: 21 Dec 2015, 10:11

Re: Version 2 of some Accessibility code.

Post by eugenesv » 08 Dec 2021, 06:04

Here is another attempt at Acc.ahk conversion to v2 that I've done while learning how to click buttons in a SaveAs dialog, it's a bit more complete vs. the originally posted (e.g. including the super useful but rather complicated Acc_Get that I needed)
But more importantly, I've separated most of the inlining as it makes it impossible to read and reason about the code (and harder to catch v1→v2 conversion errors), and also moved some of the values to a separate COM variable file

Acc.ahk file

Code: Select all

/* Acc.ahk Standard Library
  by Sean
  Updated by jethrow:
    Modified ComObjEnwrap params from (9,pacc) --> (9,pacc,1)
    Changed ComObjUnwrap to ComObjValue in order to avoid AddRef (thanks fincs)
    Added Acc_GetRoleText & Acc_GetStateText
    Added additional functions - commented below
    Removed original Acc_Children function
  v1 last updated 2/19/2012
  Converted to v2 by eugenesv 2021-12-03, last updated 2022-02-22:
    converter helpers:
      github.com/mmikeww/AHK-v2-script-converter
      github.com/FuPeiJiang/ahk_parser.js
    some notable changes:
    Removed a lot of inlining as it made the code unreadable
    ComObjEnwrap(9,pacc,1) → ComObjFromPtr(pacc)
    Added from another other Acc version @ github.com/Drugoy/Autohotkey-scripts-.ahk/blob/master/Libraries/Acc.ahk
      + Acc_ChildrenByRole
      + Acc_Get
      + Acc_SetWinEventHook
      + Acc_UnhookWinEvent
    Fixed a bunch of critical, but silent errors
  ; Used in a working example, should be ok? :)
    Acc_GetRoleText
    Acc_Role
    Acc_ChildrenByRole
    Acc_Children
    Acc_ObjectFromWindow
    Acc_Get
    Acc_Query

    Acc_Error
    Acc_Init
  ; not used
    Acc_ObjectFromEvent
    Acc_ObjectFromPoint
    Acc_WindowFromObject
    Acc_GetStateText
    Acc_SetWinEventHook
    Acc_UnhookWinEvent
    Acc_State
    Acc_Location
    Acc_Parent
    Acc_Child

*/
#Include AccVarCOM.ahk

Acc_Init(){
  static h	:= 0
  If Not h {
    h := DllCall("LoadLibrary", "Str","oleacc", "Ptr")
  }
}
Acc_ObjectFromEvent(&_idChild_, hWnd, idObject, idChild){
  Acc_Init()
  vChild := Buffer(8+2*A_PtrSize, 0)
  If DllCall("oleacc\AccessibleObjectFromEvent"
    , "Ptr" 	, hWnd      	;   [in]  HWND        hwnd
    , "UInt"	, idObject  	;   [in]  DWORD       dwId
    , "UInt"	, idChild   	;   [in]  DWORD       dwChildId
    , "Ptr*"	, &pacc:=0  	;   [out] IAccessible **ppacc
    , "Ptr" 	, vChild.Ptr	;   [out] VARIANT     *pvarChild
    )=0
  _idChild_ := NumGet(vChild, 8, "UInt")
  return ComObjFromPtr(pacc)
}

Acc_ObjectFromPoint(&_idChild_:="", x:="",y:=""){
  Acc_Init()
  DllCall("GetCursorPos", "Int64*",&pt:=0)
  if not (x==""||y=="")
    pt := x&0xFFFFFFFF|y<<32
  vChild	:= Buffer(8+2*A_PtrSize, 0)
  accObj := DllCall("oleacc\AccessibleObjectFromPoint"
    , "Int64"	, pt
    , "Ptr*" 	, &pacc:=0
    , "Ptr"  	, vChild.Ptr)
  if (accObj = 0) {
    _idChild_ := NumGet(vChild, 8, "UInt")
    Return ComObjFromPtr(pacc)
  }
}

Acc_ObjectFromWindow(hWnd, idObject:=-4){ ; OBJID_WINDOW:=0, OBJID_CLIENT:=-4
  Acc_Init()
  capIID     	:= 16
  bIID       	:= Buffer(capIID)
  idObject   	&= 0xFFFFFFFF
  numberA    	:= idObject==0xFFFFFFF0 ? 0x0000000000020400 : 0x11CF3C3D618736E0
  numberB    	:= idObject==0xFFFFFFF0 ? 0x46000000000000C0 : 0x719B3800AA000C81
  addrPostIID	:= NumPut("Int64",numberA, bIID)
  addrPPIID  	:= NumPut("Int64",numberB, addrPostIID)
  gotObject  	:= DllCall("oleacc\AccessibleObjectFromWindow"
    , "Ptr"  	, hWnd
    , "UInt" 	, idObject
    , "Ptr"  	, -capIID + addrPPIID
    , "Ptr*" 	, &pacc:=0
    )
  if (gotObject = 0) {
    return ComObjFromPtr(pacc)
  }
}

Acc_WindowFromObject(pacc){
  If DllCall("oleacc\WindowFromAccessibleObject"
    , "Ptr" 	, IsObject(pacc) ? ComObjValue(pacc) : pacc
    , "Ptr*"	, &hWnd:=0
    )=0
    return hWnd
}

Acc_GetRoleText(nRole){
	if !IsInteger(nRole) { ; autohotkey.com/boards/viewtopic.php?t=93790&view=unread#p438664
		return "Unknown object" ;;; bug in Acc_Role?, shouldn't Acc.accRole(ChildId) always return a number?
	}
  nSize := DllCall("oleacc\GetRoleText"
    , "Uint"	, nRole
    , "Ptr" 	, 0
    , "Uint"	, 0)
  VarSetStrCapacity(&sRole, 2*nSize)
  DllCall("oleacc\GetRoleText"
    , "Uint"	, nRole
    , "str" 	, sRole
    , "Uint"	, nSize+1)
  return sRole
}

Acc_GetStateText(nState){
  nSize := DllCall("oleacc\GetStateText"
    , "Uint"	, nState
    , "Ptr" 	, 0
    , "Uint"	, 0)
  VarSetStrCapacity(&sState, 2*nSize)
  DllCall("oleacc\GetStateText"
    , "Uint"	, nState
    , "str" 	, sState
    , "Uint"	, nSize+1)
  return sState
}
Acc_SetWinEventHook(eventMin, eventMax, pCallback) {
  Return  DllCall("SetWinEventHook", "Uint", eventMin, "Uint", eventMax, "Uint", 0, "Ptr", pCallback, "Uint", 0, "Uint", 0, "Uint", 0)
}
Acc_UnhookWinEvent(hHook) {
  Return  DllCall("UnhookWinEvent", "Ptr", hHook)
}
/* Win Events:
  pCallback := RegisterCallback("WinEventProc")
  WinEventProc(hHook, event, hWnd, idObject, idChild, eventThread, eventTime) {
    Critical
    Acc := Acc_ObjectFromEvent(_idChild_, hWnd, idObject, idChild)
    ; Code Here:
  }
*/

; Written by jethrow
Acc_Role(Acc, ChildId:=0) {
  try return ComObjType(Acc,"Name")="IAccessible"?Acc_GetRoleText(Acc.accRole(ChildId)):"invalid object"
}
Acc_State(Acc, ChildId:=0) {
  try return ComObjType(Acc,"Name")="IAccessible"?Acc_GetStateText(Acc.accState(ChildId)):"invalid object"
}
Acc_Error(p:="") {
  static setting:=0
  return p = "" ? setting : setting:=p
}
Acc_Children(Acc) { ;;; sometimes errors with 0xc0000005, reason unknown
  if (ComObjType(Acc,"Name") != "IAccessible") {
    NewError := Error("Invalid IAccessible Object" , -1)
  } else {
    Acc_Init()
    cChildren 	:= Acc.accChildCount
    retCountCh	:= 0
    if (cChildren=0) {
      return 0
    }
    Children   	:= []
    sizeVariant	:= (8+2*A_PtrSize) ; VARIANT=24:16
    vChildren  	:= Buffer(cChildren*sizeVariant, 0) ;;;VARIANT* pArray = new VARIANT[childCount];

    AccPtr := ComObjValue(Acc)	; (for DllCall) Pointer to the container object's IAccessible interface
    gotChildren := DllCall("oleacc\AccessibleChildren"
      , "Ptr" 	, AccPtr        	; IAccessible	*paccContainer
      , "Int" 	, 0             	; LONG       	 iChildStart
      , "Int" 	, cChildren     	; LONG       	 cChildren
      , "Ptr" 	, vChildren.Ptr 	; VARIANT    	*rgvarChildren
      , "Int*"	, &retCountCh:=0	; LONG       	*pcObtained
      )

    VARIANT_preUnion_Sz := 2+3*2 ; docs.microsoft.com/en-us/windows/win32/api/oaidl/ns-oaidl-variant
      ; typedef unsigned short VARTYPE
      ; 3×WORD    wReserved1,2,3;
    if (gotChildren = 0) {
      Loop retCountCh {
        i     	:= (A_Index-1)*sizeVariant + VARIANT_preUnion_Sz
        child 	:= NumGet(vChildren, i  , "Int64") ; llVal ?
        childX	:= NumGet(vChildren, i-8, "Int64") ; vt ?
        if (childX = 9) {
          AccChild := Acc_Query(child)
          Children.Push(AccChild)
          dbgRC := ObjRelease(child)
        } else {
          Children.Push(child)
        }
      }
      return Children.Length ? Children : 0
    } else {
      NewError := Error("AccessibleChildren DllCall Failed" , -1)
    }
  }
  NewError := Error("" , -1)
  if Acc_Error() {
    throw NewError
  }
  msgResult:=MsgBox("File:  " NewError.file "`nLine: " NewError.line "`n`nContinue Script?", , 262420)
  if (msgResult = "No") {
    ExitApp()
  }
}
Acc_ChildrenByRole(Acc, Role) {
  if (ComObjType(Acc,"Name") != "IAccessible") {
    NewError := Error("Invalid IAccessible Object" , -1)
  } else {
    Acc_Init()
    cChildren := Acc.accChildCount
    Children  := []
    vChildren := Buffer(cChildren*(8+2*A_PtrSize), 0)
    gotChildren := DllCall("oleacc\AccessibleChildren"
      , "Ptr" 	, ComObjValue(Acc)
      , "Int" 	, 0
      , "Int" 	, cChildren
      , "Ptr" 	, vChildren.Ptr
      , "Int*"	, &cChildren)
    if (gotChildren = 0) {
      Loop cChildren {
        i     	:= (A_Index-1)*(A_PtrSize*2+8)+8
        child 	:= NumGet(vChildren, i  , "Int64")
        childX	:= NumGet(vChildren, i-8, "Int64")
        if (childX = 9) {
          AccChild := Acc_Query(child), dbgRC := ObjRelease(child)
          if (Acc_Role(AccChild) = Role) {
            Children.Push(AccChild)
          }
        } else {
          if (Acc_Role(Acc,child) = Role) {
            Children.Push(child)
          }
        }
      }
      NewError := 0
      return Children.Length ? Children : 0
    } else {
      NewError := Error("AccessibleChildren DllCall Failed" , -1)
    }
  }
  if Acc_Error() {
    throw NewError ; Exception(ErrorLevel,-1)
  }
}
Acc_Get(Cmd, ChildPath:="", ChildID:=0, WinTitle:="", WinText:="", ExcludeTitle:="", ExcludeText:="") {
  global com
  static properties := Map()
    properties["Action"  ] := "DefaultAction"
    properties["DoAction"] := "DoDefaultAction"
    properties["Keyboard"] := "KeyboardShortcut"
  if IsObject(WinTitle) {
    AccObj := WinTitle
  } else {
    AccObj := Acc_ObjectFromWindow(WinExist(WinTitle,WinText,ExcludeTitle,ExcludeText), 0)
  }
  if (ComObjType(AccObj,"Name") != "IAccessible") {
    NewError := Error("Could not access an IAccessible Object" , -1)
  } else {
    ChildPath := StrReplace(ChildPath, "_", A_Space)
    AccError:=Acc_Error(), Acc_Error(true)
    Loop Parse, ChildPath, ".", A_Space {
      try {
        if isDigit(A_LoopField) {
          Children := Acc_Children(AccObj)
          m := [1, A_LoopField] ; mimic "m[2]" output in else-statement
        } else {
          RegExMatch(A_LoopField, "(\D*)(\d*)", &m)
          Children := Acc_ChildrenByRole(AccObj, m[1])
          m2 := (m[2] ? m[2] : 1)
        }
        if Not Children.Has(m2) {
          throw
        }
        AccObj := Children[m2]
      } catch {
        NewError := Error("Cannot access ChildPath Item #" A_Index " -> " A_LoopField , -1, "Item #" A_Index " -> " A_LoopField)
        Acc_Error(AccError)
        if Acc_Error() {
          throw NewError
        }
        return
      }
    }
    Acc_Error(AccError)
    Cmd := StrReplace(Cmd, A_Space)
    if properties.Has(Cmd) {
	    Cmd:=properties[Cmd]
	  } else {
      try {
        if        (Cmd = "Object") {
          ret_val := AccObj
        } else if (Cmd ~= "^(?i:Role|State|Location)$") {
          ret_val := Acc_%Cmd%(AccObj, ChildID+0)
        } else if (Cmd ~= "^(?i:ChildCount|Selection|Focus)$") {
          ret_val := AccObj["acc" Cmd]
        } else {
          ret_val := AccObj["acc" Cmd](ChildID+0)
        }
      } catch {
        NewError := Error("'" Cmd "' command NOT implemented", -1, Cmd)
        throw Error("'" Cmd "' command NOT implemented", -1, Cmd)
        ; if Acc_Error() {
        ;   throw NewError
        ; }
        ; return
      }
      NewError := 0
      return ret_val
	  }
  }
  if Acc_Error() {
    throw Error(NewError,-1)
  }
}
Acc_Location(Acc, ChildId:=0) { ; adapted from Sean's code
  global com
  xb:=Buffer(4), yb:=Buffer(4), wb:=Buffer(4), hb:=Buffer(4)
  try {
    Acc.accLocation(
      ComValue(com.pi32, xb.Ptr)
    , ComValue(com.pi32, yb.Ptr)
    , ComValue(com.pi32, wb.Ptr)
    , ComValue(com.pi32, hb.Ptr)
    , ChildId)
  } catch {
    return
  }
  retObj:= Object()
    retObj.x   := NumGet(xb, 0, "Int")
  , retObj.y   := NumGet(yb, 0, "Int")
  , retObj.w   := NumGet(wb, 0, "Int")
  , retObj.h   := NumGet(hb, 0, "Int")
    retObj.pos := "x" retObj.x " y" retObj.y " w" retObj.w " h" retObj.h
  return retObj
}
Acc_Parent(Acc) {
  try parent:=Acc.accParent
  return parent?Acc_Query(parent):0
}
Acc_Child(Acc, ChildId:=0) {
  try child:=Acc.accChild(ChildId)
  return child?Acc_Query(child):0
}
Acc_Query(Acc) {
  global IID
  try {
    pIAcc    	:= ComObjQuery(Acc, IID.IAccessible)
    retComObj	:= ComValue(com.DISPATCH, pIAcc, 1)
    return retComObj
  }
}
COM variables (required for the Acc.ahk to work)
AccVarCOM.ahk file

Code: Select all

#Requires AutoHotKey v2.0-beta.3

; AccVarCOM.ahk
; COM variables
global IID       	:= Object() ; Object to store COM interface IDs
  IID.IAccessible	:= "{618736e0-3c3d-11cf-810c-00aa00389b71}"

global com      	:= Object() ; Object to store COM variables
    com.none    	:=      0	; VT_EMPTY   	No value
  , com.null    	:=      1	; VT_NULL    	SQL-style Null
  , com.iarch   	:=   0x16	; VT_INT     	machine signed int
  , com.i8      	:=   0x10	; VT_I1      	 8-bit  signed int
  , com.i16     	:=      2	; VT_I2      	16-bit  signed int
  , com.i32     	:=      3	; VT_I4      	32-bit  signed int
  , com.i64     	:=   0x14	; VT_I8      	64-bit  signed int
  , com.uarch   	:=   0x17	; VT_UINT    	machine unsigned int
  , com.u8      	:=   0x11	; VT_UI1     	 8-bit  unsigned int
  , com.u16     	:=   0x12	; VT_UI2     	16-bit  unsigned int
  , com.u32     	:=   0x13	; VT_UI4     	32-bit  unsigned int
  , com.u64     	:=   0x15	; VT_UI8     	64-bit  unsigned int
  , com.f32     	:=      4	; VT_R4      	32-bit floating-point number
  , com.f64     	:=      5	; VT_R8      	64-bit floating-point number
  , com.bool    	:=    0xB	; VT_BOOL    	Boolean True (-1) or False (0)
  , com.str     	:=      8	; VT_BSTR    	COM string (Unicode string with length prefix)
  , com.cur     	:=      6	; VT_CY      	Currency
  , com.date    	:=      7	; VT_DATE    	Date
  , com.DISPATCH	:=      9	; VT_DISPATCH	COM object
  , com.err     	:=    0xA	; VT_ERROR   	Error code (32-bit integer)
  , com.var     	:=    0xC	; VT_VARIANT 	VARIANT (must be combined with VT_ARRAY or VT_BYREF)
  , com.un      	:=    0xD	; VT_UNKNOWN 	IUnknown interface pointer
  , com.arr     	:= 0x2000	; VT_ARRAY   	SAFEARRAY
  , com.byref   	:= 0x4000	; VT_BYREF   	Pointer to another type of value
  , com.dec     	:=    0xE	; VT_DECIMAL 	(not supported)
  , com.rec     	:=   0x24	; VT_RECORD  	User-defined type -- NOT SUPPORTED
  , com.pi32    	:= 0x4003	; byref|i32 Pointer to 32-bit signed int
  , com.pi64    	:= 0x4014	; byref|i64 Pointer to 64-bit signed int
  , com.arrvar  	:= 0x200C	; arr|var   Array variant
  , com.pvar    	:= 0x400C	; byref|var Pointer to Variant
A simplified example that identifies an element in a SaveAs Notepad++ dialog (both in the old v7 version and the newer v8 version)
AccTest_NotepadPP.ahk file

Code: Select all

#Requires AutoHotKey v2.0-beta.3

;AccTest_NotepadPP.ahk
#Include Acc.ahk
!v::Reload
!+v::NotepadPPDesktop()
SetTitleMatchMode("ReGex")	; match RegEx pattern in a title

NotepadPPDesktop() {
  nppName    	:= [ ; Array of arrays with pairs of ClassName, AccessibilityName
             	  ["SysTreeView321"   , "outline"]           	; treeview e.g. Notepad Win 7
             	, ["ToolbarWindow322" , "toolbar"]           	; toolbar  e.g. Notepad Win XP
             	, ["SysTreeView321"   , "LOCALIZED Acc NAME"]	; localized names
             	, ["ToolbarWindow322" , "LOCALIZED Acc NAME"]	;
             	]                                            	;
  btnClick   	:= "PC"                                      	; Element to find
  ControlID  	:= ""
  winID      	:= WinGetID("ahk_exe i)\\notepad\+\+\.exe$")
  winClass   	:= WinGetClass(      "ahk_id " winID)
  winProcName	:= WinGetProcessName("ahk_id " winID)
  if !(winClass = "#32770") OR !(winProcName = "notepad++.exe") {
    return
  }

  for NmPair in nppName {
    classN	:= NmPair[1]
    childP	:= NmPair[2]
    oAcc  	:= getControlNAcc(winID, classN, childP, &ControlID)
    if IsObject(oAcc) {
      break
    }
  }
  if not ControlID {
    return
  }
  if not IsObject(oAcc) {
    return
  }
  Loop oAcc.accChildCount {
    dbgPreLoop .= "@A_Index{" oAcc.accName(A_Index) "}`n"
    if (oAcc.accName(A_Index) = btnClick) {
      dbgPreLoop .= "MATCH!!!`n"
    }
  }
  ToolTip(dbgPreLoop,,,idTT:=2)
  SetTimer () => ToolTip(,,,idTT), -2000
}
getControlNAcc(winID, className, ChildPath, &ControlID){
  try ControlID := ControlGetHwnd(className,winID)
  catch as e {
    return
  } else {
    oAcc := Acc_Get("Object", ChildPath, ChildID:=0, "ahk_id " ControlID)
    return oAcc
  }
}
Last edited by eugenesv on 22 Feb 2022, 10:30, edited 5 times in total.

neogna2
Posts: 586
Joined: 15 Sep 2016, 15:44

Re: Version 2 of some Accessibility code.

Post by neogna2 » 08 Dec 2021, 08:48

eugenesv wrote:
08 Dec 2021, 06:04
Here is another attempt at Acc.ahk conversion to v2 that I've done
Great to see continued Acc work! I will try it more later.
For now I ran a small Acc_ObjectFromPoint test. This line DllCall("GetCursorPos", "Int64*",&pt) throws error Expected a Number but got an unset variable.
Fixed by changing to DllCall("GetCursorPos", "Int64*",&pt:=0)

Code: Select all

#Include "Acc.ahk"
;Acc_ObjectFromPoint test
;Open Explorer and hold mouse over a file
;Press F2 to show a MsgBox with the file's name
F2::
{
    Name := ""
    oAcc := Acc_ObjectFromPoint()
    Name := Acc_Parent(oAcc).accValue(0)
    Name := Name ? Name : oAcc.accValue(0)
    MsgBox Name
    ExitApp
}

eugenesv
Posts: 171
Joined: 21 Dec 2015, 10:11

Re: Version 2 of some Accessibility code.

Post by eugenesv » 08 Dec 2021, 09:17

Thanks, fixed it!
I removed those :=0 in places like this due to the fact that they silently :( bugged some other code where the variable wasn't unset, and as mentioned in the comments in the code, haven't done any tests to these, so there might be worse bugs than a warning!
  • Acc_ObjectFromEvent
  • Acc_ObjectFromPoint
  • Acc_WindowFromObject
  • Acc_GetStateText
  • Acc_SetWinEventHook
  • Acc_UnhookWinEvent
  • Acc_State
  • Acc_Location
  • Acc_Parent
  • Acc_Child
(by the way, not sure if you know, but this script is very helpful for the initial conversion from v1 to v2 https://github.com/mmikeww/AHK-v2-script-converter, it's even inserting those those :=0 in places such as these :) )

User avatar
kczx3
Posts: 1640
Joined: 06 Oct 2015, 21:39

Re: Version 2 of some Accessibility code.

Post by kczx3 » 08 Dec 2021, 15:20

@eugenesv Why would you use an Array for the COM variables (file required for the Acc.ahk to work) file? You're just setting properties on it... so use an object or a Map.

eugenesv
Posts: 171
Joined: 21 Dec 2015, 10:11

Re: Version 2 of some Accessibility code.

Post by eugenesv » 09 Dec 2021, 00:05

kczx3 wrote:
08 Dec 2021, 15:20
You're just setting properties on it... so use an object or a Map.
Map: I liked com.i8 more than com["i8"]
Object: no reason, is there a downside?

Post Reply

Return to “Scripts and Functions (v2)”