本帖最后由 紫電 于 2014-3-17 14:36 編輯
一、效果演示
為了減小圖片大小,圖中,我只演示了一個單元格被修改后,程序插入批注的效果。實際上功能開啟狀態(tài)下,可以在多個單元格、多個工作表、多個工作簿上都起作用,且刪除、插入等各種危險操作,均不會影響其效果。
二、技術剖析
1、基本原理
使用字典獲取工作表中的初始值或批注值,作為單元格的初始值。當工作表Change事件被觸發(fā)時,遍歷改變的單元格,與字典中的初值進行比較,如果不相符插入批注;相符,刪除批注。
2、多工作表、多工作簿
為了實現(xiàn)在多個工作表、工作簿上都能實現(xiàn)增加批注記錄原始數(shù)據(jù)的功能,需要訂閱Excel.Application中的WorkbookOpen、WorkbookActivate、 SheetActivate事件,也就是進行事件委托。為避免代碼集中在ThisAddin或者Ribbon中,這里我新建了了一個cs文件另外構(gòu)造類,這部分初始化工作在構(gòu)造函數(shù)中進行。一般情況下,通過以上三個事件之一 調(diào)用GetActiveWorksheet()實現(xiàn)字段初始化、工作表事件的委托。由于調(diào)試時不會觸發(fā)這三個事件,所以構(gòu)造函數(shù)中需要額外增加一個GetActiveWorksheet()進行初始化。
- #region 構(gòu)造函數(shù)
- public MarkChangedCells(Excel.Application app)
- {
- xlsApp = app;
- GetActiveWorksheet();//設置工作表對象
- xlsApp.WorkbookOpen += xlsApp_WorkbookOpen;//打開工作簿
- xlsApp.WorkbookActivate += xlsApp_WorkbookActivate;//激活工作簿
- xlsApp.SheetActivate += xlsApp_SheetActivate;//切換工作表
- }
- #endregion
復制代碼
3、避免工作表事件change多次被運行
問題:調(diào)試過程中,監(jiān)視到工作表的Change事件,會在切換工作表之后,被多次調(diào)用,禁用事件觸發(fā)仍然不起作用。
分析:堆中存在多個訂閱事件的方法,沒有被回收。
方案:取消事件委托之后再重新設置工作表對象,避免產(chǎn)生垃圾。
- /// <summary>
- /// 獲取活動工作表,并關聯(lián)事件,初始化字典
- /// </summary>
- /// <returns>是否成功獲取工作表</returns>
- bool GetActiveWorksheet()
- {
- try
- {
- if (null != MySh)
- {
- MySh.Change -= MySh_Change;//解除事件,否則會多次執(zhí)行Change事件
- }
- MySh = xlsApp.ActiveSheet;//啟動時,初始化FirstSh
- MySh.Change += MySh_Change;//監(jiān)視事件
- m_NickNameConvert = new RangeNickName(MySh);//重置名稱定義
- InitiateMyShData(MySh);// 初始化字典
- return true;
- }
- catch (Exception)
- {
- MySh = null;//如果獲取到的是Chart,則放棄操作
- return false;
- }
- }
復制代碼
4、復制、剪切、插入、刪除單元格,初始值錯位。
起初使用的字典結(jié)構(gòu)是Range.Address作為Key,Range.Value作為Value。分析可知,復制、剪切、插入、刪除單元格之后,單元格實際情況與字典中的情況會不一致。比如插入了一行,字典中的Range.Address不會向下偏移一行,因此新插入行下面添加的批注都是錯誤的。為了修正這個漏洞,我重新調(diào)整了字典結(jié)構(gòu)。具體措施如下:
(1)、使用GUID+Range.Value構(gòu)造初始值字典,GUID為單元格別名(需要做簡單字符串處理)。
(2)、再次構(gòu)建一個字典,加載Excel名稱定義(即單元格別名),體現(xiàn)GUID和Range.Address的對應關系。
(3)、初始值字典在加載單元格初始值、校驗單元格是否改變時,通過調(diào)用類RangeNickName來實現(xiàn)。使用Range.Address獲取GUID(單元格別名),判斷單元格別名對應的單元格與Range是否屬于同一單元格,以此來確定是否已經(jīng)進行了剪切、插入、刪除單元格操作。
(4)、提示復制操作,可以選擇覆蓋、修改粘貼區(qū)域,以此來避免產(chǎn)生大量的錯誤批注!
由于代碼較多,以下只貼出部分核心代碼。
- /// <summary>
- /// 檢測別名是否存在,即是否修改過工作表
- /// </summary>
- /// <param name="Rng">要檢測的單元格</param>
- /// <returns>存在返回名稱定義檢測結(jié)果,否則返回空字符串</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、用戶自定義開關此功能
創(chuàng)建一個Ribbon,使用Checkbox,開關此功能。開就是new,關需要使用到手動析,區(qū)別于VB的是,設為null是不行的。注冊表功能不再贅述,詳見代碼。
- public void Dispose()
- {
- if (null != MySh)
- {
- MySh.Change -= MySh_Change;//解除事件,否則會多次執(zhí)行Change事件
- }
- MySh = null;
- xlsApp = null;
- MyShData = null;
- GC.SuppressFinalize(this);//不需要再調(diào)用本對象的Finalize方法
- }
復制代碼
|