上一篇:
一個完整的安裝程序實例—艾澤拉斯之海洋女神出品(三) --高級設置一
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,就會看到一個命令行窗口一閃而過,讓人十分不爽。
于是,折騰了一下午的問題,就靠這短短的兩分鐘就解決了…
下一篇:
一個完整的安裝程序實例—艾澤拉斯之海洋女神出品(五) --補遺
更多文章、技術交流、商務合作、聯系博主
微信掃碼或搜索:z360901061

微信掃一掃加我為好友
QQ號聯系: 360901061
您的支持是博主寫作最大的動力,如果您喜歡我的文章,感覺我的文章對您有幫助,請用微信掃描下面二維碼支持博主2元、5元、10元、20元等您想捐的金額吧,狠狠點擊下面給點支持吧,站長非常感激您!手機微信長按不能支付解決辦法:請將微信支付二維碼保存到相冊,切換到微信,然后點擊微信右上角掃一掃功能,選擇支付二維碼完成支付。
【本文對您有幫助就好】元
