Skip to content

DG-Wangtao/WiXPackage

Repository files navigation

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的方式是,在我们的msiSetup 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,然后我们把它引入到我们的msiSetup 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="icon"
                 WorkingDirectory="APPLICATIONROOTDIRECTORY"/>
          <RemoveFolder Id="DesktopFolder" On="uninstall"/>
          <RegistryValue
              Root="HKCU"
              Key="Software\Microsoft\!(loc.CompanyName)\!(loc.RegistryName)"
              Name="installed"
              Type="integer"
              Value="1"
              KeyPath="yes"/>
        </Component>

WPF类库与MVVM模式


如何从零开发

下载并安装WiX

从官网下载WiX安装包后进行安装即可,这是免费的。也可以使用Visual Studio的NuGet进行搜索安装。

创建解决方案

  1. 打开Visual Studio(我使用的是VS2015),新建项目(New Project)—>其他项目类型(Other Project Types)—>Visual Studio解决方案(Visual Studio Solutions),修改项目名称与存储位置(我这里命名为WiXPackage)。
  2. 引用要打包的项目,在资源管理器中右击已经建好的空白解决方案,选择添加——>已存在的项目,在打开的对话框中找到要打包的项目添加进来(选择.csproj文件)。
  3. 创建 WiX Setup Project ,在当前解决方案上右键添加——>新项目,如果已经正确安装WiX那么会在打开的项目模板列表左边看到 Windows Installer XML ,点击之后在右边选中 Setup Project ,按自己需要修改名称并确定。此时会看到解决方案资源管理器中看到一个有 Product.wxs 文件和 References 引用文件夹。在References右键—>添加引用—>项目,添加刚才引用的要打包的项目。
  4. 创建 Bootstrapper 项目,在当前解决方案上右键添加——>新项目,同样找到 Windows Installer XML ,在右边选中 Bootstrapper Project ,按自己需要修改名称并确定。此时会看到解决方案资源管理器中看到一个有 Bundle.wxs 文件和 References 引用文件夹。
  5. 创建C#类库,在当前解决方案上右键添加——>新项目,以此找到Visaul C#—>Class Library,命名为CustomBA。向References需要添加Bootstrapper程序集的引用,在WiX的安装路径(C:\Program Files (x86)\WiX Toolset v3.10\SDK)中找到BootstrapperCore.dll并添加引用。在SDK目录下还能看见名为 BootstrapperCore.config 的文件,将它复制粘贴到这个类库项目文件中,打开它并且将他的 host 元素的assemblyName属性值改为本类库名(CustomBA)。

向自定义类库添加必要的引用

  1. 为了要使用WPF来自定义界面,还需要向C#类库添加如下引用:
    • PresentationCore
    • PresentationFramework
    • System.Xaml
    • WindowsBase
  2. 为了要使用MVVM模式,可以使用NuGet来向C#类库安装Prism,在VS菜单栏上依次点击工具—>NuGet Package—>管理NuGet解决方案,在浏览中搜索Prism并安装到CustomBA解决方案中。
  3. 在C#类库中新建一个public类,名称为CustomBootstrapperApplication,暂时先不需要添加任何内容,而后在类库的Properties\AssemblyInfo.cs文件中添加如下代码,它指定Burn调用CustomBootstrapperApplication类的Run方法作为程序入口:
using CustomBA;
using Microsoft.Tools.WindowsInstallerXml.Bootstrapper;
[assembly: BootstrapperApplication(
   typeof(CustomBootstrapperApplication))]
  1. 创建Views,ViewModels,Models文件夹,在C#类库项目中新建以上三个文件夹,依次存放MVVM模式中的Views,ViewModels和Models。

扩展BootstrapperApplication类

刚才在类库中新建了一个名为CustomBootstrapperApplication的类,现在我们对他进行修改。修改后的完整代码如下:

using CustomBA.Models;
using CustomBA.ViewModels;
using CustomBA.Views;
using Microsoft.Tools.WindowsInstallerXml.Bootstrapper;
using System;
using System.Windows.Threading;

