VSTO窗体在Word内弹窗置顶

  • 发布时间:2017年2月17日 16:51
  • 作者:杨仕航
  • 分类标签: VSTO
  • 阅读(7266)
  • 评论(0)

昨天写了一篇博文: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++工具栏上有个查找窗口的按钮。打开,鼠标移动到查找程序工具的图标上。点击鼠标不松开。

20170217/20170217161736297.png

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

20170217/20170217161921331.png

此时可以看到显示相关的句柄和类名信息。松开鼠标,再点击确定按钮。

该句柄不是我们真正要查找的句柄。可以再点击“同步”按钮,定位到Spy++监控的具体位置。

20170217/20170217162054470.png


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

20170217/20170217162212634.png

其中,"文档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++定位找到另外一个文档。如下图:

20170217/20170217163736578.png

发现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[]{} 即可。

该方法使用到反射机制。是不是很复杂啊,晕了正常。跟着代码练习一下就容易读懂。

上一篇:我的网站搭建(第44天) 添加头像字段

下一篇:VSTO窗体在Excel内弹窗置顶

相关专题: VSTO的那些坑   

评论列表

智慧如你,不想发表一下意见吗?

新的评论

清空