1. 程式人生 > >用 .Net Framework 4.0 製作的安裝程式來安裝 .Net Framework 4.0 編寫的程式

用 .Net Framework 4.0 製作的安裝程式來安裝 .Net Framework 4.0 編寫的程式

文章題目看起來有點繞,解釋一下,假如你基於框架寫了一個程式,想裝到客戶機上,但是客戶機上可能並沒有安裝框架,因此你的程式需要預先將框架安裝在目標機上,然後再執行一些安裝程式的標準功能,如建立快捷方式、建立程式組、寫入解除安裝資訊以便讓Windows能夠對程式進行解除安裝管理等,實現這個功能的方法有很多,例如使用InstallShield、Wix Toolset等均可實現此功能。


不過本文並不是介紹使用這些工具的方法,而是要使用框架來編寫一個安裝程式,實現一般安裝程式的複製檔案、建立快捷方式、建立程式組、安裝字型、安裝服務、寫入反安裝資訊等一些常見的功能,有重複發明輪子之嫌,主要目的是個人興趣,想探究一下安裝程式是怎麼實現這些有趣的功能的,覺得無聊的朋友可飄過,勿噴。


說明:

框架安裝程式:指安裝.Net Framework 4.0到客戶機上的安裝程式,框架安裝程式使用微軟提供的dotNetFx40_Full_x86_x64.exe,為32位平臺和64位平臺通用的安裝包。

應用安裝程式:指基於.Net Framework 4.0編寫的安裝程式,將應用程式安裝到客戶機上。

程式設計環境: Visual Studio 2010 + .Net Framework 4.0。


要想用安裝程式將編寫好的程式安裝到客戶機上,首先得解決安裝程式執行的問題,安裝程式是基於框架編寫的,得裝上框架才行,恩,要有雞先得有蛋。好,來理一理思路:

(1)使用一個載入程式安裝.Net Framework 4.0,這個載入程式應該是已經編譯為二進位制程式碼的可執行檔案,要求在 Windows XP 以上的作業系統中直接執行,不需依賴第三方DLL。這個載入程式需要檢查目標機上是否安裝了框架,如果已經安裝了框架,則直接啟動應用安裝程式進行安裝,如果檢測到沒有安裝,則啟動微軟提供的.Net Framework 4.0框架安裝程式進行框架的安裝。

(2)框架安裝成功後,啟動應用安裝程式,顯示安裝協議、提供程式功能選擇、選擇安裝路徑、執行安裝(建立桌面快捷方式、建立程式組、複製檔案到指定目錄、安裝字型、安裝服務、寫入反安裝資訊以便在控制面板中的“新增/刪除程式”中能看到安裝的程式、生成本地可執行映像以加快程式啟動速度等等功能),安裝完成後,提供“啟動程式”的選項,以便使用者能夠在安裝完成後立即啟動程式。

(3)需要編寫一個反安裝程式,該程式根據已經安裝的程式功能和配置檔案刪除已經安裝的程式功能。

好了,應該要寫三個程式,一步一步來。


1 載入程式的製作

載入程式的作用是檢測客戶機上是否已經安裝了.Net Framework 4.0 框架,可以通過檢查登錄檔相關鍵值的方式來實現,微軟知識庫提到可以檢測如下的登錄檔項來檢視是否安裝了.Net Framework 4.0:


HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\NET Framework Setup\NDP\v4\Full

該鍵下有名稱為“Install”的專案,如果該專案值為1(0x1)則表示該計算機已經安裝了框架,若不存在該登錄檔專案或該鍵值不為1,則可認為當前計算機尚未安裝框架。


為了能夠讓載入程式能夠獨立執行,又能比較容易的編寫,且可執行讀寫登錄檔等操作,我使用MFC框架來編寫這個載入程式,並選擇“在靜態庫中使用 MFC”這個選項,這樣就能夠將載入程式所使用的相關庫檔案打包到最終的EXE中,雖然看起來有點大(編譯後1.78MB),但是放到客戶機上可直接執行,並不需要客戶機再安裝MFC框架來執行這個載入程式。

(1)新建一個MFC應用程式,命名為Setup。


新建一個MFC應用程式


(2)在應用程式嚮導的應用程式型別設定中,選擇“基於對話方塊”和“在靜態庫中使用 MFC”,其他選擇可以使用預設設定。


在靜態庫中使用 MFC


建立完畢後,編寫一個函式來檢測是否安裝了框架,這裡借用了微軟知識庫的方法:


// 檢測是否已經安裝 .Net Framework 4.0。
#define NETFX40_FULL_REVISION 0
#define NETFX45_RC_REVISON MAKELONG(50309, 5)
bool IsNetFx4Present(DWORD dwMinimumRelease)
{
	DWORD dwError = ERROR_SUCCESS;
	HKEY hKey = NULL;
	DWORD dwData = 0;
	DWORD dwType = 0;
	DWORD dwSize = sizeof(dwData);

	dwError = ::RegOpenKeyExW(HKEY_LOCAL_MACHINE, L"SOFTWARE\\Microsoft\\NET Framework Setup\\NDP\\v4\\Full", 0, KEY_READ, &hKey);
	if (ERROR_SUCCESS == dwError)
	{
		dwError = ::RegQueryValueExW(hKey, L"Release", 0, &dwType, (LPBYTE)&dwData, &dwSize);
		if ((ERROR_SUCCESS == dwError) && (REG_DWORD != dwType))
		{
			dwError = ERROR_INVALID_DATA;
		}
		else if (ERROR_FILE_NOT_FOUND == dwError)
		{
			dwError = ::RegQueryValueExW(hKey, L"Install", 0, &dwType, (LPBYTE)&dwData, &dwSize);
			if ((ERROR_SUCCESS == dwError) && (REG_DWORD == dwType) && (dwData == 1))
			{
				// 如果安裝的是 .Net 4.0,則認為其版本號為 0。
				dwData = 0;
			}
			else
			{
				dwError = ERROR_INVALID_DATA;
			}
		}
	}

	if (hKey != NULL)
	{
		::RegCloseKey(hKey);
	}

	return ((ERROR_SUCCESS == dwError) && (dwData >= dwMinimumRelease));
}