namespace CustomBA
{
    public class CustomBootstrapperApplication :BootstrapperApplication
    {
        public static Dispatcher Dispatcher { get; set; }
        protected override void Run()
        {
            Dispatcher = Dispatcher.CurrentDispatcher;

            var model = BootstrapperApplicationModel.GetBootstrapperAppModel(this);
            var view = new InstallView();

            model.SetWindowHandle(view);

            this.Engine.Detect();

            view.Show();
            Dispatcher.Run();
            this.Engine.Quit(model.FinalResult);
        }
    }
}
  • CustomBootstrapperApplication类继承BootstrapperApplication类并重写他的Run方法,这便是安装程序的入口。
  • Run方法中,定义一个Dispatcher对象来惊醒UI线程与其他线程的通信,定义modelview来设置显示界面
  • SetWindowHandle方法是待会要在model类中要写的方法,它给Burn传递要显示的view
  • Engine.Detect()用来检测这个bundle是否已经被安装
  • Detect需要在view,viewmodelmodel实例化之后再被调用,因为它需要的一些配置信息需要被设置。
  • Show()方法则显示图形界面,Dispatcher.Run()来启动线程,当安装完成或者取消时调用Dispatcher.InvokeShutdown()可以结束线程。
  • Engine.Quit()无论安装结果如何都会关闭安装任务。

定义Model

在Models文件夹中新建BootstrapperApplicationModel类并修改为如下内容:

using Microsoft.Tools.WindowsInstallerXml.Bootstrapper;
using System;
using System.Windows;
using System.Windows.Interop;

namespace CustomBA.Models
{
    public class BootstrapperApplicationModel
    {
        private IntPtr hwnd;
        private static BootstrapperApplicationModel bootstrapperAppModel;
        public static BootstrapperApplicationModel GetBootstrapperAppModel(BootstrapperApplication bootstrapperApplication)
        {
            if (bootstrapperAppModel == null)
                bootstrapperAppModel = new BootstrapperApplicationModel(bootstrapperApplication);
            return bootstrapperAppModel;
        }
        public static BootstrapperApplicationModel GetBootstrapperAppModel()
        {
             return bootstrapperAppModel;
        }
        private BootstrapperApplicationModel(BootstrapperApplication bootstrapperApplication)
        {
            this.BootstrapperApplication =
              bootstrapperApplication;
            this.hwnd = IntPtr.Zero;
            string[] strs = GetCommandLine();
        }

        public BootstrapperApplication BootstrapperApplication { get; private set; }

        public int FinalResult { get; set; }

        public void SetWindowHandle(Window view)
        {
            this.hwnd = new WindowInteropHelper(view).Handle;
        }

        public void PlanAction(LaunchAction action)
        {
            this.BootstrapperApplication.Engine.Plan(action);
        }

        public void ApplyAction()
        {
            this.BootstrapperApplication.Engine.Apply(this.hwnd);
        }

        public void LogMessage(string message)
        {
            this.BootstrapperApplication.Engine.Log(
              LogLevel.Standard,
              message);
        }
        public void SetBurnVariable(string variableName, string value)
        {
            this.BootstrapperApplication.Engine
               .StringVariables[variableName] = value;
        }
        public string[] GetCommandLine()
        {
            return this.BootstrapperApplication.Command
               .GetCommandLineArgs();
        }
        public bool HelpRequested()
        {
            return this.BootstrapperApplication.Command.Action ==
               LaunchAction.Help;
        }
    }
    public enum InstallState
    {
        Initializing,
        Present,
        NotPresent,
        Applying,
        Cancelled,
        Applied,
        Failed,
    }
}

该类为WPF程序的Model类,它封装了继承自BootstrapperApplication(来自于BootstrapperCore.dll)的CustomBootstrapperApplication的方法,比如设置界面实例SetWindowHandle(),配置安装信息PlanAction(),执行动作包括安装/卸载/修复ApplyAction(),向Burn传递参数SetBurnVariable()等。 这个类定义为一个单例,方便在每一个viewmodel中获取拥有当前安装程序信息,因为它的构造函数需要一个BootstrapperApplication的参数,在程序入口处实例化Model并传递this作为参数,那么之后的所有viewmodel都可以访问该单例Model而无需再获取一个BootstrapperApplication来实例化Model。

定义ViewModel

InstallView窗口的ViewModel: InstallViewModel

在ViewModels文件夹中新建名为InstallViewModel的类,并修改其内容如下:

using CustomBA.Models;
using Microsoft.Practices.Prism.Commands;
using Microsoft.Practices.Prism.ViewModel;
using Microsoft.Tools.WindowsInstallerXml.Bootstrapper;
using Microsoft.Win32;
using System;
using System.IO;
using System.Windows;
using System.Windows.Input;

namespace CustomBA.ViewModels
{
    public class InstallViewModel : BaseViewModel
    {
        private string bit3264 = @"SOFTWARE\WOW6432Node\Microsoft\DeepGlint";
        private string bit32 = @"SOFTWARE\Microsoft\DeepGlint";
        private string productName = "LibraFClinet";
        private static string MyInstellerName = "DGSetup1.msi";
        private InstallState state;
        private static InstallViewModel viewmodel;
        /// <summary>
        /// 构造函数,使用单例模式
        /// </summary>
        private InstallViewModel()
        {
            this.State = InstallState.Initializing;
            this.WireUpEventHandlers();
            ///检查安装文件是否为空
            this.model.BootstrapperApplication.ResolveSource +=
               (sender, args) =>
               {
                   if (!string.IsNullOrEmpty(args.DownloadSource))
                   {
                       // Downloadable package found 
                       args.Result = Result.Download;
                   }
                   else
                   {
                       // Not downloadable 
                       args.Result = Result.Ok;
                   }
               };
        }
        public static InstallViewModel GetViewModel()
        {
            if (viewmodel == null)
                viewmodel = new InstallViewModel();
            return viewmodel;
        }
        private BootstrapperApplicationModel model
        {
            get
            {
                return BootstrapperApplicationModel.GetBootstrapperAppModel();
            }
        }
        /// <summary>
        /// 当前产品状态
        /// </summary>
        
        public InstallState State
        {
            get
            {
                return this.state;
            }
            set
            {
                if (this.state != value)
                {
                    this.state = value;
                    if (state == InstallState.NotPresent)
                        if (ChekExistFromRegistry())
                        {
                            state = InstallState.Cancelled;
                        }
                    OnPropertyChanged("State");
                    OnPropertyChanged("CancelEnabled");
                    OnPropertyChanged("InstallEnabled");
                    OnPropertyChanged("UninstallEnabled");
                    OnPropertyChanged("ProgressEnabled");
                    OnPropertyChanged("FinishEnabled");
                }
                
            }
        }
        /// <summary>
        /// 与 InstallEnabled,UninstallEnabled,ProgressEnabled,FinishEnabled
        /// 根据State的值,判断该显示哪个界面
        /// </summary>
        public bool CancelEnabled
        {
            get
            {
                return State == InstallState.Cancelled;
            }
        }
        public bool InstallEnabled
        {
            get {
                return State == InstallState.NotPresent;
            }
        }

        public bool UninstallEnabled
        {
            get
            {
                return State == InstallState.Present;
            }
        }
        public bool ProgressEnabled
        {
            get
            {
                return State == InstallState.Applying;
            }
        }
        public bool FinishEnabled
        {
            get
            {
                return State == InstallState.Applied;
            }
        }


        protected void DetectPackageComplete(object sender, DetectPackageCompleteEventArgs e)
        {
            if (e.PackageId.Equals(MyInstellerName, StringComparison.Ordinal))
            {
                this.State = e.State == PackageState.Present ?
                  InstallState.Present : InstallState.NotPresent;
            }

        }

        private void WireUpEventHandlers()
        {
            //重新定义`BootstrapperApplication`的安装包验证方式,判断是当前msi安装包是否已经安装过
            this.model.BootstrapperApplication.DetectPackageComplete += this.DetectPackageComplete;
        }
        /// <summary>
        /// 查找注册表看是否已经安装本产品的其他版本
        /// </summary>
        /// <returns></returns>
        protected bool ChekExistFromRegistry()
        {
            try
            {
                //64位
                using (RegistryKey pathKey = Microsoft.Win32.Registry.LocalMachine.OpenSubKey(bit3264))
                {
                    var strs = pathKey.GetSubKeyNames();
                    foreach (string str in strs)
                        if (str.Equals(productName))
                        {
                            return true;
                        }
                }
                //32位
                using (RegistryKey pathKey = Microsoft.Win32.Registry.LocalMachine.OpenSubKey(bit32))
                {
                    var strs = pathKey.GetSubKeyNames();
                    foreach (string str in strs)
                        if (str.Equals(productName))
                        {
                            return true;
                        }
                }
            }
            catch { }
            return false;
        }
    }
}

