Jaafar Tribak
Well-known Member
- Joined
- Dec 5, 2002
- Messages
- 9,797
- Office Version
- 2016
- Platform
- Windows
Hi all,
This is an attempt to simulate the Windows built-in screen-reader narrator but, done with vba alone.
The code is supposed to read-back the type of object currently under the mouse pointer and, if applicable, to read the object caption\text\value as well.
The code lets you choose a voice\language from the ones installed in your machine via a userform.
File demo:
ObjectToSpeech.xls
1- API code (Standard Module)
2- UserForm Module:
Tested on excel 2010/2016 x32 and x64 (excel 2007 might give erroneous results when applied to shapes)
This is an attempt to simulate the Windows built-in screen-reader narrator but, done with vba alone.
The code is supposed to read-back the type of object currently under the mouse pointer and, if applicable, to read the object caption\text\value as well.
The code lets you choose a voice\language from the ones installed in your machine via a userform.
File demo:
ObjectToSpeech.xls
1- API code (Standard Module)
VBA Code:
Option Explicit
Private Type POINTAPI
x As Long
y As Long
End Type
Private Type GUID
Data1 As Long
Data2 As Integer
Data3 As Integer
Data4(0 To 7) As Byte
End Type
#If VBA7 Then
#If Win64 Then
Private Declare PtrSafe Function AccessibleObjectFromPoint Lib "oleacc" (ByVal arg1 As LongLong, ppacc As Any, pvarChild As Variant) As Long
Private Declare PtrSafe Sub CopyMemory Lib "kernel32" Alias "RtlMoveMemory" (Destination As Any, Source As Any, ByVal Length As LongPtr)
Private Declare PtrSafe Function Currency_GetCursorPos Lib "user32" Alias "GetCursorPos" (lpPoint As Currency) As Long
#Else
Private Declare PtrSafe Function AccessibleObjectFromPoint Lib "oleacc" (ByVal lX As Long, ByVal lY As Long, ppacc As IAccessible, pvarChild As Variant) As Long
#End If
Private Declare PtrSafe Function Pnt_GetCursorPos Lib "user32" Alias "GetCursorPos" (lpPoint As POINTAPI) As Long
Private Declare PtrSafe Function DispCallFunc Lib "oleAut32.dll" (ByVal pvInstance As LongPtr, ByVal offsetinVft As LongPtr, ByVal CallConv As Long, ByVal retTYP As Integer, ByVal paCNT As Long, ByRef paTypes As Integer, ByRef paValues As LongPtr, ByRef retVAR As Variant) As Long
Private Declare PtrSafe Function StringFromGUID2 Lib "ole32" (ByRef rguid As GUID, ByVal lpsz As LongPtr, ByVal cchMax As Long) As Long
Private Declare PtrSafe Function CLSIDFromString Lib "ole32" (ByVal OleStringCLSID As LongPtr, ByRef cGUID As GUID) As Long
Private Declare PtrSafe Function CoCreateInstance Lib "ole32" (ByRef rclsid As GUID, ByVal pUnkOuter As LongPtr, ByVal dwClsContext As Long, ByRef riid As GUID, ByRef ppv As LongPtr) As Long
Private Declare PtrSafe Function SysReAllocString Lib "oleAut32.dll" (ByVal pBSTR As LongPtr, Optional ByVal pszStrPtr As LongPtr) As Long
Private Declare PtrSafe Sub SetLastError Lib "kernel32" (ByVal dwErrCode As Long)
Private Declare PtrSafe Function SetTimer Lib "user32" (ByVal hwnd As LongPtr, ByVal nIDEvent As LongPtr, ByVal uElapse As Long, ByVal lpTimerFunc As LongPtr) As LongPtr
Private Declare PtrSafe Function KillTimer Lib "user32" (ByVal hwnd As LongPtr, ByVal nIDEvent As LongPtr) As Long
Private Declare PtrSafe Function GetForegroundWindow Lib "user32" () As LongPtr
Private pAutomation As LongPtr
#Else
Private Declare Function AccessibleObjectFromPoint Lib "oleacc" (ByVal lX As Long, ByVal lY As Long, ppacc As IAccessible, pvarChild As Variant) As Long
Private Declare Function Pnt_GetCursorPos Lib "user32" Alias "GetCursorPos" (lpPoint As POINTAPI) As Long
Private Declare Function DispCallFunc Lib "oleAut32.dll" (ByVal pvInstance As Long, ByVal offsetinVft As Long, ByVal CallConv As Long, ByVal retTYP As Integer, ByVal paCNT As Long, ByRef paTypes As Integer, ByRef paValues As Long, ByRef retVAR As Variant) As Long
Private Declare Function StringFromGUID2 Lib "ole32" (ByRef rguid As GUID, ByVal lpsz As Long, ByVal cchMax As Long) As Long
Private Declare Function CLSIDFromString Lib "ole32" (ByVal OleStringCLSID As Long, ByRef cGUID As GUID) As Long
Private Declare Function CoCreateInstance Lib "ole32" (ByRef rclsid As GUID, ByVal pUnkOuter As Long, ByVal dwClsContext As Long, ByRef riid As GUID, ByRef ppv As Long) As Long
Private Declare Function SysReAllocString Lib "oleAut32.dll" (ByVal pBSTR As Long, Optional ByVal pszStrPtr As Long) As Long
Private Declare Sub SetLastError Lib "kernel32" (ByVal dwErrCode As Long)
Private Declare Function SetTimer Lib "user32" (ByVal hwnd As Long, ByVal nIDEvent As Long, ByVal uElapse As Long, ByVal lpTimerFunc As Long) As Long
Private Declare Function KillTimer Lib "user32" (ByVal hwnd As Long, ByVal nIDEvent As Long) As Long
Private Declare Function GetForegroundWindow Lib "user32" () As Long
Private pAutomation As Long
#End If
Private oVoices As Object
Sub StartObjectToSpeech()
With Application
If Not IsError(.ExecuteExcel4Macro("DontShowAgain")) Then
GoTo Start
End If
UFVoices.Show vbModal
If .ExecuteExcel4Macro("Voice") = -1& Then
Exit Sub
End If
Start:
pAutomation = -1
Call KillTimer(.hwnd, 0)
Call SetTimer(.hwnd, 0, 0, AddressOf SpeakObjectUnderMouse)
End With
End Sub
Sub FinishObjectToSpeech()
If Not oVoices Is Nothing Then
Call TextToSpeech(vbNullString, 0, 0)
Set oVoices = Nothing
End If
Call KillTimer(Application.hwnd, 0)
End Sub
Private Sub TextToSpeech(ByVal Text As String, ByVal Rate As Long, ByVal Volume As Long)
Set oVoices = CreateObject("SAPI.SpVoice")
With oVoices
Set .Voice = .GetVoices.Item(Application.ExecuteExcel4Macro("Voice"))
.Rate = Rate
.Volume = Volume
.Speak Text, 1
End With
End Sub
Private Sub Refresh_DontShowAgain()
Application.ExecuteExcel4Macro "SET.NAME(""DontShowAgain"")"
End Sub
Private Sub SpeakObjectUnderMouse()
Const IID_CUIAUTOMATION = "{FF48DBA4-60EF-4201-AA87-54103EEF594E}"
Const IID_IUIAUTOMATION = "{30CBE57D-D9D0-452A-AB13-7AC5AC4825EE}"
Const CLSCTX_INPROC_SERVER = &H1
Const CC_STDCALL = 4&
#If Win64 Then
Const PTR_LEN = 8&
#Else
Const PTR_LEN = 4&
#End If
#If Win64 Then
Dim pElement As LongLong
Dim pCurrentName As LongLong
Dim pClassName As LongLong
Dim pHelpText As LongLong
Dim pLocalisedCtrlType As LongLong
#Else
Dim pElement As Long
Dim pCurrentName As Long
Dim pClassName As Long
Dim pHelpText As Long
Dim pLocalisedCtrlType As Long
#End If
Static sPrevCurrentName As String
Static sPrevAccName As String
Static sPrevTmpText As String
Dim sCurrentName As String
Dim sClassName As String
Dim sHelpText As String
Dim sLocalisedCtrlType As String
Dim sAccName As String
Dim sTmpText As String
Dim oIAcc As IAccessible, vKid As Variant
Dim iidCuiAuto As GUID, iidIuiAuto As GUID
Dim lRet As Long, vtblOffset As Long
Dim tCurPos_PNT As POINTAPI, tCurPos_Currency As Currency
Dim oObjUnderMouse As Object
On Error Resume Next
Application.EnableCancelKey = xlDisabled
If pAutomation = 0 Then
Call KillTimer(Application.hwnd, 0): Call Refresh_DontShowAgain: Exit Sub ' If error Cancel timer & Get Out !
End If
If pAutomation = 0 Xor pAutomation = -1 Then
lRet = CLSIDFromString(StrPtr(IID_CUIAUTOMATION), iidCuiAuto)
Call DispGUID(iidCuiAuto)
lRet = CLSIDFromString(StrPtr(IID_IUIAUTOMATION), iidIuiAuto)
Call DispGUID(iidIuiAuto)
lRet = CoCreateInstance(iidCuiAuto, 0, CLSCTX_INPROC_SERVER, iidIuiAuto, pAutomation)
If lRet <> 0 Then MsgBox "Automation failed.": Exit Sub
End If
Call Pnt_GetCursorPos(tCurPos_PNT)
Set oObjUnderMouse = Application.ActiveWindow.RangeFromPoint(tCurPos_PNT.x, tCurPos_PNT.y)
vtblOffset = 7 * PTR_LEN ' IUIAutomation::ElementFromPoint 28 * PTR_LEN
#If Win64 Then
Call Currency_GetCursorPos(tCurPos_Currency)
Call vtblCall(pAutomation, vtblOffset, vbLong, CC_STDCALL, tCurPos_Currency, VarPtr(pElement))
#Else
Call vtblCall(pAutomation, vtblOffset, vbLong, CC_STDCALL, tCurPos_PNT.x, tCurPos_PNT.y, VarPtr(pElement))
#End If
vtblOffset = 23 * PTR_LEN ' IUIAutomationElement::CurrentName
Call vtblCall(pElement, vtblOffset, vbLong, CC_STDCALL, VarPtr(pCurrentName))
vtblOffset = 30 * PTR_LEN ' IUIAutomationElement::CurrentClassName
Call vtblCall(pElement, vtblOffset, vbLong, CC_STDCALL, VarPtr(pClassName))
sClassName = GetStrFromPtrW(pClassName)
If TypeName(oObjUnderMouse) = "Nothing" Or InStr(sClassName, "Net") Then
vtblOffset = 31 * PTR_LEN ' IUIAutomationElement::CurrentHelpText
Call vtblCall(pElement, vtblOffset, vbLong, CC_STDCALL, VarPtr(pHelpText))
vtblOffset = 22 * PTR_LEN ' IUIAutomationElement::CurrentLocalisedControlType
Call vtblCall(pElement, vtblOffset, vbLong, CC_STDCALL, VarPtr(pLocalisedCtrlType))
sCurrentName = GetStrFromPtrW(pCurrentName)
sHelpText = GetStrFromPtrW(pHelpText)
sLocalisedCtrlType = GetStrFromPtrW(pLocalisedCtrlType)
#If Win64 Then
Dim Ptr As LongLong
Call CopyMemory(Ptr, tCurPos_PNT, LenB(tCurPos_PNT))
Call AccessibleObjectFromPoint(Ptr, oIAcc, vKid)
#Else
Call AccessibleObjectFromPoint(tCurPos_PNT.x, tCurPos_PNT.y, oIAcc, vKid)
#End If
sAccName = oIAcc.accName(0&)
If sAccName <> sPrevAccName Or sCurrentName <> sPrevCurrentName Then
If sAccName = sCurrentName Then
sTmpText = sAccName
Else
sTmpText = sAccName & ". " & sCurrentName
End If
If sLocalisedCtrlType = sHelpText Then
sTmpText = sLocalisedCtrlType & ". " & sTmpText
Else
sTmpText = sLocalisedCtrlType & ". " & sHelpText & ". " & sTmpText
End If
GoTo SpeakText
End If
Else
sTmpText = BuildStringDescriptionFromObjectWithinSheet(oObjUnderMouse)
If sTmpText <> sPrevTmpText Then
GoTo SpeakText
End If
End If
GoTo Xit
SpeakText:
Call TextToSpeech(sTmpText, 0, 100)
Xit:
If pElement Then
vtblOffset = 2 * PTR_LEN ' IUIAutomationElement::Release
Call vtblCall(pElement, vtblOffset, vbLong, CC_STDCALL)
End If
sPrevAccName = sAccName
sPrevCurrentName = sCurrentName
sPrevTmpText = sTmpText
End Sub
Private Function BuildStringDescriptionFromObjectWithinSheet(ByVal ObjUnderMouse As Object) As String
Dim sCell As String, sFormula As String, sValue As String, sContains As String
Dim sHyperLink As String, sFalse As String, sTrue As String, sAddress As String
Dim sMinus As String, sPlus As String, sTimes As String, sDividedBy As String
Dim sGreater As String, sLess As String, sNotEqual As String
Dim sGreaterOrEqual As String, sLessOrEqual As String
Dim sTmpText As String
On Error Resume Next
With oVoices
Select Case True
Case InStr(.Voice.GetDescription, "English")
sCell = "Cell": sFormula = "Formula": sValue = "Value": sContains = "Contains"
sHyperLink = "Hyperlink": sFalse = "False": sTrue = "True": sAddress = "address"
sMinus = "minus": sPlus = "plus": sTimes = "multiplied by": sDividedBy = "Divided. by"
sGreater = "greater than": sLess = "less than": sNotEqual = "not equal to"
sGreaterOrEqual = "greater Or equal to": sLessOrEqual = "less or equal to"
Case InStr(.Voice.GetDescription, "Spanish")
sCell = "Celda": sFormula = "Formula": sValue = "Valor": sContains = "Contiene"
sHyperLink = "Hiper Enlace": sFalse = "Falso": sTrue = "Verdadero": sAddress = "direccion"
'some rudimentary arithmetic operators translations into Spanish ..(+/*-<>...)... might need to be extended.
sMinus = "menos": sPlus = "mas": sTimes = "multiplicado por": sDividedBy = "dividido por"
sGreater = "mayor que": sLess = "menor que": sNotEqual = "distinto de"
sGreaterOrEqual = "mayor o igual que ": sLessOrEqual = "menor o igual que"
Case InStr(.Voice.GetDescription, "French")
sCell = "Cellule": sFormula = "Formule": sValue = "Valeur": sContains = "Contien"
sHyperLink = "Lien HyperTexte": sFalse = "Faux": sTrue = "Vrai": sAddress = "adresse"
'some rudimentary arithmetic operators translation into French ..(+/*-<>...)... might need to be extended.
sMinus = "moins": sPlus = "plus": sTimes = "multiplié par": sDividedBy = "divisé par"
sGreater = "superieur a": sLess = "inferieur a": sNotEqual = "different de"
sGreaterOrEqual = "supérieur ou égal à": sLessOrEqual = "inférieur ou égal à"
End Select
End With
If TypeName(ObjUnderMouse) = "Range" Then
If Not IsEmpty(ObjUnderMouse) Then
sTmpText = sCell & ObjUnderMouse.Address(False, False)
If ObjUnderMouse.HasFormula Then
sTmpText = sTmpText & ". " & sContains & sFormula & ". " & ObjUnderMouse.Formula
sTmpText = Replace(sTmpText, "-", sMinus)
sTmpText = Replace(sTmpText, "+", sPlus)
sTmpText = Replace(sTmpText, "*", sTimes)
sTmpText = Replace(sTmpText, "/", sDividedBy)
sTmpText = Replace(sTmpText, "<>", sNotEqual)
sTmpText = Replace(sTmpText, ">=", sGreaterOrEqual)
sTmpText = Replace(sTmpText, "<=", sLessOrEqual)
sTmpText = Replace(sTmpText, "<", sLess)
sTmpText = Replace(sTmpText, ">", sGreater)
End If
If HasHyperLink(ObjUnderMouse) Then
sTmpText = sTmpText & ". " & sHyperLink & ". " & sAddress & _
ObjUnderMouse.Hyperlinks(1).SubAddress & ObjUnderMouse.Hyperlinks(1).Address
End If
If HasHyperLink(ObjUnderMouse) = False Then
sTmpText = sTmpText & ". " & sValue & ". " & ObjUnderMouse.Value
sTmpText = Replace(sTmpText, False, sFalse)
sTmpText = Replace(sTmpText, True, sTrue)
End If
Else
TextToSpeech vbNullString, 0, 0
End If
End If
If TypeName(ObjUnderMouse) = "OLEObject" Then
sTmpText = ObjUnderMouse.Name
If ObjUnderMouse.Name <> ObjUnderMouse.Object.Caption Then
sTmpText = sTmpText & ". " & ObjUnderMouse.Object.Caption
End If
If InStr(TypeName(ObjUnderMouse.Object), "CommandButton") = 0 And _
InStr(TypeName(ObjUnderMouse.Object), "ToggleButton") = 0 Then
sTmpText = sTmpText & ". " & ObjUnderMouse.Object.Value
sTmpText = Replace(sTmpText, False, sFalse)
sTmpText = Replace(sTmpText, True, sTrue)
End If
ElseIf TypeName(ObjUnderMouse) <> "Range" Then
sTmpText = ObjUnderMouse.Name
If ObjUnderMouse.Name <> ObjUnderMouse.Caption Then
sTmpText = sTmpText & ". " & ObjUnderMouse.Caption
End If
If TypeName(ObjUnderMouse) = "CheckBox" Or TypeName(ObjUnderMouse) = "OptionButton" Then
If ObjUnderMouse = -4146 Then
sTmpText = sTmpText & ". " & sFalse
Else
sTmpText = sTmpText & ". " & sTrue
End If
End If
sTmpText = sTmpText & ". " & ObjUnderMouse.List(ObjUnderMouse.ListIndex)
End If
BuildStringDescriptionFromObjectWithinSheet = sTmpText
End Function
#If Win64 Then
Private Function vtblCall(ByVal InterfacePointer As LongLong, ByVal VTableOffset As Long, ByVal FunctionReturnType As Long, ByVal CallConvention As Long, ParamArray FunctionParameters() As Variant) As Variant
Dim vParamPtr() As LongLong
#Else
Private Function vtblCall(ByVal InterfacePointer As Long, ByVal VTableOffset As Long, ByVal FunctionReturnType As Long, ByVal CallConvention As Long, ParamArray FunctionParameters() As Variant) As Variant
Dim vParamPtr() As Long
#End If
Dim pIndex As Long, pCount As Long
Dim vParamType() As Integer
Dim vRtn As Variant, vParams() As Variant
vParams() = FunctionParameters()
pCount = Abs(UBound(vParams) - LBound(vParams) + 1&)
If pCount = 0& Then
ReDim vParamPtr(0 To 0)
ReDim vParamType(0 To 0)
Else
ReDim vParamPtr(0 To pCount - 1&)
ReDim vParamType(0 To pCount - 1&)
For pIndex = 0& To pCount - 1&
vParamPtr(pIndex) = VarPtr(vParams(pIndex))
vParamType(pIndex) = VarType(vParams(pIndex))
Next
End If
pIndex = DispCallFunc(InterfacePointer, VTableOffset, CallConvention, FunctionReturnType, pCount, vParamType(0), vParamPtr(0), vRtn)
If pIndex = 0& Then
vtblCall = vRtn
Else
Call SetLastError(pIndex)
End If
End Function
#If Win64 Then
Private Function GetStrFromPtrW(ByVal Ptr As LongLong) As String
#Else
Private Function GetStrFromPtrW(ByVal Ptr As Long) As String
#End If
Call SysReAllocString(VarPtr(GetStrFromPtrW), Ptr)
End Function
Private Sub DispGUID(objGuid As GUID)
Dim lRet As Long
Dim sTmp As String
Dim buf(100) As Byte
lRet = StringFromGUID2(objGuid, VarPtr(buf(0)), UBound(buf) - 1)
sTmp = buf
End Sub
Private Function HasHyperLink(ByVal oRng As Range) As Boolean
HasHyperLink = CBool(oRng.Hyperlinks.Count)
End Function
Private Sub Auto_Close()
Call KillTimer(Application.hwnd, 0)
End Sub
2- UserForm Module:
VBA Code:
Option Explicit
Private Sub UserForm_Initialize()
Dim oVoice As Object, i As Long
'Populate ListBox with installed voices.
Set oVoice = CreateObject("SAPI.SpVoice")
With oVoice
If .GetVoices.Count Then
For i = 0 To .GetVoices.Count - 1
Set .Voice = .GetVoices.Item(i)
Me.ListBox1.AddItem .Voice.GetDescription
Next i
Me.ListBox1.Selected(0) = True
Else
Call SetName("Voice", -1&)
MsgBox "No Voices Installed."
Unload Me
End If
End With
End Sub
Private Sub UserForm_QueryClose(Cancel As Integer, CloseMode As Integer)
On Error Resume Next
'UserForm Cancelled.
If Application.ExecuteExcel4Macro("Voice") = -1& Or CloseMode = 0 Then
Call SetName("Voice", -1&)
Exit Sub
End If
'Don't show Voices Form again.
If Me.CheckBox1.Value Then
Call SetName("DontShowAgain", -1&)
End If
End Sub
Private Sub CommandButton1_Click()
Call SetName("Voice", CLng(ListBox1.ListIndex))
Unload Me
End Sub
Private Sub CommandButton2_Click()
Call SetName("Voice", -1&)
Unload Me
End Sub
Private Sub ListBox1_DblClick(ByVal Cancel As MSForms.ReturnBoolean)
Call SetName("Voice", CLng(ListBox1.ListIndex))
Unload Me
End Sub
Private Sub SetName(ByVal sName As String, ByVal sValue As Long)
Application.ExecuteExcel4Macro "SET.NAME(" & Chr(34) & sName & Chr(34) & "," & sValue & ")"
End Sub
Tested on excel 2010/2016 x32 and x64 (excel 2007 might give erroneous results when applied to shapes)
Last edited: