It's probably not too surprising that one of the important bits of functionality in Project Colletta is relating a window on the screen (a HWND to be precise) to a document, in particular, to the file path of the document appearing in that window. The VSTO APIs provide access to document details for all of the Office applications (what I'll refer to as the "insider" view), but do not offer any way to get hold of the window handle (the "outsider" view) and, in early (and very unreliable) prototypes I would, roughly speaking, have a global list of documents opened in Word, Excel, etc. and a separate global list of window handles, and attempt to match entries in the first list with those in the second based on window title strings. I do still end up doing this for Outlook (which goes some way towards explaining why that is the most troublesome of the Office applications Project Colletta deals with). However, AccessibleObjectFromWindow makes things an awful lot easier for Word, Excel, PowerPoint and, as it happens, Acrobat Reader. This function exists to provide accessibility clients (such as screen readers) a way to get at the object model of whatever a window happens to be displaying, i.e., a direct link from the outsider view to the insider, which is precisely what I wanted in Project Colletta. The basic pattern in Project Colletta is to watch for windows coming and going, grab ones which are of a window class of interest, and then get hold of their object model via AccessibleObjectFromWindow, and extract the file path from there.
For the first step, I use Windows Events (see SetWinEventHook for details) to get a callback when windows are created or come to the foreground or move, etc. Only the first of those is really relevant to the current discussion, but I track movement to update the location of the docbars at the bottom of the document windows. (The docbars are implemented as separate windows, owned by the main Project Colletta application, positioned to line up with the related application window - if you drag application windows around very quickly, you can detect a lag before the docbar catches up - but that is a small price to pay compared to the complexity in incorporating the docbar code into each and every supported application process!) Here's a stripped down code fragment showing the basics.
[DllImport("user32.dll", SetLastError=true)]internal extern static IntPtr SetWinEventHook( uint eventMin, uint eventMax, IntPtr hmodWinEventProc, WinEventProc lpfnWinEventProc, uint idProcess, uint idThread, uint dwFlags );
private Win32.WinEventProc winEventProc; // Keep as class member to prevent premature GC
private void WinEventHandler(IntPtr hWinEventHook, uint eventType, IntPtr hwnd, int idObject, int idChild, uint dwEventThread, uint dwmsEventTime)
{
// Only look at top level windows
if (idObject != OBJID_WINDOW || idChild != CHILDID_SELF || Win32.GetParent(hwnd) != IntPtr.Zero)
return;
switch (eventType)
{
case EVENT_OBJECT_CREATE:
RegisterWindow(hwnd);
break;
...
}
}
...
this.winEventProc = this.WinEventHandler; this.winEventHook = Win32.SetWinEventHook(EVENT_First, EVENT_Last, IntPtr.Zero, this.winEventProc, 0, 0, 2 /*WINEVENT_OUTOFCONTEXT|WINEVENT_SKIPOWNPROCESS*/ );
...
When I have a new window to examine, I need to determine if it's one I'm interested in. I could look at its process name but that's a bit cumbersome and, since I need to look at its window class anyway, I can use that. A tool like Spy++ can show you the complete window hierarchy of a running application, something we'll look at in a moment, but all I need now is the class name for the top level window. GetClassName shows us that Word's main window is of class "OpusApp," Excel is "XLMAIN," PowerPoint is "PPTFrameClass" and Acrobat Reader is "AcrobatSDIWindow" - now these are not documented and guaranteed, so I do need to check them every time a new version of the application turns up.
The documentation for AccessibleObjectFromWindow has a table at the end showing what type of object we can get from a number of application's windoww - for example, a reference to a Microsoft.Office.Interop.Word.Window can be extracted from a window of class "_WwG" - but, hang on, we've got an "OpusApp" window, not a "_WwG" - what gives? This really is where Spy++ comes into its own: you can start at the "OpusApp" window and expand all the child windows until you find a "_WwG" a couple of levels down: one of the children of the top level window is of class "_WwF" which has one child of class "_WwB" and amongst the children of that is a single "_WwG" - it is fortunate that there only one child of the required type at each level since that makes the window search very easy. Again, this is undocumented and can change between versions (and in fact, has changed for PowerPoint - the chain for PowerPoint 2013 is "PPTFrameClass" "MDIClient" "mdiClass" but is "PPTFrameClass" "MDIClient" "mdiClass" "paneClassDC" for PowerPoint 2010, and something else again for PowerPoint 2007).
Given that, once the code above detects a top level window and I confirm it is of class "OpusApp" I can run the following to get hold of the Word object model Document displayed in it:
private Word.Document GetWordDocument(IntPtr hwnd)
{
string[] classes = new string[] { "_WwF", "_WwB", "_WwG" };
var docHwnd = SetDocumentWindowByClass(hwnd, classes);
if (docHwnd != IntPtr.Zero)
{
object o = GetDocumentObject(docHwnd, typeof(Word.Window));
var window = o as Word.Window;
if (window != null)
return window.Document;
}
return null;
}
private static IntPtr GetDocumentWindowByClass(IntPtr topHwnd, string[] classes)
{
IntPtr docHwnd = topHwnd;
for (int i = 0; i < classes.Length && docHwnd != IntPtr.Zero; ++i)
docHwnd = Win32.FindWindowEx(docHwnd, IntPtr.Zero, classes[i], null);
return docHwnd;
}
private static GetDocumentObject(IntPtr docHwnd, Type documentObjectType) { Guid guid = documentObjectType.GUID; object o; if (AccessibleObjectFromWindow(docHwnd, OBJID_NATIVEOM, ref guid, out o) != 0) o = null; return o; } [DllImport("oleacc.dll")]private static extern int AccessibleObjectFromWindow(IntPtr hwnd, uint id, ref Guid iid, [MarshalAs(UnmanagedType.IDispatch)] out object ppv);private const uint OBJID_NATIVEOM = 0xFFFFFFF0;
A bit of web spelunking and playing with Spy++ showed a pleasant surprise with Acrobat Reader. It exposes a (rather limited) document model too. Underneath top level window of class "AcrobatSDIWindow" can be found a chain of "AVSplitterView", "AVSplitationPageView", "AVSplitterView", "AVScrolledPageView", "AVScrollView", "AVPageView" (earlier versions of Acrobat Reader have different window hierarchies) and from the last of those we can get an IPDDomDocument, defined in the AcrobatAccessLib COM library (which you can easily get hold of via Add Reference, COM libraries), and this exposes a path value. Unfortunately, in protected mode (which is the default in later versions of the Acrobat Reader), the path is to a temporary copy rather than to the file the user opened, so it's not so useful after all.
So, to conclude, VSTO gives you a good insider view of an application, and AccessibleObjectFromWindow can link that to the outsider view, so that you can make something associated with the window on the screen react to the contents of the document shown in that window.