安装界面InstallPage的viewmodel:InstallPageViewModel

在ViewModels文件夹中新建InstallPageViewModel类并修改以下内容:

uusing CustomBA.HelpClass;
using CustomBA.Models;
using CustomBA.Views;
using Microsoft.Practices.Prism.Commands;
using Microsoft.Tools.WindowsInstallerXml.Bootstrapper;
using System;
using System.Collections.Generic;
using System.Drawing;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Forms;
using System.Windows.Input;
using System.Windows.Media.Imaging;

namespace CustomBA.ViewModels
{
    public class InstallPageViewModel : BaseViewModel
    {
        private static string SoftWareName = "wpfapptopackage";
        private bool createShortCut;
        private string installFolder;
        private Visibility selectFileVisibility;
        private DelegateCommand BrowseCommand;
        private DelegateCommand InstallCommand;
        private DelegateCommand CloseCommand;
        private DelegateCommand ShowSelecFileCommand;

        /// <summary>
        /// InstallViewModel单例的引用,用来修改InstallViewModel.State
        /// 以实时根据当前状态更改显示的界面内容
        /// </summary>
        private InstallViewModel installViewModel
        {
            get { return InstallViewModel.GetViewModel(); }
        }
        /// <summary>
        /// Model单例的引用,用来定义事件触发执行什么操作
        /// 同时通过该引用的某些方法来执行安装功能
        /// </summary>
        private BootstrapperApplicationModel BootstrapperModel
        {
            get
            {
                return BootstrapperApplicationModel.GetBootstrapperAppModel();
            }
        }
        public InstallPageViewModel()
        {
            InitialCommand();
            SeleFileVisibility = Visibility.Collapsed;
            CreateShortCut = true;
            InstallFolder = @"C:\Program Files (x86)\DeepGlin\" + SoftWareName;
            WireUpEventHandlers();
        }

        /// <summary>
        /// 背景图片资源
        /// </summary>
        public BitmapSource BackImage
        {
            get
            {
                Bitmap bmp = CustomBA.Properties.Resources.page1;
                return System.Windows.Interop.Imaging.CreateBitmapSourceFromHBitmap(bmp.GetHbitmap(),
                    IntPtr.Zero, System.Windows.Int32Rect.Empty,
                    BitmapSizeOptions.FromWidthAndHeight(bmp.Width, bmp.Height));
            }
        }
        public BitmapSource LogoImage
        {
            get
            {
                Bitmap bmp = CustomBA.Properties.Resources.logo;
                return System.Windows.Interop.Imaging.CreateBitmapSourceFromHBitmap(bmp.GetHbitmap(),
                    IntPtr.Zero, System.Windows.Int32Rect.Empty,
                    BitmapSizeOptions.FromWidthAndHeight(bmp.Width, bmp.Height));
            }
        }
        /// <summary>
        /// 自定义安装路径的控件组是否显示
        /// </summary>
        public Visibility SeleFileVisibility
        {
            get { return selectFileVisibility; }
            set
            {
                selectFileVisibility = value;
                OnPropertyChanged("SeleFileVisibility");
            }
        }
       /// <summary>
       /// 是否创建桌面快捷方式
       /// </summary>
        public bool CreateShortCut
        {
            get { return createShortCut; }
            set
            {
                createShortCut = value;
                OnPropertyChanged("CreateShortCut");
                this.SetBurnVariable("CreateShortCut", createShortCut.ToString());
            }
        }
       /// <summary>
       /// 自定义安装路径
       /// 在用户选择的路径后再创建一个当前软件名称的文件夹,
       /// 使安装不混乱在根目录中
       /// </summary>
        public string InstallFolder
        {
            get { return installFolder; }
            set
            {
                try {
                    if (value != installFolder && ValidDir(value))
                    {
                        string[] para = value.Split('\\');
                        bool hassoftwarename = false;
                        foreach (string pa in para)
                        {
                            if (pa == SoftWareName)
                                hassoftwarename = true;
                        }
                        if (hassoftwarename)
                            installFolder = value;
                        else
                            installFolder = value + "\\" + SoftWareName;
                        OnPropertyChanged("InstallFolder");
                        this.SetBurnVariable("InstallFolder", installFolder);
                    }
                }
                catch {
                    installFolder = value;
                }
            }
        }

