WiXPackage
Customizing the Burn UI via WiX Toolset and wpf.
Install / Use
/learn @DG-Wangtao/WiXPackageREADME
WiX Toolset制作完全自定义界面的Windows安装程序
借助WiX提供的Bootstrapper和Burn技术,编写WPF MVVM图形界面类库,来实现自定义用户界面的捆绑安装。
-
目录
实现的功能
- 将使用Visual Studio 开发的windows软件打包为安装软件(.exe)
- 具有安装/卸载/修复/的功能,可判断是否已安装旧版本
- 判断是否已安装所依赖的其他软件,如.net framework,支持package从网址自动下载安装
- 获取用户输入信息作为安装包内的某些属性值,如用户姓名,是否创建快捷方式,安装路径等
项目流程
- 拥有要打包的WINDOWS项目或要打包的文件
- 建立WiX Setup Project将上述文件打包为 .msi安装文件,本项目可以创建用户界面但默认是没有,我们也不需要
- 建立Bootstrapper Project对生成的 .msi安装文件进行包装,并对依赖的外部环境或者软件进行判断安装,提供安装界面
- 建立c# wpf类库,自定义用户安装行为与界面,实现与ootstrapper Project的通信

如何使用Demo
下载安装WiX
从官网下载WiX安装包后进行安装即可,这是免费的。也可以使用Visual Studio的NuGet进行搜索安装。
下载源码到本地
下载仓库源码到本地后,使用Visual Studio(开发时使用的2015)打开解决方案Installer.sln。
添加要打包的项目引用
将打开后未能成功加载的WpfAppToPackage项目移除并添加要打包的已存在的项目,并且移除DGSetup项目中References对WpfAppToPackage的引用,添加自己要打包的项目的引用。这里这么做是为了向DGSetup项目添加要打包的文件,当然这一步也可以不做,直接删除未成功加载的项目及引用即可,这样就需要通过相对路径添加要打包的文件,看这里。
修改生成的安装包名称
右键Bootstrapper项目,选择属性,在Installer选项卡的output name属性中修改为要生成的安装包文件名,如 xxx_setup。当然也可以修改Setup Project的生成文件名,我的Demo中他的名称为DGSetup1.msi,若要修改方式与Bootstrapper项目一样,右键属性,修改output name,但要注意的是,一旦修改这一个名称,你就必须要修改如下两个个地方:
- Bootstrapper/Bundle.wxs line 40:
<MsiPackage SourceFile="..\SetupProject\bin\Release\zh-cn\DGSetup1.msi" DisplayInternalUI="no"> - CustomBA/ViewModels/InstallViewModel.CS line 18:
private static string MyInstellerName = "DGSetup1.msi";
编辑Product
在DGSetup项目中Product.wxs文件定义如和将你的文件打包,它是一种xml格式的文件,根节点必须是<Wix>,在<Product>元素中来定义打包行为。
这里只介绍当你要打包自己的文件时要做哪些更改,具体每个元素及属性的含义请看文档末尾,或者查看教程WiX 3.6
修改名称等文字信息
要说明的是,所有文字我都放在了zh-cn/zh-cn.wxl文件中,比如产品名称与在注册表中的名称,公司名称,电话,帮助/关于等网址,这样当需要修改这些文字时只需要修改zh-cn.wxl文件即可,在Product.wxs中引用他们可以用这种方式:!(loc.IdValue),如:
zh-cn.wxl:
<WixLocalization Culture="zh-cn" Codepage="936" Language="4" xmlns="http://schemas.microsoft.com/wix/2006/localization">
<String Id="ProductName">深瞳人眼摄像机客户端</String>
... ...
Product.wxs:
<Product Id="*" Name="!(loc.ProductName)" Language="4"
Version="1.1.0.0"
Manufacturer="!(loc.Manufacturer)" UpgradeCode="B9F8908C-A947-4D44-8D46-A9804C877629">
... ...
修改要打包安装的文件
在34~38行定义了需要打包的文件,如果有外部依赖库需要添加也可以放在这里:
<ComponentGroup Id="ProductComponents">
<Component Id="ProductComponent" Directory="INSTALLFOLDER" Guid="84D3BAE6-2758-4FBD-9714-1E052A066830">
<File Source="$(var.WpfAppToPackage.TargetPath)" Id="myapplication.exe" KeyPath="yes"></File>
</Component>
</ComponentGroup>
这里采用的方式引用要打包的WpfAppToPackage项目的输出目录,也可以采用相对路径的形式Source="..\..\LibraFClient\LibraFClient\bin\Release\LibraFClient.exe" 或者绝对路径也可以,但需要注意,采用相对路径时需要手动设置Guid的值。
当你要打包多个文件时,要注意每一个<Component>最好只含有一个<File>,而且<File>的KeyPath的值最好为yes,因为KeyPath文件可以在丢失后使用“修复”功能重新得到,而一个Component只能有一个KeyPath文件。
批量添加多个文件
将路径下所有文件引入
当需要打包几十几百个文件时,如果自己手动添加<File>节点就太麻烦了,所以WiX提供了heat.exe工具来批量添加指定文件夹下的所有文件。最简单的使用heat.exe的方式是,在我们的msi即Setup Project项目名称上右键->属性,找到Build Events选项卡,在里面的Pre-build Event Command Line中添加如下一行命令:
yourwixtoolsetpath\heat.exe dir "yourdirpath" -dr INSTALLFOLDER -cg yourComponentId -gg -scom -sreg -sfrag -out "youroutputpath\UtilityHeat.wxs"
yourwixtoolsetpath:wix toolset的安装位置,路径中不要包含空格dir:要添加打包的文件夹路径,后面是它的值,用双引号括起来-dr:安装时要把这些文件放置的路径,是Product.wxs中某一<Directory>的Id的值,不需要双引号-cg:在Product.wxs中某一<ComponentRef>的Id的值-out:heat.exe会生成一个wxs文件,-out便是指明这个文件放在什么地方,当然文件名字可以随自己设置
这样在编译项目的时候应该会创建一个wxs,然后我们把它引入到我们的msi即Setup Project项目中就可以了,因为WiX支持跨文件读取节点,所以这个wxs文件内定义的所有内容和Product.wxs中定义的内容都会相互引用。
还采取另外一种方式,那就是找到heat.exe然后在命令行中调用它并指定参数:
我的heat.exe路径是C:\Program Files (x86)\WiX Toolset v3.10\bin\heat.exe,所以打开命令行工具,定位到C:\Program Files (x86)\WiX Toolset v3.10\bin,然后调用heat.exe并给他相同的参数:
C:\Program Files (x86)\WiX Toolset v3.10\bin>heat.exe dir "yourdirpath" -dr INSTALLFOLDER -cg yourComponentId -gg -scom -sreg -sfrag -out "youroutputpath\UtilityHeat.wxs"
控制台会提示你生成成功,然后再把生成的文件引入到项目当中即可。
修改生成的wxs文件
生成成功之后打开wxs文件,会看到他里面是包含了所有要打包的文件引用,但是路径都好像不太对,可以使用批量查找修改的方式将所有路径更改到正确的地方。一般WiX会按照当时要打包的文件夹名称创建一个新的文件夹,然后把所有文件放到里面,如果需要的话可以对文档上方的 <DirectoryRef>进行修改。
添加自定义变量
当需要获取除安装路径/是否创建桌面快捷方式这两种用户安装时输入的信息,如用户名时,可以使用<Property>元素,它的Id属性唯一标识它,而Value属性则是它的值,他可以定义在Product.wxs中<Product>与</Product>之间任意的位置,通过[IdName]可以引用它的值。当然要获取用户输入的信息,还需要其他的设置:
- 在product.wxs中定义
Property:<Property Id="USERNAME" Value=""></Property> - 在Bundle.wxs中引用msi包时,
<MsiPackage SourceFile="..\SetupProject\bin\Release\zh-cn\DGSetup1.msi" DisplayInternalUI="no">
<MsiProperty Name="USERNAME" Value="[Username]"/>
</MsiPackage>
- 在viewmodel中,调用
(BootstrapperApplication的实例).Engine.StringVariables[USERNAME] = Username;
如何创建桌面与开始桌面快捷方式
编辑Bundle
在Bootstrapper项目中,Bundle.wxs用于定义要安装哪些依赖程序,以及如何验证这些依赖程序已经安装,并且定义安装界面的样式。这里只说明你打包自己的文件时需要修改的地方,元素与属性的详细说明请查看文档末尾或者教程。
修改文字信息
和Product一样,我将所有文字信息都放在了zh-cn/zh-cn.wxl文件中,如软件名称,公司名,联系电话等等。引用方式和Product相同:!(loc.IdName)。
引用wpf类库
在<BootstrapperApplicationRef Id="ManagedBootstrapperApplicationHost" >与</BootstrapperApplicationRef>之间通过<Payload>来添加程序集的引用,这里引用了自定义的wpf界面类库CustomBA.dll,以及其他依赖的dll:
<BootstrapperApplicationRef Id="ManagedBootstrapperApplicationHost" >
<Payload SourceFile="$(var.CustomBA.TargetDir)CustomBA.dll" />
<Payload SourceFile="$(var.CustomBA.TargetDir)BootstrapperCore.config" />
<Payload SourceFile="$(var.CustomBA.TargetDir)Microsoft.Practices.Prism.Composition.dll" />
<Payload SourceFile="$(var.CustomBA.TargetDir)Microsoft.Practices.Prism.Interactivity.dll" />
<Payload SourceFile="$(var.CustomBA.TargetDir)Microsoft.Practices.Prism.Mvvm.Desktop.dll" />
<Payload SourceFile="$(var.CustomBA.TargetDir)Microsoft.Practices.Prism.Mvvm.dll" />
<Payload SourceFile="$(var.CustomBA.TargetDir)Microsoft.Practices.Prism.PubSubEvents.dll" />
<Payload SourceFile="$(var.CustomBA.TargetDir)Microsoft.Practices.Prism.SharedInterfaces.dll" />
</BootstrapperApplicationRef>
定义变量
<?define VariableName=Value ?>,在Bundle中定义一个名为VariableName值为Value的变量,可通过$(var.VariableName)进行引用。
安装多个msi或者exe文件
在<Chain>当中,通过<ExePackage>来指定要安装.exe文件,<MsiPackage>来指定要安装.msi文件,SourceFile属性则指定文件源路径,DisplayInternalUI="no"表示不显示该文件执行时显示的界面来实现静默安装,Compressed="no"表示不把这个.exe或者.msi压缩到最终生成的安装包中以减小最终文件的大小,DownloadUrl="$(var.DoNetDownloadUrl)" 指定当安装时若从 SourceFile指定路径找不到该文件则从该网址中下载。不过在你build项目时需要该文件存在于你的SourceFile路径中。
<Chain DisableRollback="yes">
<MsiPackage SourceFile="..\SetupProject\bin\Release\zh-cn\DGSetup.msi" DisplayInternalUI="no">
</MsiPackage>
<MsiPackage SourceFile="..\SetupProject\bin\Release\zh-cn\DGSetup1.msi" DisplayInternalUI="no">
</MsiPackage>
</Chain>
获取用户输入信息并传递给Product
如这里所说,要把变量从viewmdoel传给Product需要进行依次的传值。InstallPageViewModel是安装界面的viewmodel,它定义了一个属性名为“CreateShortCut”的公有bool变量,绑定到用户是否勾选了“创建桌面快捷方式”,并在其setter语句块中调用:
set{
... ...
this.SetBurnVariable("CreateShortCut", bol);
}
将值传入到burn中,在Bundle.wxs中<Chain>中的<MsiPackage>与</MsiPackage>中间获取该值:
<MsiPackage SourceFile="..\SetupProject\bin\Release\zh-cn\DGSetup1.msi" DisplayInternalUI="no">
<MsiProperty Name="CreateShortcutDeskTop" Value="[CreateShortCut]"/>
</MsiPackage>
在Product.wxs中可直接通过[CREATESHORTCUTDESKTOP](将变量名改为大写)获取Bundle传递的值,而后在创建桌面快捷方式的<Component>中加一个验证条件,当CREATESHORTCUTDESKTOP属性的值为True时创建桌面快捷方式,否则不创建:
<Component Id="ApplicationShortcutDeskTop">
+ <Condition>
+ <![CDATA[CREATESHORTCUTDESKTOP="True"]]>
+ </Condition>
<Shortcut Id="ApplicationDeskTopShortcut"
Name="!(loc.ProductName)"
Description="!(loc.Title)"
Target="[#myapplication.exe]" Icon="ico
