若没打开文档,VSTO设置菜单按钮变灰或不可用

  • 发布时间:2017年2月6日 12:10
  • 作者:杨仕航
  • 分类标签: VSTO
  • 阅读(7351)
  • 评论(0)

VSTO开发出来的功能,大部分是对文档进行处理。有时候只是打开Excel,Word等软件,却没打开任何文档。

这时候大部分Ribbon菜单上的按钮是不能使用。例如下图:

20170206/20170206095737006.png

把不需要的菜单按钮处理成不可用,即变灰。

当然,我们不能千篇一律把整个Ribbon标签页变灰。有些菜单按钮即使没有文档也有效,例如设置和关于等功能就不需要变灰处理。


这种功能需要根据文档格式判断。

Excel使用Application.Workbooks.Count获取文档个数;

Word使用Application.Documents.Count获取文档个数。


进一步思考,我们应当何时判断?

自然而然可以想到使用事件。可以在加载插件、文档打开、文档关闭3个事件下手。

看起来可行,但行不通。举个例子。

Excel文档关闭会触发Workbook的Close和WindowDeactivate两个事件。

Close事件无法判断是否真正关闭文档,有可能会取消关闭。故使用WindowDeactivate测试。新建一个空白的Excel文档,在WindowDeactivate事件加入如下测试代码。

Private Sub Workbook_WindowDeactivate(ByVal Wn As Window)
    MsgBox Application.Workbooks.Count
End Sub


注意,接下来只关闭该文档,不关闭整个Excel程序。

20170206/20170206101033015.png

点击关闭,选择不保存文档。弹窗提示如下:

20170206/20170206101127990.png

没有得到想象的结果,还是返回文档个数为1。

这是因为执行该事件的时候,该文档还打开着。可看到上图的文档还在。关闭消息框之后,文档才真正关闭。

(不用试Close事件了,也是一样逻辑)


这条路行不通,换一条路。

VSTO是用C#或VB.net语言来开发。我们可以使用多线程。

创建一个线程专门每隔几秒判断文档个数。

若文档个数为0,则设置菜单按钮变灰;文档个数不为0,则设置菜单按钮可正常使用。


这里我使用Excel为例写代码,其他Office的实现方法类似。

因为Excel除了文档是否打开之外,还有一种特殊情况:是否处于单元格编辑状态


新建一个类,这里我先给出全部代码,代码下面再讲解重点的位置和使用方法。

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;

//命名空间,根据自己情况修改
namespace Excel_Addin
{
    /// <summary>
    /// Excel状态
    /// </summary>
    public enum EnumExcelStatus 
    { 
        /// <summary>
        /// 无工作簿
        /// </summary>
        NoneWorkbook,
        /// <summary>
        /// 有工作簿,处于编辑状态
        /// </summary>
        Edited,
        /// <summary>
        /// 有工作簿,不处于编辑状态
        /// </summary>
        UndEdited 
    }

    /// <summary>
    /// Ribbon菜单控制类
    /// </summary>
    class ClsExcelRibbon:IDisposable
    {
        /// <summary>
        /// 记录之前的Excel状态
        /// </summary>
        private static EnumExcelStatus enumExcelStatus = EnumExcelStatus.UndEdited;
        private static Thread thdCheckStatus;

        /// <summary>
        /// 判断Excel状态
        /// </summary>
        private static EnumExcelStatus CheckExcelStatus()
        {
            //3种状态:有工作簿,未编辑;有工作簿,编辑中,无工作簿
            Microsoft.Office.Interop.Excel.Application app = Globals.ThisAddIn.Application;
            try
            {
                if (app.Workbooks.Count == 0)
                {
                    return EnumExcelStatus.NoneWorkbook;
                }
                else if (app.CommandBars["Standard"].Controls[1].Enabled)
                {
                    return EnumExcelStatus.UndEdited;
                }
                else
                {
                    return EnumExcelStatus.Edited;
                }
            }
            catch { return EnumExcelStatus.UndEdited; }
        }

