Welcome to MSDN Blogs Sign in | Join | Help

Automate Office Task Pane with Active Accessibility

The background

Almost every Office application provides a powerful object model(OM) to allow developers to programmatically access its functionalities. However, there are always things that are inaccessible from OM. And a lot of task panes fall into this category. For example, in Office XP and above, there is no direct way to clear the contents of the Office clipboard (Note, not the system clipboard, which is relatively easy to empty). The only way to do it is clicking the "Clear All" button on the Office Clipboard task pane. Fortunately Active Accessibility (MSAA) provides a way to do it from your program.
 
Then what is MSAA? I don't think I can explain it better than what you can find on MSDN. So please check out the the MSAA section if you want to know more about it. I would also highly recommend Dmitri Klementiev's article in MSDN magazine as a great start point to work with MSAA.

The idea

It is pretty straightforward:
  1. Find the Task pane window (or the Clipboard child window inside it)
  2. Use AccessibleObjectFromWindow API to retrieve its IAccessible interface
  3. Use AccessibleChildren API or methods provided by IAccessible to navigate through the Accessiblity tree and find the wanted UI element(in this case, it is the "Clear All" button in the Clipboard task pane).
  4. Interact with the found UI element using IAccessible interface

The code sample

I might find time later to split the code and explain it by parts. For now, it is all in one here:

Option Explicit

 

Const CHILDID_SELF = 0&

 

Const ROLE_TITLEBAR = &H1&

Const ROLE_MENUBAR = &H2&

Const ROLE_SCROLLBAR = &H3&

Const ROLE_GRIP = &H4&

Const ROLE_SOUND = &H5&

Const ROLE_CURSOR = &H6&

Const ROLE_CARET = &H7&

Const ROLE_ALERT = &H8&

Const ROLE_WINDOW = &H9&

Const ROLE_CLIENT = &HA&

Const ROLE_MENUPOPUP = &HB&

Const ROLE_MENUITEM = &HC&

Const ROLE_TOOLTIP = &HD&

Const ROLE_APPLICATION = &HE&

Const ROLE_DOCUMENT = &HF&

Const ROLE_PANE = &H10&

Const ROLE_CHART = &H11&

Const ROLE_DIALOG = &H12&

Const ROLE_BORDER = &H13&

Const ROLE_GROUPING = &H14&

Const ROLE_SEPARATOR = &H15&

Const ROLE_TOOLBAR = &H16&

Const ROLE_STATUSBAR = &H17&

Const ROLE_TABLE = &H18&

Const ROLE_COLUMNHEADER = &H19&

Const ROLE_ROWHEADER = &H1A&

Const ROLE_COLUMN = &H1B&

Const ROLE_ROW = &H1C&

Const ROLE_CELL = &H1D&

Const ROLE_LINK = &H1E&

Const ROLE_HELPBALLOON = &H1F&

Const ROLE_CHARACTER = &H20&

Const ROLE_LIST = &H21&

Const ROLE_LISTITEM = &H22&

Const ROLE_OUTLINE = &H23&

Const ROLE_OUTLINEITEM = &H24&

Const ROLE_PAGETAB = &H25&

Const ROLE_PROPERTYPAGE = &H26&

Const ROLE_INDICATOR = &H27&

Const ROLE_GRAPHIC = &H28&

Const ROLE_STATICTEXT = &H29&

Const ROLE_TEXT = &H2A&

Const ROLE_PUSHBUTTON = &H2B&

Const ROLE_CHECKBUTTON = &H2C&

Const ROLE_RADIOBUTTON = &H2D&

Const ROLE_COMBOBOX = &H2E&

Const ROLE_DROPLIST = &H2F&

Const ROLE_PROGRESSBAR = &H30&

Const ROLE_DIAL = &H31&

Const ROLE_HOTKEYFIELD = &H32&

Const ROLE_SLIDER = &H33&

Const ROLE_SPINBUTTON = &H34&

Const ROLE_DIAGRAM = &H35&

Const ROLE_ANIMATION = &H36&

Const ROLE_EQUATION = &H37&

Const ROLE_BUTTONDROPDOWN = &H38&

Const ROLE_BUTTONMENU = &H39&

Const ROLE_BUTTONDROPDOWNGRID = &H3A&

Const ROLE_WHITESPACE = &H3B&

Const ROLE_PAGETABLIST = &H3C&

Const ROLE_CLOCK = &H3D&

 

Type tGUID

    lData1            As Long

    nData2            As Integer

    nData3            As Integer

    abytData4(0 To 7) As Byte

End Type

 

Type AccObject

    objIA As IAccessible

    lngChild As Long

End Type

 

Const WM_GETTEXT = &HD

Public lngChild As Long, strClass As String, strCaption As String

 

Declare Function AccessibleObjectFromWindow Lib "oleacc" _

    (ByVal hWnd As Long, ByVal dwId As Long, _

    riid As tGUID, ppvObject As Object) As Long

 

Declare Function AccessibleChildren Lib "oleacc" _

    (ByVal paccContainer As IAccessible, ByVal iChildStart As Long, _

     ByVal cChildren As Long, rgvarChildren As Variant, _

     pcObtained As Long) As Long

 

Declare Function FindWindow Lib "user32" Alias "FindWindowA" ( _

    ByVal lpClassName As String, _

    ByVal lpWindowName As String) As Long

 

Declare Function GetParent Lib "user32" (ByVal hWnd As Long) As Long

Declare Function EnumChildWindows Lib "user32" (ByVal hwndParent _

    As Long, ByVal lpEnumFunc As Long, ByVal lParam As Long) As Long

Declare Function GetClassName Lib "user32" Alias "GetClassNameA" (ByVal hWnd As Long, _

   ByVal lpClassName As String, ByVal nMaxCount As Long) As Long

 

Declare Function SendMessage Lib "user32" Alias "SendMessageA" (ByVal hWnd As Long, _

   ByVal wMsg As Long, ByVal wParam As Long, ByVal lParam As String) As Long

Declare Function FindWindowEx Lib "user32" Alias "FindWindowExA" (ByVal hWnd1 As Long, _

   ByVal hWnd2 As Long, ByVal lpClass As String, ByVal lpCaption As String) As Long

 

'Retrieve window class name

Function GetWndClass(ByVal hWnd As Long) As String

    Dim buf As String, retval As Long

    buf = Space(256)

    retval = GetClassName(hWnd, buf, 255)

    GetWndClass = Left(buf, retval)

End Function

 

'Retrieve window title

Function GetWndText(ByVal hWnd As Long) As String

    Dim buf As String, retval As Long

    buf = Space(256)

    retval = SendMessage(hWnd, WM_GETTEXT, 255, buf)

    GetWndText = Left(buf, InStr(1, buf, Chr(0)) - 1)

End Function

 

'The call back function used by EnumChildWindows

Function EnumChildWndProc(ByVal hChild As Long, ByVal lParam As Long) As Long

    Dim found As Boolean

      

    EnumChildWndProc = -1

    If strClass > "" And strCaption > "" Then

        found = StrComp(GetWndClass(hChild), strClass, vbTextCompare) = 0 And _

            StrComp(GetWndText(hChild), strCaption, vbTextCompare) = 0

    ElseIf strClass > "" Then

        found = (StrComp(GetWndClass(hChild), strClass, vbTextCompare) = 0)

    ElseIf strCaption > "" Then

        found = (StrComp(GetWndText(hChild), strCaption, vbTextCompare) = 0)

    Else

        found = True

    End If

 

    If found Then

        lngChild = hChild

        EnumChildWndProc = 0

    Else

        EnumChildWndProc = -1

    End If

End Function

 

'Find the window handle of a child window based on its class and titie

Function FindChildWindow(ByVal hParent As Long, Optional cls As String = "", Optional title As String = "") As Long

    lngChild = 0

    strClass = cls

    strCaption = title

    EnumChildWindows hParent, AddressOf EnumChildWndProc, 0

    FindChildWindow = lngChild

End Function

 

'Retrieve the IAccessible interface from a window handle

'Reference:Jean Ross,Chapter 17: Accessibility in Visual Basic,Advanced Microsoft Visual Basic 6.0, 2nd Edition

Function IAccessibleFromHwnd(hWnd As Long) As IAccessible

    Dim oIA     As IAccessible

    Dim tg      As tGUID

    Dim lReturn As Long

   

    ' Define the GUID for the IAccessible object

    ' {618736E0-3C3D-11CF-810C-00AA00389B71}

    With tg

        .lData1 = &H618736E0

        .nData2 = &H3C3D

        .nData3 = &H11CF

        .abytData4(0) = &H81

        .abytData4(1) = &HC

        .abytData4(2) = &H0

        .abytData4(3) = &HAA

        .abytData4(4) = &H0

        .abytData4(5) = &H38

        .abytData4(6) = &H9B

        .abytData4(7) = &H71

    End With

   

    ' Retrieve the IAccessible object for the form

    lReturn = AccessibleObjectFromWindow(hWnd, 0, tg, oIA)

    Set IAccessibleFromHwnd = oIA

End Function

 

'Recursively looking for a child with specified accName and accRole in the accessibility tree

Function FindAccessibleChild(oParent As IAccessible, strName As String, lngRole As Long) As AccObject

   

    Dim lHowMany    As Long

    Dim avKids()    As Variant

    Dim lGotHowMany As Long, i As Integer

    Dim oChild As IAccessible

       

    FindAccessibleChild.lngChild = CHILDID_SELF

    If oParent.accChildCount = 0 Then

        Set FindAccessibleChild.objIA = Nothing

        Exit Function

    End If

   

    lHowMany = oParent.accChildCount

    ReDim avKids(lHowMany - 1) As Variant

    lGotHowMany = 0

 

    If AccessibleChildren(oParent, 0, lHowMany, avKids(0), lGotHowMany) <> 0 Then

        MsgBox "Error retrieving accessible children!"

        Set FindAccessibleChild.objIA = Nothing

        Exit Function

    End If

   

    'To do: the approach described in http://msdn.microsoft.com/msdnmag/issues/0400/aaccess/default.aspx

    ' are probably better and more reliable

    On Error Resume Next

    For i = 0 To lGotHowMany - 1

        If IsObject(avKids(i)) Then

            If StrComp(avKids(i).accName, strName) = 0 And avKids(i).accRole = lngRole Then

                Set FindAccessibleChild.objIA = avKids(i)

                Exit For

            Else

                Set oChild = avKids(i)

                FindAccessibleChild = FindAccessibleChild(oChild, strName, lngRole)

                If Not FindAccessibleChild.objIA Is Nothing Then

                    Exit For

                End If

            End If

        Else

            If StrComp(oParent.accName(avKids(i)), strName) = 0 And oParent.accRole(avKids(i)) = lngRole Then

                Set FindAccessibleChild.objIA = oParent

                FindAccessibleChild.lngChild = avKids(i)

                Exit For

            End If

        End If

    Next i

   

End Function

 

Function FindAccessibleChildInWindow(hwndParent As Long, strName As String, lngRole As Long) As AccObject

    Dim oParent As IAccessible

    Set oParent = IAccessibleFromHwnd(hwndParent)

    If oParent Is Nothing Then

        Set FindAccessibleChildInWindow.objIA = Nothing

    Else

        FindAccessibleChildInWindow = FindAccessibleChild(oParent, strName, lngRole)

    End If

End Function

 

'Using Active Accessibility to clear Office clipboard

'Input: app - the application object of an Office application

'Assumption: Clipboard task pane is shown in the Office application (app object)

Function ClearOfficeClipboard(app As Object) As Boolean

    Dim oButton As AccObject, fShow As Boolean

    'Get the IAccessible interface and child id (wrapped in the AccObject type)

    'Notice: the second parameter, accName "Clear All" is localized!

    'You can find out the accName and accRole using Spy, Inspect32 or other tools

    oButton = FindAccessibleChildInWindow(GetOfficeClipboardHwnd(app), "Clear All", ROLE_PUSHBUTTON)

    If oButton.objIA Is Nothing Then

        MsgBox "Unable to locate the ""Clear All"" button!"

        ClearOfficeClipboard = False

    Else

        oButton.objIA.accDoDefaultAction oButton.lngChild

        ClearOfficeClipboard = True

    End If

End Function

 

'Generic routine to retrieve the window handle of the active window of an Office Application

Function GetOfficeAppHwnd(app As Object) As Long

    GetOfficeAppHwnd = FindWindow(vbNullString, GetOfficeAppWindowTitle(app))

End Function

 

'Retrieve the window handle of the task pane

'Notice: the task pane window title is localized!

'You can find out the window class and title using Spy, Inspect32 or other tools

Function GetOfficeTaskPaneHwnd(app As Object) As Long

    GetOfficeTaskPaneHwnd = FindChildWindow(GetOfficeAppHwnd(app), _

        "MsoCommandBar", "Task Pane")

End Function

 

'Retrieve the window handle of the clipboard child window inside task pane

'The window title of the clipboard window seems to be language independent,

'making it a better start point to searching our UI element than the task pane window

Function GetOfficeClipboardHwnd(app As Object) As Long

    GetOfficeClipboardHwnd = FindChildWindow(GetOfficeAppHwnd(app), , "Collect and Paste 2.0")

End Function

 

'Generic routine to retrieve the window title of the active window of an Office application

Function GetOfficeAppWindowTitle(app As Object) As String

    On Error GoTo ErrorHandler

    Select Case app.Name

    Case "Microsoft Word"

        GetOfficeAppWindowTitle = app.ActiveWindow.Caption & " - " & app.Name

    Case Else

        GetOfficeAppWindowTitle = app.Name & " - " & app.ActiveWindow.Caption

    End Select

    Exit Function

ErrorHandler:

    MsgBox "Unsupported Office application!"

    GetOfficeAppWindowTitle = ""

End Function


How to use
MSAA comes with most Windows versions. You only need add a reference to "Accessibility" (%windir%\system32\oleacc.dll) to use this code in your VB/VBA project. MSAA SDK tools is a must-have if you want to do more than clearing the clipboard.
 
Update - 07/14/2005:
 
The above code will only work on the English version of Office. For other languages, you need find the correct value for three strings that may be localized:
1) The caption of the Clipboard Task Pane window. In English, it is "Task Pane";
2) The caption of the window containing all those clipboard control buttons and item lists. It is also a direct child of the "Task Pane" window. The English equivalent is "Collect and Paste 2.0";
3) The accName of the "Clear All" button.
 
You can use SPY or Inspect32/AccExplorer32 from MSAA SDK tools to find out 1) and 2). You can not get 3) from SPY. But most likely it is the shown localized caption of the "Clear All" button. You could confirm that with Inspect32/AccExplorer32.
 
Published Wednesday, December 22, 2004 9:43 PM by guowu
Filed under: ,

Comments

# re: Automate Office Task Pane with Active Accessibility

Thursday, July 14, 2005 11:14 AM by Bork
Hello!

I've implemented your code, but awfully doesn't work in our system. I think it could be some Language problem. We use Hungarian Word 2003 in the office.
I've tried to change the button name "Clear All" to the Hungarian correspondence, but it still doesn't works. I've debugged the macro, and it didn't find the toolbar window. So somewhere here could be the solution. But where do you set the window name? Or any other suggestions?
Thanks

# re: Automate Office Task Pane with Active Accessibility

Thursday, July 14, 2005 2:39 PM by guowu
It is a localization problem. Thanks for pointing this out. Please see the update in the post.

# re: Automate Office Task Pane with Active Accessibility

Monday, July 18, 2005 5:30 AM by Bork
Hello!

Thanks for the quick answer!
I've modified your code by using 3 constant instead of the "Task Pane", "Collect and Paste 2.0" and "Clear All" Strings. I've retrieved the locale names by using CLR Script program's http://www.clrsoftware.com/ Window Information Finder utility.

So, the hungarian equivalent is this:

Const WindowClearAll As String = "Elemek összegyűjtése és beillesztése 2.0"
Const BtnClearAll As String = "Az összes törlése"
Const WindowTaskPane As String = "Munkaablak"

ps: is there any way to get known, what is the actuall language version? I'm thinking about some IFDEF extensions.

# re: Automate Office Task Pane with Active Accessibility

Tuesday, July 19, 2005 6:48 AM by Bork
I've inserted two more lines to the code, because when I create a new document to copy some text into it, not all the time works the function. So I open, and then close the task pane.


Function ClearOfficeClipboard(app As Object) As Boolean
Dim oButton As AccObject, fShow As Boolean
'Get the IAccessible interface and child id (wrapped in the AccObject type)
'Notice: the second parameter, accName "Clear All" is localized!
'You can find out the accName and accRole using Spy, Inspect32 or other tools

WordBasic.EditOfficeClipboard 'Inserted by Bork

oButton = FindAccessibleChildInWindow(GetOfficeClipboardHwnd(app), BtnClearAll, ROLE_PUSHBUTTON)
If oButton.objIA Is Nothing Then
MsgBox "Unable to locate the "" " & BtnClearAll & " "" button!"
ClearOfficeClipboard = False
Else
oButton.objIA.accDoDefaultAction oButton.lngChild
ClearOfficeClipboard = True
End If

