有时候我们须要在程序中运行还有一个程序的安装。这就须要我们去自己定义msi安装包的运行过程。

 

比方我要做一个安装管理程序,能够依据用户的选择安装不同的子产品。当用户选择了三个产品时,假设分别显示这三个产品的安装交互UI显然是不恰当的。我们期望用一个统一的自己定义UI去代替每一个产品各自的UI

 

平时使用msiexec.exe习惯了,所以最直接的想法就是在一个子进程中运行:

         msiexec.exe /qn


这样固然是能够完毕任务,可是不是太简陋了? 安装開始后我们想取消这次安装怎么办? 或者我们还想要拿到一些安装进度的信息。

 

事实上能够通过调用三个windowsAPI 轻松搞定这个事儿!

以下的C# demo用一个自己定义Form来指示多个MSI文件的安装过程。

Form上放的是一个滚动栏,而且配合一个不断更新的label

 

以下是安装过程中的UI



点击Cancelbutton取消安装后的UI:



先看一下这三个API:

[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)


[DllImport("msi.dll", CharSet = CharSet.Auto)]

internal static extern MsiInstallUIHandler MsiSetExternalUI([MarshalAs(UnmanagedType.FunctionPtr)] MsiInstallUIHandlerpuiHandler, 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。感兴趣的同学能够看看。

[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.cs
public 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.cs
internal 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方法中循环就能够了。

 

 总结一下,通过调用几个windows API,我们能够实现对msi安装过程的控制。

这比调用msiexec.exe更灵活,也为程序日后加入新的功能打下了基础。


感谢葡萄哥Nick 投稿





posted on 2017-07-14 17:17  lxjshuju  阅读(346)  评论(0编辑  收藏  举报