实现思路
平时使用 msiexec.exe 习惯了,所以最直接的想法就是在一个子进程中执行:
msiexec.exe /qn
这样固然是能够完成任务,但是不是太简陋了? 安装开始后我们想取消这次安装怎么办? 或者我们还想要拿到一些安装进度的信息。
其实可以通过调用三个 windows API 轻松搞定这个事儿!下面的 C# demo 用一个自定义 Form 来指示多个 MSI 文件的安装过程。Form 上放的是一个滚动条,并且配合一个不断更新的 label。先看看 demo 长什么样子。
下面是安装过程中的 UI:
点击 Cancel 按钮取消安装后的 UI:
主要接口介绍
我们先来了解一下主要用到的几个 win32 API。
首先是 MsiSetInternalUI 方法:
[DllImport("msi.dll", CharSet = CharSet.Auto)]internal static extern int MsiSetInternalUI(int dwUILevel, IntPtr phWnd);
在调用 msiexec.exe 时,我们通过指定 /q 参数让安装过程显示不同的 UI。如果不显示UI的话就要使用参数 /qn 。MsiSetInternalUI 方法就是干这个事儿的。通过下面的调用就可以去掉 msi 中自带的 UI:
NativeMethods.MsiSetInternalUI(2, IntPtr.Zero)
接下来是 MsiSetExternalUI 方法:
[DllImport("msi.dll", CharSet = CharSet.Auto)]internal static extern MsiInstallUIHandler MsiSetExternalUI([MarshalAs(UnmanagedType.FunctionPtr)] MsiInstallUIHandler puiHandler, NativeMethods.InstallLogMode dwMessageFilter, IntPtr pvContext);
MsiSetExternalUI 函数允许指定一个用户定义的外部 UI handler 用来处理安装过程中产生的消息。这个外部的 UI handler 会在内部的 UI handler 被调用前调用。 如果在外部的 UI handler 中返回非 0 的值,就说明这个消息已经被处理。
这个外部的 UI handler 就是 MsiSetExternalUI 方法的第一个参数,我们通过实现这个 handler 来处理自己感兴趣的消息, 比如当安装进度变化后去更新进度条。或者通过它传递我们的消息给 msi,比如说告诉 msi,停止安装,执行 cancel 操作。使用这个方法需要注意的是,当你完成安装后一定要把原来的 handler 设回去。否则以后执行 msi 安装包可能会出问题。
MSDN 上有一个 MsiInstallUIHandler 的 demo,感兴趣的同学可以去看看。
下面是 MsiInstallProduct 方法:
[DllImport("msi.dll", CharSet = CharSet.Auto)]internal static extern uint MsiInstallProduct([MarshalAs(UnmanagedType.LPWStr)] string szPackagePath, [MarshalAs(UnmanagedType.LPWStr)] string szCommandLine);
正如其名,这个是真正干活儿的方法。
实在忍不住要介绍第四个方法,虽然它对实现当前的功能来说是可选的,但对一个产品来说,它却是用来救命的。
[DllImport("msi.dll", CharSet = CharSet.Auto)]internal static extern uint MsiEnableLog(GcMsiUtil.NativeMethods.InstallLogMode dwLogMode, [MarshalAs(UnmanagedType.LPWStr)] string szLogFile, uint dwLogAttributes);
这个方法会把安装 log 保存到你传递给它的文件路径。有了它生活就会 happy 很多,很多… 否则当用户告诉你安装失败时,你一定会抓狂的。
主要代码
好了,下面是 MyInstaller demo 的主要代码:
InstallProcessForm.cspublic partial class InstallProcessForm : Form { private MyInstaller _installer = null; private BackgroundWorker _installerBGWorker = new BackgroundWorker(); internal InstallProcessForm() { InitializeComponent(); _installer = new MyInstaller(); _installerBGWorker.WorkerReportsProgress = true; _installerBGWorker.WorkerSupportsCancellation = true; _installerBGWorker.DoWork += _installerBGWorker_DoWork; _installerBGWorker.RunWorkerCompleted += _installerBGWorker_RunWorkerCompleted; _installerBGWorker.ProgressChanged += _installerBGWorker_ProgressChanged; this.Shown += InstallProcessForm_Shown; } private void InstallProcessForm_Shown(object sender, EventArgs e) { // 当窗口打开后就开始后台的安装 _installerBGWorker.RunWorkerAsync(); } private void _installerBGWorker_ProgressChanged(object sender, ProgressChangedEventArgs e) { // 消息通过 e.UserState 传回,并通过label显示在窗口上 string message = e.UserState.ToString(); this.label1.Text = message; if (message == "正在取消安装 ...") { this.CancelButton.Enabled = false; } } private void _installerBGWorker_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e) { // 安装过程结束 } private void _installerBGWorker_DoWork(object sender, DoWorkEventArgs e) { BackgroundWorker bgWorker = sender as BackgroundWorker; // 开始执行安装方法 _installer = new MyInstaller(); string msiFilePath = "xxx.msi"; // msi file path _installer.Install(bgWorker, msiFilePath); } private void CancelButton_Click(object sender, EventArgs e) { _installer.Canceled = true; _installerBGWorker.CancelAsync(); } } MyInstaller.csinternal class MyInstaller { private BackgroundWorker _bgWorker = null; public bool Canceled { get; set; } public void Install(BackgroundWorker bgWorker, string msiFileName) { _bgWorker = bgWorker; NativeMethods.MyMsiInstallUIHandler oldHandler = null; try { string logPath = "test.log"; NativeMethods.MsiEnableLog(NativeMethods.LogMode.Verbose, logPath, 0u); NativeMethods.MsiSetInternalUI(2, IntPtr.Zero); oldHandler = NativeMethods.MsiSetExternalUI(new NativeMethods.MyMsiInstallUIHandler(MsiProgressHandler), NativeMethods.LogMode.ExternalUI, IntPtr.Zero); string param = "ACTION=INSTALL"; _bgWorker.ReportProgress(0, "正在安装 xxx ..."); NativeMethods.MsiInstallProduct(msiFileName, param); } catch(Exception e) { // todo } finally { // 一定要把默认的handler设回去。 if(oldHandler != null) { NativeMethods.MsiSetExternalUI(oldHandler, NativeMethods.LogMode.None, IntPtr.Zero); } } } //最重要的就是这个方法了,这里仅演示了如何cancel一个安装,更多详情请参考MSDN文档 private int MsiProgressHandler(IntPtr context, int messageType, string message) { if (this.Canceled) { if (_bgWorker != null) { _bgWorker.ReportProgress(0, "正在取消安装 ..."); } // 这个返回值会告诉msi, cancel当前的安装 return 2; } return 1; } }internal static class NativeMethods { [DllImport("msi.dll", CharSet = CharSet.Auto)] internal static extern int MsiSetInternalUI(int dwUILevel, IntPtr phWnd); [DllImport("msi.dll", CharSet = CharSet.Auto)] internal static extern MyMsiInstallUIHandler MsiSetExternalUI([MarshalAs(UnmanagedType.FunctionPtr)] MyMsiInstallUIHandler puiHandler, NativeMethods.LogMode dwMessageFilter, IntPtr pvContext); [DllImport("msi.dll", CharSet = CharSet.Auto)] internal static extern uint MsiInstallProduct([MarshalAs(UnmanagedType.LPWStr)] string szPackagePath, [MarshalAs(UnmanagedType.LPWStr)] string szCommandLine); [DllImport("msi.dll", CharSet = CharSet.Auto)] internal static extern uint MsiEnableLog(NativeMethods.LogMode dwLogMode, [MarshalAs(UnmanagedType.LPWStr)] string szLogFile, uint dwLogAttributes); internal delegate int MyMsiInstallUIHandler(IntPtr context, int messageType, [MarshalAs(UnmanagedType.LPWStr)] string message); [Flags] internal enum LogMode : uint { None = 0u, Verbose = 4096u, ExternalUI = 20239u } }
简单说明一下,用户定义的 UI 运行在主线程中,使用 BackgroundWorker 执行安装任务。在安装进行的过程中可以把 cancel 信息传递给 MsiProgressHandler,当MsiProgressHandler 检测到 cancel 信息后通过返回值告诉 msi 的执行引擎,执行 cancel 操作(msi的安装过程是相当严谨的,可不能简单的杀掉安装进程了事!)。
这样,一个支持 cancel 的自定义 UI 的安装控制程序就 OK了(demo哈)。如果要安装多个 msi 只需在 Install 方法中循环就可以了。
转载于:https://blog.51cto.com/12953214/1942277