CommandBars("Task Pane").Visible = False 'Inserted by Bork

End Function

But I still can't find how can I identify the version and languge of the word (or VBA?).

# re: Automate Office Task Pane with Active Accessibility

Tuesday, July 19, 2005 10:54 AM by guowu
Thanks for the nice addition. Application.Version and Application.Build will return Word's version and build. Application.Language will return the lcid of Word's UI language. Something like:

Select Case Application.Language
Case MsoLanguageID.msoLanguageIDEnglishUS
'
Case MsoLanguageID.msoLanguageIDHungarian
'
End Select

probably will be what you want. Notice that this is the language of Word user interface, which can be changed via Office language settings if you have Office MUI pack installed. Therefore it may not be the same as the install language of Word.

# re: Automate Office Task Pane with Active Accessibility

Saturday, November 19, 2005 2:12 PM by Sam
Hi!

I tried your code in Word 2003, it worked very well. But it doesn't work in Excel 2003. It cannot retrieve the windows handle of task pane. Do you know what the reason is?

Thanks for your work.

Sam

# re: Automate Office Task Pane with Active Accessibility

Saturday, November 19, 2005 8:55 PM by Sam
I later found out that the FindWindow doesn't work. It returns 0. Could you help me solve this problem?
Thanks.

# re: Automate Office Task Pane with Active Accessibility

Monday, November 21, 2005 12:24 PM by guowu
Sam, I was not able to reproduce the problem you mentioned. Are you saying the FindWindow call in the GetOfficeAppHwnd function return 0?
If so, can you tell me what is the window title returned from GetOfficeAppWindowTitle? Also, what is the actual Excel window title that you see?

# re: Automate Office Task Pane with Active Accessibility

Thursday, January 19, 2006 8:35 AM by Ideefiks
I had the same problem initially, the problem is that the code assumes a couple of things when determining the name of the Office App Window, being:
- The option 'Hide file extensions for known file types' is not selected in Windows Explorer. When this is selected the extension .xls is NOT shown in your App window however the code looks for it
- Your File Window (e.g. Excel workbook) is maximised, otherwise the App Window shows simply 'Microsoft Excel', NOT the file name

It should not be too difficult to modify the code to trap these things, but don't have the time to code now.

BTW: Great bit of code GUOWU, thanks!

# re: Automate Office Task Pane with Active Accessibility

Sunday, January 22, 2006 5:17 PM by Ideefiks
Found some time ...

This traps the maximizing issue:

'Generic routine to retrieve the window title of the active window of an Office application
Private Function GetOfficeAppWindowTitle(app As Object) As String
On Error GoTo ErrorHandler
Select Case app.Name
Case "Microsoft Word"
If app.ActiveWindow.WindowState = xlMaximized Then
GetOfficeAppWindowTitle = app.ActiveWindow.Caption & " - " & app.Name
Else
GetOfficeAppWindowTitle = app.Name
End If
Case Else
If app.ActiveWindow.WindowState = xlMaximized Then
GetOfficeAppWindowTitle = app.Name & " - " & app.ActiveWindow.Caption
Else
GetOfficeAppWindowTitle = app.Name
End If
End Select
Exit Function
ErrorHandler:
MsgBox "Unsupported Office application!"
GetOfficeAppWindowTitle = ""
End Function

This traps the extension:

'Generic routine to retrieve the window handle of the active window of an Office Application
Private Function GetOfficeAppHwnd(app As Object) As Long
Dim AppTitle As String
AppTitle = GetOfficeAppWindowTitle(app)
GetOfficeAppHwnd = FindWindow(vbNullString, AppTitle)
If GetOfficeAppHwnd = 0 Then
GetOfficeAppHwnd = FindWindow(vbNullString, Left(AppTitle, Len(AppTitle) - 4))
End If
End Function

# re: Automate Office Task Pane with Active Accessibility

Monday, January 23, 2006 3:54 PM by guowu
Ideefiks, thansk for the great catch and fixes! I will update the code with your changes.

# re: Automate Office Task Pane with Active Accessibility

Tuesday, August 15, 2006 9:12 PM by xyz
Is there a way to push content into office clipboard programmatically?

# Guoqiang Wu s Blog Automate Office Task Pane with Active Accessibility | Hair Growth Products

New Comments to this post are disabled
 
Page view tracker