2

我想打开一个文件,该文件最初保存到 SQL 表,但在调用 ShellExecuteEx 之前保存到磁盘。保存后,我现在有一个有效的文件路径,理论上应该能够使用此功能来实现我的目标。我需要该程序在其适当的程序中打开文件并等到该程序关闭后再继续。目前代码将启动正确的应用程序并打开传递的文件,但它无需等待(我知道这是因为我显示一条消息以指示应用程序何时终止)并且我编写的应该启动正确程序的应用程序关闭. 它显示消息,然后启动程序。我承认我并不完全理解 ShellExecuteEx 的工作原理,并且我已经将我在网上找到的代码与我的代码结合使用来达到预期的结果。您将在下面找到代码。任何帮助将不胜感激。

procedure Fire(FileStr: String);
var
  SEInfo: TShellExecuteInfo;
  ExitCode: DWORD;
  ExecuteFile, ParamString, StartInString: string;
begin
  ExecuteFile:= FileStr;
  FillChar(SEInfo, SizeOf(SEInfo), 0) ;
  SEInfo.cbSize := SizeOf(TShellExecuteInfo);
  with SEInfo do 
  begin
    fMask := SEE_MASK_NOCLOSEPROCESS;
    Wnd := Application.Handle;
    lpFile := PChar(ExecuteFile) ;
    nShow := SW_SHOWNORMAL;
  end;

  if ShellExecuteEx(@SEInfo) then
  begin
    repeat
      Application.ProcessMessages;
      GetExitCodeProcess(SEInfo.hProcess, ExitCode) ;
    until (ExitCode <> STILL_ACTIVE) or Application.Terminated;
    ShowMessage('App terminated') ;
  end
    else ShowMessage('Error starting App') ;
end;

我意识到我正在写入磁盘的文件尚未完成,这就是该消息出现在程序之前的原因。在将文件写入磁盘的调用之后添加另一个 application.Processmessages 解决了这个问题。但是,当被调用的应用程序打开时,GetExitCodeProcess 仍然不会返回 STILL_ACTIVE 的值。

在做了更多的研究之后,我决定提出一个新问题,更明确地指出我想要完成的事情。如果有人有兴趣关注它,你可以在这里找到它

我如何等到我启动的程序完成使用后才删除文件?

4

2 回答 2

4

就目前而言,问题中没有足够的信息来给你一个答案来准确解释你的场景中发生了什么。你还没有做足够的调试。所以这个答案将更具说教性质。我将尝试教你如何诊断这种性质的问题。

首先,让我们澄清一下代码中明显的问题,这些问题很重要,但对于实际问题来说是次要的。

  • 您正在运行一个繁忙的等待循环。这总是一个坏主意。您正在等待的主 GUI 线程中消耗 100% 的 CPU。有两种明显的方法可以避免这种情况。按照 Marko 的建议,使用线程来执行调用ShellExecuteEx和随后的等待。或用于MsgWaitForMultipleObjects执行可中断的等待以处理输入事件。
  • 如果ShellExecuteEx确实向您返回了进程句柄,则您会泄漏它。当 API 函数返回句柄时,您通常负责在完成处理后关闭它。这需要调用CloseHandle. 文档说明了这一点:调用应用程序负责在不再需要时关闭句柄。
  • 您没有执行全面的错误检查。您调用 Win32 API 函数并未能检查其返回值是否存在错误。稍后会详细介绍,但执行正确的错误检查至关重要。

现在,让我们仔细看看您在问题中报告的问题。问题是调用ShellExecuteEx成功,文档被打开,但是在显示文档的进程关闭之前,忙等待返回。您的繁忙循环如下所示:

repeat
  Application.ProcessMessages;
  GetExitCodeProcess(SEInfo.hProcess, ExitCode);
until (ExitCode <> STILL_ACTIVE) or Application.Terminated;

这里的代码真的不多。如果这个循环比你预期的更早返回,那怎么会发生呢?ExitCode <> STILL_ACTIVE循环在何时或Application.Terminated何时终止True。首先要做的是隔离哪些条件会导致循环终止。一些简单的调试会产生这些信息。我很难相信您不小心终止了您的应用程序,因此我将在此基础上继续,Application.Terminated并且False测试ExitCode终止了循环。

所以,让我们再看一下对 的调用GetExitCodeProcess。这对我来说立即很可疑,因为您没有执行任何错误检查。现在,我碰巧有你可能缺乏的额外信息。具体来说,SEInfo.hProcess可能不包含进程句柄。这使我更容易预测问题,但您可以在调试器下学习所有这些。再次从文档中:

新启动的应用程序的句柄。此成员在返回时设置并且始终为 NULL,除非 fMask 设置为 SEE_MASK_NOCLOSEPROCESS。即使 fMask 设置为 SEE_MASK_NOCLOSEPROCESS,如果没有启动进程,hProcess 也会为 NULL。例如,如果要启动的文档是 URL,并且 Internet Explorer 的实例已经在运行,它将显示该文档。没有启动新进程,并且 hProcess 将为 NULL。

注意 ShellExecuteEx 并不总是返回一个 hProcess,即使一个进程作为调用的结果被启动。例如,当您使用 SEE_MASK_INVOKEIDLIST 调用 IContextMenu 时,hProcess 不会返回。

所以,也许正在发生的事情是您的文档正在由 shell 以某种方式处理,使得hProcess返回给您的值为NULL,即0。发生这种情况时,调用GetExitCodeProcess失败并返回False。那是您没有检查的错误情况。您只需调用GetExitCodeProcess并忽略它是否成功。如果该调用没有成功,那么ExitCode就不会被分配一个有意义的值,并且尝试读取根本就是一个错误ExitCode。只有GetExitCodeProcess返回时才有意义True

还有另一种更微妙的故障模式。调用可能会ShellExecuteEx返回一个有效的进程句柄。但是启动的进程通过将请求传递给不同的进程然后终止来处理请求。从您的角度来看,显示文档的进程仍在运行,但ShellExecuteEx提供给您的进程已终止。这通常发生在显示文档的进程的实例已经在运行时。例如,尝试在 Delphi IDE 打开时调用Fire传递文件的函数。.pas启动了一个新进程,但它立即将文件交给正在运行的 Delphi IDE 并终止。

好了,暂时能想到的就这么多。我希望这可以帮助您追踪您的场景中真正发生的事情。不过,这可能不是您希望听到的消息,因为我怀疑您会发现使用ShellExecuteEx和等待它返回的进程句柄的方法无法满足您的需求。不幸的是,使用 shell 打开文档比最初看起来要复杂得多。

于 2013-11-01T09:29:07.503 回答
0

你的repeat..until循环在这里是错误的。避免使用Application.ProcessMessages()使您的应用程序不阻塞,因为这是不好的做法,并且可能/将导致无法预料的问题和奇怪的错误。

相反,使用线程等待应用程序终止:

unit uSEWaitThread;

interface

uses
  Winapi.Windows, System.Classes;

type
  TSEWaitThread = class(TThread)
  private
    FFile     : String;
    FSESuccess: Boolean;
    FExitCode : DWORD;
    FParentWnd: HWND;

  protected
    procedure Execute; override;

  public
    constructor Create(const AFile: String; const AParentWindowHandle: HWND; const AOnDone: TNotifyEvent);

    property ExitCode           : DWORD read FExitCode;
    property ShellExecuteSuccess: Boolean read FSESuccess;
  end;

implementation

uses
  Winapi.ShellApi;


constructor TSEWaitThread.Create(const AFile: String; const AParentWindowHandle: HWND; const AOnDone: TNotifyEvent);
begin
  FreeOnTerminate := TRUE;

  FFile := AFile;
  FParentWND := AParentWindowHandle;
  FSESuccess := FALSE;
  FExitCode := 0;
  OnTerminate := AOnDone;

  inherited Create;
end;

procedure TSEWaitThread.Execute;
var
  exec_info: TShellExecuteInfo;
begin
  FillChar(exec_info, SizeOf(TShellExecuteInfo), 0);
  exec_info.cbSize := SizeOf(TShellExecuteInfo);
  exec_info.fMask := SEE_MASK_NOCLOSEPROCESS or SEE_MASK_FLAG_NO_UI;
  exec_info.Wnd := FParentWnd;
  exec_info.lpFile := PChar(FFile);
  exec_info.nShow := SW_SHOWNORMAL;

  if ShellExecuteEx(@exec_info) then
  begin
    FSESuccess := TRUE;
    WaitForSingleObject(exec_info.hProcess, INFINITE);
    GetExitCodeProcess(exec_info.hProcess, FExitCode);
  end;
end;

end.

以下行将启动线程:

TSEWaitThread.Create('C:\test.txt', Handle, SEWaitThreadDone);

以及将处理线程结果的回调函数示例:

procedure TForm1.SEWaitThreadDone(Sender: TObject);
var
  se_wait_thread: TSEWaitThread;
begin
  se_wait_thread := Sender as TSEWaitThread;

  if se_wait_thread.ShellExecuteSuccess then
  begin
    ShowMessage(Format('App exit code: %d', [se_wait_thread.ExitCode]));
  end
  else
    ShowMessage('Error Starting App');
end;
于 2013-10-31T18:17:49.153 回答