在Diagnostics.Process.Start()完成之前,应用程序不会退出

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

目前,我打开Excel文件,而macros仍在运行,我退出我的应用程序。 主窗口closures,但我可以看到MyApp.exe在任务pipe理器中运行,直到macros完成,此时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文件,然后退出我的应用程序,而不必等待Excelmacros完成。 这可能吗?

经过大量的实验和研究,我知道发生了什么事情。 这是Excel实现的一个不幸的副作用,以及原生Windows函数ShellExecuteExSystem.Diagnostics.Process使用)的工作方式。 特别是,直到auto_runmacros完成后,Excel才会确认完成了对ShellExecuteEx的DDE命令,并且在Process类使用该方法时, ShellExecuteEx将不会返回,直到发生或者(非常重要),直到两分钟过去。

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

我发现了几个解决方法,没有一个是完美的,但所有这些都可以正常工作。

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

我最喜欢的解决方法是简单地使用一个单独的线程来启动过程。 这并不能解决过程本身的问题。 但是其余的进程可以closures,只有一个线程等待超时(或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运行所必需的工作。

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

我不喜欢上述的一件事是,在Process.Start()方法实际返回之前,它将应用程序closures。 虽然它的工作,这似乎是远远超过“犹太教/非犹太教”的路线。 🙂

所以第三个select是完全绕过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似乎可靠工作,而10毫秒没有。

最后注意,如果你可以改变Excel的工作簿,你应该可以在auto_open例程中设置一个计时器,然后再运行实际的初始化代码,让auto_open立即返回。 这样做会否定任何需要在C#程序中调用启动代码的需要。 🙂

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