导语:相信使用过Visual Studio的小伙伴都感受过VS向导的强大,通过应用程序向导,我们可以很方便地搭建应用程序,通过代码向导,可以大大提高我们编写代码的效率。但VS的内置向导模板有时候并不能满足我们一些特殊场景的需求,比如基于第三方库的程序,每次都要手动配置一堆配置,编写重复的框架代码,Copy-Paste大法又容易犯错。本文介绍了Visual Studio扩展自定义向导的完整步骤以及核心的技术要点,通过自定义向导,可以简化许多场景下的环境配置以及框架搭建操作。
VS的自定义向导,同样可以通过VS本身来开发,而且VS也为向导工程提供了向导来生成所需要的基本框架(有点类似编译器的自举)。打开VS,点击File->New Project,选择Visual C++节点(本文主要讨论C++项目的自定义向导,VS同样支持其他语言自定义向导),点击Custom Wizard,设置好相关的信息后,点击Finish,一个向导工程就创建完毕了:
这时,我们打开一个新的VS,再打开New Project页面,会发现Templates列表中已经出现了我们自定义的向导:
当前的向导已经具备创建一个空的工程的能力,我们还需要了解很多相关内容才能编写出功能强大的自定义向导。
下图显示了一个命名为MyWizard的向导工程的默认文件:
总体来说,这些文件会在VS的向导引擎中被用到,大致流程及相关文件如下:
文件有点多,我们一个一个介绍。
HTML Files:这个文件夹中有一个命名为default.htm的文件,这个文件定义了我们的向导与用户的交互对话框,VS中使用Design预览下这个htm文件(VS2005的预览功能实在太差,截图使用的是VS2015的预览窗口):
这个对话框,其实是一个基于HTML的Dialog,上面的控件与布局,都是通过HTML来描述的,默认生成的页面效果如下图:
通过defaul.htm,我们可以提供一些自定义选项给用户来自定义自己的工程配置(比如各种工程配置,生成的文件的名字等各种VS能够提供的几乎所有功能),对于熟悉HTML的同学,编写这个文件几乎毫无障碍,默认的htm文件已经给出了很全面的范例(不过还是要吐槽一下布局,table套table,看得人眼花缭乱,还好VS有强大的设计编码拆分窗口)。如果你的向导不需要用户自定义配置,那么default.htm不是必须的,在建立向导工程时,去掉User interface的勾选框,这个default.htm就不会生成,用户在New Project点击OK的时候,会直接执行后边的工程创建操作。
需要特别提到的是,在HTML文件中,可以使用<SYMBOL></SYMBOL>声明一些符号:
<SYMBOL NAME='SAMPLE_CHECKBOX' TYPE=checkbox VALUE=true></SYMBOL>
这些符号对应了相关控件的默认值,比如NAME为SAMPLE_CHECKBOX的SYMBOL定义了ID为SAMPLE_CHECKBOX的值为TRUE:
<INPUT CLASS="CheckBox" TYPE="checkbox" ID="SAMPLE_CHECKBOX">
那么这个勾选框控件在展示的时候就是默认勾选的状态。
后边在脚本文件中,我们可以通过相关的语句去读取这个值(详见后边的Script Files的介绍:var bCheck= wizard.FindSymbol('SAMPLE_CHECKBOX');用户勾选了Sample checkbox,会返回true,否则就返回false。
在这里我们可以编辑HTML来设置GTEST相关的一些选项,比如是否生成测试类的某些方法以及配置、属性继承:
Image Files:这个目录可以放置我们在向导default.htm中使用的自定义图片资源。注意到最外边有一些gif文件,这些是生成的默认向导工程所使用的图片文件。
Miscellaneous Files:这里边比较重要的几个文件是:.ico、.vsdir、.vsz和Templates.inf文件。
(1).ico文件其实就是我们在New Project对话框看到的模板项目的图标,替换这个文件就可以改变在New Project对话框中的图标。
(2).vsdir是一个纯文本文件,文件内容如下:
GoogleTestProject.vsz| |GoogleTestProject|1|TODO: Wizard Description.| |6777| |GoogleTestProject.
.vsdir是Visual Studio Shell程序与向导项目中的项之间提供路由服务的文本文件,其中包含了很多的字段,以 “|”分隔(微软官方文档VSDir 文件组件给出了每个字段的含义)。其中,我们一般只需要关注TODO的部分和最后一个字段,TODO这个是我们的向导在New Project界面显示的描述字段,最后一个字段是工程的默认名称Name。
(3).vsz文件标识向导引擎并提供上下文和可选的自定义参数,它也是一个纯文本文件:
VSWIZARD 7.0
Wizard=VsWizard.VsWizardEngine.8.0
Param="WIZARD_NAME = GoogleTestProject"
Param="ABSOLUTE_PATH = D:\Code\VSWizard\GoogleTestProject\GoogleTestProject"
Param="FALLBACK_LCID = 1033"
头两行表示了向导版本号以及向导引擎的版本,不同版本不能混用,对于VS2005为:
VSWIZARD 7.0
Wizard=VsWizard.VsWizardEngine.8.0
VS2015则是:
VSWIZARD 7.0
Wizard=VsWizard.VsWizardEngine.14.0
自定义参数的格式为:
Param="<PARAM_NAME> = <PARAM_VALUE>"
VS中本身会有一些内置的参数,具体可查阅微软官方文档:向导 .vsz 文件中的自定义参数。
WIZARD_NAME这个定义了向导的名称,在向导PATH未指定的时候,向导会根据这个名字取查找相关文件;
ABSOLUTE_PATH告诉引擎在什么位置去查找相关的向导文件,一般在开发阶段会使用这个ABSOLUTE_PATH方便调试,后边部署的时候需要删掉或者使用RELATIVE_PATH(相对路径);
FALLBACK_LCID是指本地化相关的配置,1033表示英文(我用的是英文版VS所以这里是1033),2052表示简体中文,后边部署的时候会讲到相关的本地化问题。
(4)Templates.inf文件用来配置哪些文件需要拷贝到工程中,它也是一个纯文本文件,我们可以在Template Files下新建一些我们想要拷贝到新工程的文件,然后在Templates.inf中添加新行就可以了。inf文件本身也是一个模板文件,可以使用模板指令(如[!if] [!else]等,参见后文Template Files介绍)。
Script Files:前面说到htm文件定义了与用户的交互界面,那么default.js便是用来定义相关的事件响应逻辑,当用户在自定义配置对话框点击完成之后,后边的处理都会交给这个js文件来完成,我们重点关注OnFinish这个函数,用户点击完成之后,这个函数会被调用,默认生成的代码。很清晰,首先,通过wizard(向导的内置对象)获取用户设置的工程路径以及工程的名字,然后调用CreateCustomProject创建工程(实际上是创建了一个基本的.vcproj文件),然后添加相关的配置,设置文件分类(定义哪些属于头文件、源文件、资源文件),然后根据.inf文件渲染创建一个临时的.inf文件,将Template Files中的文件拷贝到我们新建的工程中,删除临时的.inf文件,最后保存新建的工程。
第一眼去读这些函数,你会觉得很莫名其妙,凭空就能使用的对象和函数是从哪来的?其实,对于使用js来操作VS,是属于VS扩展模型的一部分,它底层基于COM,使用C++、C#、JS等都能够进行操作(各类VS扩展插件都是基于该模型编写),具体参考微软文档:扩展 Visual Studio 环境。
在VS安装目录下,比我的机器上的VS2005英文版:C:\Program Files (x86)\Microsoft Visual Studio 8\VC\VCWizards\1033\common.jsC:\Program Files (x86)\Microsoft Visual Studio 8\VC\VCWizards\1033\Script.js。文件中封装了大量的相关函数,可以直接在default.js中使用,其实在htm文件中,我们可以看到,它们会被提前加载进来:
<SCRIPT>
var strPath = window.external.FindSymbol("PRODUCT_INSTALLATION_DIR");
strPath += "VCWizards/";
strPath += window.external.GetHostLocale();
var strScriptPath = strPath + "/Script.js";
var strCommonPath = strPath + "/Common.js";
document.scripts("INCLUDE_SCRIPT").src = strScriptPath;
document.scripts("INCLUDE_COMMON").src = strCommonPath;
</SCRIPT>
VS向导的向导为了示范如何使用wizard、dte等内置对象,所以生成的default.js比较复杂,其实对于一般的需求,使用Common.js中的函数就可以完成绝大部分功能,我重写了一下,以下代码框架足够使用:
// 向导完成按钮回调 必须实现
function OnFinish(selProj, selObj)
{
try
{
// 读取工程的路径和名称
var strProjectPath = wizard.FindSymbol('PROJECT_PATH');
var strProjectName = wizard.FindSymbol('PROJECT_NAME');
// 创建工程 并初始化通用配置
selProj = CreateProject(strProjectName, strProjectPath);
AddCommonConfig(selProj, strProjectName);
SetupFilters(selProj);
// 添加自定义的额外配置
AddConfig(selProj, strProjectName);
// 根据Templates.inf配置渲染模板
AddFilesToNewProjectWithInfFile(selProj, strProjectName); // 保存工程
selProj.Object.Save();
}
catch(e)
{
if (e.description.length != 0)
SetErrorInfo(e);
return e.number
}
}
function AddConfig(proj, strProjectName)
{
try
{
// Debug配置
var config = proj.Object.Configurations('Debug');
var CLTool = config.Tools('VCCLCompilerTool');
// TODO: Add compiler settings
var LinkTool = config.Tools('VCLinkerTool');
// TODO: Add linker settings
// Release配置
config = proj.Object.Configurations('Release');
var CLTool = config.Tools('VCCLCompilerTool');
// TODO: Add compiler settings
var LinkTool = config.Tools('VCLinkerTool');
// TODO: Add linker settings
}
catch(e)
{
throw e;
}
}
// 模板文件名转换回调 必须实现
function GetTargetName(strName, strProjectName, strResPath, strHelpPath)
{
try
{
var strTarget = strName;
if (strName == 'readme.txt')
strTarget = 'ReadMe.txt';
// 把文件名为root的部分替换为工程名
if (strName.substr(0, 4) == "root")
{
strTarget = strProjectName + strName.substr(4);
}
return strTarget;
}
catch(e)
{
throw e;
}
}
// 文件属性设置回调 必须实现
function SetFileProperties(projfile, strName)
{
return false;
}
通过查阅Common.js中的代码,我们可以学习到更多相关的对象以及函数的使用,也可以了解到inf文件一些常见的指令前缀(比如CopyOnly、OpenFile等),这里就不展开说明了。
Template Files:这个文件夹存放了我们向导需要拷贝到新工程的所有模板文件,它们可以是.h、.cpp代码文件,也可以是.ico、.txt、.rc等资源文件,任何想要生成在工程中的文件都可以放到这里,Template Files一般需要配合Templates.inf来完成相关的渲染与拷贝操作。因为用户创建一个工程的时候,难免会带上一些自定义的参数,比如使用过MFC向导的同学应该知道,我们可以指定生成的类的文件名、是否使用ATL、是动态链接还是静态链接到MFC库、使用多字节字符集还是使用Unicode字符集等,这就使得模板文件不可能是一成不变的,文件的内容必然会有变量然后有类似宏替换的操作,VS向导引擎提供了一些模板文件和Templates.inf文件中可以使用的模板指令,来完成这些需求:
指令 | 说明 |
---|---|
[ !if ] | 开始控制结构以检查条件。 |
[ !else ] | [ !if ] 控制结构的组成部分。检查另一个条件。 |
[ !endif ] | 结束 [!if] 控制结构的定义。 |
[ !output ] | 可通过下列两种方式使用:[ !output "string" ] 提供字符串。[ !output SYMBOL_STRING ] 提供符号 SYMBOL_STRING 的值。 |
[ !loop ] | 可通过下列两种方式使用:[ !loop = 5 ][ !loop = NUM_OF_PAGES ],其中 NUM_OF_PAGES 是具有数值的符号。 |
[ !endloop ] | 结束循环结构。 |
[ !loop ]可通过下列两种方式使用:
[ !endloop ]结束循环结构。
比如我们编写一个GTEST测试类的向导,用户可以有选择的生成或者不生成一些方法,那么模板文件可以这样编写:
#include <gtest/gtest.h>
class [!output PROJECT_NAME]
: public testing::Test
{
public:
[!output PROJECT_NAME]();
~[!output PROJECT_NAME]();
[!if GENERATE_SETUP_TEARDOWN]
public:
virtual void SetUp();
virtual void TearDown();
[!endif]
[!if GENERATE_SETUPCASE_TEARDOWNCASE]
public:
static void SetUpTestCase();
static void TearDownTestCase();
[!endif]
};
[!output PROJECT_NAME] 是一条输出语句,表示测试类的名称就是工程的名字。[!if GENERATE_SETUP_TEARDOWN] [!endif] 是一条判断语句,中间包含了SetUp()/TearDown()方法,如果GENERATE_SETUP_TEARDOWN这个符号(可以在htm文件中定义)为true,那么代表需要生成SetUp()/TearDown()成员函数,反之就不会生成该成员函数,对于SetUpTestCase()和TearDownTestCase()同理。
向导工程其实没有编译生成的概念,因为所有的文件都是以脚本形式存在,向导的调试主要集中在default.js文件,VS强大的调试功能在此时同样能够派上用场,官方的文档对于JS调试给出的方案其实是针对ASP.NET下的,所以那些所谓的在IE中去除禁用调试选项以及设置调试cookie纯粹是在误导(我也不知道为什么微软要把这些帮助链接关联在一起,也许是机器人干的吧)。
其实调试向导很简单,新开一个VS,然后在编写向导的VS中点击Debug->Attach to Process,Attach to类型选择Script(这一步很关键,选错类型断点会无效):
进程选择新开的那个VS进程:
点击Attach,即可关联调试进程,然后在default.js中掐断点,在被调试VS中新建我们的GoogleTestProject类型工程,点击OK后,如果有断点触发,我们可以在编写向导的VS中查看各种调试信息,比如变量、函数栈等,后续的操作就跟平常调试代码一样了:
自定义向导的部署本质上只需要拷贝文件到相应的目录,假设VS(以VS2005英文版为例)安装在以下目录:
C:\Program Files (x86)\Microsoft Visual Studio 8\
那么相应的向导文件存放下以下两个目录下:
C:\Program Files (x86)\Microsoft Visual Studio 8\VC\vcprojects。
C:\Program Files (x86)\Microsoft Visual Studio 8\VC\VCWizards。
vcprojects存放.ico、.vsdir和.vsz文件,这个目录下可以新建子目录,新建的子目录会在New Project对话框中表现为一个新的分类。
例如我们将GoogleTestProject.ico、GoogleTestProject.vsdir和GoogleTestProject.vsz拷贝到以下目录:
C:\Program Files (x86)\Microsoft Visual Studio 8\VC\vcprojects\GoogleTestProject。
在New Project对话框中看到的效果就是:
VCWizards目录存放HTML、Images、Scripts、Templates等文件夹及相关文件,比如对于GoogleTestProject,我们可以把相关文件放置到以下目录:
C:\Program Files (x86)\Microsoft Visual Studio 8\VC\VCWizards\GoogleTestProject。
对于编写好的向导,在部署集成到VS中时,需要修改.vsz文件中的ABSOLUTE_PATH字段,一般直接删除掉,如果有特殊需要可以使用RELATIVE_PATH,存放的位置要跟.vsz中设定的一致,否则向导引擎会因为无法找到对应模板文件报错。如果你需要部署向导到其他版本的VS,那么需要修改.vsz中的引擎版本,另外,如果是其他的语言版本,需要修改LangID(英文是1033、中文为2052),具体可以参考微软官方文档:将向导本地化为多种语言。
1.为什么修改了.ico、.vsz和.vsdir文件后没有生效(比如图标没变化)?
VS在第一次创建向导工程的时候会将上述三个文件直接拷贝到VS安装目录的vcprojects目录下,你在修改这些文件的时候,往往只是修改了工程目录下的文件,VS不会帮你同步这些更改到vcprojects目录,所以修改之后需要手动copy过去。
2.调试向导出现“没有对象”错误弹窗,或者工程建好后相应文件没有拷贝或者加入到新工程?
单身狗看到这个窗口是不是受到了万点暴击伤害?额~这个问题往往是你的default.js中没有实现必须要实现的回调函数造成,比如Common.js中的CreateProject函数需GetTargetName函数获取拷贝的目标文件名称,以及SetFileProperties函数来设置文件属性,遇到这种情况,在default.js中实现这些缺失的函数就可以了(参考我前文给出的框架代码)。
3.调试的时候断点无法命中?
在编写调试期间,必须要保证你的.vsz文件描述的模板文件的目录指向向导工程目录下,也就是默认的ABSOLUTE_PATH,否则断点是不能命中的。当然,在后期部署的时候一定记得删掉ABSOLUTE_PATH或者使用RELATIVE_PATH。
4.模板文件中的中文,编辑的时候好好的,新工程中渲染完毕就成了乱码?
这很可能是因为你设置了错的LangID造成的,比如英文版VS创建的向导工程,默认的本地化语言使用的就是英语,那么在对应的模板文件中就不要使用中文。使用中文的话需要设置LangID为中文的编号2052,并且部署的时候拷贝到正确的文件夹下。
最后留给大家个问题:如果要完成向导自动化的部署,大家想的到有什么好的方法吗?