本帖最后由 紫电 于 2014-3-17 14:36 编辑
一、效果演示
为了减小图片大小,图中,我只演示了一个单元格被修改后,程序插入批注的效果。实际上功能开启状态下,可以在多个单元格、多个工作表、多个工作簿上都起作用,且删除、插入等各种危险操作,均不会影响其效果。
二、技术剖析
1、基本原理
使用字典获取工作表中的初始值或批注值,作为单元格的初始值。当工作表Change事件被触发时,遍历改变的单元格,与字典中的初值进行比较,如果不相符插入批注;相符,删除批注。
2、多工作表、多工作簿
为了实现在多个工作表、工作簿上都能实现增加批注记录原始数据的功能,需要订阅Excel.Application中的WorkbookOpen、WorkbookActivate、 SheetActivate事件,也就是进行事件委托。为避免代码集中在ThisAddin或者Ribbon中,这里我新建了了一个cs文件另外构造类,这部分初始化工作在构造函数中进行。一般情况下,通过以上三个事件之一 调用GetActiveWorksheet()实现字段初始化、工作表事件的委托。由于调试时不会触发这三个事件,所以构造函数中需要额外增加一个GetActiveWorksheet()进行初始化。
- #region 构造函数
- public MarkChangedCells(Excel.Application app)
- {
- xlsApp = app;
- GetActiveWorksheet();//设置工作表对象
- xlsApp.WorkbookOpen += xlsApp_WorkbookOpen;//打开工作簿
- xlsApp.WorkbookActivate += xlsApp_WorkbookActivate;//激活工作簿
- xlsApp.SheetActivate += xlsApp_SheetActivate;//切换工作表
- }
- #endregion
复制代码
3、避免工作表事件change多次被运行
问题:调试过程中,监视到工作表的Change事件,会在切换工作表之后,被多次调用,禁用事件触发仍然不起作用。
分析:堆中存在多个订阅事件的方法,没有被回收。
方案:取消事件委托之后再重新设置工作表对象,避免产生垃圾。
- /// <summary>
- /// 获取活动工作表,并关联事件,初始化字典
- /// </summary>
- /// <returns>是否成功获取工作表</returns>
- bool GetActiveWorksheet()
- {
- try
- {
- if (null != MySh)
- {
- MySh.Change -= MySh_Change;//解除事件,否则会多次执行Change事件
- }
- MySh = xlsApp.ActiveSheet;//启动时,初始化FirstSh
- MySh.Change += MySh_Change;//监视事件
- m_NickNameConvert = new RangeNickName(MySh);//重置名称定义
- InitiateMyShData(MySh);// 初始化字典
- return true;
- }
- catch (Exception)
- {
- MySh = null;//如果获取到的是Chart,则放弃操作
- return false;
- }
- }
复制代码
4、复制、剪切、插入、删除单元格,初始值错位。
起初使用的字典结构是Range.Address作为Key,Range.Value作为Value。分析可知,复制、剪切、插入、删除单元格之后,单元格实际情况与字典中的情况会不一致。比如插入了一行,字典中的Range.Address不会向下偏移一行,因此新插入行下面添加的批注都是错误的。为了修正这个漏洞,我重新调整了字典结构。具体措施如下:
(1)、使用GUID+Range.Value构造初始值字典,GUID为单元格别名(需要做简单字符串处理)。
(2)、再次构建一个字典,加载Excel名称定义(即单元格别名),体现GUID和Range.Address的对应关系。
(3)、初始值字典在加载单元格初始值、校验单元格是否改变时,通过调用类RangeNickName来实现。使用Range.Address获取GUID(单元格别名),判断单元格别名对应的单元格与Range是否属于同一单元格,以此来确定是否已经进行了剪切、插入、删除单元格操作。
(4)、提示复制操作,可以选择覆盖、修改粘贴区域,以此来避免产生大量的错误批注!
由于代码较多,以下只贴出部分核心代码。
- /// <summary>
- /// 检测别名是否存在,即是否修改过工作表
- /// </summary>
- /// <param name="Rng">要检测的单元格</param>
- /// <returns>存在返回名称定义检测结果,否则返回空字符串</returns>
- public string NickName(Excel.Range Rng)
- {
- InitiateNames(m_TargetSh);// 强制刷新字典,防止插入删除操作导致的错位
- string sRefersTo = "="+ m_TargetSh.Name+"!" + Rng.Address;
- string sGuid ;
- if (m_Names_Sheet.ContainsKey(sRefersTo))
- {
- sGuid = m_Names_Sheet[sRefersTo];
- }
- else
- {
- sGuid = "";
- }
- return sGuid;
- }
- /// <summary>
- /// 向工作表中添加单元格别名,返回别名
- /// </summary>
- /// <param name="Rng">需要添加别名的单个单元格</param>
- /// <returns>返回别名</returns>
- public string AddRangeNickName(Excel.Range Rng, bool Visible = false)
- {
- string sGuid;//获取GUID
- InitiateNames(m_TargetSh);// 强制刷新字典,防止插入删除操作导致的错位
- string sRefersTo = "=" + m_TargetSh.Name + "!" + Rng.Address;
- if (!m_Names_Sheet.ContainsKey(sRefersTo))
- {
- sGuid = AddNickName(sRefersTo, Visible);//添加名称
- }
- else
- {
- sGuid = m_Names_Sheet[sRefersTo];//返回原有的名称定义
- }
- return sGuid;//返回别名
- }
复制代码
5、用户自定义开关此功能
创建一个Ribbon,使用Checkbox,开关此功能。开就是new,关需要使用到手动析,区别于VB的是,设为null是不行的。注册表功能不再赘述,详见代码。
- public void Dispose()
- {
- if (null != MySh)
- {
- MySh.Change -= MySh_Change;//解除事件,否则会多次执行Change事件
- }
- MySh = null;
- xlsApp = null;
- MyShData = null;
- GC.SuppressFinalize(this);//不需要再调用本对象的Finalize方法
- }
复制代码
|