65.9K
CodeProject 正在变化。 阅读更多。
Home

Source Insight 的文件切换标签栏

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.94/5 (11投票s)

2009年1月4日

CPOL

6分钟阅读

viewsIcon

320639

downloadIcon

2187

为 Source Insight 注入一个类似 uEdit 或 Visual Studio 的文件切换标签栏


tabsiplus.gif

介绍 

本文介绍的是 TabSiPlus,一个用于 Source Insight 的增强程序。Source Insight 是一个很棒的工具,许多程序员,甚至包括一些 Visual Studio 用户,都使用 Source Insight 作为默认的编码工具。Source Insight 有许多功能,可以方便程序员查看和编写代码。我喜欢这些功能,但我认为它缺少一个重要的功能,那就是文件切换标签栏。人们可能会好奇地问“文件切换标签栏是什么玩意儿”?抱歉,我真的不知道该怎么命名它,许多软件都支持这个组件,但给它的名字却各不相同。一张图胜过千言万语,请看下图,红线圈起来的东西就是我所说的“文件切换标签栏”。

ueditor.gif
visualStudio.gif


有人可能会说,使用菜单栏上的“窗口”菜单项可以实现文件切换功能,但我想要一种更快速、更直接的方式,一个小小的文件切换标签栏正合我意。不幸的是,Source Insight 并不支持这个功能栏,即使是最新的 3.5.x 版本也没有。我是一个完美主义者(但只是个伪完美主义者),和许多懒惰的程序员一样,我会自己制作工具来满足我的懒惰,所以我做了一个名为 TabSiPlus 的增强工具。它为 Source Insight 窗口插入一个切换栏,用户只需单击鼠标即可切换文件,这正是我想要的。

工作原理

为一个 MDI(多文档界面)程序添加切换栏其实不是一件难事,这个网站上有很多文章(附有示例代码)可以帮助程序员为他们的程序添加切换栏,但不幸的是,我没有 Source Insight 的源代码。唯一的方法是将我的代码注入到 Source Insight 的主进程中,对于 3.x 版本,这个进程通常是 insignt3.exe。由于本文的重点不是如何向远程进程注入代码,所以对代码注入感兴趣的朋友可以在本站搜索文章“向远程进程注入代码的三种方法”以获取更详细的信息。

用于 Source Insight 的 TabSiPlus 包含两个组件:“TabSiHost.exe”和“TabSiPlus.dll”。“TabSiHost.exe”是一个小型的启动程序,它的职责是监视系统,寻找新的 Source Insight 程序实例,然后将包含在“TabSiPlus.dll”中的代码注入到 insignt3.exe(针对 Source Insight 3.x)中。“TabSiPlus.dll”包含了所有的功能代码,它不能自行运行,需要 TabSiHost.exe 将其加载到 insignt3.exe 进程上下文中。

在 Windows 系统上有几种方法可以找到 Source Insight 程序的新实例,“TabSiHost.exe”使用了最简单的方法:枚举系统上的所有窗口,并根据特定的类名或窗口标题进行过滤。Source Insight (insignt3.exe) 的主窗口有一个固定的类名“si_Frame”,所以“TabSiHost.exe”只关注类名为“si_Frame”的窗口。为安全起见,“TabSiHost.exe”还会检查窗口的标题,寻找“Source Insight”这个字符模式。这里是代码,FindSourceInsightFrameWindow 和 EnumWindowsProc 函数的职责是枚举所有窗口,而 IsSourceInsightFrameWnd 函数的职责是根据特定的类名和窗口标题来过滤窗口。

			//
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;

	//class name is si_Framem and windows title has "Source Insignt"
	if((lstrcmp(lpszSiFrameWndClass,szClassName) == 0) && (StrStr(szTitle,lpszSourceInsight) != NULL))
	{
		if(StrStr(szTitle,lpszTextMark) != NULL)//Is windows had been Hooked?
			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;//find Source Insight program window
		}
	}

	return bSuccess;
}

HWND FindSourceInsightFrameWindow()
{
	HWND hSiFrmWnd = NULL;
	
	BOOL bRtn = ::EnumWindows(EnumWindowsProc,(LPARAM)&hSiFrmWnd);
	if(!bRtn && hSiFrmWnd != NULL)
		return hSiFrmWnd;
	else
		return NULL;
}

我们应该注意避免对一个 Source Insight 程序窗口进行两次或多次挂钩,重复挂钩单个窗口会导致未知的行为。IsSourceInsightFrameWnd 函数使用了一个小技巧来过滤掉已经被挂钩的 Source Insight 程序窗口,那就是:只有当 Source Insight 程序窗口的标题中没有“ with TabSiPlus”这个字符串标记时,才认为它未被挂钩。当 TabSiPlus 被注入到 Source Insight 进程中时,它会通过添加一个附加字符串“ with TabSiPlus”来修改 Source Insight 程序的窗口标题,我稍后会说明这一点。