如果檢測到沒有安裝框架,則直接啟動 .Net Framework 4.0 安裝程式。這裡還有一個問題需要處理,.Net Framework 4.0 安裝程式在執行安裝時需要 Windows Imaging Component 的支援,如果客戶機上未安裝此元件,則框架安裝程式將無法繼續,經過試驗,在新裝的 MSDN Windows Server 2003 SP1 上該元件是未安裝的,而對於 Windows XP SP3,Windows Vista,Windows 7 等以上的作業系統,都是已經包含了該元件的,所以為了保證框架安裝程式能夠正常安裝,必需保證作業系統已經包含了該元件,因此需要檢測作業系統上是否已經安裝了WIC(Windows Imaging Component)。經過一番網上查閱,發現有兩種方法來間接判斷是否已經安裝了WIC,一種是通過檢測登錄檔相關鍵值的方式,即檢測下列登錄檔鍵值是否存在:


HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\WIC

另外一種方法是可以通過檢測系統目錄是否包含了WindowsCodecs.dll 這個檔案來進行判定,只要系統安裝了WIC,系統目錄下就會包含這個檔案,我使用檢測檔案的這種方法。


// 檢查計算機是否安裝了 Windows Imaging Component,通過檢測系統目錄是否存在對應的檔案:WindowsCodecs.dll 來進行判定。
bool IsWicInstalled()
{
	// 獲取系統目錄。
	int MAX_PATH_LENGTH = 64;
	wchar_t systemDirectory[MAX_PATH_LENGTH];
	int nLength = ::GetSystemDirectoryW(systemDirectory, MAX_PATH_LENGTH);
	
	// 函式呼叫失敗或者返回的系統路徑超過緩衝區長度。
	if ((nLength == 0) || (nLength > (MAX_PATH_LENGTH + 1)))
	{
		return false;
	}

	// 生成檔名。
	CString wicFileName = CString(systemDirectory);
	if (wicFileName.GetAt(wicFileName.GetLength() - 1) != '\\')
	{
		wicFileName += L"\\"; 
	}
	wicFileName += L"WindowsCodecs.dll";

	// 查詢檔案是否存在。
	WIN32_FIND_DATA fData;
	HANDLE hFile;
	hFile = FindFirstFile(wicFileName, &fData);
	bool isFileExist = (hFile != INVALID_HANDLE_VALUE);
	FindClose(hFile);

	return isFileExist;
}

如果檢測到WIC尚未安裝,則首先執行WIC的補丁安裝程式,這個補丁安裝程式可以使用命令列引數來定義安裝行為,安裝程式的可用選項如下圖所示:



我使用的是 /passive /norestart 這兩個引數,通過如下方式來啟動安裝程式並獲取其退出碼,當退出碼為0時,表示安裝程式正常退出,可用繼續框架安裝程式的安裝了。


// 從命令列啟動安裝並返回退出碼。
DWORD CSetupApp::InstallPrerequisiteAndReturnExitCode(CString cmdline)
{
	STARTUPINFO si = {0};
	si.cb = sizeof(si);
	PROCESS_INFORMATION pi = {0};

	BOOL bLaunchedSetup = ::CreateProcess(NULL, 
								cmdline.GetBuffer(),
								NULL, NULL, FALSE, 0, NULL, NULL, 
								&si,
								&pi);
	DWORD dwExitCode;
	if (bLaunchedSetup)
	{
        // 持續等待相關程序退出。
		::WaitForSingleObject(pi.hProcess, INFINITE);
		::GetExitCodeProcess(pi.hProcess, &dwExitCode);
		::CloseHandle(pi.hProcess);
	}

	return dwExitCode;
}

2 .Net Framework 4.0 安裝程式安裝進度的獲取

在進行框架安裝程式的安裝時,有兩種方式可以選擇,一種是讓框架安裝程式以主動模式安裝,這樣可以看到進度顯示,且不需要使用者進行互動(如單擊接受協議、下一步等),可以通過在呼叫框架安裝程式時使用引數 /passive 來解決(下圖列出了框架安裝程式能夠使用的命令列開關),這種方式比較簡單,處理過程就和上述的安裝WIC元件無太大差別,都是以特定引數啟動安裝程式,然後獲取退出碼來判斷安裝是否正常完成。這裡有點特殊的是,如果框架安裝程式正常退出,則退出碼為0,如果正常退出但需要重啟,則退出碼為3010。



