用 .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。


技术分享


(2)在应用程序向导的应用程序类型设置中,选择“基于对话框”和“在静态库中使用 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];
	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" guid="11d3ff3c-c890-4f62-9e44-a88457fd9c18" launchapplication="Server.exe" ngen="true">
      <shortcut name="服务端" type="program" icon="Server.ico" target="Server.exe">
      <shortcut name="用户手册" type="program" icon="Help.ico" target="Help.chm">
      <shortcut name="卸载" type="program" icon="Uninstaller.ico" target="Uninstaller.exe">
      <shortcut name="测试程序(服务端)" type="desktop" icon="Server.ico" target="Server.exe">
      <service name="UdpServer" target="UdpServer.exe">
    </service></shortcut></shortcut></shortcut></shortcut></feature>
   <feature name="客户端" code="Client" data="Client" guid="954f3e22-a1b1-4fb3-8adc-1ef61d979d19" launchapplication="Client.exe" ngen="true">
      <shortcut name="客户端" type="program" icon="Client.ico" target="Client.exe">
      <shortcut name="用户手册" type="program" icon="Help.ico" target="Help.chm">
      <shortcut name="卸载" type="program" icon="Uninstaller.ico" target="Uninstaller.exe">
      <shortcut name="测试程序(客户端)" type="desktop" icon="Client.ico" target="Client.exe">
<span name="微软雅黑" type="TrueType" file="msyh.ttf">
    </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}\'" guid="\'{4}\'" launchapplication="\'{3}\'">",
            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。

郑重声明:本站内容如果来自互联网及其他传播媒体,其版权均属原媒体及文章作者所有。转载目的在于传递更多信息及用于网络分享,并不代表本站赞同其观点和对其真实性负责,也不构成任何其他建议。