關於如何做一個Source Insight外掛外掛程式的全過程都已經寫完了,這麼一點東西拖了一年的時間才寫完,足以說明我是一個很懶的人,如果不是很多朋友的關心和督促,恐怕是難以完成了。許多朋友希望順著本文的思路也作一個類似於TabSiPlus功能的Source Insight外掛外掛程式,抱歉讓他們等了這麼長時間,看了本文或許能讓大家消消氣(大頭在後面)。其實即使不是為了給Source Insight做外掛外掛程式,本文的很多方法都可以用於給其它軟體做外掛。
儘管本文介紹了做TabSiPlus外掛外掛程式的完整過程,但是要做一個有使用價值的外掛外掛程式還有很多細節要注意,首先是穩定,插入到Source Insight進程中的代碼一定要考慮周到,仔細地測試所有分支,確保不能頻繁地掛死Source Insight;其次,附加的功能一定不能干擾Source Insight的正常工作,比如視窗訊息的處理;最後就是在介面上要能和Source Insight融為一體,外掛程式建立的視窗一定不能覆蓋Source Insight的視窗。
以上都是空話,現在用具體的例子來說幾個細節,比如TabsiPlus提供了直接根據標籤關閉文件視窗的功能,由於文件視窗建立的時候已經擷取到視窗的控制代碼,所以TabsiPlus的第一個版本就使用DestroyWindow() API直接關閉了文件視窗,從外表看確實大到了效果,但是卻隱藏了一個BUG,那就是雖然視窗被關閉了,但是Source Insight並不知道文件視窗被關閉了,相應的檔案依然處於開啟狀態,如果文檔修改過,這樣關閉視窗甚至不會提醒使用者儲存文檔。在自己的程式中關閉視窗當然直接DestroyWindow()就行了,但是既然你的代碼是“寄人籬下”,就要按照“別人”的規矩來。通過Spy++觀察Source Insight視窗的訊息,發現Source Insight視窗只對WM_CLOSE訊息會有正常的反映,也就是說Source Insight可能在OnClose()中處理了關閉檔案和提示儲存修改的操作(很奇怪DestroyWindow()後為什麼沒有觸發Source Insight的OnClose()處理被調用,看來遠程注入的代碼確實有很多需要注意的地方),後來的版本使用SendMessage將一個WM_CLOSE訊息發給文件視窗,這樣就很好地解決了這個問題。
上面的問題還沒完,讓視窗消失就算是關閉了嗎,有沒有考慮視窗的Focus? 如果關閉某個擁有“焦點(Focus)”的子視窗,Windows會啟用此焦點視窗的一個兄弟視窗,通常是上一個擁有焦點的視窗,這個相信使用Windows的人都知道,我也是這麼認為的,但是,這一點在外掛中失靈了,在TabSiPlus的線程中,關閉當前擁有焦點的文件視窗後,其它的文件視窗標題列竟然都是灰的,也就是Source Insight的MDIClient視窗沒有選擇上一個焦點視窗啟用,怎麼辦?看看TabSiPlus中關閉文件視窗的代碼:
void CTabBarsWnd::CloseSIWindow(CSiWindow*& pWindow)
{
ASSERT(pWindow);
HWND hPrevActive = NULL;
if(pGlobalActiveSIWindow != NULL && pWindow != pGlobalActiveSIWindow)
{
DebugTracing(gnDbgLevelNormalDebug,_T("CTabBarsWnd::CloseSIWindow() pGlobalActiveSIWindow = %x"),pGlobalActiveSIWindow);
hPrevActive = pGlobalActiveSIWindow->GetSafeHwnd();
m_iLockUpdates++;
}
pWindow->SendMessage(WM_CLOSE, 0, 0);//now close it!
if(hPrevActive != NULL)
{
::PostMessage(::GetParent(hPrevActive), WM_MDIACTIVATE,(WPARAM)hPrevActive, 0);
}
}
核心只有一句:
pWindow->SendMessage(WM_CLOSE, 0, 0);//now close it!
卻要圍繞它做很多事情。
再來看一個問題,有沒有考慮過Tab標籤欄上的標籤與實際開啟的文件視窗個數不一致的情況?雖然我們Hook可MDI_CREATE訊息,但是依然有一些視窗建立是TabsiPlus外掛程式無法感知的,比如Source Insight支援內建宏語言,通過宏進行視窗操作TabsiPlus外掛程式無法感知,還有一種情況是Source Insight對於一些不啟用的文檔通常不是立即建立視窗,而是在啟用的時候才建立視窗顯示文檔,當使用者通過Windows菜單看到的已經開啟的檔案與你的Tab標籤欄不一致會怎麼想?沒有好的辦法,TabSiPlus使用一個定時器處理這種不一致,具體代碼在CTabBarsWnd::OnTimer()中。
還有一個問題,如何安全地關閉外掛外掛程式?有一種方法先關閉Source Insight,然後關閉載入器TabSiHost.exe,然後再開啟Source Insight。讓自己接受這種方案都很難,更何況別人,如果能夠在外掛程式中提供一個介面,通過使用者選擇可以直接退出外掛程式就好了,實現這一點關鍵是Source Insight內部關閉Tab標籤視窗後如何中止載入器TabSiHost.exe,如果不中止TabSiHost.exe,TabSiHost.exe會再次載入TabsiPlus.dll外掛程式。TabsiPlus通過核心對象完成與TabSiHost.exe的同步:
void CTabBarsWnd::ShutDownTabSiPlus()
{
HANDLE hAnotherTabSiHostEvent = NULL;
LPCTSTR szGlobalKernelName = _T("Local//TabSiHostIsAlreadyRunning");
hAnotherTabSiHostEvent = CreateEvent( NULL, TRUE, FALSE, szGlobalKernelName );
DWORD dwer = GetLastError();
if(dwer == ERROR_ALREADY_EXISTS)
{
ResetEvent( hAnotherTabSiHostEvent );
}
::CloseHandle(hAnotherTabSiHostEvent);
g_pSiMDIClientWnd->SetManaging(false);
DestroyWindow();
}
還有,TabSiPlus內部視窗之前傳遞資料都是通過自訂訊息進行的,原因就是Tab標籤視窗與Source Insight的視窗是工作在不同的線程中的,線上程之間只有控制代碼是安全的,向視窗控制代碼發送訊息要比直接操縱資料要安全。還有,當使用了Source Insight的尋找字串功能時,Source Insight會開啟一個視窗顯示搜尋的結果,這個視窗的視窗類別名和代碼視窗一樣,都是si_Sw,但是其視窗標題卻和代碼視窗的標題不一樣,這個要區分。其它的細節還有很多,就不一樣列舉了,具體看代碼吧。
羅嗦了半天,代碼在哪裡?本來想隨本文一起上傳的,但是這個Blog上傳附件太麻煩,只好放到我的CSDN資源裡了,大家可以到我的空間下載原始碼。本文附帶的代碼是一份精簡的TabSiPlus外掛程式代碼,為了大家理解代碼,我去掉了全部裝飾性的代碼和附加功能代碼,包括很多預防性代碼,這樣做地目的就是為了大家在學習原始碼時能夠將注意力集中在架構上而不是枝節瑣事。儘管如此,這是一個完整的可工作的Tab標籤欄,示範了本文寫的全部內容。除了My Code之外,原始碼中還使用了一些自由代碼,使用時請注意相關作者的權利要求。
代碼的編譯很簡單,用VC 6.0開啟直接Build就行了,為什麼不升級到VC7 or VC8?其實我很懶。調試的時候注意相關的資源檔要在同一個目錄中,TaiSiHost.exe的調試比較簡單,直接載入就行了,調試TabSiPlus.dll比較麻煩,首先關閉已經開啟的Source Insight程式,然後在Project Setting/Debug 視窗中設定“Executable for debug session:”為Source Insight的主程式,通常是Insight3.exe,再然後運行TabSiHost.exe,最後就可以按F5開始調試了。整個過程就是:按下F5後,VC根據調試設定啟動insight3.exe,已經啟動並執行TabSiHost.exe發現啟動了Source Insight,就會遠程代碼注入到insight3.exe,於是insight3.exe就會載入TabSiPlus.dll,這樣就可以調試了。
Source Insignt檔案標籤外掛:TabSiPlus的:
http://www.winmsg.com/download/tabsiplus.zip