关于本站
1、基于Django+Bootstrap开发
2、主要发表本人的技术原创博客
3、本站于 2015-12-01 开始建站
VSTO开发出来的功能,大部分是对文档进行处理。有时候只是打开Excel,Word等软件,却没打开任何文档。
这时候大部分Ribbon菜单上的按钮是不能使用。例如下图:
把不需要的菜单按钮处理成不可用,即变灰。
当然,我们不能千篇一律把整个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程序。
点击关闭,选择不保存文档。弹窗提示如下:
没有得到想象的结果,还是返回文档个数为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; } } } }
见代码第14~28行,这个是为了方便管理,避免混乱。
同样为了避免重复修改菜单按钮的状态,需要记录当天的状态。再循环判断的时候,若当前的状态和记录的状态一致,则无需修改执行修改状态的代码。(见第38、74、75行的代码)
见代码第47~63行,是否有文档很容易判断。而判断是否出于编辑状态比较困难。
找了不少方法,后来采用现在这种方法:判断Excel自己菜单按钮的状态,从而判断是否出于编辑状态。
见代码第119~135行,一个插件菜单按钮百来个都是很正常,若一个个写具体的按钮名称去修改,开发效率很低。
我使用遍历插件全部菜单按钮(当然,只是自己的菜单,非系统自带的菜单)的方法。
判断每个菜单按钮的Tag属性。
由于这里存在3种状态,不能简单在Tag属性只写一个值。我们可以使用逗号将多个标识文本隔开。在判断的时候,Split分割,检查是否存在该标识文本。
首先定义一个线程,见代码第39行。
再看看代码第71~96行,写个循环每个0.8秒判断一次Excel的状态。
启动该线程的方法我也封装好了,在代码第103~112行。
多线程还有个回收资源的问题。Excel随时可能关闭,需要关闭的时候,自动清理。
见代码第33行,该类继承了IDisposable接口。该接口需要实现Dispose方法,见代码第138~152行。
首先,需要给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(); }
相关专题: VSTO的那些坑