给VC增加注释/反注释的功能
——杨科 注:本文可随意转载, 但请保留我的署名(CopyLeft)
用过VB,PowerBuilder的朋友一定知道在它们的工具条上有两个按钮,用来完成对选中的代码进行注释或反注释,而在VC中如果要注释一段选中的代码,除了在要注释的代码中添加/* 和 */外,就是对每一行都使用单行注释//。其实在VC中提供了编写插件的功能,VC将开发环境中的各种对象以COM接口的形式暴露出来,而且提供的插件应用程序向导可以完成大部分的框架代码,所以我们只需要添加我们想要的功能就可以了。所以我们可以利用这些COM接口来对VC的开发环境进行操作,这样我们就可以完成对选中的代码的注释和反注释功能。
下面具体描述以下开发这个插件的过程:
首先创建一个新工程,类型选择DevStudio Add-in Widzard,填写工程名称后,单击OK继续。在接下来的页面中选中Provides a toolbar,这会使我们创建的插件具有一个工具条,不需要选中Responds to Developer Studio events,因为我们不需要对开发环境中的事件作出响应,在上面的两个文本框中,可以随便输入一些你对这个插件的描述及功能介绍,单击Finish完成向导。
对了,首先声明以下,在VC中每一个Add-in都是一个COM组件,在Add-in的向导中生成的程序是用MFC和ATL共同实现的,所以在生成的原代码中你会看到两个分别叫做theApp和_Module的全局变量。另外向导为我们生成了一个成为ICommands的接口,我们必须在这个接口添加适当的方法来完成我们需要的功能。
让我们看一下AppWidzard为我们生成了哪些类,首先我们会看到一个CCommands的类,而且其中实现了我们在上面提到的ICommands接口。一个称为CDSAddIn的类,这个类中实现两个方法OnConnection(), OnDisconnection(), 这两个方法我们不会直接在程序中调用,而是由VC集成环境调用的,当VC启动时它会首先查询关于Add-ins的注册信息,然后调用相应组件的OnConnection()方法,所以在这个方法中我们应该把我们要实现的命令添加到VC的继承环境中,当这个插件被卸载或VC关闭时,VC会调用OnDisconnection()方法,在这里我们应该释放我们在OnConnection()中分配的资源。
剩下的就是应用程序类了,另外还有一些必须的全局函数DllGetClassObject, DllCanUnloadNow, DllRegisterServer和DllUnregisterServer是COM组件的几个通用实现,可以查阅关于COM的书籍来了解这些知识。
AppWidzard生成的代码中在ICommands接口中实现了一个与应用程序同名的方法,通常这个方法名称并不是我们需要的,所以下面就开始来改写这个方法,把它改成CommentCode。双击ICommands接口中的与你应用程序同名的方法,VC会打开一个与你的应用程序同名的一个扩展名是odl的文件,找到一行叫做HRESULT YourAppCommandMethod()并把YourAppCommandMethod改成CommentCode(),然后打开Commands.cpp和Commands.h把其中的YourAppCommandMethod都改成CommentCode()。
现在我们修改DSAddIn.cpp,找到OnConnection方法,直接找到下面这断代码:
LPCTSTR szCommand = _T("YourAppCommand");
VARIANT_BOOL bRet;
CString strCmdString;
strCmdString.LoadString(IDS_CMD_STRING);
strCmdString = szCommand + strCmdString;
CComBSTR bszCmdString(strCmdString);
CComBSTR bszMethod(_T("YourAppCommandMethod"));
CComBSTR bszCmdName(szCommand);
VERIFY_OK(pApplication->AddCommand(bszCmdString, bszMethod, 0, m_dwCookie, &bRet));
并把它改成下面的样子:
LPCTSTR szCommand = _T("CommentCode");
VARIANT_BOOL bRet;
CString strCmdString;
strCmdString.LoadString(IDS_CMD_STRING);
strCmdString = szCommand + strCmdString;
CComBSTR bszCmdString(strCmdString);
CComBSTR bszMethod(_T("CommentCode"));
CComBSTR bszCmdName(szCommand);
VERIFY_OK(pApplication->AddCommand(bszCmdString, bszMethod, 0, m_dwCookie, &bRet));
由于上面的代码涉及到了字符串资源,所以我们再来看看上面提到的IDS_CMD_STRING的内容,打开这个字符串资源改成下面的形式:
\nComment Code\nComment the selected code\nComment the selected code
现在我们可以编译这个应用程序,应该不会出现错误,单击运行会弹出一个对话框要你指定一个宿主应用程序来调用这个DLL, 因为我们是为VC开发插件应用,所以找到你VC的可执行文件目录并选中msdev.exe。当另一个VC启动后,选择Tools¦Customize...,丛属性页种选择Add-ins and Macro Files标签,单击Browse按钮,选择你刚生成的DLL文件,这时旁边的列表框,会出现你编写的插件的名字,单击Close关闭对话框,这是在工具条的下面会出现一个小工具条,将鼠标移动工具条的按钮上面,会出现Comment the selected code的代码提示, 单击按钮会弹出一个对话框,这是AppWidzard对这个命令的缺省实现。
下面我们该实现我们的具体功能的代码了。由于VC把它集成环境中对象通过COM接口暴露出来,所以我们就利用这些对象来实现注释代码的功能。这里有两个问题:1、我们怎么能知道VC中当前打开的文档是否是源代码而不是图形或对话框,2、我们怎么才能知道当前源代码文档中的被选择的文本。下面我通过实际的代码来解释这两个问题。
1、查找当前打开的源代码编辑其中的文档并查找当前选中的文本。这里我写了一个函数因为在实现反注释的时候我们还需要完成相同的功能。请参阅代码中的注释(注意相应接口指针的释放):
HRESULT CCommands::GetTextSelection(IApplication *pApplication, ITextSelection** pTextSelection)
{
IDispatch* pDispatch;
//通过有集成环境传进来的应用程序对象查找当前处于活动状态的文档对象,注意返回的是一个IDispatch接口指针。
HRESULT hr = pApplication->get_ActiveDocument(&pDispatch);
//此处应注意即使get_ActiveDocument()函数成功返回pDispatch指针仍有可能为空(即VC中没有文档打开),所以此处要判 //断pDispatch是否为空
if (SUCCEEDED(hr) && pDispatch != NULL)
{
//取得IGenericDocument接口的指针,我们可以通过该接口指针来查询当前的活动文档是否是文本文档。
IGenericDocument* pDocument;
hr = pDispatch->QueryInterface(IID_IGenericDocument, (void**)&pDocument);
if (FAILED(hr))
{
pDispatch->Release();
return E_NOINTERFACE;
}
pDispatch->Release();
//在此处查询ITextDocument接口,如果能成功返回,就说明当前文档是文本文档。
ITextDocument* pTextDocument;
hr = pDocument->QueryInterface(IID_ITextDocument, (void**)&pTextDocument);
if (FAILED(hr))
{
pDocument->Release();
return E_NOINTERFACE;
}
pDocument->Release();
//现在我们有了当前的活动的文本文档,我们就可以通过get_Selection函数来获得当前选中的文本,这里要求的还是 //一个IDispatch接口的指针
hr = pTextDocument->get_Selection(&pDispatch);
if (FAILED(hr))
{
pTextDocument->Release();
return E_NOINTERFACE;
}
pTextDocument->Release();
//通过QueryInterface()方法取回ITextSelection的接口指针。
hr = pDispatch->QueryInterface(IID_ITextSelection, (void**)pTextSelection);
if (FAILED(hr))
{
pDispatch->Release();
return E_NOINTERFACE;
}
pDispatch->Release();
}
else
return E_NOINTERFACE;
return S_OK;
}
上面的方法已经解决了这两个问题,接下来的任务就是实现注释被选中的代码。请参阅下面的代码(省略了错误处理的部分代码)
HRESULT CCommands::CommentSelectedCode(ITextSelection *pTextSelection)
{
long lTopLine = -1;
long lBottomLine = -1;
long lCurLine;
long lCurColumn;
//取得被选中文本的最上面一行的行号
HRESULT hr = pTextSelection->get_TopLine(&lTopLine);
//取得被选中文本的最下面一行的行号
hr = pTextSelection->get_BottomLine(&lBottomLine);
long iLine;
CString s;
_variant_t v((long)dsMove);
//循环,针对选中的每一行,在前面加上VC的单行注释//
for(iLine = lTopLine; iLine <= lBottomLine; iLine++)
{
hr = pTextSelection->MoveTo(iLine, 1, v);
if (SUCCEEDED(hr))
{
pTextSelection->SelectLine();
BSTR bstrLineText;
hr = pTextSelection->get_Text(&bstrLineText);
if (SUCCEEDED(hr))
{
s = bstrLineText;
s = _T("//") + s;
pTextSelection->put_Text(s.AllocSysString());
}
}
}
return S_OK;
}
上面的代码就完成了对选中的代码的注释问题,下面添加对选中的代码的反注释功能,其中的代码大体相同,这里主要讲解如何间命令添加到VC的开发环境中去。
在ClassView中右键单击ICommands接口,选择Add method...,添加UncommentCode方法,参照上面的代码实现相应的功能。
打开CDSAddIn类的OnConnection方法,在添加CommentCode方法的下面添加下面的代码:
szCommand = _T("UncommentCode");
strCmdString.LoadString(IDS_CMD_UNCOMMENT);
strCmdString = szCommand + strCmdString;
bszCmdString = strCmdString;
bszMethod = _T("UncommentCode");
CComBSTR bszCmdUncommentName = szCommand;
VERIFY_OK(pApplication->AddCommand(bszCmdString, bszMethod, 1, m_dwCookie, &bRet));
然后在OnConnection()方法的下面的代码中
if (bFirstTime == VARIANT_TRUE)
{
VERIFY_OK(pApplication->AddCommandBarButton(dsGlyph, bszCmdName, m_dwCookie));
}
添加一行,如下面的代码
if (bFirstTime == VARIANT_TRUE)
{
VERIFY_OK(pApplication->AddCommandBarButton(dsGlyph, bszCmdName, m_dwCookie));
VERIFY_OK(pApplication->AddCommandBarButton(dsGlyph, bszCmdUncommentName, m_dwCookie));
}
在上面的pApplication->AddCommand()调用中,第三个参数1,指定了该命令所对应的图像在位图资源中的位置。
适当的修正工程的资源,完成的应用程序应该可以完成VB,PowerBuilder中的代码的注释和反注释功能。
另外,由于VC中的插件是通过COM技术实现的,所以能支持COM规范的语言都可以用来编写VC插件。