另外一種方法是讓框架安裝程式在後臺執行,自己編寫程式碼獲取進度並予以顯示。我感興趣的當然是後面一種方法。經過查閱微軟的知識庫,發現微軟已經為這個問題編寫一個連結器示例程式。該連結器使用記憶體共享的方法將安裝程序相關的資訊通知呼叫方。呼叫方通過訪問指定名稱的記憶體資料結構來獲取安裝進度。具體方法是在呼叫框架安裝程式時就為其指定一個名稱唯一的通道,這可以通過開關 /pipe 來進行指定。對應的,微軟在連結器中定義了一個數據結構來表示當前的安裝進度,如下所示:


// MMIO data structure for inter-process communication
struct MmioDataStructure
{
    // Is download done yet?
    bool m_downloadFinished;
    // Is installer operation done yet?
    bool m_installFinished;
    // Set to cause downloader to abort
    bool m_downloadAbort;
    // Set to cause installer operation to abort
    bool m_installAbort;
    // HRESULT for download
    HRESULT m_hrDownloadFinished;
    // HRESULT for installer operation
    HRESULT m_hrInstallFinished;
    // Internal error from MSI if applicable
    HRESULT m_hrInternalError;
    // This gives the windows installer step being executed if an error occurs while processing an MSI, for example, "Rollback"
    WCHAR m_szCurrentItemStep[MAX_PATH];
    // Download progress 0 - 255 (0 to 100% done)                                       
    unsigned char m_downloadProgressSoFar;
    // Install progress 0 - 255 (0 to 100% done)
    unsigned char m_installProgressSoFar;
    // Event that chainer creates and chainee opens to sync communications.
    WCHAR m_szEventName[MAX_PATH]; 
};

呼叫方通過不斷讀取該資料結構的資料成員來獲取安裝進度,此時可以在安裝介面上放置一個進度條控制元件,然後定義一個計時器,以指定的間隔不斷訪問安裝進度成員變數,更新顯示即可。當安裝進度達到最大值,且檢測成員變數 m_installFinished 為 true 時,表示安裝已結束,檢測退出碼是否為指示成功的相應值就可以判斷框架是否安裝成功了。以下我對微軟的示例做了一點簡單的修改以適應我的安裝程式。


class MmioChainer : protected MmioChainerBase
{
public:
    MmioChainer (LPCWSTR sectionName, LPCWSTR eventName)
        : MmioChainerBase(CreateSection(sectionName), CreateEvent(eventName))
    {
        Init(eventName);
    }

    virtual ~MmioChainer ()
    {
        ::CloseHandle(GetEventHandle());
        ::CloseHandle(GetMmioHandle());
    }

public:
    using MmioChainerBase::IsDone;
    using MmioChainerBase::Abort;
    using MmioChainerBase::IsAborted;
    using MmioChainerBase::GetInstallResult;
    using MmioChainerBase::GetInstallProgress;
    using MmioChainerBase::GetDownloadResult;
    using MmioChainerBase::GetDownloadProgress;
    using MmioChainerBase::GetCurrentItemStep;

    HRESULT GetInternalErrorCode()
    {
        return GetInternalResult();
    }

    bool Launch(const CString& args)
    {
        CString cmdline = L"Prerequisite\\dotNetFramework4\\dotNetFx40_Full_x86_x64.exe /pipe installing " + args;
        STARTUPINFO si = {0};
        si.cb = sizeof(si);
        PROCESS_INFORMATION pi = {0};

        BOOL bLaunchedSetup = ::CreateProcess(NULL, 
                                    cmdline.GetBuffer(),
                                    NULL, NULL, FALSE, 0, NULL, NULL, 
                                    &si,
                                    &pi);

        if (bLaunchedSetup != 0)
        {
            handleThread = pi.hThread;
            handleProcess = pi.hProcess;
        }
        else
        {
            handleThread = NULL;
            handleProcess = NULL;
        }

        return (bLaunchedSetup != 0);
}

    void CloseThreadAndProcess()
    {
	::CloseHandle(handleThread);
	::CloseHandle(handleProcess);
    }

private:
    HANDLE handleThread, handleProcess;

private:
    static HANDLE CreateSection(LPCWSTR sectionName)
    {
        return ::CreateFileMapping (INVALID_HANDLE_VALUE,
                                NULL,
                                PAGE_READWRITE,
                                0,
                                sizeof(MmioDataStructure),
                                sectionName);
    }
    static HANDLE CreateEvent(LPCWSTR eventName)
    {
        return ::CreateEvent(NULL, FALSE, FALSE, eventName);
    }
};

其中記憶體共享的關鍵是使用函式MapViewOfFile建立記憶體對映,具體可以參考微軟的MSDN。


3 安裝流程

參考InstallShield的安裝程式,我設計了以下的安裝程式介面:



(1)安裝協議接受介面。顯示安裝使用協議,安裝使用協議存放在一個RTF格式的文件中,當安裝程式執行時使用富文字框控制元件自動載入,這樣也可以很方便的修改安裝使用協議。只有當用戶勾選了同意安裝協議才能單擊下一步按鈕。



(2)程式功能選擇介面。使用者在此介面選擇不同的程式功能,現在只支援一次安裝單個的程式功能,可以擴充套件為一次安裝多個程式功能,這樣安裝程式的適用性更好一些。有興趣的朋友可以在此基礎上進一步修改。



(3)安裝路徑選擇介面。可以讓使用者選擇不同的安裝路徑。



(4)安裝摘要介面。顯示簡單的摘要資訊。



(5)執行安裝介面。將使用者選擇的功能安裝到客戶機上。



(6)完成安裝介面。可以選擇是否立即啟動程式。


4 安裝程式結構及安裝配置檔案

整個安裝程式結構如下:


Setup
|--Setup.exe
|--Installer
|--|--Data.zip
|--|--Installer.exe
|--|--Installer.xml
|--|--Ionic.Zip.dll
|--|--License.rtf
|--Prerequisite
|--|--dotNetFramework4
|--|--|--dotNetFx40_Full_x86_x64.exe
|--|--wic
|--|--|--wic_x86_chs.exe

其中Setup.exe為載入程式,負責檢測和安裝WIC和框架,Installer資料夾內為應用安裝程式,負責將應用安裝到客戶機上。Prerequisite資料夾內包含了WIC和框架的安裝程式。解除安裝程式已經打包到了Data.zip中,當安裝應用程式時直接複製到安裝目錄下以便執行解除安裝。Installer.exe為應用安裝主程式。Installer.xml為安裝配置檔案。Ionic.Zip.dll為解壓縮元件。License.rtf為安裝協議。

為了和安裝流程相適應,同時為了控制安裝程式的行為,我採用XML檔案的方式來定義應用安裝程式的一些特性。


<?xml version="1.0" encoding="utf-8"?>
<installer title="測試安裝程式" code="Test" version="1.0.0.0" publisher="我">
  <features>
    <feature name="服務端" code="Server" data="Server" ngen="true" launchapplication="Server.exe" guid="11d3ff3c-c890-4f62-9e44-a88457fd9c18">
      <shortcut type="program" name="服務端" target="Server.exe" icon="Server.ico">
      <shortcut type="program" name="使用者手冊" target="Help.chm" icon="Help.ico">
      <shortcut type="program" name="解除安裝" target="Uninstaller.exe" icon="Uninstaller.ico">
      <shortcut type="desktop" name="測試程式(服務端)" target="Server.exe" icon="Server.ico">
      <service name="UdpServer" target="UdpServer.exe">
    </service></shortcut></shortcut></shortcut></shortcut></feature>
   <feature name="客戶端" code="Client" data="Client" ngen="true" launchapplication="Client.exe" guid="954f3e22-a1b1-4fb3-8adc-1ef61d979d19">
      <shortcut type="program" name="客戶端" target="Client.exe" icon="Client.ico">
      <shortcut type="program" name="使用者手冊" target="Help.chm" icon="Help.ico">
      <shortcut type="program" name="解除安裝" target="Uninstaller.exe" icon="Uninstaller.ico">
      <shortcut type="desktop" name="測試程式(客戶端)" target="Client.exe" icon="Client.ico">
      <span name="微軟雅黑" file="msyh.ttf" type="TrueType" style="">
    </span></shortcut></shortcut></shortcut></shortcut></feature>
  </features>
</installer>

各個元素的意義:


title:安裝程式的標題,會顯示在安裝介面上。
code:安裝程式的代號,用於在使用者選擇安裝路徑後附加在安裝路徑上,例如使用者選擇了安裝路徑為C:\Program Files,則最終應用程式安裝在C:\Program Files\$code下。
version:應用程式的版本。用於顯示在“Windows安裝/解除安裝程式”介面。
publisher:應用程式的釋出者,用於顯示在“Windows安裝/解除安裝程式”介面。
feature:安裝程式的功能。如果應用程式有多個功能,可以分別列出供使用者進行選擇安裝。
feature\name:應用程式的功能名稱。
feature\code:功能程式碼。用於生成安裝路徑。
feature\data:安裝檔案在ZIP壓縮包中的資料夾名稱。不同的安裝功能對應了不同的安裝檔案,將這些安裝檔案集中在一起放置在一個壓縮包中。不同的功能在壓縮包中以不同的資料夾名稱予以命名,當用戶選擇了某個功能時,將對應的資料夾內容解壓縮到目標目錄。
feature\ngen:安裝完成後,是否對主程式執行本機映像生成以提高啟動速度。
feature\launchApplication:安裝完畢後需要啟動執行的應用程式。
feature\guid:程式功能的唯一ID,用於在生成反安裝資訊時作為登錄檔鍵值的名稱。
shortcut:表示一個快捷方式。
shortcut\type:快捷方式的型別,program表示為程式組的快捷方式,desktop表示為桌面的快捷方式。
shortcut\name:快捷方式的名稱。
shortcut\target:快捷方式所關聯的應用程式或檔案。
shortcut\icon:快捷方式所使用的圖示檔案。
service:表示一個服務。
service\name:服務的名稱。用於在解除安裝時識別服務使用。
service\target:服務對應的服務檔案。
font:表示要安裝的字型。
font\name:字型的名稱。
font\file:字型檔名。
font\type:字型的型別。

所有檔案均以相對路徑的方式進行表示,例如圖示檔案,如果快捷方式使用了一個圖示檔案,在安裝程式的對應路徑下的應該放置一個名稱為“icon”的資料夾,把使用的圖示檔案放在其中,字型放置在名稱為“font”的資料夾中,以便安裝程式複製和引用。


5 複製檔案