TabSiPlus 程序使用 CreateRemoteThread API 在 insignt3.exe 进程上下文中加载并启动“TabSiPlus.dll”,让我们看看它是如何工作的。“TabSiPlus.dll”是一个普通的 Windows DLL(动态链接库),使用了一些 MFC(微软基础类库)的特性。当 TabSiPlus.dll 被加载到 insignt3.exe 进程上下文中时,CTabSiPlusApp 类的构造函数会首先被调用,然后是 CTabSiPlusApp::InitInstance() 函数,这是 MFC 的机制,我们可以在这两个函数中放入一些初始化代码。最后是导出函数 Initialize(),它由远程线程调用,由于这个远程线程在函数调用后就会结束,所以这是在 insignt3.exe 进程上下文中创建本地线程并启动我们 UI 的最后机会。下面是 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() 是一个存根函数,它调用 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);//get current Project path from registry

	DebugTracing(gnDbgLevelNormalDebug,_T("CTabSiPlusApp::Initialize, %s"),g_szCurDircetory);

	//Create main UI Thread
	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() 创建一个本地线程,这很重要,因为 CTabSiPlusApp::Initialize() 是在远程线程的上下文中运行的,它会在这个函数调用后结束。TabSiPlus 程序需要挂钩 Source Insight 窗口的内部窗口消息,并创建一个文件切换标签栏作为 Source Insight 主窗口的子窗口,所以它需要创建另一个本地线程。Source Insight 是一个标准的 MDI(多文档界面)程序,insight3.exe 拥有一个主框架窗口(类名为 si_Frame),这个主框架窗口包含一个标准的 MDI 客户端窗口(类名为 MDIClient),而这个客户端窗口是子框架窗口(类名为 si_Sw)的容器,所有这些窗口的类名都是固定的,它们的窗口继承关系如下:

wnd_class.gif

本地线程的 InitInstance() 函数会做一些窗口挂钩操作,并创建一个文件切换标签栏窗口。首先,TabSiPlus 程序找到并挂钩主框架窗口,有三个目的:枚举所有子窗口以找到 MDI 客户端窗口,挂钩 WM_SETTEXT 消息以修改窗口标题(添加“ with TabSiPlus”),以及挂钩 WM_DESTROY 消息以在主框架窗口销毁前获得销毁标签栏窗口的机会。其次,TabSiPlus 程序挂钩 MDI 客户端窗口。这个窗口有四个消息需要被挂钩,它们是 WM_WINDOWPOSCHANGING、WM_MDICREATE、WM_MDIDESTROY 和 WM_MDIACTIVATE。挂钩 WM_WINDOWPOSCHANGING 很重要,TabSiPlus 程序修改窗口坐标以“偷取”一些空间来显示文件切换标签栏窗口。挂钩 WM_MDICREATE、WM_MDIDESTROY 和 WM_MDIACTIVATE 让 TabSiPlus 程序有机会了解子代码视图窗口(类名为 si_Sw)的状态,并改变标签栏的状态,如添加、删除或高亮一个文件切换标签。最后,TabSiPlus 程序挂钩所有子代码视图窗口。对于每个代码视图窗口,TabSiPlus 程序挂钩 WM_GETTEXT 和 WM_WINDOWPOSCHANGING 消息。下面是 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(); //CWnd::FromHandle(hSiFrameWnd);
	g_pSiFrameWnd->Attach(hWndSIFrame);

	HWND hMDIWnd = g_pSiFrameWnd->GetMDIClientWnd(); //get MDI client window
	
  // create the tabs window
	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;//this is important
	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 程序通过挂钩 WM_WINDOWPOSCHANGING 消息来从 Source Insight 窗口“偷取”一些空间,现在是时候显示标签栏窗口并填充这块空间了。标签栏窗口是一个普通的 rebar 窗口,包含两个带区,一个是工具栏按钮,另一个是 SysTabControl。

全部内容就是这些了,查看代码以获取更多细节。

关于代码

代码是用 C++ 编写的,使用 VC6 作为编译工具。要编译源代码,请确保你已经安装了 Visual Studio 6 的 SP6 和 Platform SDK。代码仅在 Windows XP SP2 和 SP3 下测试通过,应该也能在 Windows 2000 和 2003 上正常工作,Vista 嘛?也许可以。

© . All rights reserved.