在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函数ShellExecuteEx
( System.Diagnostics.Process
使用)的工作方式。 特别是,直到auto_run
macros完成后,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()
命令一秒钟,这在我的电脑上已经足够长了,让Process
和ShellExecuteEx
完成了让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)来终止进程,并为底层操作系统提供指定的退出代码。