[动手] VC6Extend: 一个让 VC6.0 显示行号的插件
2011-06-06 01:37
633 查看
注:源代码的下载链接已在文末提供。
在写了 [思考] 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 结构体的指针,其结构如下:
用 spy++ 观察这个垂直滚动条的消息:
就可以发现这里的 nPos 为当前在视图上显示的第一行的行号索引,这是很有用的信息。
至于要拦截这个消息,自然是需要借助于 SetWindowsHookEx 这个 API 了,由于这里关心的是窗口消息的拦截,所以调用方法是:
而这个钩子函数的代码如下:
回到创建自定义控件的时机的问题,其实也很简单,在拦截到 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 进程的内存中搜索当前视图的首行索引和最大行索引数据,可以发现下述结构:
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和删除注册表信息)。
在写了 [思考] 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和删除注册表信息)。
源代码下载在此:http://files.cnblogs.com/yonken/VC6Extend.zip
SVN : http://subversion.assembla.com/svn/custom-draw-control-in-mfc/VC6Extend相关文章推荐
- VC6.0显示行号插件(含使用方法、注册方法,无使用限制)
- Win7、Win8可用中文版VC6.0及其插件VC助手、行号显示
- VC6.0显示行号插件:VC6LineNumberAddin.dll
- VC6.0 显示代码行号和WndTab插件
- VC6.0显示行号的插件
- jquery datagrid行号显示的一个小Bug
- 自己动手编写一个VS插件(八)
- easyui--datetimebox插件,下拉框的日期不能全部显示,即一个月份中的天数未全部显示
- 自己动手写一个jQuery插件(第二篇)
- 利用JQuery及其插件做出一个上传图片及利用prototype插件显示剪裁图片的例题
- VC6在Win7下使用显示行号的插件
- win7旗舰版安装VC++6.0番茄插件和行号显示
- [置顶] 自己动手写一个Android Studio插件
- 自己动手编写一个VS插件(四)——创建工具栏之二
- Jquery 类似新浪微博,鼠标移到头像,用浮动窗口显示用户信息,已做成一个jquery插件
- linux 设置gedit文本编辑器-字体颜色、显示行号、插件、制表符等
- 自己动手编写一个VS插件(六)
- VC6中显示行号的插件
- Jquery一个显示当前时间的简单插件
- 自己动手写一个前端路由插件