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

时间:2014-11-05 16:54:49

标签: c# .net multithreading excel excel-vba vba

我有一个打开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宏完成。这可能吗?

2 个答案:

答案 0 :(得分:2)

经过大量的实验和研究,我知道发生了什么。这是Excel实现的不幸副作用以及本机Windows函数ShellExecuteExSystem.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()命令延迟一秒,这在我的计算机上足够长,让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,
}

使用这种技术,我能够使用更短的超时。 100毫秒似乎可靠地工作,而10毫秒没有。

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

答案 1 :(得分:1)

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