應用安裝程式的一個重要任務是複製安裝檔案到指定的目錄,一般在其他安裝軟體中,都是要使用者逐個指定要建立的資料夾和檔案。要是也這樣來準備安裝檔案,不免有些繁瑣,我採用的是將所有需要安裝到客戶機上的檔案打包為一個ZIP格式的壓縮包,然後再釋放到使用者選擇的安裝路徑下。如果一個安裝程式有多個程式功能(如服務端、客戶端有不同的安裝檔案),則將其分別放置在壓縮包內的不同資料夾下,在使用者選擇了要安裝的程式功能後,根據功能解壓縮指定資料夾的檔案。在這裡我使用的是DotNetZipLib-1.9的壓縮和解壓縮元件。可以先使用其他壓縮工具將需要安裝到客戶機上的檔案打包為一個ZIP包,如WinRAR,WinZip等工具將各個功能以資料夾的形式分別存放。



在安裝時使用如下的程式碼直接將檔案釋放到使用者選擇的安裝路徑上:


/// <summary>
/// 解壓縮安裝檔案到指定的目錄。
/// </summary>
/// <returns></returns>
private bool UnzipFile()
{
    // 解壓縮安裝檔案到指定目錄。
    try
    {
        string dataFileName = Path.Combine(Application.StartupPath, "data.zip");
        string targetPath = aInstallerConfiguration.Destionation;
        if (Directory.Exists(targetPath) == false)
        {
            Directory.CreateDirectory(targetPath);
        }

        using (ZipFile aZipFile = new ZipFile(dataFileName, Encoding.GetEncoding("gb2312")))
        {
            aProgressBar.Maximum = aZipFile.Entries.Count;
            foreach (ZipEntry aEntry in aZipFile.Entries)
            {
                if (aEntry.FileName.StartsWith(aInstallerConfiguration.Feature.Data, StringComparison.OrdinalIgnoreCase))
                {
                    aEntry.Extract(targetPath, ExtractExistingFileAction.OverwriteSilently);
                    aProgressTip.Text = string.Format("複製檔案 {0}", aEntry.FileName);
                    Application.DoEvents();
                    if (this.aProgressBar.Value < this.aProgressBar.Maximum)
                    {
                        this.aProgressBar.Value++;
                    }
                }
            }
        }
        this.aProgressBar.Value = this.aProgressBar.Maximum;
    }
    catch (Exception ex)
    {
        MessageDisplayer.DisplayError("複製安裝檔案發生錯誤。", ex.Message);
        return false;
    }

    return true;
}

需要注意的是,DotNetZipLib對於中文支援似乎有問題,使用WinRAR壓縮的ZIP包,如果其中有檔案或資料夾的名稱包含中文字元,即使使用GB2312編碼進行解壓縮,解壓縮後的檔名仍舊為亂碼,但是使用DotNetZipLib本身以GB2312編碼對包含中文名稱的檔案或資料夾進行壓縮,然後再解壓縮,資料夾或檔名是正常的,不會產生亂碼。


6 建立桌面快捷方式

在 .Net Framework 4.0 的類中,並沒有相應的類來完成這個功能,因此需要藉助其他元件的功能來實現。最簡便的方法是使用Windows Script Host Object Model 庫(其為一ActiveX元件,一般位於system32下,名稱為wshom.ocx),可以使用新增COM引用的方式來建立對該元件的引用。


IWshRuntimeLibrary.IWshShell_Class shell = new IWshShell_Class();
foreach (Shortcut aShortcut in aInstallerConfiguration.Feature.Shortcuts)
{
    string destDirectory = aShortcut.Destination == ShortcutDestination.Program ? featureDirectory : desktopDirectory;
    string pathLink = Path.Combine(destDirectory, aShortcut.Name + ".lnk");
    IWshRuntimeLibrary.IWshShortcut shortcut = (IWshRuntimeLibrary.IWshShortcut)shell.CreateShortcut(pathLink);
    shortcut.TargetPath = Path.Combine(this.aInstallerConfiguration.Destionation, this.aInstallerConfiguration.Feature.Code + "\\" + aShortcut.Target);
    shortcut.Arguments = string.Empty;
    shortcut.Description = aShortcut.Name;
    shortcut.WorkingDirectory = Path.GetDirectoryName(shortcut.TargetPath);
    shortcut.IconLocation = Path.Combine(this.aInstallerConfiguration.Destionation, this.aInstallerConfiguration.Feature.Code + "\\Icon\\" + aShortcut.Icon);
    shortcut.WindowStyle = 1;
    shortcut.Save();
}
shell = null;

使用其中的 IWshShortcut 介面,可以進行快捷方式的建立,其中的幾個引數意義解釋如下:


TargetPath:連結目標的路徑,即該快捷方式所關聯的程式的路徑。
Arguments:引數,執行程式時附加的引數。
Description:快捷方式的描述,將作為快捷方式的名稱進行顯示。
WorkingDirectory:工作目錄,關聯的程式所在目錄名稱。
IconLocation:快捷方式使用的圖示檔案路徑,可以使用EXE中的圖示資源。
WindowStyle:啟動程式時視窗的風格。

定義完畢後儲存就建立了一個快捷方式。需要注意的是,快捷方式的儲存位置是在CreateShortcut 方法中指定的,因為建立的是桌面快捷方式,所以首先需要獲取“桌面”這個特殊資料夾的路徑,這個可以通過 C# 中的 Environment 類實現,如下所示:


string desktopDirectory = Environment.GetFolderPath(Environment.SpecialFolder.Desktop);

該類有一個 GetFolderPath 方法,可以為其指定引數獲取不同的系統目錄,很是方便。


7 建立程式組

建立程式組和建立快捷方式實質上沒有太大的差別,只不過程式組是把一組快捷方式集中放置在一個資料夾中,而這個資料夾有點特殊,叫程式資料夾,一樣的可以通過呼叫 Environment 類的 GetFolderPath 方法為其指定引數來獲取這個特殊資料夾:


string startMenuDirectory = Environment.GetFolderPath(Environment.SpecialFolder.Programs, Environment.SpecialFolderOption.DoNotVerify);

獲取到之後,可以在其下建立資料夾,建立快捷方式等。


8 安裝字型

如果程式需要使用一些特殊字型,如微軟的雅黑字型,客戶機上不一定會安裝,為了保證程式的顯示效果,需要將這些字型安裝到客戶機上。一般程式設計師都知道,系統資料夾中有一個 Fonts 資料夾,其中儲存了系統已經安裝的字型。有時為了省事,直接將字型檔案複製到這個資料夾,系統會自動為你註冊字型,但是在安裝程式中,光是將檔案複製到這個資料夾下是不起作用的,少了後面註冊的那一步,在系統環境下複製是因為有系統的Applet 來幫助進行註冊,在安裝程式這個環境中只能自己註冊。.Net Framework 4.0中只有列舉系統已安裝字型的類,並沒有提到可以幫助註冊字型的類,怎麼辦?還是請 Windows 32 DLL 函數出場吧!


[DllImport("Gdi32.dll", SetLastError = true)]
private static extern int AddFontResource(string lpName);

可以使用 AddFontResource 函式對字型進行註冊,需要注意的是該函式註冊字型只對當前的 Windows 會話有效,一旦重新啟動後,該字型在系統字型列表中將會消失,為了永久註冊該字型,需要將字型資訊寫入登錄檔,登錄檔鍵值為:


HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Fonts

將字型名稱和字型檔名寫入登錄檔,再將字型檔案複製到系統字型資料夾即可完成字型的永久註冊,這樣在系統重啟後,仍然有該字型的相關資訊。在註冊字型後,一般需要通過傳送一條字型改變的訊息來通知其他程式,以便其他程式根據需要調整顯示。程式碼如下:


public class FontInstaller
{
    [DllImport("Gdi32.dll", SetLastError = true)]
    private static extern int AddFontResource(string lpName);
    [DllImport("User32.dll", SetLastError = true)]
    private static extern int SendMessage(IntPtr hWnd, int nMsg, IntPtr wParam, IntPtr lParam);

    private IntPtr HWND_BROADCAST = (IntPtr)0xFFFF;
    private int WM_FONTCHANGE = 0x001D;

    public void InstallFont(Font theFont)
    {
        try
        {
            // 檢查字型是否存在。
            string systemFontFileName = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.Fonts), Path.GetFileName(theFont.File));
            if (File.Exists(systemFontFileName))
            {
                return;
            }

            // 複製字型到系統字型資料夾。
            File.Copy(theFont.File, systemFontFileName, true);

            // 新增字型到系統字型列表。注意使用此函式新增只對當前 Windows 會話有效,重啟後丟失。
            int fontAdded = AddFontResource(systemFontFileName);

            // 廣播字型改變訊息以便其他程式適時更改顯示。
            fontAdded = SendMessage(HWND_BROADCAST, WM_FONTCHANGE, (IntPtr)0, (IntPtr)0);

            // 將字型資訊寫入登錄檔以便重啟後仍可使用。
            RegistryKey keyFonts = Registry.LocalMachine.OpenSubKey("SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion\\Fonts", true);
            if (keyFonts != null)
            {
                // 檢查鍵值是否已經存在。
                string valueName = string.Format("{0} ({1})", theFont.Name, theFont.Type);
                if (keyFonts.GetValue(valueName) == null)
                {
                    keyFonts.SetValue(valueName, Path.GetFileName(theFont.File), RegistryValueKind.String);
                }
            }
        }
        catch (Exception)
        {
            throw;
        }
    }
}

9 安裝服務

有的時候,應用程式是一個服務端,需要在伺服器上以系統服務的方式執行,要求在安裝程式的時候將服務註冊到系統中。這可以通過 TransactedInstaller 來實現。


/// <summary>
/// 安裝服務。
/// </summary>
/// <returns></returns>
private bool InstallServices()
{
    // 獲取服務的檔名稱。
    foreach (KeyValuePair<string string=""> aService in aInstallerConfiguration.Feature.Services)
    {
        string servicesFileName = Path.Combine(aInstallerConfiguration.Destionation, aInstallerConfiguration.Feature.Code, aService.Value);
        if (File.Exists(servicesFileName) == false)
        {
            MessageDisplayer.DisplayError("服務檔案不存在。");
            return false;
        }

        try
        {
            string[] cmdline = { };
            TransactedInstaller transactedInstaller = new TransactedInstaller();
            AssemblyInstaller assemblyInstaller = new AssemblyInstaller(servicesFileName, cmdline);
            transactedInstaller.Installers.Add(assemblyInstaller);
            transactedInstaller.Install(new System.Collections.Hashtable());
        }
        catch (Exception ex)
        {
            MessageDisplayer.DisplayError("安裝服務發生錯誤。", ex.Message);
            return false;
        }
    }
            

    return true;
}
</string>

