在 C# 中执行 msi 安装

简介:

有时候我们需要在程序中执行另一个程序的安装,这就需要我们去自定义 msi 安装包的执行过程。

需求

比如我要做一个安装管理程序,可以根据用户的选择安装不同的子产品。当用户选择了三个产品时,如果分别显示这三个产品的安装交互 UI 显然是不恰当的。我们期望用一个统一的自定义 UI 去取代每个产品各自的 UI。

实现思路

平时使用 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.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 更灵活,也为程序日后添加新的功能打下了基础。


本文转自sparkdev博客园博客,原文链接:http://www.cnblogs.com/sparkdev/p/5932429.html,如需转载请自行联系原作者

相关文章
|
5月前
|
开发框架 前端开发 .NET
C#编程与Web开发
【4月更文挑战第21天】本文探讨了C#在Web开发中的应用,包括使用ASP.NET框架、MVC模式、Web API和Entity Framework。C#作为.NET框架的主要语言,结合这些工具,能创建动态、高效的Web应用。实际案例涉及企业级应用、电子商务和社交媒体平台。尽管面临竞争和挑战,但C#在Web开发领域的前景将持续拓展。
169 3
|
5月前
|
SQL 开发框架 安全
C#编程与多线程处理
【4月更文挑战第21天】探索C#多线程处理,提升程序性能与响应性。了解C#中的Thread、Task类及Async/Await关键字,掌握线程同步与安全,实践并发计算、网络服务及UI优化。跟随未来发展趋势,利用C#打造高效应用。
181 3
|
10天前
|
API C#
C# 一分钟浅谈:文件系统编程
在软件开发中,文件系统操作至关重要。本文将带你快速掌握C#中文件系统编程的基础知识,涵盖基本概念、常见问题及解决方法。文章详细介绍了`System.IO`命名空间下的关键类库,并通过示例代码展示了路径处理、异常处理、并发访问等技巧,还提供了异步API和流压缩等高级技巧,帮助你写出更健壮的代码。
25 2
|
14天前
|
SQL 开发框架 安全
并发集合与任务并行库:C#中的高效编程实践
在现代软件开发中,多核处理器普及使多线程编程成为提升性能的关键。然而,传统同步模型在高并发下易引发死锁等问题。为此,.NET Framework引入了任务并行库(TPL)和并发集合,简化并发编程并增强代码可维护性。并发集合允许多线程安全访问,如`ConcurrentQueue<T>`和`ConcurrentDictionary<TKey, TValue>`,有效避免数据不一致。TPL则通过`Task`类实现异步操作,提高开发效率。正确使用这些工具可显著提升程序性能,但也需注意任务取消和异常处理等常见问题。
26 1
|
23天前
|
安全 程序员 编译器
C#一分钟浅谈:泛型编程基础
在现代软件开发中,泛型编程是一项关键技能,它使开发者能够编写类型安全且可重用的代码。C# 自 2.0 版本起支持泛型编程,本文将从基础概念入手,逐步深入探讨 C# 中的泛型,并通过具体实例帮助理解常见问题及其解决方法。泛型通过类型参数替代具体类型,提高了代码复用性和类型安全性,减少了运行时性能开销。文章详细介绍了如何定义泛型类和方法,并讨论了常见的易错点及解决方案,帮助读者更好地掌握这一技术。
43 11
|
15天前
|
安全 数据库连接 API
C#一分钟浅谈:多线程编程入门
在现代软件开发中,多线程编程对于提升程序响应性和执行效率至关重要。本文从基础概念入手,详细探讨了C#中的多线程技术,包括线程创建、管理及常见问题的解决策略,如线程安全、死锁和资源泄露等,并通过具体示例帮助读者理解和应用这些技巧,适合初学者快速掌握C#多线程编程。
49 0
|
2月前
|
存储 C#
揭秘C#.Net编程秘宝:结构体类型Struct,让你的数据结构秒变高效战斗机,编程界的新星就是你!
【8月更文挑战第4天】在C#编程中,结构体(`struct`)是一种整合多种数据类型的复合数据类型。与类不同,结构体是值类型,意味着数据被直接复制而非引用。这使其适合表示小型、固定的数据结构如点坐标。结构体默认私有成员且不可变,除非明确指定。通过`struct`关键字定义,可以包含字段、构造函数及方法。例如,定义一个表示二维点的结构体,并实现计算距离原点的方法。使用时如同普通类型,可通过实例化并调用其成员。设计时推荐保持结构体不可变以避免副作用,并注意装箱拆箱可能导致的性能影响。掌握结构体有助于构建高效的应用程序。
59 7
|
2月前
|
图形学 C# 开发者
全面掌握Unity游戏开发核心技术:C#脚本编程从入门到精通——详解生命周期方法、事件处理与面向对象设计,助你打造高效稳定的互动娱乐体验
【8月更文挑战第31天】Unity 是一款强大的游戏开发平台,支持多种编程语言,其中 C# 最为常用。本文介绍 C# 在 Unity 中的应用,涵盖脚本生命周期、常用函数、事件处理及面向对象编程等核心概念。通过具体示例,展示如何编写有效的 C# 脚本,包括 Start、Update 和 LateUpdate 等生命周期方法,以及碰撞检测和类继承等高级技巧,帮助开发者掌握 Unity 脚本编程基础,提升游戏开发效率。
41 0