经过大量的实验和研究,我知道发生了什么。这是 Excel 实现的不幸副作用以及本机 Windows 函数 ShellExecuteEx(System.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() 命令延迟一秒钟,这在我的计算机上足够长,足以让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,
}
使用这种技术,我可以使用更短的超时时间。 100ms 似乎工作可靠,而 10ms 则不然。
最后请注意,如果您可以更改 Excel 工作簿,您应该可以在 auto_open 例程中设置一个计时器,然后运行实际的初始化代码,让 auto_open 立即返回。这样做将完全不需要在 C# 程序中摆弄启动代码。 :)