Introduction
This article is about the TabSiPlus, a Plus program for Source Insight. Source Insight is a great tool, lot's of programmer, even some Visual Studio user, using Source Insight as default coding tool. Source Insight has a lot of features to easy the programmers way for viewing and writing code. I like those features, but I think it missing an important one, that is File Switch Tab. People may wondering and asking "what's the File Switch Tab going on"? Sorry, I really don't known how to name it, many software support this component but give it a very different name. One picture better than thousands of words, look the following pictures, stuffs which encircle by red line is what I called "File Switch Tab".
People may argue that using "windows" menu item on menu bar can perform file switching function, but I want a fast and direct way, a small file switch table bar is the bulls eye. Unluckily, Source Insight does not support this bar, even the most recently 3.5.x version. I am a completist (but a fake completist), like many lazy programmers, I make tools myself to cater for my laziness, so I made a plus in tools named TabSiPlus. It insert a switch bar to Source Insight window, user can switch files just a single mouse click on it, that's what I want.
How it works
Adding a switch bar to a MDI(Multi-Document Interface) program is really not a hard work, there are many articles (with example codes) on this web site which help programmers add switch bar to their program, but unfortunately, I don't have source code of Source Insight. The only way is injecting my code to main process of Source Insight, for 3.x version, generally, it is insignt3.exe. Since the emphases of this article is not about how to inject code to remote process, so people who wilder about code injecting can search the article "Three ways to inject your code to remote process" on this site for more detail information.
TabSiPlus for Source Insight has two components: "TabSiHost.exe" and "TabSiPlus.dll". "TabSiHost.exe" is a little stub program, it's duty is spy on the system and find new instance of Source Insight program, than inject the code contain in "TabSiPlus.dll" to insignt3.exe (for Source Insight 3.x). "TabSiPlus.dll" contain the whole function code, it can't run itself, it need TabSiHost.exe to load it to insignt3.exe process context.
There are several ways to find new instance of Source Insignt program on windows system, "TabSiHost.exe" using the simplest way: enumerating all windows on system and filtering them by certian class name or windows title. Source Insight(insignt3.exe) main windows has a fixed class name "si_Frame", so "TabSiHost.exe" put interest only in window that class name is "si_Frame". For safe reason, "TabSiHost.exe" also check the window's title and look for characters pattern: Source Insight. Here is the code, the duty of FindSourceInsightFrameWindow and EnumWindowsProc function is enumerating all windows and the duty of IsSourceInsightFrameWnd function is filtering the window with special class name and windows title.
LPCTSTR lpszSourceInsight = _T("Source Insight");
LPCTSTR lpszSiFrameWndClass = _T("si_Frame");
LPCTSTR lpszTextMark = _T(" with TabSiPlus");
BOOL IsSourceInsightFrameWnd(HWND hWnd)
{
TCHAR szClassName[128],szTitle[256];
int nRtn = GetClassName(hWnd,szClassName,128);
if(nRtn == 0)
return FALSE;
nRtn = GetWindowText(hWnd,szTitle,256);
if(nRtn == 0)
return FALSE;
if((lstrcmp(lpszSiFrameWndClass,szClassName) == 0) && (StrStr(szTitle,lpszSourceInsight) != NULL))
{
if(StrStr(szTitle,lpszTextMark) != NULL) return FALSE;
return TRUE;
}
return FALSE;
}
BOOL CALLBACK EnumWindowsProc(HWND hwnd,LPARAM lParam)
{
BOOL bSuccess = TRUE;
if(hwnd != NULL && IsSourceInsightFrameWnd(hwnd))
{
if(lParam)
{
HWND *pHwnd = (HWND *)lParam;
*pHwnd = hwnd;
bSuccess = FALSE; }
}
return bSuccess;
}
HWND FindSourceInsightFrameWindow()
{
HWND hSiFrmWnd = NULL;
BOOL bRtn = ::EnumWindows(EnumWindowsProc,(LPARAM)&hSiFrmWnd);
if(!bRtn && hSiFrmWnd != NULL)
return hSiFrmWnd;
else
return NULL;
}
We should pay attention to avoid hooking a Source Insight program windows twice or more, duplicate hooking a single window would result in unknown behavior. IsSourceInsightFrameWnd function using a little trick to filter the Source Insight program window which had been hooked, that is, Source Insight program window had been hooked only it's window title has no string mark: " with TabSiPlus". When TabSiPlus was inject into a Source Insight process, it modified the Source Insight program window title by adding a pad string " with TabSiPlus", I'll discover it later.
TabSiPlus program using CreateRemoteThread API to load and start "TabSiPlus.dll" in insignt3.exe process context, let's look how it works. "TabSiPlus.dll" is a normal windows DLL(Dynamic-Link Library) which using some MFC(Microsoft Foundation Class) features. When TabSiPlus.dll has been loaded into insignt3.exe process context, the constructor of class CTabSiPlusApp would be invoked first, and than is CTabSiPlusApp::InitInstance() function, that's is the mechanism of MFC, we can put some initialization code in this two functions. The last is export function Initialize(), it invoked by remote thread, since that this remote thread will be end after this function call, so it is the last chance to create a local thread under insignt3.exe process context and start our UI. Here is the detail of Initialize():
TABSIPLUSDLL_API BOOL WINAPI Initialize()
{
AFX_MANAGE_STATE(AfxGetStaticModuleState());
DebugTracing(gnDbgLevelNormalDebug, _T("TabSiPlus.dll> Initialize() enter") );
BOOL res = theApp.Initialize();
DebugTracing(gnDbgLevelNormalDebug, _T("TabSiPlus.dll> Initialize() returned %d"), res );
return res;
}
Initialize()
is a stub function, it call a same name function in class CTabSiPlusApp.
BOOL CTabSiPlusApp::Initialize()
{
DebugTracing(gnDbgLevelNormalDebug,_T("CTabSiPlusApp::Initialize() Start"));
if(IsAnotherTabSiPlusDll())
return FALSE;
InitGlobalVar();
::GetCurrentDirectory(MAX_PATH,g_szCurDircetory);
GetCurrentSiProjectPath(g_szCurProjectPath, MAX_PATH);
DebugTracing(gnDbgLevelNormalDebug,_T("CTabSiPlusApp::Initialize, %s"),g_szCurDircetory);
g_pTabWndUIThread = (CTabWndUIThread *)AfxBeginThread(RUNTIME_CLASS(CTabWndUIThread),THREAD_PRIORITY_NORMAL,0,0,NULL);
bInitialized = TRUE;
DebugTracing(gnDbgLevelNormalDebug,_T("CTabSiPlusApp::Initialize() end"));
return TRUE;
}
CTabSiPlusApp::Initialize()
create a local thread, this is important, because CTabSiPlusApp::Initialize()
is running in the context of remote thread, it would be end after this function call. TabSiPlus program need hook internal windows message of Source Insight window and create a file switch table bar as child window of Source Insight main window, so it need create another local thread. Source Insight is a standard MDI(Multi-Document Interface) program, insight3.exe hold a main frame window (class name is si_Frame), this main frame window hold a standard MDI(Multi-Document Interface) client window (class name is MDIClient), and this client window is a container of child frame window (class name is si_Sw), all these window's class name is fixed, there is the window inheriting relation:
The InitInstance()
function of local thread would do some windows hook and create a file switch table bar window. Firstly, TabSiPlus program find and hook main frame window, for three purpose: enumerating all child window to find MDI Client window, hook WM_SETTEXT message to modified window's title (add " with TabSiPlus") and hook WM_DESTROY message to get a chance to destroy tabbar window before main frame window destroyed. Secondly, TabSiPlus program hook MDI client window. Four messages of this window should be hooked, there are WM_WINDOWPOSCHANGING, WM_MDICREATE, WM_MDIDESTROY and WM_MDIACTIVATE. hook WM_WINDOWPOSCHANGING is important, TabSiPlus program modify the window coordinate to steal some space to display file switch table bar window. Hook WM_MDICREATE, WM_MDIDESTROY and WM_MDIACTIVATE let TabSiPlus program have a chance to know the status of child code view window (class name is si_Sw) and change table bar status, add, delete or highlight a file switch table. Lastly, TabSiPlus program hook all child code view window. For each code view window, TabSiPlus program hook WM_GETTEXT and WM_WINDOWPOSCHANGING message. Here is the detail of CTabWndUIThread::InitInstance()
:
BOOL CTabWndUIThread::InitInstance()
{
AFX_MANAGE_STATE(AfxGetStaticModuleState());
BOOL bSuccess = FALSE;
SetThreadNativeLanguage();
HWND hWndSIFrame = FindSourceInsightFrameWindow();
if(hWndSIFrame == NULL)
return FALSE;
DWORD dwSIPID = 0;
GetWindowThreadProcessId(hWndSIFrame,&dwSIPID);
if(dwSIPID != GetCurrentProcessId())
return FALSE;
g_pSiFrameWnd = new CSIFrameWnd(); g_pSiFrameWnd->Attach(hWndSIFrame);
HWND hMDIWnd = g_pSiFrameWnd->GetMDIClientWnd();
m_pTabbarWnd = new CTabBarsWnd();
m_pTabbarWnd->Create(CWnd::FromHandle(g_pSiFrameWnd->GetSafeHwnd()),
RBS_BANDBORDERS | RBS_AUTOSIZE | RBS_FIXEDORDER | RBS_DBLCLKTOGGLE,
WS_CHILD | WS_VISIBLE | WS_CLIPCHILDREN | WS_CLIPSIBLINGS | CBRS_TOP | CBRS_SIZE_FIXED, AFX_IDW_REBAR );
m_pMainWnd = m_pTabbarWnd; g_pSiFrameWnd->SetTabbarWnd(m_pTabbarWnd->GetSafeHwnd());
g_MdiChildMng.SetTabbarWnd(m_pTabbarWnd->GetSafeHwnd());
m_pTabbarWnd->SetWindowPos(CWnd::FromHandle(hMDIWnd)->GetWindow(GW_HWNDPREV), 0, 0, 0, 0, SWP_NOMOVE | SWP_NOSIZE);
g_pSiMDIClientWnd = new CSiMDIWnd();
g_pSiMDIClientWnd->SetTabbarWnd(m_pTabbarWnd->GetSafeHwnd());
DebugTracing(gnDbgLevelNormalDebug,_T("MDI Client Attach..."));
g_pSiMDIClientWnd->Attach(hMDIWnd);
DebugTracing(gnDbgLevelNormalDebug,_T("MDI Client Enum..."));
g_pSiMDIClientWnd->EnumMdiChildWnd(g_MdiChildMng,TRUE);
DebugTracing(gnDbgLevelNormalDebug,_T("MDI Client Enum end (%d)"),g_MdiChildMng.GetChildCount());
pGlobalActiveSIWindow = g_MdiChildMng.LookupMdiChild(g_pSiMDIClientWnd->MDIGetActive(NULL));
return TRUE;
}
TabSiPlus program hook WM_WINDOWPOSCHANGING message to steal some space from Source Insight window, now it's time to display table bar window and fill this space. Table bar window is a normal rebar window contain two bands, one is a toolbar button and the other is a SysTabControl.
Here is all, check out the code for more detail.
About The Code
Code is written in C++, using VC6 as compile tool. To compile the source code, make sure you have SP6 for Visual Studio 6 and Platform SDK installed. The Code just tested under Windows XP with SP2 and SP3, it should be work fine with Windows 2000 and 2003, Vista? maybe.