        /// <summary>
        /// 界面四个按钮的单击事件
        /// </summary>

        public ICommand btn_browse
        {
            get { return BrowseCommand; }
        }
        public ICommand btn_install
        {
            get { return InstallCommand; }
        }
        public ICommand btn_cancel
        {
            get { return CloseCommand; }
        }
        public ICommand btn_show
        {
            get { return ShowSelecFileCommand; }
        }
        /// <summary>
        /// 初始化按钮点击命令要调用的函数
        /// </summary>
        private void InitialCommand()
        {
            BrowseCommand = new DelegateCommand(Browse, IsValid);
            InstallCommand = new DelegateCommand(Install, IsValid);
            CloseCommand = new DelegateCommand(Close, IsValid);
            ShowSelecFileCommand = new DelegateCommand(Show, IsValid);
        }
        /// <summary>
        /// 打开选择安装路径窗口并获取用户选择的路径
        /// </summary>
        public void Browse()
        {
            var folderBrowserDialog = new FolderBrowserDialog { SelectedPath = InstallFolder };

            if (folderBrowserDialog.ShowDialog() == System.Windows.Forms.DialogResult.OK)
            {
                InstallFolder = folderBrowserDialog.SelectedPath;
            }
        }
        /// <summary>
        /// 开始安装
        /// 调用PlanAction()方法而不是ApplyAction()
        /// 此时只是开始执行配置安装信息,并不会执行安装进程
        /// </summary>
        public void Install()
        {
            this.BootstrapperModel.PlanAction(LaunchAction.Install);
        }
        /// <summary>
        /// 取消安装,关闭安装进程
        /// </summary>
        public void Close()
        {
             installViewModel.State = InstallState.Cancelled;
             CustomBootstrapperApplication.Dispatcher.InvokeShutdown();
        }
        /// <summary>
        /// 显示/关闭自定义安装界面
        /// </summary>
        public void Show()
        {
            if (SeleFileVisibility == Visibility.Collapsed)
                SeleFileVisibility = Visibility.Visible;
            else
                SeleFileVisibility = Visibility.Collapsed;
        }
        /// <summary>
        /// 当安装进程开始时触发事件
        /// 将当前状态更改位Applying
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        protected void ApplyBegin(object sender, ApplyBeginEventArgs e)
        {
            this.installViewModel.State = InstallState.Applying;
        }
        /// <summary>
        /// 开始安装时调用的方法位PlanAction()
        /// 该方法执行完成之后触发本事件
        /// 事件中调用ApplyAction()方法开始执行安装进程
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        protected void PlanComplete(object sender, PlanCompleteEventArgs e)
        {
            if (installViewModel.State == InstallState.Cancelled)
            {
                CustomBootstrapperApplication.Dispatcher
                  .InvokeShutdown();
                return;
            }
            this.BootstrapperModel.ApplyAction();
        }

        /// <summary>
        /// 注册BootstrapperApplication的两个事件
        /// </summary>
        private void WireUpEventHandlers()
        {
            this.BootstrapperModel.BootstrapperApplication.PlanComplete += this.PlanComplete;
            this.BootstrapperModel.BootstrapperApplication.ApplyBegin += this.ApplyBegin;
        }

        /// <summary>
        /// pathj是否是正确的文件夹路径
        /// </summary>
        /// <param name="path"></param>
        /// <returns></returns>
        public bool ValidDir(string path)
        {
            try
            {
                string p = new DirectoryInfo(path).FullName;
                return true;
            }
            catch
            {
                return false;
            }
        }
        /// <summary>
        /// 向Burn传递用户参数
        /// </summary>
        /// <param name="variableName"></param>
        /// <param name="value"></param>
        public void SetBurnVariable(string variableName, string value)
        {
            this.BootstrapperModel.SetBurnVariable(variableName, value);
        }
        public bool IsValid()
        {
            return true;
        }
    }
}

其他ViewModel的用法与这两个类似,不在赘述,请按照提供的源码自行设置

