关于本站
1、基于Django+Bootstrap开发
2、主要发表本人的技术原创博客
3、本站于 2015-12-01 开始建站
昨天写了一篇博文:VSTO窗体在Excel内弹窗置顶。
既然Excel可以处理该功能需求。那么在Word中,是否也可以完成一样的功能?
按照在Excel中的方法,插入ClsWinWrap类的代码。
using System;
using System.Windows.Forms;
//命名空间自行修改
namespace WordAddin1
{
/// <summary>
/// IWin32Window接口类,为了实现弹出的窗体在Excel内
/// </summary>
public class ClsWinWrap:IWin32Window
{
private IntPtr m_Handle;
//构造函数,参数是父窗口的句柄
public ClsWinWrap(IntPtr handle)
{
this.m_Handle = handle;
}
//构造函数,参数是父窗口的句柄
public ClsWinWrap(int handle)
{
this.m_Handle = new IntPtr(handle);
}
public IntPtr Handle
{
get { return m_Handle; }
}
//打开窗体,Show参数使用该类自身
public void Show(Form frm)
{
frm.Show(this);
}
//模式打开窗体,Show参数使用该类自身
public DialogResult ShowDialog(Form frm)
{
return frm.ShowDialog(this);
}
}
}下一步,获取Word Application的句柄。
发现Word Application没有类似Excel.Application.Hwnd的属性。
出师不利啊,既然没有提供属性直接获取Hwnd属性。那么,我们自己写代码获取句柄。
获取句柄可以利用windows的api函数FindWindow获取。
FindWindow需要窗体类名或者窗体标题即可获取窗体的句柄。
那么我们怎么知道Word Application的窗体类名?
可以使用微软的Spy++(自己电脑没有可以自行搜索)。打开一个Word文档和Spy++。
Spy++工具栏上有个查找窗口的按钮。打开,鼠标移动到查找程序工具的图标上。点击鼠标不松开。

再把鼠标移动到Word文档的标题上。

此时可以看到显示相关的句柄和类名信息。松开鼠标,再点击确定按钮。
该句柄不是我们真正要查找的句柄。可以再点击“同步”按钮,定位到Spy++监控的具体位置。

我们一开始查找到的句柄是Ribbon菜单所在容器的句柄。可以通过结构树,看到最顶层的Application。

其中,"文档1 - Microsoft Word"是窗体标题。"OpusApp"是我们要查找的类名。
回到VSTO,继续写让窗口在Word内部打开并内部置顶的代码。
新建一个静态类,获取Word Application的句柄代码如下:
using System;
using System.Runtime.InteropServices;
using Word = Microsoft.Office.Interop.Word;
//命名空间自行修改
namespace WordAddin1
{
/// <summary>
/// 获取Office对象
/// </summary>
static class ClsOfficeObject
{
[DllImport("user32.dll", EntryPoint = "FindWindow")]
private static extern int FindWindow(string lpClassName, string lpWindowName);
public static Word.Application Application
{
get { return Globals.ThisAddIn.Application; }
}
/// <summary>
/// 获取当前Word的句柄
/// </summary>
public static int Hwnd
{
get { return FindWindow("OpusApp", null); }
}
}
}那么,在Word内部打开窗体的代码如下:
ClsWinWrap clsWin = new ClsWinWrap(ClsOfficeObject.Hwnd); FormTest frm = new FormTest(); clsWin.Show(frm); //打开窗口,指定它的父亲是Word
再设置窗体的相关属性,可以实现Word内部打开窗体。

但,真的这么简单就搞定了吗?如果这么简单的话,我就不用再写一篇文章。
再打开一个Word文档,同时打开两个。点击打开测试窗体只会在其中一个文档中置顶,和另外一个文档不关联。
另外一个文档会遮住该窗体。
我用Spy++定位找到另外一个文档。如下图:

发现OpusApp不唯一。也就是说Word程序结构和Excel的不同。难怪Word Application没有Hwnd属性。
后来,发现设置窗体的所属只能在Show方法设置。该方法若窗体已经打开了,则不能再执行。
我们可以再切换Word文档的事件控制。将所有打开的窗体Hide隐藏。再重新Show显示出来即可。
using System;
using System.Windows.Forms;
using System.Reflection;
using System.Runtime.InteropServices;
using Word = Microsoft.Office.Interop.Word;
//命名空间自行修改
namespace WordAddin1
{
/// <summary>
/// 窗体管理类,需要在Global级实例化对象(因为需要绑定事件)
/// </summary>
public class ClsFormManage
{
private Dictionary<Type, Form> FormConllent = new Dictionary<Type, Form>();
/// <summary>
/// 判断是否已经包含该窗体的实例了
/// </summary>
public bool ContainForm(Type type)
{
return this.FormConllent.ContainsKey(type);
}
/// <summary>
/// 根据窗体类,获取一个实例
/// </summary>
/// <param name="type">窗体类类型</param>
/// <param name="args">构造函数需要的参数</param>
/// <returns></returns>
public Form GetInstanceForm(Type type, object[] args)
{
Form frm;
if (!this.FormConllent.ContainsKey(type))
{
//实例化对象
List<Type> types = new List<Type>();
foreach (var item in args)
{
types.Add(item.GetType());
}
ConstructorInfo cti = type.GetConstructor(types.ToArray());
frm = (Form)cti.Invoke(args);
this.FormConllent.Add(type, frm);
//显示窗体
ClsWinWrap clsWin = new ClsWinWrap(ClsOfficeObject.Hwnd);
clsWin.Show(frm);
frm.FormClosing += frm_FormClosing; //绑定窗体关闭事件
}
else
{
frm = this.FormConllent[type];
frm.Activate();
}
return frm;
}
void frm_FormClosing(object sender, FormClosingEventArgs e)
{
//移除窗体
Form frm = (Form)sender;
Type type = frm.GetType();
if (this.FormConllent.ContainsKey(type))
{
this.FormConllent.Remove(type);
}
}
public ClsFormManage()
{
//绑定文档激活事件
Globals.ThisAddIn.Application.WindowActivate += Application_WindowActivate;
}
//激活文档(遍历句柄,设置窗体归属)
void Application_WindowActivate(Word.Document Doc, Word.Window Wn)
{
ClsWinWrap clsWinWrap = new ClsWinWrap(ClsOfficeObject.Hwnd);
Word.Application app = ClsOfficeObject.Application;
app.ScreenUpdating = false;
try
{
foreach (Form item in FormConllent.Values)
{
item.Hide();
item.Show(clsWinWrap);
}
}
catch (Exception ex)
{
MessageBox.Show(ex.Message, "Error");
}
app.ScreenUpdating = true;
}
}
}接着,打开ThisAddin.cs文件。加入如下代码:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Xml.Linq;
using Word = Microsoft.Office.Interop.Word;
using Office = Microsoft.Office.Core;
//命名空间自行修改
namespace WordAddin1
{
public partial class ThisAddIn
{
/// <summary>
/// 窗口管理
/// </summary>
public ClsFormManage FormManage;
private void ThisAddIn_Startup(object sender, System.EventArgs e)
{
FormManage = new ClsFormManage();
}
}将FormManage定位为全局的变量。
那么我们打开窗体的代码改成如下:
ClsFormManage clsFormManage = Globals.ThisAddIn.FormManage;
FormTest frm = clsFormManage.GetInstanceForm(typeof(FormTest), new object[] { });使用ClsFormManage的GetInstanceForm获取一个示例。
其中第2个参数是写FormTest初始化的参数,若没有需要传递的参数写 new object[]{} 即可。
该方法使用到反射机制。是不是很复杂啊,晕了正常。跟着代码练习一下就容易读懂。
相关专题: VSTO的那些坑