Coding Life

  博客园 :: 首页 :: 博问 :: 闪存 :: 新随笔 :: 联系 :: 订阅 订阅 :: 管理 ::

注:源代码的下载链接已在文末提供。    

在写了 [思考] Visual Studio 插件 GDIWatch 实现浅析 之后我一直都在想自己来实现这个 GDIWatch,上个星期的周末找了点时间来了解了一下VC6.0 的插件开发的大概过程和方法,并且翻译了另外一篇文章: [翻译] Undocumented Visual C++ (6.0),这次小假期决定先动手写一个插件让 VC 显示行号,就当是练习吧,有了这个经验应该就可以知道实现 GDIWatch 的具体思路。

很久以前我就想如果 VC6.0 能提供行号显示那就舒服多了,当时找到了一个国人在2007年写的一个插件,地址是:

http://sites.google.com/site/davidhowecn2/vc6linenumberaddin

由于国内无法访问,那就在这贴一下该页面的截图吧:

点击查看原图

顺便扯一下:第一次用上这个插件的时候我还是学生一名,当时刚开始学习编程和使用 VC,装上 VA 感觉非常爽,但是由于少了行号显示总觉得不习惯,最后就找到这个页面了,目前国内共享软件已经不太可能行得通了吧,真是光阴似箭、时过境迁啊。

闲话说完,不管是什么软件,只要是好用而又收费的,自然是免不了被破解的命运,这个插件对于现在的我来说很容易搞掂,不过我自然是和“极少数”群众那样乐于使用和谐版本软件的,所以我是直接百度搜到的,下面顺便贴个国内能访问的和谐地址:

http://tunps.com/vc6linenumberaddin

当时的我自然是不太明白这个插件具体是怎么实现的,我甚至以为 VC 必然提供了某种接口可以方便地做到这种功能,比方说查询当前行号的范围和最大行数的接口、响应 view 的滚动事件的机制等等。

当然实际并非如此,参考微软关于插件开发的文档:

Overview: Developer Studio Objects (http://msdn.microsoft.com/en-us/library/aa242700(v=VS.60).aspx)

基本上没有啥好用的接口或 event 可以帮助实现这种功能。

既然正路不好走,咱们就只好走些“歪路”了,首先要研究一下怎么能在 VC 的每个 view 旁边“画”出这些行号,而在研究怎么“画”之前,先来看看VC的窗口布局吧:

点击查看原图

可以看到其中窗口类为 “Afx:400000:8” 的就是中间负责呈现源代码的视图,最天真的想法就是设法子类化这个窗口,重载其 OnPaint 函数(即拦截 WM_PAINT 消息),先画上行号,同时还要使原本绘制的内容向右侧偏移一点,这自然是不可行的,因为VC有很多别的功能是依赖于视图的坐标的,所以不可能这么干。

我想到的方法就是创建一个自定义窗口,使其紧靠着视图,并设法调整这个自定义窗口和视图的大小和位置,然后响应“某种潜在的消息”来更新并绘制行号。

从VC的这个典型的 MDI 程序的窗口布局来看,应该是在MDI child 下创建一个和视图窗口同级的自定义控件比较适合,至于怎么创建这个控件以及创建的时机下面再说。

接下来就是要找出前面提到的“某种潜在的消息”以便通知这个自定义控件更新行号的显示,这里最应该注意的就是上图中红色方框内的垂直滚动条了,行号需要变化的时候说明滚动条也需要变化,所以可以拦截它的消息,而这个消息就是:

SBM_SETSCROLLINFO Message (Windows)

http://msdn.microsoft.com/en-us/library/bb787571(VS.85).aspx

这个消息的 lParam 是一个指向 SCROLLINFO 结构体的指针,其结构如下:

typedef struct tagSCROLLINFO {
  UINT cbSize;
  UINT fMask;
  int  nMin;
  int  nMax;
  UINT nPage;
  int  nPos;
  int  nTrackPos;
} SCROLLINFO, **LPCSCROLLINFO;

用 spy++ 观察这个垂直滚动条的消息:

就可以发现这里的 nPos 为当前在视图上显示的第一行的行号索引,这是很有用的信息。

至于要拦截这个消息,自然是需要借助于 SetWindowsHookEx 这个 API 了,由于这里关心的是窗口消息的拦截,所以调用方法是:

m_hWndProcHook = ::SetWindowsHookEx(WH_CALLWNDPROC, CallWndProcHook, NULL, ::GetCurrentThreadId());

而这个钩子函数的代码如下:

LRESULT CALLBACK CVCLineNumberModule::CallWndProcHook(int code, WPARAM wParam, LPARAM lParam)
{
	CVCLineNumberModule* pModule = (CVCLineNumberModule*)g_pAddinManager->GetModule(CVCAddinManager::AMT_LINENUMBER);
	
	LRESULT lResult = CallNextHookEx(pModule->m_hWndProcHook, code, wParam, lParam);

	if ( HC_ACTION == code )
	{
		LPCWPSTRUCT lpcWPST = reinterpret_cast<LPCWPSTRUCT>(lParam);
		if (lpcWPST)
		{
			if ( SBM_SETSCROLLINFO == lpcWPST->message )
			{
				BOOL bRedraw				= lpcWPST->wParam;
				LPSCROLLINFO pScrollInfo	= reinterpret_cast<LPSCROLLINFO>(lpcWPST->lParam);
				if ( pScrollInfo && (pScrollInfo->fMask & SIF_POS) )
				{
					HWND hParentWnd = GetParent(lpcWPST->hwnd);	// AfxMDIFrameWnd42
					if ( hParentWnd && (hParentWnd = GetParent(hParentWnd)) && IsMDIChildWnd(hParentWnd) )
					{
						int& nTopLineIndex = pScrollInfo->nPos;
						pModule->CheckUpdateLineNumberWndForMDIChildWnd(hParentWnd, nTopLineIndex, bRedraw);
					}
				}
			}
		}
	}

	return lResult;
}

回到创建自定义控件的时机的问题,其实也很简单,在拦截到 SBM_SETSCROLLINF 这个消息的时候先判断其对应的视图是否已经有行号控件,如有则更新行号,否则创建行号控件并更新之。

至于如何判断是否已有行号控件,我是采用了给这个行号控件一个特殊的窗口类名来标记的,这样只需按窗口布局搜索一下是否有这个窗口类名的就知道了。

最后,还必须知道行号字体的高度大小,否则就无法和右侧视图中显示的内容对齐了。VC 的源代码编辑器的字体配置信息是存储在注册表的这个位置的:

HKEY_CURRENT_USER\Software\Microsoft\DevStudio\6.0\Format\Source Window\

其下分别有 FontFace 和 FontSize 两个信息可供使用,首先按照这两个值创建一个字体,在行号控件的 OnPaint 中使用这个字体,同时每一行的高度也是由这个字体决定。

在创建行号控件的时候,必然需要调整视图窗口的大小以便让行号显示出来,而视图窗口又是那个窗口类为 “AfxMDIFrame42” 的子窗口,所以应该把这个包含那几个滚动条和视图窗口在内的全部窗口都调整一下宽度,同时需要注意的是行号窗口本身也需要根据当前显示行号内容而调整其宽度。为了实现这一功能,可以子类化这个  “AfxMDIFrame42”  窗口,响应 WM_WINDOWPOSCHANGING 消息,然后根据计算好的行号控件的宽度来调整其宽度以适应显示内容。

最后需要一提的是,如果取得垂直滚动条的窗口句柄然后调用 GetScrollInfo,返回的 SCROLLINFO 结构体中的 nMin 和 nMax 分别对应起始行号索引(总为零)和最大行号索引。

综合上面的内容,最终完成了下面的效果:






[Update]

2011-6-12早:汗,此代码还是有很多bug,看来还要找些时间fix。。。

2011-6-12晚:由于发现完全依赖 SBM_SETSCROLLINFO 消息来实现出来的效果不好,很多时候(比如进行粘贴、撤销等会影响行号的操作时)行号并不能即时得到正确的更新,所以现在的最新实现方法是这样的:

仿照 Undocumented Visual C++ (6.0) 一文中提到的技巧,使用其 OpenVC 插件可以发现 VC 的 view 是有下面的继承关系:

点击查看原图

结合 CheatEngine 等工具在 MSDEV.EXE 进程的内存中搜索当前视图的首行索引和最大行索引数据,可以发现下述结构:

class CLexDocTemplate;
class CAutoTextDoc;

// size == 164 (Non-derived 24)
class CPartDoc : public COleDocument
{
public:
	BYTE				m_arrBYTE[16];
	
	CLexDocTemplate*	m_pLexDocTemplate;
	
	CAutoTextDoc*		m_pAutoTextDoc;
};

// size == 180 (Non-derived 16)
class CIDEDoc : public CPartDoc
{
public:
	BYTE				m_arrBYTE[16];
};

class CDocTrackSlob;

// size == 20 (Non-derived 16)
class CSlob : public CObject
{
public:
	BYTE				m_arrBYTE1[8];
	
	CDocTrackSlob*		m_pDocTrackSlob;
	
	BYTE				m_arrBYTE2[4];	
};

// size == 24 (Non-derived 4)
class CTextDocSlob : public CSlob
{
public:
	BYTE				m_arrBYTE[4];
};

// size == 472 (Non-derived 292)
class CTextDoc : public CIDEDoc
{
public:
	// offset == 180
	BYTE				m_arrBYTE1[4];
	
	CObject*			m_pObject1;
	
	CPtrArray			m_PtrArray;			// size == 20
	
	BYTE				m_arrBYTE2[20];
	
	CTextDoc*			m_pTextDoc;
	
	// offset == 232
	BYTE				m_arrBYTE3[152];

	int					m_nMaxLineIndex;	// offset == 0x180 / 384

	BYTE				m_arrBYTE4[4];
	
	CTextDocSlob		m_TextDocSlob;		// size == 24
	
	BYTE				m_arrBYTE5[48];
	
	CLexDocTemplate*	m_pLexDocTemplate;
	
	CObject*			m_pObject2;	
};

class CPackage;

// size == 44 (Non-derived 12)
class CPack : public CCmdTarget
{
public:
	CPackage*			m_pPackage;
	
	BYTE				m_arrBYTE[8];
};

// size == 72 (Non-derived 28)
class CPackage : public CPack
{
public:
	BYTE				m_arrBYTE[28];
};

// size == 72 (Non-derived 4)
class CSlobWnd : public CView
{
public:
	CObList*			m_pObList;
};

// size == 76 (Non-derived 4)
class CPartView : public CSlobWnd
{
public:
	CPack*				m_pPack;	
};

// size == 80 (Non-derived 4)
class CIDEView: public CPartView
{
public:
	BYTE				m_arrBYTE[4];
};

class CAutoTextSel;

// size == 364 (Non-derived 284)
class CTextView : public CIDEView
{
public:
	// offset == 80
	BYTE				m_arrUnknownBYTE1[28];
	
	POINT				m_stCaretPos;
	
	BYTE				m_arrUnknownBYTE2[20];
	
	CAutoTextSel*		m_pAutoTextSel;
	
	// offset == 140
	BYTE				m_arrUnknownBYTE3[4];
	
	int					m_nTopLineIndex;			// offset == 0x90 / 144
	
	BYTE				m_arrUnknownBYTE4[12];
	
	CObject				m_Object1;					// size == 4
	
	BYTE				m_arrUnknownBYTE5[116];
	
	CObject				m_Object2;					// size == 4
	
	BYTE				m_arrUnknownBYTE6[4];
	
	CTextView*			m_pTextView;
	
	CCmdTarget			m_CmdTarget;
	
	BYTE				m_arrUnknownBYTE7[40];
};

m_nTopLineIndex 和 m_nMaxLineIndex 这两个数据分别是 CTextView 和 CTextDoc 的成员变量,这比起 拦截 SBM_SETSCROLLINFO 得到的信息可靠多了。

现在就不再依靠 SBM_SETSCROLLINFO 来更新行号,而是直接 subclass VC 的 这个 CTextView,在其 OnPaint 中通知行号窗口更新行号即可。

编译好后,运行 RegisterRelease.bat 或 RegisterDebug.bat (看名字就知道啥区别了)即可实现插件的自动“安装”(注册DLL和添加注册表信息从而不必在 VC 中手动指定插件位置启用插件),而 UnregisterRelease.bat 或 UnregisterDebug.bat 则是自动“卸载”(反注册DLL和删除注册表信息)。 

源代码下载在此:https://files.cnblogs.com/yonken/VC6Extend.zip

SVN : http://subversion.assembla.com/svn/custom-draw-control-in-mfc/VC6Extend

posted on 2011-06-06 01:37  yonken  阅读(9587)  评论(13编辑  收藏  举报