原文:一个完整的Installshield安装程序实例—艾泽拉斯之海洋女神出品(四) --高级设置二
上一篇:一个完整的安装程序实例—艾泽拉斯之海洋女神出品(三) --高级设置一
4. 根据用户选择的组件,从外部文件夹拷贝相应的文件到安装目标路径的文件夹中
这个用途常见于配置文件和授权文件的应用,同一程序,授权给不同的用户,只需要不同的配置和授权文件。如果将配置和授权文件每次都打包在安装程序里,那么变更一个用户就需要重新打包一次,这是一个浪费时间和精力的行为。如果将授权和配置文件(当然内容是加密过的)放在外部文件夹中,每次安装的时候从这个文件夹中读取拷贝,那么会是一个比较通用型的安装程序。
另外,本程序的好几个feature用到了相同的库,如果直接在feature下加库文件也可以,但是每一个feature都加一次这个库文件夹,整个安装程序就会变得很庞大,因此比较理想的情况是选到了这个feature的时候从外部拷贝这些库文件。
这里我们先不包括文档这个feature的说明,文档feature另有详细说明。
1. 这个功能需要在OnFirstUIAfter()函数体中实现,选择After Move Data | OnFirstUIAfter选项,即在选择了移动哪些数据后这个操作生效。
2. 之前我们已经接触过了如何判断是否选择了某个Feature,这里也需要判断是否选择了某个Feature,并且根据这个Feature来拷贝对应的外部文件
首先定义一些需要的变量并且进行赋值,蓝色字体即为所定义变量和赋值语句
function OnFirstUIAfter()
//feature name
STRING szFeatureName1;
STRING szFeatureName2;
STRING szFeatureName3;
STRING szFeatureName4;
STRING szFeatureName5;
STRING szSrcFile1;
STRING szSrcFile2;
STRING szTarFolder1;
STRING szTarFolder2;
NUMBER nResult;
STRING szTitle, szMsg1, szMsg2, szOption1, szOption2;
NUMBER bOpt1, bOpt2;
begin
//feature 定义
szFeatureName1 ="Server";
szFeatureName2 ="Client";
szFeatureName3 ="Watch_Portion";
szFeatureName4 ="Log_Portion";
szFeatureName5 ="Report_Portion";
//需要拷贝的源文件
szSrcFile1 = "Test\\lib\\*.*";
szSrcFile2 = "Test\\databaselib\\*.*";
//拷贝的目的地,目标文件夹
szTarFolder1 = "lib\\*.*";
szTarFolder2 = "databaselib\\*.*";
3. 对每一个feature进行判断,进行相应的文件拷贝
在OnFirstUIAfter()的begin和end之间添加如下代码:
//copy the lib to the target ,copy the necessary file to the target
if (FeatureIsItemSelected(MEDIA, szFeatureName1)=1) then
CopyFile(SRCDISK^szSrcFile1, TARGETDIR^szTarFolder1);
CopyFile(SRCDISK^"Test\\configure\\title.gif", TARGETDIR^"Server\\ title.gif");
CopyFile(SRCDISK^"Test\\configure\\background.gif", TARGETDIR^" Server \\ background.gif");
CopyFile(SRCDISK^"Test\\configure\\configure.dat", TARGETDIR^" Server \\configure.dat ");
endif;
if (FeatureIsItemSelected(MEDIA, szFeatureName2)=1) then
CopyFile(SRCDISK^szSrcFile1, TARGETDIR^szTarFolder1);
CopyFile(SRCDISK^"Test\\configure\\configure.dat", TARGETDIR^"Client\\configure.dat ");
CopyFile(SRCDISK^"Test\\configure\\license.dat", TARGETDIR^" Client \\license.dat");
endif;
if (FeatureIsItemSelected(MEDIA, szFeatureName3)=1) then
CopyFile(SRCDISK^szSrcFile1, TARGETDIR^szTarFolder1);
CopyFile(SRCDISK^"Test\\configure\\configure", TARGETDIR^" Watch Portion \\configure");
CopyFile(SRCDISK^"Test\\configure\\license.dat", TARGETDIR^" Watch Portion \\license.dat");
endif;
if (FeatureIsItemSelected(MEDIA, szFeatureName4)=1) then
CopyFile(SRCDISK^szSrcFile1, TARGETDIR^szTarFolder1);
endif;
if (FeatureIsItemSelected(MEDIA, szFeatureName5)=1) then
CopyFile(SRCDISK^szSrcFile1, TARGETDIR^szTarFolder1);
endif;
4. 代码解释
if (FeatureIsItemSelected(MEDIA, szFeatureName1)=1) then
CopyFile(SRCDISK^szSrcFile1, TARGETDIR^szTarFolder1);
CopyFile(SRCDISK^"Test\\configure\\title.gif", TARGETDIR^"Server\\ title.gif");
CopyFile(SRCDISK^"Test\\configure\\background.gif", TARGETDIR^" Server \\ background.gif");
CopyFile(SRCDISK^"Test\\configure\\configure.dat", TARGETDIR^" Server \\configure.dat ");
endif;
**************************************************************************************
FeatureIsItemSelected(MEDIA, szFeatureName1) 这个函数用于判断用户是否选择了某feature。Help里对这个函数是这样描述的:FeatureIsItemSelected ( szFeatureSource, szFeature );
参数一:szFeatureSource,大意好像是feature的来源,具体不是很明白到底指什么,反正help自带的例子里写的MEDIA照抄没有错。
参数二:szFeatureName1,就是 feature的名字了
如果返回值为1,则说明用户选择了这个feature
**************************************************************************************
CopyFile(SRCDISK^szSrcFile1, TARGETDIR^szTarFolder1);
拷贝文件的函数。Help里是这样描述的:CopyFile ( szSrcFile, szTargetFile );
参数一:szSrcFile,源文件,可带路径,要带有扩展名的文件名。当这个文件带路径时,则从这个指定路径下拷贝指定的文件;如果是不带路径的,则直接从安装文件所在盘的盘符下寻找指定的文件来进行拷贝。如果要拷贝某个文件夹下的一系列文件,可以使用通配符。
参数二:目标文件,可带路径,要带有扩展名的文件名。当这个文件带路径时,则将文件拷贝到这个指定路径下;如果是不带路径的,则将文件拷贝到安装路径下。支持通配符。
小结:上面这段代码的意思是:如果用户选择了某个feature,则从安装程序所在的盘下面的一些文件夹下拷贝文件到目标路径下的一些对应文件夹下。这里记住拷贝文件一定要带上文件的全名,包括扩展名。
5. 如果用户选择了文档feature,则把文档文件夹拷贝进来,并且对该文件夹进行遍历,为每一个文档创建一个在开始菜单下的快捷方式
1. 这个功能仍然在After Move Data | OnFirstUIAfter()的函数里实现
先定义一些变量并赋值,蓝色字体
function OnFirstUIAfter()
//feature name
STRING szFeatureName6;//feature名
STRING szSrcFile3; //需要拷贝的源文件
STRING szTarFolder3; //拷贝的目的地,带文件名
STRING szTarFolder4; //拷贝的目标文件夹,后面有一个函数要用到不带文件名的目标路径
STRING szDocFile, szDocFileName;// szDocFile,查找函数返回的查询得到文件名;szDocFileName,要查找的文件名
NUMBER nResult; //数字型变量,存放函数的返回结果
begin
//feature 定义
szFeatureName6 ="Document";
//需要拷贝的源文件
szSrcFile3 = "Docs\\*.*";
//拷贝的目的地,目标文件夹
szTarFolder3 = TARGETDIR^"Docs\\*.*";
szTarFolder4 = TARGETDIR^"Docs";//文档的存放路径,不带文件名
2. 仍然在begin和end之间的函数体内把下面的代码拷贝进去即可
if (FeatureIsItemSelected(MEDIA, szFeatureName6)=1) then //如果选择了此feature
if(CopyFile(SRCDISK^szSrcFile3, szTarFolder3)=0) then //那么把要拷贝的文件拷贝过去
nResult = FindAllFiles(TARGETDIR^"Docs", "*.pdf", szDocFile, RESET); //对拷贝过去的文件进行查找,该函数会在第一个符合条件//的文件处停止
while (nResult = 0)
LongPathToQuote(szDocFile, TRUE );
ParsePath (szDocFileName, szDocFile, FILENAME_ONLY);//对查找到的文件获取文件名
AddFolderIcon(FOLDER_PROGRAMS^"Test\\Docs",szDocFileName, szDocFile, "", TARGETDIR^"Docs\\icons\\help.ico" , 0 ,"" , REPLACE ); //为该文件创建快捷方式,快捷方式的显示名就是刚才获取的文件名
nResult = FindAllFiles(TARGETDIR^"Docs", "*.pdf", szDocFile, CONTINUE);//从上一个查找的位置继续向下查找,进行循环
endwhile;
endif;
endif;
3. 代码解释
***************************************************************************************
if (FeatureIsItemSelected(MEDIA, szFeatureName6)=1) then
endif;
如果用户选择了文档feature,则进行一些相应操作
***************************************************************************************
if(CopyFile(SRCDISK^szSrcFile3, szTarFolder3)=0) then
endif;
这里执行了两步操作:
第一步,从源盘的Docs文件夹下把所有文件都拷贝安装路径的Docs文件夹下,注意在定义变量的时候使用了通配符
第二步,如果拷贝成功,则返回值为0,那么进行下一步相应操作
**************************************************************************************
nResult = FindAllFiles(TARGETDIR^"Docs", "*.pdf", szDocFile, RESET);
查找目标文件夹下所有后缀名为pdf的文件,从文件夹的开始位置进行查找,查找成功则返回0。
这个函数在这里有一个巧妙的应用,因为这个函数会在查找到第一个符合条件的文件时就会停止继续向下查找,因此利用静态变量的传值不同,来实现对文件夹的全部查找。
Help里的解释如下:
FindAllFiles ( szDir, szFileName, svResult, nOp );
参数一:szDir,被查找的文件夹
参数二:szFileName,需要查找的文件的名字,支持通配符,例如*.*,*.pdf,*.doc
参数三:svResult,函数会在查找到第一个符合条件的文件时停止,返回这个符合条件的文件的文件名,带全路径和含扩展名的文件名
参数四:nOp, 静态变量。CONTINUE,从上一次查找的位置开始查找,这个特性我们呆会儿会用到;RESET,从文件夹的开始位置进行查找;CANCEL,释放被上一次的FindAllFiles查找的函数。在Windows NT系统下,需要在安装过程中使用带CANCEL的FindAllFiles来释放之前的查找,确保安装的正确性(因此我怀疑查找有bug,这个函数用来弥补这个bug…)。
**************************************************************************************
LongPathToQuote(szDocFile, TRUE );
szDocFile为上一个函数查找到的第一个符合条件的文件名,带完整路径,这个LongPathToQuote函数加上这个文件名上的括号;否则下面一个函数无法解析不带括号的长文件名。
Help里的解释如下:
LongPathToQuote ( svPath, nParameter );
参数一:svPath,长文件名
参数二:nParameter,静态变量。 TRUE,为长文件名加上括号;FALSE,为长文件名脱去括号。
**************************************************************************************
ParsePath (szDocFileName, szDocFile, FILENAME_ONLY);
解析带路径的长文件名,返回文件本身的文件名
Help里的解释如下:
ParsePath ( svReturnString, szPath, nOperation );。
参数一:svReturnString为返回的解析过的文件名,
参数二:szPath,即被解析的长文件名
参数三:nOperation,静态变量,指定用何种方式来解析。这里使用FILENAME_ONLY,也就说返回值为不带路径、不包含扩展名的文件名。这个文件名被下面一步用作显示的快捷方式的名称。
**************************************************************************************
AddFolderIcon(FOLDER_PROGRAMS^"Test\\Docs",szDocFileName, szDocFile, "", TARGETDIR^"Docs\\icons\\help.ico" , 0 ,"" , REPLACE );
创建一个快捷方式,使用指定的图标。
Help里的解释如下:
AddFolderIcon ( szProgramFolder, szItemName, szCommandLine, szWorkingDir, szIconPath, nIcon, szShortCutKey, nFlag );
参数一:szProgramFolder, 要创建的快捷方式所在的文件夹。这里FOLDER_PROGRAMS指开始 | 所有程序,因此我们的快捷方式将会出现在开始 | 所有程序 | Test的Docs下;如果要添加到桌面上,可以设置为FOLDER_DESKTOP;FOLDER_STARTUP 指添加为启动项;FOLDER_STARTMENU添加到开始菜单下。
参数二:szItemName,help里解释很晦涩,解释为要添加到文件夹下的图标的名称,即出现的图标旁边的那个字符串。其实就是我们常说的快捷方式的名称。这里填写被解析出来的那个不带路径也不带扩展名的文件名。
参数三:szCommandLine,全限定路径的文件名或文件夹名,可包含命令行参数。这里传入刚才查找到的文件名,包含路径、文件名和扩展名。读者可能注意到这个参数被做了一些预处理,这个处理也是折腾了几次才搞出来的,不同的操作系统默认路径也是有是否带引号的差别的,这里需要显式地指定一下,以免在不同操作系统上运行时引起不同的结果。
参数四:szWorkingDir,工作目录。Help里的解释如下:设置这个目录为你的应用程序文件所在的地方;要设置包含了应用程序的目录为工作目录,则可传一个空字符串给这个参数。这个参数一开始我并未理解其含义,不过传空字符串也没有出错;在后来经理提出新要求:允许用户自行选择是否在桌面上创建快捷方式时无意中明白这个参数的含义;请读者随便寻找一个自己计算机上的任意位置的快捷方式,右键点击选择“属性”,这个szWorkingDir就是属性面板上的“起始位置”,值为这个快捷方式所指的应用程序所在的文件夹的路径。至少在我试验的程序里,创建开始菜单的快捷方式和桌面快捷方式,这个参数要求的值还是略有不同的,开始菜单里创建,可以直接传空字符串;而桌面快捷方式,传控字符串总是会出错,查看属性面板里的“起始位置”值为空,因此手动地传了快捷方式所指向的应用程序的所在文件夹的路径,后面在“安装结束时允许用户选择创建桌面快捷方式”话题里有详细说明。
参数五:szIconPath,带全限定名的图标的路径,即包含路径、文件名和扩展名
参数六:nIcon。如果不是使用Windows图标的话,统统指定为0;Windows图标我没有研究过,Help里说可以指定为0,1,2,3…n我猜测是不是图标文件本身包含了多个图标,而我可以指定使用哪个图标?
参数七:szShortCutKey,热键,一般用不到。如果有需要可以设置为比如"Ctrl + Alt + 1"这种形式。
参数八:nFlag,静态变量,多个用途。这个程序里我们使用了REPLACE,即永远使用当前这个快捷方式的属性;RUN_MAXIMIZED ,当从这个快捷方式登录程序时,程序界面最大化;RUN_MINIMIZED,当从这个快捷方式登录程序时,程序界面最小化; NULL,无任何操作(不知道这个无任何操作适用于何种情况?)。
小结:这段代码的重点在于
1) 实现对文件夹下的文件的遍历。因为之前笔者的文档都打包在程序里,苦于文档的名称和数量常常变更,每做一次都要耗费人力物力,而且在光盘里仍然需要单独放置一个文档文件夹供用户在没有安装程序前的随时查看,重复打包安装使得安装内容容量巨大,以至于从刻录小光盘改成刻录大光盘,从VCD盘改成DVD盘。这段代码在用户选择了安装文档的条件下,对外部文件夹进行了拷贝,并且读取文件夹下所有的pdf文件(依次类推,只要设置了正确的过滤条件,可以读取文件夹下想要的文件)。难点就在于将文件夹下的文件一个个读取出来并且获取该文件的信息。
2) 对读取的文件创建快捷方式,这个难点在于8个参数的理解。我在互联网上搜索了一阵子,并且啃了一阵子help,但是可能自己外语水平不是很过关,以至于第四个参数没有完全理解到底是什么意思,所见的例子也很单调并且偷懒,能赋””的地方都给赋了””,无语~~~~
整个安装程序做下来这一段代码是最难的,FindAllFiles在Help里解释是当碰到第一个符合条件的文件就会停下来,因此如何读取全部文件,并且获取文件信息,代码的撰写也是费了很大的功夫,并且参考了别人的程序修改出来的。
6. 在安装结束时,显示readme.txt文件
这是个很有用的设置,但是在InstallScript工程里不是默认自带的,因此也需要脚本编程实现。
这段代码的位置是在After Move Data | OnFirstUIAfter()的函数里实现的
1. 首先,在安装的时候把readme.txt文件从源盘拷贝到安装目录下。把这段代码拷贝到After Move Data | OnFirstUIAfter()的begin和end;之间即可。README.TXT文件放置在源盘的根目录下,并且在安装时拷贝到安装目录下。
CopyFile(SRCDISK^"README.TXT", TARGETDIR^"README.TXT");
这段代码意味着当安装执行的时候,这个文件总会被拷贝过去。
2. 创建一个Finish界面,并在界面上设置询问是否显示readme.txt文件的选项。
之前我们看到当我们第一次选取了After Move Data | OnFirstUIAfter()选项时,系统会为我们创建如下代码(当然不创建也不要紧,自己敲就是了)
这个就是结束界面。Installscript工程默认安装完毕后,界面直接消失,而不会出现一个带有Finish按钮的界面让用户点击了以后才结束整个安装过程。
这段代码就是创建了一个Finish界面了,我们要对这段代码进行改造,使之出现一个是否显示readme的选项。
把上图中从Disable(STATUSEX);起到SdFinishEx这行的代码,全部替换成如下代码:
Disable(STATUSEX);
ShowObjWizardPages(NEXT);
bOpt1 = TRUE;
bOpt2 = TRUE;
szMsg1 = SdLoadString(IFX_SDFINISH_MSG1);
szTitle="";
szMsg1="";
szMsg2="";
szOption1="Show Readme";
szOption2="";
SdFinishEx(szTitle, szMsg1, szMsg2, szOption1, szOption2, bOpt1, bOpt2);
if (bOpt1=TRUE) then
if(FindFile(TARGETDIR, "README.TXT", szDocFile)=0) then
LaunchApp ( WINDIR^"Notepad.exe" , TARGETDIR^"README.TXT" );
endif;
endif;
3. 代码解释
*******************************************************************************************
Disable(STATUSEX);
使默认的安装设置对话框无效。
*******************************************************************************************
ShowObjWizardPages(NEXT);
顺序执行这个OnFirstUIAfter()的代码,如果参数为BACK,则逆序执行
*******************************************************************************************
SdLoadString(IFX_SDFINISH_MSG1);
返回参数所关联的字符串值,这个参数应当是一个资源ID。
*******************************************************************************************
SdFinishEx(szTitle, szMsg1, szMsg2, szOption1, szOption2, bOpt1, bOpt2);
参数一:szTitle,即显示在界面上的左上角的标题,如果传空值,则显示默认值
参数二:szMsg1,安装结束的界面上允许最多有两个可选项,这个参数可以显示第一个选项的一些相关说明,如果赋空则不显示任何说明
参数三:szMsg2,解释同上
参数四:szOption1,选项名。这个是一个Checkbox,如果设置为空则不显示,如果赋值则显示一个Checkbox并且在这个Checkbox旁边显示这个所赋的简短值。
参数五:szOption2,解释同上。
参数六:第一个选项的状态,如果设置为TRUE,则第一个选项Checkbox默认为选中状态,FALSE则为未选中状态。
参数七:第二个选项的状态,解释同上。
*******************************************************************************************
if (bOpt1=TRUE) then
判断是否选择了checkbox。如果用户选择了这个选项,则进行下一步操作
*******************************************************************************************
if(FindFile(TARGETDIR, "README.TXT", szDocFile)=0) then
为了保险起见,需要进一步判断一下这个readme.txt是否被拷贝进来了
Help里解释如下:
FindFile ( szPath, szFileName, svResult );
参数一:szPath,文件所在的路径,不包含文件名
参数二:szFileName,文件名,包含扩展名
参数三:szDocFile,返回的文件名
如果查找成功,则返回值为1
*******************************************************************************************
LaunchApp ( WINDIR^"Notepad.exe" , TARGETDIR^"README.TXT" );
打开readme文件
Help里没有对这个函数的专门的解释,但是有个例子,以至于我看了好几遍才看懂要表达的意思
参数一:应用程序,也就是你用什么工具来打开第二个参数指定的文件。我们这里用记事本打开,因此要引用一下Windows下自带的程序Notepad.exe,路径为WINDIR^"Notepad.exe" 。如果是一些不是Windows自带的程序,比如PDF,DOC,还需要从注册表里得到所安装的目标位置,从这个目标位置得到要用的工具。有兴趣的朋友可以试验一下。
参数二:要打开的文件,带路径,包含扩展名
小结:这个界面我曾经试图写在OnFirstUIBefore()里的结尾部分,用Dlg_SdFinish来实现,但是总是发现虽然结束界面能出来,但是上一个界面不能消失掉的情况。因为这个资料也不好找,仓促之间试验出上述所说的办法,估计是等安装界面结束后补上一个界面来达到这个效果的;其实我本人是比较讨厌结束的时候有这么一个要看readme的选项的,一般自己装到这种软件,都是去掉钩选框,不看readme的;但是如果直接结束掉,不出这个结束界面又觉得提示不足,有时候不能确定安装程序有没有结束,所以私下里还是比较想去掉readme选项,而直接显示一个只有一个finish按钮的界面的。
7. 在安装结束时,允许用户选择是否显示桌面快捷方式
有时候我们会看到别的安装程序在安装过程中允许用户选择是否要在桌面上显示快捷方式,一开始因为我们公司的分布式系统的组件太多了,不想显示在桌面上,而且觉得和在开始菜单中显示快捷方式的原理是一样的,因此也就轻轻带过;后来经理抱怨说没有桌面快捷方式,总是要去开始菜单找,觉得麻烦,而且客户是使用专用计算机运行我们的程序,也就是桌面上会很干净,希望我能够做这个功能出来。我试了一下,发现和在开始菜单中显示快捷方式还是有一点不同的,也是值得写出来的,至少可以让读者少走一些弯路。
1. 首先要显示一个允许用户选择是否显示桌面快捷方式的界面,这个界面上要有一个checkbox(钩选框),当钩选了以后,安装程序就要在安装时为用户显示桌面快捷方式。
这段代码的位置是在After Move Data | OnFirstUIAfter()的函数里实现的,也就是和“显示readme文件”的功能放在一起。
把从Disable(STATUSEX);起到SdFinishEx这行的代码,全部替换成如下代码:
Disable(STATUSEX);
ShowObjWizardPages(NEXT);
bOpt1 = TRUE;
bOpt2 = TRUE;
szMsg1 = SdLoadString(IFX_SDFINISH_MSG1);
szTitle="";
szMsg1="";
szMsg2="";
szOption1="Show Readme";
szOption2="Create Shortcut on Desktop?";
SdFinishEx(szTitle, szMsg1, szMsg2, szOption1, szOption2, bOpt1, bOpt2);
2. 代码解释
与上面的“显示readme文件”中的代码相比,只动了一个地方,即szOption2="Create Shortcut on Desktop?";
这个是一个Checkbox,如果值设置为空则不显示,如果赋值则显示一个Checkbox并且在这个Checkbox旁边显示这个所赋的简短值。
这里我们需要它显示出来,这样在界面上用户就会看到一个钩选框询问是否要显示桌面快捷方式。
3. 接下来我们要对用户所做的选择做一些判断,并且显示桌面快捷方式,在这段代码后面加上
if(bOpt2=TRUE) then
if (FeatureIsItemSelected(MEDIA, szFeatureName1)=1) then
szDocFile = TARGETDIR^"Server\\server.bat";
LongPathToQuote(szDocFile, TRUE );
AddFolderIcon(FOLDER_DESKTOP, "Server" , szDocFile, TARGETDIR^"Server" , TARGETDIR^"Server\\icons\\appClient.ico" , 0 ,"" , REPLACE );
endif;
4. 代码解释
因为上面对这些函数的每个参数都有详细解释了,所以这里就不做一一解释了,只对要注意的地方做说明。
这里,一开始,笔者对第四个参数仍然传的是空字符串,但是创建的快捷方式总是不能运行,对比属性面板才发现,桌面快捷方式的“起始位置”的值居然是空的,看来Help解释的“当传空值的时候,默认为快捷方式所指的应用程序所在的目录”并未生效,只好老老实实地把运行目录的值手动地传进去。
读者可能注意到在AddFolderIcon函数里的第三个参数被做了一些预处理,这个处理也是折腾了几次才搞出来的,不同的操作系统默认路径也是有是否带引号的差别的,这里需要显式地指定一下,以免在不同操作系统上运行时引起不同的结果。
8. 在安装结束后,启动指定的程序
在全部安装完毕后,启动指定的程序,向Windows安装一个服务。或者也可使用于安装结束后的程序的自启动。
1. 这部分很明显是要在安装全部结束后进行的,因此放在After Move Data | OnEnd里
2. 把OnEnd()的代码替换如下
function OnEnd()
STRING szFeatureName;
STRING serviceTarget;
STRING szDocFile;
begin
/*
//这个服务所需的文件只有在钩选了某feature时候才会被拷贝,并且也只有在用户钩选安装了此feature时候才会在安装结束时安装此服务,因此首要判断是否选择了此feature,然后寻找到该执行文件,并且进行安装
*/
szFeatureName="Watch_Portion";
serviceTarget=TARGETDIR^"watch.exe";
if (FeatureIsItemSelected(MEDIA, szFeatureName)=1) then
if(FindFile(TARGETDIR, " watch.exe ", szDocFile)=0) then
if (LaunchApp (serviceTarget, "") < 0) then
MessageBox ("Unable to launch "+serviceTarget+".", SEVERE);
endif;
endif;
endif;
end;
3. 代码解释
***************************************************************************************
if (FeatureIsItemSelected(MEDIA, szFeatureName)=1) then
endif;
首先判断这个feature是否被用户选择安装。因为在这个应用程序里这个服务只与此feature相关,因此要做一下判断,如果用户没有安装这个feature,就不需要启动这个服务了。
当用户选择了这个feature时,返回值为0
***************************************************************************************
if(FindFile(TARGETDIR, " watch.exe ", szDocFile)=0) then
endif;
这个是判断一下文件是否被正确地拷贝过去了,这个文件应该位于安装目录下,名为watch.exe。当该文件存在时,返回值为0
***************************************************************************************
if (LaunchApp (serviceTarget, "") < 0) then
endif;
启动该服务;如果启动失败,则返回小于0的值。
这里LaunchApp的用法和上面第6段的用法略有不同。这个函数的本意是启动第一个参数指定的运行程序来打开第二个参数指定的文件。这里第二个参数指定为空,因为没有要打开的文件;第一个参数指向我们需要启动的可执行程序即可。
***************************************************************************************
MessageBox ("Unable to launch "+serviceTarget+".", SEVERE);
如果上一步中判断到程序未能正确启动,则弹出一个错误提示框体现用户。
小结:这段代码的用法非常简单,但是如果用在适当的安装程序里会非常重要;笔者的安装程序,在一开始的时候需要用户安装完毕后手动地去安装目录里找到这个服务并且启动,使人感觉非常不友好;现在在安装完毕后做到了静默启动,用户无需做任何事情。而且这个服务需要JDK的支持,配合上述第2段中判断是否安装了JDK这个应用,就不会出现安装了此服务但是无法运行的局面。
9. 安装结束后,为JDK设置一个环境变量
之前提到了,要在安装本系统时判断是否安装了JDK,在最初笔者所做的安装盘中,还要让用户手动地去为JDK设置环境变量JAVA_HOME,设置环境变量对于外行来说简直就是天方夜谭,在JAVA论坛新手区最常见就是求助设置环境变量的问题了,因此,这个功能最好还是由安装程序代劳为妙。
1. 这段代码在After Move Data | OnFirstUIAfter()里
//write the environment variable
szKey = "SOFTWARE\\JavaSoft\\Java Development Kit\\1.6.0_04";
RegDBSetDefaultRoot(HKEY_LOCAL_MACHINE);
if (RegDBKeyExist(szKey)=1) then//如果该注册表值存在
if(RegDBGetKeyValueEx(szKey,"JavaHome",nvType,svValue,nvSize)=0) then//获取注册表值成功
szKey = "SYSTEM\\CurrentControlSet\\Control\\Session Manager\\Environment";
if(RegDBSetKeyValueEx(szKey, "JAVA_HOME", REGDB_STRING, svValue, -1)<0) then
MessageBox ("Javahome create failed, please set it manually!", SEVERE);
endif;
endif;
endif;
2. 代码解释
****************************************************************************
RegDBKeyExist(szKey)
判断JDK1.6.0_04的注册表值是否存在;要判断JDK1.6.0_04是否被安装,只有通过注册表来判断啦,同理可得,要是自己开发的一套系统中有多个安装程序,而且相互关联,就得朝注册表里写入值了。
如果返回值为1,则说明存在该键值;
如果返回值小于0,则说明该键值不存在。
****************************************************************************
RegDBGetKeyValueEx(szKey,"JavaHome",nvType,svValue,nvSize)
因为设置JAVA_HOME环境变量需要JDK的安装位置,所以要根据注册表来寻找到这个安装位置,而幸运的是,该键值下的JavaHome键名所对应的值就是JDK的安装位置。
Help里对该函数的解释如下:
RegDBGetKeyValueEx ( szKey, szName, nvType, svValue, nvSize );
参数一:szKey, 要查找的注册表的键,这里我们查找SOFTWARE\\JavaSoft\\Java Development Kit\\1.6.0_04
参数二:szName,一些注册表键下面会有一些键名,如果你去看一下我们查找的键,会发现该键下存在多个键名,这里我们只要查找JavaHome键名对应的值,因此,指定szName为JavaHome
参数三:nvType,返回该键名对应的值的类型,比如字符型,数字型;当时笔者还犯了一个错误,以为这个参数是需要笔者指定类型的,因此写了一个REGDB_STRING,结果编译出错,搞了半天发现这个参数是个返回值,汗一个。
参数四:svValue,返回该键名对应的值
参数五:nvSize,返回该键名对应的值的字节数
****************************************************************************
szKey = "SYSTEM\\CurrentControlSet\\Control\\Session Manager\\Environment";
RegDBSetKeyValueEx(szKey, "JAVA_HOME", REGDB_STRING, svValue, -1)
如果搜索注册表发现JDK已经安装了,就去读一下注册表的键值,并且设置我们所需要的环境变量,这两句话就是用来设置环境变量的。
环境变量也是利用注册表键值设置函数RegDBSetKeyValueEx来实现的,这个键是一个特殊的位置,一定是"SYSTEM\\CurrentControlSet\\Control\\Session Manager\\Environment",我们对该函数进行进行详细说明。
RegDBSetKeyValueEx ( szKey, szName, nType, szValue, nSize );
函数作用:设置注册表键值
参数一:szKey注册表里的键,这里,我们需要设置环境变量的值,因此这里固定传值为"SYSTEM\\CurrentControlSet\\Control\\Session Manager\\Environment"
参数二:szName,键名,这里我们需要设置的是名为JAVA_HOME的环境变量
参数三:nType,被设置的键的类型,这里是字符串型,并且不带%PATH%之类的符号,也不转行
参数四:szValue,就是键值了,这里我们已经从上面得到了JDK的安装路径,就把安装路径传进去
参数五:nSize,help里说明如果键类型为REGDB_STRING, REGDB_STRING_EXPAND, 或者 REGDB_NUMBER时,都可以设置该值为-1,installshield会自动为我们计算正确的长度,而当键类型为REGDB_BINARY 和REGDB_STRING_MULTI时,就必须传该键值的实际大小进去。
小结:Installshield默认键值位置是在HKEY_CLASSES_ROOT下的,因此在这里,我们需要在进行搜索键值和设置键值的操作之前使用RegDBSetDefaultRoot(HKEY_LOCAL_MACHINE);这句话来设置一下默认的根键值为HKEY_LOCAL_MACHINE;另,在网上看了一个帖子,当时匆匆看了一下,说是设置的键值会在反安装时候卸载掉,我倒是没有在自己的安装程序里发现这个问题,不过可以研究一下;作者说当时为了解决这个问题,是在代码头加上DISABLE(LOGGING);代码尾加上ENABLE(LOGGING)来实现的,虽然我没有碰到这个问题,但是还是很感谢这位作者,因为当时他也说了,根本找不到资料,自己啃了天书般的HELP来解决,而自己一旦解决了问题,就分享出来,以便于大家少走弯路。
10. 完美卸载
在第一部分的第9点我们提到过InstallScript工程里自带的Uninstall快捷方式的缺陷,这里我们将会创建一个可以实现全部卸载的卸载方式,这个卸载方式会以快捷方式出现在开始菜单下,利用安装程序本身的反安装功能来实现
3. 这段代码在After Move Data | OnFirstUIAfter()里,和其他创建快捷方式的代码放一起
function OnFirstUIAfter()
STRING szfilename,szFolder ,szmsg1,szmsg2;
NUMBER nresult;
begin
//创建删除快捷方式
szfilename = UNINSTALL_STRING +" /UNINSTALL";
nresult = StrFind(szfilename,".exe");
if nresult >=0 then
StrSub(szmsg1,szfilename,0,nresult + 4);
StrSub(szmsg2,szfilename,nresult + 4,200);
LongPathToQuote(szmsg1, FALSE );
LongPathToQuote(szmsg2, FALSE );
szfilename = "\"" + szmsg1 + "\"" +szmsg2;
endif;
AddFolderIcon(FOLDER_PROGRAMS^"Test","Uninstall",szfilename,WINDIR,"",0,"",REPLACE);
End;
4. 代码解释
****************************************************************************
szfilename = UNINSTALL_STRING +" /UNINSTALL";
参数一:UNINSTALL_STRING这个静态变量指向的就是我们的安装程序,也就是setup.exe,不过指向的位置不是我们的源盘里的setup.exe,而是C:\Program Files\InstallShield Installation Information\{0D9DF66A-44E5-4754-A522-2AD6C9D5CDBE}\setup.exe;Installshield创建的安装文件在安装时总会在这个文件夹里创建对应信息,一长串数字型序列码就是安装程序的Product ID。利用这个setup.exe就可以进行反安装
参数二:/UNINSTALL,告诉程序启动这个setup.exe时为非安装状态,即修复、重新安装和卸载状态。
因此,这个字符串的值应该是这种形式:
"C:\Program Files\InstallShield Installation Information\{0D9DF66A-44E5-4754-A522-2AD6C9D5CDBE}\setup.exe" -runfromtemp -l0x0409 /UNINSTALL
****************************************************************************
nresult = StrFind(szfilename,".exe");
寻找到“.exe”这个字符串在szfilename这个字符串中的位置。
Help里对这个函数的描述如下:
StrFind (szString, szFindMe);
参数一:szString,被查找的源字符串
参数二:szFindMe,要查找的字符串
返回值为要查找的字符串在源字符串中的位置,如果返回值小于0,则说明源字符串中找不到要查找的字符串
****************************************************************************
StrSub(szmsg1,szfilename,0,nresult + 4);
StrSub(szmsg2,szfilename,nresult + 4,200);
如果要查找的字符串存在,那么源字符串就是正确的;这两句语句就对源字符串进行截断,得到想要的子串。
szmsg1应该为C:\Program Files\InstallShield Installation Information\{0D9DF66A-44E5-4754-A522-2AD6C9D5CDBE}\setup.exe
而szmsg2应该为 -runfromtemp -l0x0409 /UNINSTALL
Helpl里的解释如下:
StrSub ( svSubStr, szString, nStart, nLength );
参数一:svSubStr返回的结果字符串
参数二:szfilename源字符串
参数三:开始截断的位置。如果指定的位置大于整个被解析的字符串长度,则返回一个空字串。
参数四:结束截断的位置。如果指定的位置大于整个被解析的字符串长度,则默认为结束截断的位置是字符串的结尾处。
****************************************************************************
LongPathToQuote(szmsg1, FALSE );
LongPathToQuote(szmsg2, FALSE );
这两句的作用是对上面解析出的两个子串脱去括号。原本笔者参考的例子里没有这两句,在自己计算机上运行正常,但是换了一台计算机后,创建出的卸载快捷方式无效,查看快捷方式的指向发现和原来计算机的指向略有差别,查阅了一些资料得知Windows下的长文件名就有这个缺陷,每个操作系统解析出来的可能会有所不同,主要是引号的麻烦。在笔者自己的计算机上获取的长文件名是不带引号的,因此,解析正确;而测试的那台计算机上获取的文件名却是带引号的,这就造成了解析后拼凑的字符串的差别。这里就要显式地为解析出来的子串脱一下引号。
****************************************************************************
szfilename = "\"" + szmsg1 + "\"" +szmsg2;
拼凑出正确的可执行文件的长文件名,带路径,包含扩展名
****************************************************************************
AddFolderIcon(FOLDER_PROGRAMS^"Test","Uninstall",szfilename,WINDIR,"",0,"",REPLACE);
添加一个快捷方式到开始 | 所有程序 | Test下;照抄即可。
小结:可能读者会比较奇怪这一段代码的写法,因为中间那段if endif;代码看上去简直就是多此一举。在Installshield7之前,一直是这样写的:
szfilename = UNINSTALL_STRING +" /UNINSTALL";
AddFolderIcon(FOLDER_PROGRAMS^"Test","Uninstall",szfilename,WINDIR,"",0,"",REPLACE);
从Installshield8开始,长文件名一直有引号封闭不正确的问题,因此if endif;代码完全是为了解决这个问题而存在的,而上面提到的两个脱去引号的语句,是笔者在前人基础上修改加上的,因为发现解析出来的字串要是不脱一下括号还是有问题。
这个快捷方式运行的时候,出现界面和在安装完毕后再次运行安装程序出现的界面相同。选择Remove即可进行卸载。
这个卸载不会把程序运行时产生的文件卸载掉,比如日志文件、配置信息文件等;会把安装目录中所有从安装程序中安装的文件都卸载掉,包括安装时从外部拷贝的文件。利用Project Assistant创建的卸载快捷方式则无法卸载掉安装时从外部拷贝的文件。
11. 完美卸载之卸载时触发命令(卸载Windows服务)
在做完这个安装程序后,以为可以结束了,没想到经理又提出了一个新的要求,因为之前的安装里(参阅第二部分的第8小节),在安装完毕后,启动了一个指定程序,这个指定程序干的事情就是向Windows写了一个服务进去(有兴趣的同学可以去看看Java Service相关资料,是一个把Java程序注册为Windows服务的一个工具或者说是组件更合适些);所以,这里希望能够在卸载的时候能够把这个服务给卸载掉。
首先我们介绍一下两条Windows cmd命令:
1) SC stop XXX
这条命令用于停止某个名叫XXX的正在运行的Windows服务
2) SC delete XXX
这条命令用于删除某个名叫XXX的Windows服务
一开始我的思路是这样的,获取安装程序的卸载状态,然后调用这两条命令来删除服务;没想到这个“获取安装程序的卸载状态”让我浪费了整整一个下午的时间,只知道MAINTENANCE是程序的反安装状态,而这个反安装状态是有可能包括“重装”、“修复”和“卸载状态”的,当然我可以让反安装界面只能处于卸载状态,只要把前面创建卸载快捷方式中的szfilename = UNINSTALL_STRING +" /UNINSTALL"; 这句话改成szfilename = UNINSTALL_STRING +" /REMOVEONLY"; 就可以了;但是试验出来是不等我确认删除,这个服务就卸载掉了,原因是这个界面一出来就是MAINTENANCE状态,而程序捕获了这个状态后,是不管我是否按下了确认按钮就会去做这个操作了。
后来想在Onbegin里添加一个SdWelcomeMaint函数的判断,结果是判断倒是成功的,但是多了另一个重复界面。
看来这个思路可能是有问题的,然后满地google之,还是吞硬币的小猪的一篇文章给了启发,原文地址找不到了,只找到了这篇http://school.ogdev.net/ArticleShow.asp?id=1699&categoryid=7,这里面其实是谈反安装时候不执行OnMaintUIBefore函数的问题,我想既然这个函数是反安装时候“应该执行的”,那么就看看这个函数吧。
于是 打开Before Move Data | OnMainUIBefore
打开一看,大喜过望,这个函数里明明白白地显示了反安装时候的所有界面。
于是顺着向下看,找到Dlg_SdFeatureTree。
这里红色圈出来的一行代码明确地告诉我们:如果为反安装状态,那么卸载所有组件!OK,代码只要添在这里就可以了。
这里就运用了一个函数LaunchAppAndWait来达到目的。其实一开始我还在想是不是要写批处理文件来执行呢,结果是不需要,直接写在这个函数里就可以了。
LaunchAppAndWait ( szProgram, szCmdLine, nOptions );
参数一:szProgram,要运行的程序。在Help里有这样一句解释:想在命令行里指定要运行的程序,那么可以对这个参数传空值
参数二: szCmdLine,命令行参数;很奇妙的参数,这里我们就可以写入我们想要的批处理语句了。
参数三:静态变量,操作类型,这里LAAW_OPTION_HIDDEN可以使批处理窗口隐藏掉,如果使用了LAAW_OPTION_WAIT,就会看到一个命令行窗口一闪而过,让人十分不爽。
于是,折腾了一下午的问题,就靠这短短的两分钟就解决了…
下一篇:一个完整的安装程序实例—艾泽拉斯之海洋女神出品(五) --补遗