        /// <summary>
        /// 监控线程
        /// </summary>
        private static void ThreadFunction()
        {
            while (true)
            {
                try { 
                    EnumExcelStatus enumStatus = CheckExcelStatus();
                    if (enumExcelStatus != enumStatus)
                    {
                        enumExcelStatus = enumStatus;
                        if (enumStatus == EnumExcelStatus.NoneWorkbook)
                        {
                            //没文档的情况
                            SetItemEnabled("NoDoc");
                        }
                        else if (enumStatus == EnumExcelStatus.Edited)
                        {
                            //有文档,单元格编辑状态
                            SetItemEnabled("Edited");
                        }
                        else
                        {
                            //有文档,单元格非编辑状态
                            SetItemEnabled("UndEdited");
                        }
                    }
                }
                catch { }
                Thread.Sleep(800);
            }
        }

        /// <summary>
        /// 开启线程
        /// </summary>
        public static void StartThread()
        {
            try
            {
                thdCheckStatus = new Thread(ThreadFunction);
                thdCheckStatus.IsBackground = true;
                thdCheckStatus.Start();
            }
            catch { }
        }

        /// <summary>
        /// 根据标签设置状态
        /// </summary>
        private static void SetItemEnabled(string check_label)
        {
            //MainRibbon 换成自己的Ribbon的类名
            MainRibbon myRibbon = Globals.Ribbons.GetRibbon<MainRibbon>();
            
            //遍历Ribbon自己菜单的选卡
            foreach (var tab in ribbon.Tabs)
            {
                //遍历选卡上的Group
                foreach (var group in tab.Groups)
                {
                    //遍历Group上的菜单控件
                    foreach (var item in group.Items)
                    {
                        //判断控件的Tag属性值
                        item.Enabled = item.Tag.ToString().Split(',').Contains(check_label);
                    }
                } 
            }
        }

        public void Dispose()
        {
            try
            {
                if (thdCheckStatus != null)
                {
                    //终止线程
                    thdCheckStatus.Abort();
                }
            }
            finally
            {
                thdCheckStatus = null;
            }
        }
    }
}


1、添加Excel状态枚举

见代码第14~28行,这个是为了方便管理,避免混乱。

同样为了避免重复修改菜单按钮的状态,需要记录当天的状态。再循环判断的时候,若当前的状态和记录的状态一致,则无需修改执行修改状态的代码。(见第38、74、75行的代码)


2、Excel状态的判断

见代码第47~63行,是否有文档很容易判断。而判断是否出于编辑状态比较困难。

找了不少方法,后来采用现在这种方法:判断Excel自己菜单按钮的状态,从而判断是否出于编辑状态。


3、更改菜单按钮状态的方法

见代码第119~135行,一个插件菜单按钮百来个都是很正常,若一个个写具体的按钮名称去修改,开发效率很低。

我使用遍历插件全部菜单按钮(当然,只是自己的菜单,非系统自带的菜单)的方法。

判断每个菜单按钮的Tag属性。

由于这里存在3种状态,不能简单在Tag属性只写一个值。我们可以使用逗号将多个标识文本隔开。在判断的时候,Split分割,检查是否存在该标识文本。


4、多线程管理

首先定义一个线程,见代码第39行。

再看看代码第71~96行,写个循环每个0.8秒判断一次Excel的状态。

启动该线程的方法我也封装好了,在代码第103~112行。

多线程还有个回收资源的问题。Excel随时可能关闭,需要关闭的时候,自动清理。

见代码第33行,该类继承了IDisposable接口。该接口需要实现Dispose方法,见代码第138~152行。


5、如何使用

首先,需要给Ribbon的菜单按钮设置Tag属性。可参考如下:

1)全部状态都可用:NoDoc,Edited,UnEdited

2)只有在没有文档时可用:NoDoc

3)在没有文档和有文档,单元格非编辑状态时可用:NoDoc,UnEdited

4)在没有文档时不可用,有文档,单元格非编辑状态可用:Edited

大部分情况,只会用到第2、4种。


接着,在插件加载事件的方法中,加入如下代码:

private void ThisAddIn_Startup(object sender, System.EventArgs e)
{
    //开启判断Excel状态线程
    ClsExcelRibbon.StartThread();
}


上一篇:Python异步(多线程和协程)

下一篇:Python如何开发桌面软件

相关专题: VSTO的那些坑   

评论列表

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

新的评论

清空