一個完整的Installshield安裝程序實例 --高級設置二

4. 根據用戶選擇的組件,從外部文件夾拷貝相應的文件到安裝目標路徑的文件夾中

這個用途常見於配置文件和授權文件的應用,同一程序,授權給不同的用戶,只需要不同的配置和授權文件。如果將配置和授權文件每次都打包在安裝程序裏,那麼變更一個用戶就需要重新打包一次,這是一個浪費時間和精力的行爲。如果將授權和配置文件(當然內容是加密過的)放在外部文件夾中,每次安裝的時候從這個文件夾中讀取拷貝,那麼會是一個比較通用型的安裝程序。

另外,本程序的好幾個feature用到了相同的庫,如果直接在feature下加庫文件也可以,但是每一個feature都加一次這個庫文件夾,整個安裝程序就會變得很龐大,因此比較理想的情況是選到了這個feature的時候從外部拷貝這些庫文件。

這裏我們先不包括文檔這個feature的說明,文檔feature另有詳細說明。

1. 這個功能需要在OnFirstUIAfter()函數體中實現,選擇After Move Data | OnFirstUIAfter選項,即在選擇了移動哪些數據後這個操作生效。

clip_image002

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的選項。

clip_image004

把上圖中從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裏

clip_image006

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. 這段代碼在Before 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. 這段代碼在Before 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即可進行卸載。

clip_image008

這個卸載不會把程序運行時產生的文件卸載掉,比如日誌文件、配置信息文件等;會把安裝目錄中所有從安裝程序中安裝的文件都卸載掉,包括安裝時從外部拷貝的文件。利用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

clip_image010

打開一看,大喜過望,這個函數裏明明白白地顯示了反安裝時候的所有界面。

於是順着向下看,找到Dlg_SdFeatureTree。

clip_image012

這裏紅色圈出來的一行代碼明確地告訴我們:如果爲反安裝狀態,那麼卸載所有組件!OK,代碼只要添在這裏就可以了。

clip_image014

這裏就運用了一個函數LaunchAppAndWait來達到目的。其實一開始我還在想是不是要寫批處理文件來執行呢,結果是不需要,直接寫在這個函數裏就可以了。

LaunchAppAndWait ( szProgram, szCmdLine, nOptions );

參數一:szProgram,要運行的程序。在Help裏有這樣一句解釋:想在命令行裏指定要運行的程序,那麼可以對這個參數傳空值

參數二: szCmdLine,命令行參數;很奇妙的參數,這裏我們就可以寫入我們想要的批處理語句了。

參數三:靜態變量,操作類型,這裏LAAW_OPTION_HIDDEN可以使批處理窗口隱藏掉,如果使用了LAAW_OPTION_WAIT,就會看到一個命令行窗口一閃而過,讓人十分不爽。

於是,折騰了一下午的問題,就靠這短短的兩分鐘就解決了… 


海洋女神博客http://www.cnblogs.com/Cindy_weiwei/archive/2009/05/19/1460238.html

發佈了15 篇原創文章 · 獲贊 3 · 訪問量 5萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章