解除安裝服務則相反,只不過需要使用 TransactedInstaller的Uninstall方法。


10 執行本機映像生成

使用框架編寫的程式生成的是中間程式碼,在客戶機上啟動執行時需要經過編譯,為了提高啟動速度,提高記憶體使用效率,可以在安裝階段使用框架提供的NGEN工具對程式執行本機映像生成。那麼如何獲取NGEN的安裝路徑以便呼叫呢?可以通過訪問登錄檔的方式來獲取框架的安裝路徑。


private bool Ngen()
{
    // 檢查是否需要執行本機映像生成。
    if (aInstallerConfiguration.Feature.Ngen == false)
    {
        return true;
    }

    // 對安裝的程式集執行本機映像生成以提高啟動速度。
    try
    {
        aProgressTip.Text = string.Format("優化程式效能...");
        Application.DoEvents();

        // 呼叫 NGen 執行本地映像生成。
        RegistryKey keyLocalMachine = Registry.LocalMachine;
        RegistryKey keyNetFramework4 = keyLocalMachine.OpenSubKey(@"SOFTWARE\Microsoft\NET Framework Setup\NDP\v4\Full");
        if (keyNetFramework4 != null)
        {
            object netFramework4InstallPath = keyNetFramework4.GetValue("InstallPath");
            if (netFramework4InstallPath != null)
            {
                string ngenFileName = Path.Combine(netFramework4InstallPath.ToString(), "ngen.exe");
                if (File.Exists(ngenFileName))
                {
                    // 生成執行本機映像生成的命令列並啟動之。
                    ProcessStartInfo aStartInfo = new ProcessStartInfo();
                    aStartInfo.CreateNoWindow = true;
                    aStartInfo.WindowStyle = ProcessWindowStyle.Hidden;
                    aStartInfo.FileName = ngenFileName;
                    aStartInfo.Arguments = string.Format("install \"{0}\"",
                                Path.Combine(this.aInstallerConfiguration.Destionation, this.aInstallerConfiguration.Feature.Code, this.aInstallerConfiguration.Feature.LaunchApplication));
                    Application.DoEvents();

                    // 無限期等待?
                    Process.Start(aStartInfo).WaitForExit();
                }
            }
            keyNetFramework4.Close();
        }
        keyLocalMachine.Close();
    }
    catch (Exception)
    {
        return false;
    }

    return true;
}

11 生成反安裝配置資訊並寫入登錄檔

為了便於反安裝程式的工作,需要在安裝時將使用者選擇的一些安裝設定資訊儲存起來,當用戶執行解除安裝程式時,解析這些配置檔案,根據配置進行解除安裝。反安裝程式是和安裝檔案放置在一起的,在安裝時一起釋放到使用者選擇的安裝路徑下,這和其他的安裝程式類似。通過在程式組中建立快捷方式來指向反安裝程式,這樣使用者就可以在開始選單的程式組中對程式進行解除安裝操作。