创建View

关于界面元素的定义,样式,文字,binding的数据,都是基础的WPF操作,请通过源码自行查看。需要说明的是,当你打开InstallView时xaml中会提示“对象未被设置到实例”,这是因为我们的单例Model只能再程序入口实例化,而这个入口不会在CustomBA项目编译时执行,只会通过Bootstrapper安装进程调用。

将类库引入Bootstrapper

当前面的工作做完之后编译CustomeBA项目,注意使用Release方式而不是Debug哦。如果一切顺利的话你会得到一个CustomBA.dll库文件。

  • 首先,在Bootstrapper项目的引用文件夹中添加对CustomBA.dll的引用。
  • 而后在Bundle.wxs文件<Bundle></Bundle>之间添加如下代码即可将我们自定义的类库和WPF程序需要的类库引入Bootstrapper项目:
  <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>

使用Setup Project打包你的程序

在创建的Setup Project项目中打开Product.wxs文件,将

<Directory Id="TARGETDIR"
           Name="SourceDir">

修改为

<Directory Id="TARGETDIR"
           Name="SourceDir">
  <Directory Id="ProgramFilesFolder">
    <Directory Id="MyProgramDir"
               Name="Install Practice">

      <Component Id="CMP_InstallMeTXT"
                 Guid="E8A58B7B-F031-4548-9BDD-7A6796C8460D">

        <File Id="FILE_MyProgramDir_InstallMeTXT"
              Source="$(var.MyAppname.TargetPath)"
              KeyPath="yes" />
      </Component>
    </Directory>
  </Directory>
</Directory>

其中MyAppname为你已经引用的要打包的项目名称,当然你也可以用相对路径或则绝对路径指定任意的文件,这段代码表示要把这里所有的文件打包为msi安装文件,安装时会把文件安装在C:\ProgrameFiles(86)\Install Practice中。 此时若要Build该项目,要先保证<Product>Manufacturer已经被赋值。生成之后你会在bin/Release目录下看到一个msi文件。

使用Bootstrapper打包msi文件

我们已经在Bootstrapper项目中引入了自定义的用户界面,接下来向它添加已经打包好的msi文件就可以生成拥有自定义界面的安装程序了。 在Bundle.wxs文件中,将<Chain>结点由原来的:

<Chain>
      <!--TODO: Define the list of chained packages.-->
   </Chain>

改为:

<Chain  DisableRollback="yes">
      <MsiPackage SourceFile="..\SetupProject\bin\Release\zh-cn\DGSetup1.msi" DisplayInternalUI="no">
      </MsiPackage>
    </Chain>

这里的SourceFile属性要改为你自己的msi文件路径,同时也不要忘了<Bundle>Manufacturer属性要赋值。而后就可以Build这个项目了,你会在bin/Release文件夹中看到一个.exe安装文件了,运行一下看看是不是你的wpf界面。 关于其他常见的设置,我在前文或者后文中都写到了,比如创建快捷方式,验证.net framework版本,从界面向安装包传值等等,请按目录查看,或者查看末尾提供的参考教程链接。


Product文件中元素与属性的简单说明

Product元素

  • Id: 该属性为GUID,可以使用Visual Studio->Tools->Create GUID的第4/5/6类来生成,也可以直接赋值为“*”,该属性每一次build都需要不同的值
  • Language:语言序号,1033为en-us,4为zh-cn
  • Version:软件版本号,用于产品升级
  • Manufacturer:制造商,公司名或这开发者名称,必填!
  • UpgradeCode:GUID,产品识别与更新的标识

Package元素

<Package InstallerVersion="200" Compressed="yes" InstallScope="perMachine" />
  • InstallScope:值为"perMachine"表明将软件安装到当前机器,值为"perUser"则将软件安装到当前用户。

MajorUpgrade元素

当产品更新时检验版本,若已安装高级版本则显示提示信息

MediaTemplate元素

用于将打包的文件声明一个存储的磁盘文件区域

Directory元素

<Directory Id="TARGETDIR" Name="SourceDir">
      <Directory Id="INSTALLFOLDER"/>
      <Directory Id="ProgramMenuFolder">
        <Directory Id="AppStartMenuFolder" Name="!(loc.ProductName)"/>
      </Directory>
      <Directory Id="DesktopFolder"></Directory>
    </Directory>
  • INSTALLFOLDER:安装软件时选中的安装目录,ProgramFilesFolder为系统C盘的ProgrameFiels文件夹,因为要用户自定义安装位置,因此要把自动生成的<Directory Id="ProgramFilesFolder">这一层删除。
  • Name:文件夹名称
  • 桌面与开始菜单快捷方式的位置为:
  <Directory Id="ProgramMenuFolder">
     <Directory Id="AppStartMenuFolder" Name="!(loc.ProductName)"/>
  </Directory>
     <Directory Id="DesktopFolder"></Directory>

Feature元素

<Feature>指定要执行打包的具体内容,内部包含若干<Component>,每一个<Component>定义一个行为或者文件。

Component元素

<Component> ,<ComponentGroup> ,<ComponentRef><ComponentGroupRef>,Group当中可以包含多个Component,Ref通过IdComponent或者ComponentGroup关联,Ref元素声明要添加一个拥有某Id值的Component或者ComponentGroup,而其具体信息可以在另外的地方进行定义,如;

<Feature Id="ProductFeature" Title="!(loc.Title)" Level="1">
      <ComponentGroupRef Id="ProductComponents" />
    </Feature>
...
<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>

一个Component之中最好只放一个文件,每一个文件最好都设置KeyPath为“yes”,这是因为KeyPath文件可以在丢失后使用“修复”功能重新得到,而一个Component只能有一个KeyPath文件。

File元素

一个<File>表示要在msi中打包并由客户安装到计算机的文件

创建快捷方式

桌面快捷方式

在Product.wxs的<Directory Id="TARGETDIR" Name="SourceDir">中间添加如下内容:

<Directory Id="DesktopFolder"></Directory>

在Product.wxs的任意位置添加如下内容:

 <DirectoryRef Id="DesktopFolder">
        <Component Id="ApplicationShortcutDeskTop">
          <Shortcut Id="ApplicationDeskTopShortcut"
                 Name="!(loc.ProductName)"
                 Description="!(loc.Title)"
                 Target="[#myapplication.exe]" Icon="icon"
                 WorkingDirectory="APPLICATIONROOTDIRECTORY"/>
          <RemoveFolder Id="DesktopFolder" On="uninstall"/>
          <RegistryValue
              Root="HKCU"
              Key="Software\Microsoft\!(loc.CompanyName)\!(loc.RegistryName)"
              Name="installed"
              Type="integer"
              Value="1"
              KeyPath="yes"/>
        </Component>
    </DirectoryRef>

(loc.Name)是引用同跟目录下.wxl文件中定义的字符串。 在<Feature>中添加如下内容:

<ComponentRef Id="ApplicationShortcutDeskTop" />
StartMenu快捷方式

在Product.wxs的<Directory Id="TARGETDIR" Name="SourceDir">中间添加如下内容:

<Directory Id="ProgramMenuFolder">
        <Directory Id="AppStartMenuFolder" Name="!(loc.ProductName)"/>
      </Directory>

在Product.wxs的任意位置添加如下内容:

<DirectoryRef Id="AppStartMenuFolder">
      <Component Id="ApplicationShortcutMenu" Guid="5AE52100-3B6E-4BA7-8A87-ED50F790B316">
        <Shortcut Id="ApplicationStartMenuShortcut"
                  Name="!(loc.ProductName)"
                  Description="!(loc.Title)"
                  Target="[#myapplication.exe]" Icon="icon"
                  WorkingDirectory="APPLICATIONROOTDIRECTORY"/>
        <RemoveFolder Id="CleanUpShortCut" Directory="AppStartMenuFolder" On="uninstall"/>
        <RegistryValue Root="HKCU" Key="Software\Microsoft\!(loc.CompanyName)\!(loc.RegistryName)" 
                       Name="installed" Type="integer" Value="1" KeyPath="yes"/>
      </Component>
    </DirectoryRef>

<Feature>中添加如下内容:

<ComponentRef Id="ApplicationShortcutMenu" />

添加到注册表

在Product.wxs的<Directory Id="TARGETDIR" Name="SourceDir">中间添加如下内容:

<ComponentRef Id="WriteToRegistry" />

在Product.wxs的任意位置添加如下内容:

<Component Id="WriteToRegistry"
                 Guid="1ECA3238-1B7B-4818-9A02-FAD3D6773613">

        <RegistryValue Id="RegistryValue"
                       KeyPath="yes"
                       Action="write"
                       Root="HKLM"
                       Key="Software\Microsoft\!(loc.CompanyName)\!(loc.RegistryName)"
                       Name="!(loc.ProductName)"
                       Value="!(loc.ProductName)"
                       Type="string" />
      </Component>

条件判断

当我们从用户获得传过来的值,或者自己在Product中定义了一个<Property>时,我们想根据该属性值的不同判断某些操作是否进行,则在该<Component>中添加如下内容:

<Condition>
            <![CDATA[PROPERTYNAME=VALUE]]>
          </Condition>

当你的PROPERTYNAME属性值为VALUE时该操作才会执行。


Bundle文件中元素与属性的简单说明

Chain元素

<Chain>中间可以放置任意多个安装文件,如果你想依次执行多个msi或者exe安装文件,只需要像这样:

 <Chain  DisableRollback="yes">
      <ExePackage ISourceFile="..\SetupProject\bin\Release\zh-cn\DGSetup.exe" DisplayInternalUI="no">
      </ExePackage>
      <MsiPackage SourceFile="..\SetupProject\bin\Release\zh-cn\DGSetup1.msi" DisplayInternalUI="no">
      </MsiPackage>
    </Chain>

添加就是了,其中<MsiPackage>用来引用msi安装文件,<ExePackage>用来引用exe安装文件。

判断当前操作系统版本是否满足

在Bootstrapper项目中添加引用WixBalExtension程序集并在<Bundel>结点添加命名空间的引用:

<Wix xmlns="http://schemas.microsoft.com/wix/2006/wi" 
     xmlns:bal="http://schemas.microsoft.com/wix/BalExtension" >

<Bundle>元素任意位置添加如下内容:

<bal:Condition Message="!(loc.windowsversionmes)">
      <![CDATA[VersionNT >= v6.1]]>
    </bal:Condition>

6.1表示windows7。

判断framework版本并下载安装

在Bootstrapper项目中添加引用WixUtilExtension程序集并在<Bundel>结点添加命名空间的引用:

<Wix xmlns="http://schemas.microsoft.com/wix/2006/wi" 
      xmlns:util="http://schemas.microsoft.com/wix/UtilExtension">

<Bundle>元素任意位置添加如下内容:

 <util:RegistrySearch Root="HKLM" Key="SOFTWARE\Microsoft\Net Framework Setup\NDP\v4\Full" Value="Version" Variable="Netfx4FullVersion" />
    <util:RegistrySearch Root="HKLM" Key="SOFTWARE\Microsoft\Net Framework Setup\NDP\v4\Full" Value="Version" Variable="Netfx4x64FullVersion" Win64="yes" />

<Chain>添加:

<ExePackage Id="Netfx4Full" Cache="yes" Compressed="no"  PerMachine="yes" 
            Permanent="yes" Vital="yes" SourceFile="..\dotnetfx40_full_x86_x64.exe"
            InstallCommand="/q /norestart "  DownloadUrl="http://go.microsoft.com/fwlink/?LinkId=164193" 
            DetectCondition="Netfx4FullVersion AND (NOT VersionNT64 OR Netfx4x64FullVersion)">
      </ExePackage>

在你Build项目时需要在SourceFile指定的位置存在dotnetfx40_full_x86_x64.exe文件,但Compressed="no" 使得该文件不会被打包进你的安装文件,当用户只拿到安装包来安装时若系统中不存在.net 4.0时安装文件就会自动从DownloadUrl下载文件进行安装。

参考内容

WiX下载地址

http://wixtoolset.org/releases/ ,虽然已经提供v3.11版本,但当我已经安装.net framework 4.0后,v3.11依旧提示需要安装.net 3.0,所以我使用的是wix v3.10.3 。

参考教程

WiX 3.6: A Developer's Guide to Windows Installer XML WiX Toolset Manual Table of Contents 可能会有错误或者偏差的地方,欢迎交流指正!

About

Customizing the Burn UI via WiX Toolset and wpf.

Resources

Stars

Watchers

Forks

Packages

No packages published

Languages