1、新建解决方案,命名QingLong
2、添加Windows窗体应用(.NET Framework),命名TestWinForm.该窗体仅用于测试使打包使用,实际项目中可根据自己的需要新建对应的项目
3、添加类库(.NET Framework),命名TestCustomInstallerLib,该项目用于打包时的自定义操作使用的类库,该项目中包括一个自定义安装程序类,用于安装使用
4、TestCustomInstallerLib项目添加CustomInstaller
CustomInstaller中主要用到了安装程序类 System.Configuration.Install.Installer官方文档,这是 .NET Framework 中所有自定义安装程序的基类,继承该类就可以在计算机上安装应用程序的组件
通过 Installers 属性,安装程序包含作为子级的其他安装程序的集合。 执行安装程序时,它会循环调试其子级,并调用 Install、Commit、Rollback或 Uninstall。 有关 Installers 集合中对象的示例,请参阅 EventLogInstaller。
Context 属性包含有关安装的信息。 例如,有关安装的日志文件的位置的信息,保存 Uninstall 方法所需的信息的文件的位置,以及运行安装可执行文件时输入的命令行。 有关安装可执行文件的示例,请参阅installutil.exe (安装程序工具)。
Install、Commit、Rollback和 Uninstall 方法并不总是在 Installer的同一实例上调用。 例如,你可以使用 Installer 来安装和提交应用程序,然后释放对该 Installer的引用。
稍后,卸载应用程序会创建对 Installer的新引用,这意味着 Uninstall 方法在 Installer的其他实例上调用。 出于此原因,请不要在安装程序中保存计算机的状态。
相反,请使用跨调用保留并传入 Install、Commit、Rollback和 Uninstall 方法的 IDictionary。
步骤如下:
TestCustomInstallerLib项目删除Class1.cs文件,然后添加新建一个安装程序类,命名CustomInstaller
CustomInstaller.cs内容如下:
using Microsoft.Win32; using System; using System.Collections; using System.Collections.Generic; using System.ComponentModel; using System.Configuration.Install; using System.Diagnostics; using System.IO; using System.Reflection; namespace TestCustomInstallerLib { [RunInstaller(true)] public partial class CustomInstaller : System.Configuration.Install.Installer { public CustomInstaller() { InitializeComponent(); } protected override void OnAfterInstall(IDictionary savedState) { //获取自定义安装用户界面上的端口值 string portId = this.Context.Parameters["PortId"]; string path = this.Context.Parameters["targetdir"]; Logger($"OnAfterInstall添加 targetdir savedState:{path}"); //开机启动 1、硬编码,2设置Setup Projects的注册表编辑器 //1、安装完成以后可以把硬编码把该软件写到注册表中,这样可以设置开机启动, //2、当然还有另外一种开机启动的方式是可以使用Setup Projects的注册表编辑器的来进行注册 savedState.Add("savedState", path); Assembly asm = Assembly.GetExecutingAssembly(); string asmpath = asm.Location.Remove(asm.Location.LastIndexOf("\")) + "\"; Logger($"OnAfterInstall asmpath:{asmpath}"); SetAutoStart(true, "TestWinForm", asmpath + "TestWinForm.exe"); //Process.Start(asmpath + "\ServiceXStart.exe");//要执行的程序 Process.Start(asmpath + "TestWinForm.exe");//要执行的程序 //Process.Start(path + "MyTestWinFrm.exe");//要执行的程序 base.OnAfterInstall(savedState); } protected override void OnBeforeUninstall(IDictionary savedState) { Trace.Listeners.Clear(); Trace.AutoFlush = true; Trace.Listeners.Add(new TextWriterTraceListener(@"F:TestOnBeforeUninstall.txt")); Trace.WriteLine($"{DateTime.Now:yyyy-MM-dd HH:mm:ss} OnBeforeUninstall"); //Process[] processes = Process.GetProcessesByName("ServiceXStart"); Process[] processes = Process.GetProcessesByName("TestWinForm"); foreach (Process item in processes) { Trace.WriteLine($"{DateTime.Now:yyyy-MM-dd HH:mm:ss} OnBeforeUninstall 进程:" + item); item.Kill(); item.WaitForExit(); item.Close(); } base.OnBeforeUninstall(savedState); } protected override void OnAfterUninstall(IDictionary savedState) { Trace.Listeners.Clear(); Trace.AutoFlush = true; Trace.Listeners.Add(new TextWriterTraceListener(@"F:Test123.txt")); Trace.WriteLine($"{DateTime.Now:yyyy-MM-dd HH:mm:ss} OnBeforeUninstall"); try { Assembly asm = Assembly.GetExecutingAssembly(); string asmpath = asm.Location.Remove(asm.Location.LastIndexOf("\")) + "\"; Logger($"AfterUninstall123456 asmpath:{asmpath}"); if (Directory.Exists(asmpath)) { RemoveSubDirectory(new DirectoryInfo(asmpath)); Trace.WriteLine($"删除目录:{asmpath} 成功"); } Trace.WriteLine("AfterUninstall 完成了。。。。"); } catch (Exception ex) { Trace.WriteLine($"AfterUninstall删除异常:原因{ex}"); } base.OnAfterUninstall(savedState); } /// <summary> /// 卸载完成后删除多余的文件 /// </summary> /// <param name="uper"></param> private static void RemoveSubDirectory(DirectoryInfo directory) { Logger($"目录信息 directory:{directory}"); foreach (FileInfo subFile in directory.GetFiles()) { if (subFile.Exists) { Trace.WriteLine($"{DateTime.Now:yyyy-MM-dd HH:mm:ss} Exists 删除文件名称666: {subFile.FullName},{subFile.Attributes},{subFile.FullName}"); } else { Trace.WriteLine($"{DateTime.Now:yyyy-MM-dd HH:mm:ss} no Exists 删除文件名称000: {subFile.FullName}"); //subFile.Delete(); } } foreach (DirectoryInfo sub in directory.GetDirectories()) { if (sub.GetFiles().Length > 0 || sub.GetDirectories().Length > 0) RemoveSubDirectory(sub); sub.Delete(true); Logger($"要删除的目录信息 sub:{sub}"); } Logger($"目录成功"); } /// <summary> /// 将应用程序设为或不设为开机启动 /// </summary> /// <param name="onOff">自启开关</param> /// <param name="appName">应用程序名</param> /// <param name="appPath">应用程序完全路径</param> public static bool SetAutoStart(bool onOff, string appName, string appPath) { Logger($"注册表设置的开机启动项:{onOff},{appName},{appPath}"); return true; #region MyRegion //bool isOk = true; ////如果从没有设为开机启动设置到要设为开机启动 //if (!IsExistKey(appName) && onOff) //{ // Logger($"------设置注册表自动启动----不存在开机启动项,即将添加开机启动项------"); // isOk = SelfRunning(onOff, appName, @appPath); //} ////如果从设为开机启动设置到不要设为开机启动 //else if (IsExistKey(appName) && !onOff) //{ // Logger($"------设置注册表自动启动----存在开机启动项,但未开启,即将开启启动项------"); // isOk = SelfRunning(onOff, appName, @appPath); //} //return isOk; #endregion } /// <summary> /// 判断注册键值对是否存在,即是否处于开机启动状态 /// </summary> /// <param name="keyName">键值名</param> /// <returns></returns> private static bool IsExistKey(string keyName) { try { bool _exist = false; RegistryKey local = Registry.LocalMachine; RegistryKey runs = local.OpenSubKey(@"SOFTWAREMicrosoftWindowsCurrentVersionRun", true); if (runs == null) { RegistryKey key2 = local.CreateSubKey("SOFTWARE"); RegistryKey key3 = key2.CreateSubKey("Microsoft"); RegistryKey key4 = key3.CreateSubKey("Windows"); RegistryKey key5 = key4.CreateSubKey("CurrentVersion"); RegistryKey key6 = key5.CreateSubKey("Run"); runs = key6; } string[] runsName = runs.GetValueNames(); foreach (string strName in runsName) { if (strName.ToUpper() == keyName.ToUpper()) { _exist = true; return _exist; } } return _exist; } catch { return false; } } /// <summary> /// 写入或删除注册表键值对,即设为开机启动或开机不启动 /// </summary> /// <param name="isStart">是否开机启动</param> /// <param name="exeName">应用程序名</param> /// <param name="path">应用程序路径带程序名</param> /// <returns></returns> private static bool SelfRunning(bool isStart, string exeName, string path) { try { RegistryKey local = Registry.LocalMachine; RegistryKey key = local.OpenSubKey(@"SOFTWAREMicrosoftWindowsCurrentVersionRun", true); if (key == null) { local.CreateSubKey("SOFTWARE//Microsoft//Windows//CurrentVersion//Run"); } //若开机自启动则添加键值对 if (isStart) { key.SetValue(exeName, path); key.Close(); Logger("------设置注册表自动启动----开启----成功------"); } else//否则删除键值对 { string[] keyNames = key.GetValueNames(); foreach (string keyName in keyNames) { if (keyName.ToUpper() == exeName.ToUpper()) { key.DeleteValue(exeName); key.Close(); Logger("------设置注册表自动启动----关闭----成功------"); } } } } catch (Exception ex) { Logger($"------设置注册表自动启动----异常----原因{ex}------"); return false; } return true; } /// <summary> /// 记录日志 /// </summary> /// <param name="message"></param> private static void Logger(string message) { try { string fileName = @"F:Testlog.txt"; if (!File.Exists(fileName)) { File.Create(fileName); Trace.Listeners.Clear(); Trace.AutoFlush = true; Trace.Listeners.Add(new TextWriterTraceListener(fileName)); } Trace.WriteLine($"{DateTime.Now:yyyy-MM-dd HH:mm:ss}" + message); } catch (Exception ex) { Trace.Listeners.Clear(); Trace.AutoFlush = true; Trace.Listeners.Add(new TextWriterTraceListener(@"F:Testlog.txt")); Trace.WriteLine($"Logger出错,错误信息:{ex}"); } } } }
5、Advanced Installer15.2 制作安装包,本文使用的是Advanced Installer15.2版本
打开Advanced Installer,点击左上角的 文件 -新建,创建新的项目,通用模板-安装项目 选择 企业版 语言选择 中文简体,点击 创建项目
选择 左侧的 资源 -文件和文件夹,可以看到需要打包的文件以及文件夹信息
创建桌面快捷方式和菜单程序
选择 桌面 添加桌面快捷方式和应用程序快捷文件夹图标
开机启动
添加开机启动,2种方式,1、种如下操作,复制添加桌面快捷方式 粘贴到开始菜单 启动项中,2、选择资源 注册表,添加对应的信息,如下
注册表添加开机启动
安装必备组件
安装必备组件,选择 需求 运行环境, 预定义的运行环境,勾选时会自动下载
Advanced Installer支持直接在安装包中包含.net Framework,这样在安装软件的时候会检测你的PC上面是否安装了对应的framework的版本,如果没有,则会自动给你安装Framework。并且支持离线和在线两种方式的安装如下图
这里勾选的系统版本即为安装必须的系统版本,除此之外,Advanced Installer还支持安装的时候检测机器上面的浏览器版本、sql server数据库版本、IIS版本等选项。如果当前安装机器上面的版本低于要求的配置,就会提示错误。
需要注意的是离线和在线,各有好处,离线安装方便,但是安装文件可能会很大,在线安装需要安装时有网络否则无法安装,具体使用,自己根据需要来选择。
自定义安装配置界面------用户界面
选择 用户界面 对话框 msi 程序包 首次安装 选择右键新建对话框或者添加对话框,选择新建对话框就可以了
新建对话框
EDIT_1_PROP 这个属性名称的文本框的值,在自定义的intsaller安装类中会使用,其他的文本框的名称也可以修改成自己需要的名称,但是必须要和下文中要用的名称一致才行,否则在installer自定义安装类中就获取不到了安装界面设置的值了
自定义安装配置界面------自定义行为
选择 自定义行为 自定义操作 添加自定义操作 .NET Iintsaller Class action 选择自定义的intsaller类库,该类库必须继承System.Configuration.Install.Installer实现,本文使用的是TestCustomInstallerLib,
获取用户自定义安装界面时,输入的信息,这里只举例获取prop的,其他的都是一样的,prop是给自定义的安装程序installer中使用的,可以通过Context.Parameters["prop"]来获取,通过EDIT_1_PROP 这个属性名称获取用户界面输入的文本框的值,
这样通过prop与[EDIT_1_PROP]就可以在Context.Parameters["prop"]来获取界面输入的值了
注意,如果要在C#里面读取安装文件的根目录
1、C#里面读取安装文件的根目录
Assembly asm = Assembly.GetExecutingAssembly();
string asmpath = asm.Location.Remove(asm.Location.LastIndexOf("\")) + "\";
2、定义一个变量来接受安装文件的根目录,C#里面读取安装文件的根目录
string path = this.Context.Parameters["targetdir"].ToString();
另外用户界面还可以添加其他的对话框,例如数据库连接测试,在安装的过程中,组件会自动检测我们系统的sql Server实例,我们输入用户名密码之后,点击“Test Sql Connection”按钮,可以检测是否连接成功。
安装界面如下:
修改TestCustomInstallerLib,CustomInstaller中的
//获取自定义安装用户界面上的端口值 string portId = this.Context.Parameters["prop"]; string path = this.Context.Parameters["targetdir"];
点击左上角的,全部重新构建,即可。
补充:用户界面的控件都可以修改的,可以根据需要修改成自己的,包安装的主题背景图片等,如下: