使用 WIX 自定义操作提示用户在安装/卸载时关闭应用程序






4.97/5 (19投票s)
当您需要在安装/卸载时提示用户关闭某些应用程序时。
简介
我的一个项目需要在安装和卸载期间检查特定进程和应用程序是否未运行。我还想让用户关闭它们,而不是自动终止它们。我浏览了 WIX 文档,发现了 Util Extensions 中的 CloseApplication 元素,它应该完全符合我的要求。但看起来使用此元素实现我想要的功能并非易事。在网上搜索时,我发现许多关于此元素未解决的问题。最终,我决定创建自己的 CustomAction ClosePrompt
,它已通用化,可以在任何 WIX 项目中重复使用,无需更改。
功能
此解决方案允许:
- 在 WXS 脚本中参数化要查找的进程
- 如果在安装/卸载期间定义了进程正在运行,对话框消息将要求用户关闭应用程序
- 如果阻止应用程序已关闭,对话框消息将自动关闭,进程将继续
和卸载
使用代码
您可以将项目添加到您的解决方案中,或在 WXS 脚本中使用构建的库 ClosePromptCA.CA.dll。
WXS 脚本
要提示用户关闭应用程序,您需要在 Product 节点下设置两个属性 -PromptToCloseProcesses
和 PromptToCloseDisplayNames
。第一个属性保存要关闭的进程名称列表,第二个属性保存相应的显示名称列表。确保您提供了正确的进程名称,例如对于记事本,它将是 "notepad",而不是 "notpad.exe"。<Property Id="PromptToCloseProcesses" Value="Process1,Process2,Process3" />
<Property Id="PromptToCloseDisplayNames" Value="Application name1,Application name2,Application name3" />
然后您应该设置 CustomAction。您应该提供指向 ClosePromptCA.CA.dll 的正确路径,而不是 $(var.BuildOutputDir)ClosePromptCA.CA.dll。在 CostFinalize
之后启动自定义操作会在按下安装按钮后立即进行检查。
<Binary Id="ClosePromptBinary" SourceFile="$(var.BuildOutputDir)ClosePromptCA.CA.dll" />
<CustomAction Id="CloseAppsPrompt" BinaryKey="ClosePromptBinary"
DllEntry="ClosePrompt" Return="check" />
<InstallExecuteSequence>
<Custom Action="CloseAppsPrompt" After="CostFinalize"></Custom>
...
</InstallExecuteSequence>
自定义操作
此 ClosePrompt 实现为每个阻止执行的进程显示对话框。您可能只想显示一个包含阻止应用程序列表的对话框窗口。这需要进行小的重新设计,但在概念上是相同的。
自定义操作
ClosePrompt
是自定义操作的入口点。在创建两个数组之后,代码会遍历进程并调用相应 PromptCloseApplication
对象的提示。如果该进程未运行或者用户已关闭应用程序,则返回 true,否则如果用户拒绝关闭,则返回 false,并且自定义操作返回失败。
public class CustomActions
{
[CustomAction]
public static ActionResult ClosePrompt(Session session)
{
session.Log("Begin PromptToCloseApplications");
try
{
var productName = session["ProductName"];
var processes = session["PromptToCloseProcesses"].Split(',');
var displayNames = session["PromptToCloseDisplayNames"].Split(',');
if (processes.Length != displayNames.Length)
{
session.Log(@"Please check that 'PromptToCloseProcesses' and" +
@" 'PromptToCloseDisplayNames' exist and have same number of items.");
return ActionResult.Failure;
}
for (var i = 0; i < processes.Length; i++)
{
session.Log("Prompting process {0} with name {1} to close.", processes[i], displayNames[i]);
using (var prompt = new PromptCloseApplication(productName, processes[i], displayNames[i]))
if (!prompt.Prompt())
return ActionResult.Failure;
}
}
catch(Exception ex)
{
session.Log("Missing properties or wrong values. Please check that" +
" 'PromptToCloseProcesses' and 'PromptToCloseDisplayNames' exist and have " +
"same number of items. \nException:" + ex.Message);
return ActionResult.Failure;
}
session.Log("End PromptToCloseApplications");
return ActionResult.Success;
}
}
PromptCloseApplication
如果进程正在运行,则应显示对话框。如您所见,创建了 ClosePromptForm
窗口窗体,之后代码会搜索此对话框窗体的相应所有者。如果我们不提供任何所有者时显示对话框,安装程序窗口和对话框窗体将是独立的,用户甚至可能不会注意到对话框窗口。因此,代码会搜索正确的句柄,然后在显示对话框时使用它 _form.ShowDialog(new WindowWrapper(_mainWindowHanle))
。
当您安装产品时,MSI 安装程序窗口将被称为产品名称,并在其后添加 " Setup",当产品被卸载时,卸载程序窗口将被称为产品名称,但在这种情况下,窗口类是对话框,其类名将始终是 "#32770"。使用 FindWindow
API 函数代码查找安装/卸载情况的相应窗口句柄。
此外,代码使用一个计时器,因此如果用户关闭了阻止应用程序,对话框窗口将被关闭,安装/卸载将继续。在这种情况下,用户可以关闭对话框窗口,进程将以失败退出。
public class PromptCloseApplication : IDisposable
{
private readonly string _productName;
private readonly string _processName;
private readonly string _displayName;
private System.Threading.Timer _timer;
private Form _form;
private IntPtr _mainWindowHanle;
[DllImport("user32.dll", SetLastError = true)]
public static extern IntPtr FindWindow(string lpClassName, string lpWindowName);
public PromptCloseApplication(string productName, string processName, string displayName)
{
_productName = productName;
_processName = processName;
_displayName = displayName;
}
public bool Prompt()
{
if (IsRunning(_processName))
{
_form = new ClosePromptForm(String.Format("Please close running " +
"instances of {0} before running {1} setup.", _displayName, _productName));
_mainWindowHanle = FindWindow(null, _productName + " Setup");
if (_mainWindowHanle == IntPtr.Zero)
_mainWindowHanle = FindWindow("#32770", _productName);
_timer = new System.Threading.Timer(TimerElapsed, _form, 200, 200);
return ShowDialog();
}
return true;
}
bool ShowDialog()
{
if (_form.ShowDialog(new WindowWrapper(_mainWindowHanle)) == DialogResult.OK)
return !IsRunning(_processName) || ShowDialog();
return false;
}
private void TimerElapsed(object sender)
{
if (_form == null || IsRunning(_processName) || !_form.Visible)
return;
_form.DialogResult = DialogResult.OK;
_form.Close();
}
static bool IsRunning(string processName)
{
return Process.GetProcessesByName(processName).Length > 0;
}
public void Dispose()
{
if (_timer != null)
_timer.Dispose();
if (_form != null && _form.Visible)
_form.Close();
}
}
ClosePromptForm
窗体设置了一些属性,例如大小、minbox、maxbox 等,使其看起来像普通的对话框窗口。它还具有简单的初始化,带有文本消息和按钮点击处理程序,该处理程序设置对话框结果:
public partial class ClosePromptForm : Form
{
public ClosePromptForm(string text)
{
InitializeComponent();
messageText.Text = text;
}
private void OkButtonClick(object sender, EventArgs e)
{
DialogResult = DialogResult.OK;
Close();
}
}
关注点
正如我上面写的,您可以以不同的方式使用它,在一个对话框窗口中提供要关闭的整个进程列表,或允许从此对话框中杀死所有阻止进程。因此,您可以根据您的需要进一步自定义它。
历史
- 2013年8月16日:进程检查已改进。源代码在 github 上,请参阅顶部的链接。
- 2013年5月16日:添加了关于进程名称的注释。
- 2013年4月26日:初始版本。