【问题标题】:Application won't exit before Diagnostics.Process.Start() finishes在 Diagnostics.Process.Start() 完成之前应用程序不会退出
【发布时间】:2014-11-05 16:54:48
【问题描述】:

我有一个可以打开 Excel .xlsm 文件的应用程序。 Excel 文件在 Auto_Open() 中有一堆长时间运行的代码,可能需要几分钟才能完成。

目前,我打开了 excel 文件,当宏仍在运行时,我退出了我的应用程序。主窗口关闭,但我可以看到 MyApp.exe 在任务管理器中运行,直到宏完成,此时 MyApp.exe 进程结束。

    private void btnOpenExcel_Click(object sender, RoutedEventArgs e)
    {
        System.Diagnostics.Process.Start(excelFilePath);
    }

    private void btnClose_Click(object sender, RoutedEventArgs e)
    {
        Application.Current.Shutdown();
        //Also tried this.Close();
    }

我希望能够打开 Excel 文件,然后退出我的应用程序,而无需等待 Excel 宏完成。这可能吗?

【问题讨论】:

  • 请出示您的代码。见*.com/help/mcve
  • @PeterDuniho 完成。希望对您有所帮助。
  • 试试 System.Windows.Forms.Application.Exit()
  • 这很奇怪;我从未见过Process.Start 有这种行为。我自己的应用程序导出 Excel 文档并使用 Process.Start 启动它们,当 Excel 仍处于打开状态时,应用程序会干净利落地关闭。您的应用是等到宏执行完毕,还是等到 Excel 退出?
  • @MikeStrobel 一旦宏执行完毕,进程就会退出。 Excel 可以保持打开状态。

标签: c# .net multithreading excel vba


【解决方案1】:

经过大量的实验和研究,我知道发生了什么。这是 Excel 实现的不幸副作用以及本机 Windows 函数 ShellExecuteExSystem.Diagnostics.Process 使用)的工作方式。特别是,在 auto_run 宏完成之前,Excel 不会确认对 ShellExecuteEx 的 DDE 命令已完成,并且当以 Process 类使用它的方式调用时,ShellExecuteEx 将不会返回,直到发生这种情况或(非常重要)直到两分钟过去。

(文档说有一分钟的超时,但在我的 Windows 8.1 机器上是两分钟)。

我发现了一些变通方法,没有一个完全优雅,但所有这些都应该可以正常工作。

注意:下面的所有代码示例都将放在 click 事件处理程序中,除非另有说明(即互操作声明)。

我首选的解决方法是简单地使用单独的线程来启动进程。这并不能解决进程本身剩余的问题。但是进程的其余部分可以关闭,让单独的线程等待超时(或完成auto_open,以先到者为准):

Thread thread = new Thread(() => Process.Start(target));

thread.IsBackground = false;
thread.SetApartmentState(ApartmentState.STA);
thread.Start();

ShellExecuteEx等待时出现的问题之一是 Windows 实际上需要您的 STA 线程停留足够长的时间以便它发出DDE 命令到 Excel 打开给定的文件。这意味着任何绕过ShellExecuteEx 延迟的尝试都存在 Excel 根本无法启动或无法打开请求的文件的风险。

也就是说,如果您愿意接受这种风险或通过更长的超时时间来减轻风险(但不一定只要 Windows 规定的两分钟超时时间),您可以采取其他几种方法。

第二种方法是将Close() 调用排队等待稍后执行。这利用了ShellExecuteEx 仍在运行消息泵这一事实,因此即使Process.Start() 方法没有返回,您仍然可以在Form 子类中执行代码。这方面的一个例子是:

BeginInvoke((Action)(async () =>
    {
        await Task.Delay(1000);
        Close();
    }));
Process.Start(target);

这会使Close() 命令延迟一秒钟,这在我的计算机上足够长,足以让ProcessShellExecuteEx 完成使Excel 运行所需的工作。

注意:我确实尝试过更短的超时,但发现它不可靠。也就是说,在 100 毫秒而不是 1000 毫秒时,Excel 根本没有启动。在 500 毫秒时,它开始但通常不会实际加载工作簿。整整一秒钟,它在我配备 SSD 的笔记本电脑上是可靠的。我实际上不知道延迟在哪里,但可能是在驱动器较慢的机器上,需要更长的超时时间。

我不喜欢上面的一点是它在Process.Start() 方法实际返回之前就关闭了应用程序。虽然它有效,但这似乎有点超出“犹太/非犹太”线。 :)

所以第三种选择是完全绕过Process 类并直接调用ShellExecuteEx。这样做,您仍然需要等待,否则 Excel 将无法可靠启动。但是您可以在对ShellExecuteEx 的调用完成后等待,因此应用程序清理对我来说似乎更干净。也就是说,它是一个完全正常的程序退出,允许您进行所有您可能想做的日常管理。

由于互操作声明,它有点长,但效果很好:

SHELLEXECUTEINFO sei = new SHELLEXECUTEINFO();

sei.fMask = ShellExecuteMaskFlags.SEE_MASK_FLAG_NO_UI;
sei.nShow = ShowCommands.SW_NORMAL;
sei.lpFile = target;

if (!Interop.ShellExecuteEx(sei))
{
    int hr = Marshal.GetLastWin32Error();
    Exception e = Marshal.GetExceptionForHR(hr);
    // Throw, display message box, whatever you like here
}

await Task.Delay(100);
Close();

互操作声明如下所示(未使用的枚举值已被省略):

class Interop
{
    [DllImport("shell32.dll", CharSet = CharSet.Auto, SetLastError = true)]
    public static extern bool ShellExecuteEx(SHELLEXECUTEINFO lpExecInfo);
}

[StructLayout(LayoutKind.Sequential)]
public class SHELLEXECUTEINFO
{
    public int cbSize;
    public ShellExecuteMaskFlags fMask;
    public IntPtr hwnd;
    [MarshalAs(UnmanagedType.LPTStr)]
    public string lpVerb;
    [MarshalAs(UnmanagedType.LPTStr)]
    public string lpFile;
    [MarshalAs(UnmanagedType.LPTStr)]
    public string lpParameters;
    [MarshalAs(UnmanagedType.LPTStr)]
    public string lpDirectory;
    public ShowCommands nShow;
    public IntPtr hInstApp;
    public IntPtr lpIDList;
    [MarshalAs(UnmanagedType.LPTStr)]
    public string lpClass;
    public IntPtr hkeyClass;
    public uint dwHotKey;
    public IntPtr hIcon;
    public IntPtr hProcess;

    public SHELLEXECUTEINFO()
    {
        this.cbSize = Marshal.SizeOf(this);
    }
}

public enum ShowCommands : int
{
    SW_NORMAL = 1,
}

[Flags]
public enum ShellExecuteMaskFlags : uint
{
    SEE_MASK_FLAG_NO_UI = 0x00000400,
}

使用这种技术,我可以使用更短的超时时间。 100ms 似乎工作可靠,而 10ms 则不然。

最后请注意,如果您可以更改 Excel 工作簿,您应该可以在 auto_open 例程中设置一个计时器,然后运行实际的初始化代码,让 auto_open 立即返回。这样做将完全不需要在 C# 程序中摆弄启动代码。 :)

【讨论】:

  • 优秀的答案。这是完美的。
【解决方案2】:

最简单的方法是调用 Environment.Exit(-1) 来终止进程并为底层操作系统提供指定的退出代码。

【讨论】:

    最近更新 更多