/// <summary>
/// 生成反安裝配置檔案。
/// </summary>
/// <returns></returns>
private bool BuildUnintallXml()
{
    // 生成反安裝配置檔案。
    try
    {
        aProgressTip.Text = string.Format("生成反安裝配置檔案...");
        Application.DoEvents();

        StringBuilder aUninstallXml = new StringBuilder();
        aUninstallXml.AppendLine("<?xml version=\"1.0\" encoding=\"utf-8\"?>");
        aUninstallXml.AppendLine("<uninstaller>");

        // 記錄程式名稱。
        aUninstallXml.AppendLine(string.Format("  <title>{0}</title>", this.aInstallerConfiguration.ApplicationName));

        // 記錄安裝的程式功能。
        aUninstallXml.AppendLine(string.Format("  <feature name="\'{0}\'" code="\'{1}\'" data="\'{2}\'" launchapplication="\'{3}\'" guid="\'{4}\'">",
            this.aInstallerConfiguration.Feature.Name,
            this.aInstallerConfiguration.Feature.Code,
            this.aInstallerConfiguration.Feature.Data,
            this.aInstallerConfiguration.Feature.LaunchApplication,
            this.aInstallerConfiguration.Feature.Guid));

        // 記錄生成的桌面快捷方式。
        foreach (Shortcut aShortcut in this.aInstallerConfiguration.Feature.Shortcuts)
        {
            if (aShortcut.Destination == ShortcutDestination.Desktop)
            {
                aUninstallXml.AppendLine(string.Format("  <desktop name="\'{0}\'">", aShortcut.Name));
            }
        }

        // 記錄安裝的服務。
        foreach (var aKeyValue in this.aInstallerConfiguration.Feature.Services)
        {
            aUninstallXml.AppendLine(string.Format("  <service name="\'{0}\'" target="\'{1}\'">", aKeyValue.Key, aKeyValue.Value));
        }

        aUninstallXml.AppendLine("</service></desktop></feature></uninstaller>");
        File.WriteAllText(Path.Combine(this.aInstallerConfiguration.Destionation, this.aInstallerConfiguration.Feature.Code + "\\Uninstaller.xml"), aUninstallXml.ToString());
    }
    catch (Exception ex)
    {
        MessageDisplayer.DisplayError("生成反安裝配置檔案發生錯誤。", ex.Message);
        return false;
    }

    return true;
}

完成了安裝資訊的記錄,如何將安裝的程式讓系統知道,並能夠在系統的“新增/刪除程式”介面進行解除安裝操作呢?實際上,一般安裝的程式都會在登錄檔的下列鍵值註冊一些資訊以便讓系統知道如何呼叫解除安裝程式:


HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall

該鍵值包含以下關鍵專案:


DisplayName:在程式列表中顯示的程式名稱。
DisplayVersion:顯示的程式版本。
InstallLocation:安裝路徑。
Publisher:程式釋出者。
UninstallString:表示反安裝裝程式的路徑和呼叫引數(原來是這樣來實現解除安裝的啊!)。
VersionMajor:主版本號。
VersionMinor:次版本號。

/// <summary>
/// 將解除安裝資訊寫入登錄檔。
/// </summary>
/// <returns></returns>
private bool WriteUninstallRegistry()
{
    bool isSuccessed = true;
    try
    {
        aProgressTip.Text = "將反安裝資訊寫入登錄檔...";
        Application.DoEvents();

        RegistryKey keyLocalMachine = Registry.LocalMachine;
        RegistryKey keyUninstall = keyLocalMachine.OpenSubKey(@"SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall", true);
        if (keyUninstall != null)
        {
            string FeatureKeyName = string.Format("{{{0}}}", this.aInstallerConfiguration.Feature.Guid);
            RegistryKey keyFeature = keyUninstall.CreateSubKey(FeatureKeyName);
            if (keyFeature != null)
            {
                keyFeature.SetValue("DisplayIcon", Path.Combine(this.aInstallerConfiguration.Destionation, this.aInstallerConfiguration.Feature.Code, this.aInstallerConfiguration.Feature.LaunchApplication), RegistryValueKind.String);

                string displayName = this.aInstallerConfiguration.ApplicationName;
                if (this.aInstallerConfiguration.Features.Count > 1)
                {
                    displayName = string.Format("{0}({1})", this.aInstallerConfiguration.ApplicationName, this.aInstallerConfiguration.Feature.Name);
                }
                keyFeature.SetValue("DisplayName", displayName, RegistryValueKind.String);
                keyFeature.SetValue("DisplayVersion", this.aInstallerConfiguration.Version.ToString(), RegistryValueKind.String);
                keyFeature.SetValue("InstallLocation", Path.Combine(this.aInstallerConfiguration.Destionation, this.aInstallerConfiguration.Feature.Code), RegistryValueKind.String);
                keyFeature.SetValue("Publisher", this.aInstallerConfiguration.Publisher, RegistryValueKind.String);
                keyFeature.SetValue("UninstallString", Path.Combine(this.aInstallerConfiguration.Destionation, this.aInstallerConfiguration.Feature.Code, "Uninstaller.exe"), RegistryValueKind.String);
                keyFeature.SetValue("VersionMajor", this.aInstallerConfiguration.Version.Major, RegistryValueKind.DWord);
                keyFeature.SetValue("VersionMinor", this.aInstallerConfiguration.Version.Minor, RegistryValueKind.DWord);
                keyFeature.Close();
            }
            else
            {
                isSuccessed = false;
                MessageDisplayer.DisplayError("無法建立登錄檔專案。");
            }
                    
            keyUninstall.Close();
        }
        keyLocalMachine.Close();
    }
    catch (Exception ex)
    {
        isSuccessed = false;
        MessageDisplayer.DisplayError("寫入登錄檔資訊傳送錯誤。", ex.Message);
    }

    return isSuccessed;
}

只要寫入了這些資訊,就可以在系統的“新增/刪除程式”列表中看到你的程式了,恩,不錯!


12 應用程式的解除安裝

應用程式的解除安裝相對來說就簡單一些了。主要是讀取安裝時生成的反安裝配置檔案,刪除複製的檔案、建立的快捷方式、解除安裝服務,字型以及框架可根據需要決定是否解除安裝。需要注意的是在刪除快捷方式時路徑的處理,不要把整個桌面資料夾或者程式組資料夾都給刪掉了,這樣就麻煩了,剛開始測試的時候,沒有考慮周全,結果把整個程式組都給刪掉了,點選“開始”->“所有程式”,所有程式組都沒了,還好是在虛擬機器中進行的測試。


13 總結

本文初步實現了一個可用的安裝程式,探究了安裝程式的一些行為。可供感興趣的朋友繼續在此基礎上深化提高。例如實現多個程式功能同時安裝,支援在安裝前檢查程式是否安裝,如果已安裝則提供修復和重安裝選項等。


14 程式碼下載及版權說明

(1)完整程式碼我已經放置在CSDN的下載網站上(下載連結),有興趣的朋友可以下載自己修改。其中的Data.zip檔案我只放了圖示檔案,其他的檔案均為0位元組的檔案,如果你需要測試安裝程式,可將檔案自行替換。

(2)對於修改和使用下載程式碼對系統造成異常或任何其他損失,本文作者不負連帶責任。

(3)轉載本文和使用下載的程式碼請註明出處以尊重作者的勞動。本文部分程式碼為微軟MSDN的示例程式碼,在程式碼中使用了DotNetZipLib元件,請遵守各自的License。