我有一个打开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宏完成。这可能吗?
答案 0 :(得分:2)
经过大量的实验和研究,我知道发生了什么。这是Excel实现的不幸副作用以及本机Windows函数ShellExecuteEx
(System.Diagnostics.Process
使用)的工作方式。特别是,在ShellExecuteEx
宏完成之前,Excel不会向auto_run
确认完成DDE命令,并且在Process
类使用它ShellExecuteEx
的方式调用时直到发生这种情况才会返回,或者(非常重要)直到两分钟过去。
(文档说有一分钟超时,但在我的Windows 8.1机器上是两分钟)。
我发现了一些解决方法,没有一个完全优雅,但所有这些都应该可以正常工作。
注意:下面的所有代码示例都将放在click事件处理程序中,除非另有说明(即interop声明)。
我首选的解决方法是简单地使用单独的线程来启动该过程。这并不能解决流程本身存在的问题。但是剩下的进程可以关闭,让单独的线程等待超时(或auto_open
完成,以先到者为准):
Thread thread = new Thread(() => Process.Start(target));
thread.IsBackground = false;
thread.SetApartmentState(ApartmentState.STA);
thread.Start();
当ShellExecuteEx
没有等待时出现的一个问题是,Windows实际上需要你的STA线程需要足够长时间才能挂起来向Excel发出DDE命令以打开给定文件。这意味着任何绕过ShellExecuteEx
延迟的尝试都会冒着Excel根本无法启动或者没有打开请求文件的风险。
也就是说,如果你愿意接受这种风险或者用更长的超时时间来缓解风险(但不一定只要Windows强制的两分钟超时),你可以采取其他几种方法。
第二种方法是将Close()
调用队列以便以后执行。这利用了ShellExecuteEx
仍在运行消息泵的事实,因此即使Process.Start()
方法尚未返回,您仍然可以在Form
子类中获取要执行的代码。这方面的一个例子是:
BeginInvoke((Action)(async () =>
{
await Task.Delay(1000);
Close();
}));
Process.Start(target);
这会将Close()
命令延迟一秒,这在我的计算机上足够长,让Process
和ShellExecuteEx
完成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,
}
使用这种技术,我能够使用更短的超时。 100毫秒似乎可靠地工作,而10毫秒没有。
最后请注意,如果您可以更改Excel工作簿,您应该能够在auto_open
例程中设置一个计时器,然后运行实际的初始化代码,让auto_open
立即返回。这样做可以完全不需要在C#程序中使用启动代码。 :)
答案 1 :(得分:1)
最简单的方法是调用Environment.Exit(-1)来终止进程并为底层操作系统提供指定的退出代码。