Launching Office Apps Programmatically
There are at least 9 different ways to start or connect to an Office app programmatically in managed code, as summarized in this table:
PIA Interop |
Using the Office PIAs is the most RAD approach, with the greatest level of design-time and compile-time support. This has to be the preferred approach for almost all scenarios. A variation of this is the use of embedded PIA types using ComImport, as described here and here. |
Process.Start |
The simplest fire-and-forget approach, useful if you want to launch any executable but don’t need to interact with the app’s OM afterwards. |
Activator.CreateInstance |
Internally, this uses reflection to find and execute an appropriate constructor for the specified object. This can be slow, but useful if you intend to continue using reflection to work with the app – which you might do if you want to eliminate use of the PIAs altogether. |
Marshal.BindToMoniker |
Internally, this p/invokes to the Win32 BindToMoniker API. Useful if you have a narrow interest in the app’s exposed object that deals with a particular file type. In other words, if you want to work with a particular doc/workbook/presentation/etc using only a limited subset of the OM. |
Marshal.GetActiveObject |
Internally, p/invokes to the Win32 GetActiveObject. This will throw an exception if the object’s server is not already running, as it looks up the target ProgID in the ROT. One of the classic uses of this API is to determine whether or not the target app is already running. |
VisualBasic.CreateObject |
A compatibility API, which internally calls Activator.CreateInstance. |
VisualBasic.GetObject |
A compatibility API, which internally calls either Activator.CreateInstance or Marshal.BindToMoniker. In other words, it will connect to an already-running instance of the app if it finds one, otherwise it will create a new instance. |
ActivateMicrosoftApp |
This is a method exposed from the Excel Application object (and only the Excel Application object), used for activating or starting other Office apps (specifically, Access, FoxPro, Outlook, PowerPoint, Project, or Word). This approach does not give you access to the target app’s OM, so its effect is very similar to Process.Start. |
AccessibleObjectFromWindow |
Given the HWND for the target app (which you can find using FindWindowEx), this gets you access to the app’s OM. This is useful if your starting point is an HWND, or if you’re specifically focused on the app’s IAccessible implementation, and only minimally interested in the rest of the OM. |
Here’s some code that illustrates each of these approaches – this is implemented in an Excel add-in, where each of the “launch/connect” methods is invoked via a Ribbon button click handler. The comments should make it clear enough what's going on, and you can see the full source-code in the attached solution zip:
public partial class ThisAddIn
{
private string pptxFile;
private PowerPoint.Application ppt;
private PowerPoint.Presentation pptx;
private void ThisAddIn_Startup(object sender, System.EventArgs e)
{
Uri codeBaseUri =
new Uri(Assembly.GetExecutingAssembly().CodeBase);
pptxFile = Path.Combine(
Path.GetDirectoryName(codeBaseUri.AbsolutePath), "Test.pptx");
}
private void ThisAddIn_Shutdown(object sender, System.EventArgs e)
{
// Make sure to release any references to COM objects.
ppt = null;
pptx = null;
GC.Collect();
GC.WaitForPendingFinalizers();
GC.Collect();
GC.WaitForPendingFinalizers();
}
// Use the PIAs to interop with PowerPoint.
internal void InteropCreate()
{
ppt = null;
pptx = null;
ppt = new PowerPoint.Application();
ppt.Visible = Office.MsoTriState.msoTrue;
pptx = ppt.Presentations.Open(
pptxFile,
Office.MsoTriState.msoFalse,
Office.MsoTriState.msoFalse,
Office.MsoTriState.msoTrue);
}
// Use Process.Start to start a PowerPoint process. This does not
// give you access to the target app's OM.
internal void ProcessStart()
{
ProcessStartInfo si = new ProcessStartInfo();
si.FileName =
@"C:\Program Files (x86)\Microsoft Office\Office12\Powerpnt.exe";
si.Arguments = pptxFile;
Process.Start(si);
}
// Internally, Activator.CreateInstance uses reflection to find and
// execute an appropriate constructor for the specified object.
internal void CreateInstance()
{
Type t = Type.GetTypeFromProgID("PowerPoint.Application");
object o = Activator.CreateInstance(t);
// Note: we could cast the return from Activator.CreateInstance
// to the PIA type that we expect, if we wanted to use the PIAs.
t.InvokeMember(
"Visible", BindingFlags.Public | BindingFlags.SetProperty,
null, o, new object[] { true }, null);
object p = t.InvokeMember(
"Presentations",
BindingFlags.Public | BindingFlags.GetProperty,
null, o, null, null);
Type t2 = p.GetType();
t2.InvokeMember("Open",
BindingFlags.Public | BindingFlags.InvokeMethod,
null, p, new object[] { pptxFile }, null);
}
// Internally, Marshal.BindToMoniker p/invokes to Win32 BindToMoniker.
internal void BindToMoniker()
{
ppt = null;
pptx = null;
pptx = (PowerPoint.Presentation)Marshal.BindToMoniker(pptxFile);
pptx.Application.Visible = Office.MsoTriState.msoTrue;
}
// Internally, Marshal.GetActiveObject p/invokes to Win32
// GetActiveObject. This will throw an exception if the object's
// server is not already running. Win32 GetActiveObject looks up the
// target ProgID in the Running Object Table.
internal void GetActiveObject()
{
ppt = null;
pptx = null;
try
{
ppt = (PowerPoint.Application)
Marshal.GetActiveObject("PowerPoint.Application");
ppt.Visible = Office.MsoTriState.msoTrue;
pptx = ppt.Presentations.Open(
pptxFile,
Office.MsoTriState.msoFalse,
Office.MsoTriState.msoFalse,
Office.MsoTriState.msoTrue);
}
catch (COMException cex)
{
// If the target app is not already running, GetActiveObject
// will throw a COMException (0x800401E3): Operation
// unavailable (Exception from HRESULT: 0x800401E3
// (MK_E_UNAVAILABLE)).
Debug.WriteLine(cex.ToString());
}
}
// Internally, CreateObject calls Activator.CreateInstance - that is,
// it creates a new instance of the target application (if the app is
// single-use).
internal void CreateObject()
{
ppt = null;
pptx = null;
// GetObject requires the object class name, plus optionally a
// machine server name.
ppt = (PowerPoint.Application)
Microsoft.VisualBasic.Interaction.CreateObject(
"PowerPoint.Application", "");
ppt.Visible = Office.MsoTriState.msoTrue;
pptx = ppt.Presentations.Open(
pptxFile,
Office.MsoTriState.msoFalse,
Office.MsoTriState.msoFalse,
Office.MsoTriState.msoTrue);
}
// Internally, GetObject calls either Marshal.BindToMoniker or
// Activator.CreateInstance - that is, it either uses an existing
// instance of the target app, or creates a new instance.
internal void GetObject()
{
ppt = null;
pptx = null;
// GetObject requires either the executable filename or the object
// class name.
ppt = (PowerPoint.Application)
Microsoft.VisualBasic.Interaction.GetObject(
"", "PowerPoint.Application");
ppt.Visible = Office.MsoTriState.msoTrue;
pptx = ppt.Presentations.Open(
pptxFile,
Office.MsoTriState.msoFalse,
Office.MsoTriState.msoFalse,
Office.MsoTriState.msoTrue);
}
// ActivateMicrosoftApp activates a Microsoft application if it is
// running or starts a new instance of it if it is not. Restricted
// to these apps: Access, FoxPro, Outlook, PowerPoint, Project, Word.
// Note that this does not give you access to the target app's OM.
internal void ActivateMicrosoftApp()
{
this.Application.ActivateMicrosoftApp(
Excel.XlMSApplication.xlMicrosoftPowerPoint);
}
[DllImport("User32")]
public static extern int GetClassName(
int hWnd, StringBuilder lpClassName, int nMaxCount);
// Callback passed to EnumChildWindows to find any window with the
// registered classname "paneClassDC" - this is the class name of
// PowerPoint's accessible document window.
public bool EnumChildProc(int hwnd, ref int lParam)
{
StringBuilder windowClass = new StringBuilder(128);
GetClassName(hwnd, windowClass, 128);
if (windowClass.ToString() == "paneClassDC")
{
lParam = hwnd;
}
return true;
}
public delegate bool EnumChildCallback(int hwnd, ref int lParam);
[DllImport("User32")]
public static extern bool EnumChildWindows(
int hWndParent, EnumChildCallback lpEnumFunc, ref int lParam);
[DllImport("User32")]
public static extern int FindWindowEx(
int hwndParent, int hwndChildAfter, string lpszClass,
int missing);
// AccessibleObjectFromWindow gets the IDispatch pointer of an object
// that supports IAccessible, which allows us to get to the native OM.
[DllImport("Oleacc.dll")]
private static extern int AccessibleObjectFromWindow(
int hwnd, uint dwObjectID,
byte[] riid,
ref PowerPoint.DocumentWindow ptr);
// Get the window handle for a running instance of PowerPoint.
internal void GetAccessibleObject()
{
ppt = null;
pptx = null;
try
{
// Walk the children of the desktop to find PowerPoint’s main
// window.
int hwnd = FindWindowEx(0, 0, "PP12FrameClass", 0);
if (hwnd != 0)
{
// Walk the children of this window to see if any are
// IAccessible.
int hWndChild = 0;
EnumChildCallback cb =
new EnumChildCallback(EnumChildProc);
EnumChildWindows(hwnd, cb, ref hWndChild);
if (hWndChild != 0)
{
// OBJID_NATIVEOM gets us a pointer to the native
// object model.
uint OBJID_NATIVEOM = 0xFFFFFFF0;
Guid IID_IDispatch =
new Guid("{00020400-0000-0000-C000-000000000046}");
PowerPoint.DocumentWindow ptr = null;
int hr = AccessibleObjectFromWindow(
hWndChild, OBJID_NATIVEOM,
IID_IDispatch.ToByteArray(), ref ptr);
if (hr >= 0)
{
ppt = ptr.Application;
ppt.Visible = Office.MsoTriState.msoTrue;
pptx = ppt.Presentations.Open(
pptxFile,
Office.MsoTriState.msoFalse,
Office.MsoTriState.msoFalse,
Office.MsoTriState.msoTrue);
}
}
}
}
catch (Exception ex)
{
Debug.WriteLine(ex.ToString());
}
}
}
Now, wasn’t that exciting? Note that in almost all circumstances, you’ll want to use PIA interop (or the embedded types variation) rather than any of the other approaches. Note that the basic functionality of the embedded types approach is greatly enhanced in .NET CLR 4.0 as described by Misha here and here.
Comments
- Anonymous
February 18, 2009
A bit late, but... Thanks very much for this! - Anonymous
February 20, 2009
Hi,Can I use one of these techniques to access the ToggleRibbon method in the Word 2007 PIA?Currenttly my app is built with a reference to the office word 2003 PIA. About the only thing I need from the 2007 PIA is the Window.ToggleRibbon() method. If i could work that in via reflection my life would be simpler.Thanks - Anonymous
February 20, 2009
Mario - you can find more information about targeting multiple versions of Office here:http://blogs.msdn.com/andreww/archive/2008/09/02/version-specific-ui-in-add-ins.aspxhttp://blogs.msdn.com/andreww/archive/2008/06/24/add-ins-for-multiple-office-versions-without-pias-pt2-or-vtblgap.aspxFor your specific question, the answer is that any of the approaches in this post would work - all they are is alternative ways to get the app launched and connected. Once you've got a connection to the app, you could use reflection code like this to toggle the ribbon:if (word.Version == "12.0"){ Word.Window window = word.ActiveWindow; Type windowType = window.GetType(); windowType.InvokeMember( "ToggleRibbon", BindingFlags.Public | BindingFlags.InvokeMethod, null, window, null);}