Delphi如何防止应用程序关闭。

6
我正在尝试防止我的应用程序被Windows关闭。该应用程序运行在Windows 8上,并使用XE6编写。我尝试了以下代码,但似乎完全被忽略了。为了测试它,我只是通过任务管理器向其发送“结束任务”。我需要的是一种让我的应用程序在用户关闭应用程序、通过任务管理器或Windows关机时完成其工作的方法。正常关闭不是问题,这由FormCloseQuery事件处理。但是其他两种方法我无法使其工作。直到Windows XP为止,可以通过捕获wm_endsession和wm_queryendsession来实现,从Vista开始,您需要使用ShutDownBlockReasonCreate,它返回true,但无论如何似乎都不起作用。
procedure WMQueryEndSession(var Msg : TWMQueryEndSession); message WM_QUERYENDSESSION;
procedure WMEndSession(var Msg: TWMEndSession); message WM_ENDSESSION;

function ShutdownBlockReasonCreate(hWnd: HWND; Reason: LPCWSTR): Bool; stdcall; external user32;
function ShutdownBlockReasonDestroy(hWnd: HWND): Bool; stdcall; external user32;


procedure TForm1.WMEndSession(var Msg: TWMEndSession);
begin
  inherited;

  Msg.Result := lresult(False);
  ShutdownBlockReasonCreate(Handle, 'please wait while muting...');
  Sleep(45000); // do your work here
  ShutdownBlockReasonDestroy(Handle);
end;

procedure TForm1.WMQueryEndSession(var Msg: TWMQueryEndSession);
begin
  inherited;
  Msg.Result := lresult(False);
  ShutdownBlockReasonCreate(Handle, 'please wait while muting...');
  Sleep(45000); // do your work here
  ShutdownBlockReasonDestroy(Handle);
end;

更新

将消息结果更改为true并删除休眠不会有任何变化。

procedure TForm1.WMEndSession(var Msg: TWMEndSession);
begin
  inherited;
  Msg.Result := lresult(True);
  ShutdownBlockReasonDestroy(Application.MainForm.Handle);
  ShutdownBlockReasonCreate(Application.MainForm.Handle, 'please wait while muting...');
end;

procedure TForm1.WMQueryEndSession(var Msg: TWMQueryEndSession);
begin
  inherited;
  Msg.Result := lresult(True);
  ShutdownBlockReasonDestroy(Application.MainForm.Handle);
  ShutdownBlockReasonCreate(Application.MainForm.Handle, 'please wait while muting...');
end;

1
请参考如何暂停Windows关机操作 - LU RD
1
你不能说函数是“骗局”。检查ShutDownBlockReasonCreate的返回值,如果返回false,则使用GetLastError查找失败原因。当你不费心检查返回值以找出API为什么不工作时,你不能说“API不起作用”。 - Ken White
如果我从一个按钮调用该函数,它将返回true。但是我无法在WMQueryEndSession中检查结果,因为应用程序在我能够检查其值之前就关闭了。 - GuidoG
尝试了上面链接中的代码,但不起作用。这可能是在Windows 8中再次更改了吗?或者XE6可能是问题所在? - GuidoG
1
为了测试它,我只需通过任务管理器向其发送“结束任务”即可。这与您正在测试的代码无关,此测试不会向您的应用程序窗口发送WM_QUERYENDSESSION、WM_ENDSESSION消息。 - Sertac Akyuz
2
你可以使用 OutputDebugString 来查看函数的结果,并在 IDE 的消息窗口中查看它,即使你从 IDE 运行应用程序后也是如此。始终检查 API 调用结果;如果你不这样做,就不能因为你没有正确使用它们而责怪它们不能正常工作。 - Ken White
3个回答

12
根据文档,为了阻止关机,您需要在响应WM_QUERYENDSESSION时返回FALSE。此外,在此消息处理程序中不能执行工作。工作必须发生在别处。如果您没有及时回复此消息,系统将不会等待您。
  • 在开始工作之前调用ShutdownBlockReasonCreate
  • 在工作过程中从WM_QUERYENDSESSION返回FALSE。不要在处理此消息时工作。立即返回。
  • 工作完成后调用ShutdownBlockReasonDestroy
WM_QUERYENDSESSION的处理程序可能如下:
procedure TMainForm.WMQueryEndSession(var Msg: TWMQueryEndSession);
begin
  if Working then
    Msg.Result := 0
  else
    inherited;
end;

执行工作的代码需要在工作开始前调用 ShutdownBlockReasonCreate,在工作结束后调用ShutdownBlockReasonDestroy,并确保上面使用的Working属性在工作期间评估为True

如果您的工作阻塞了主线程,则会遇到麻烦。主线程必须响应,否则系统将不会等待您。将工作放在线程中通常是向前走的方法。如果您的主窗口不可见,则没有机会阻止关闭。详细信息在此处解释:http://msdn.microsoft.com/en-us/library/ms700677.aspx

如果收到WM_ENDSESSION,那么为时已晚。系统无论如何都将关闭。

要测试它,我只需通过任务管理器向其发送“结束任务”即可。

这与阻止关机无关。测试关闭阻止的方式是注销。如果用户坚持终止您的进程,则很少有什么可以做的。Sertac的答案详细介绍了此问题。

最后,请勿忽略API调用的返回值,这也是非常不好的习惯。请不要这样做。


此外,“如果一个应用程序在响应 WM_QUERYENDSESSION 或 WM_ENDSESSION 时超时,Windows 将终止它。” 因此你的 “Sleep(45000); // do your work here” 会导致此超时。http://msdn.microsoft.com/en-us/library/ms700677(v=vs.85).aspx - Andreas Hausladen
我尝试返回TRUE,但没有任何影响。即使没有sleep命令,它仍然不起作用。返回TRUE并省略ShutDownBlockReasonDestroy也不起作用。我已经尝试了几乎我能想到的所有组合,尝试了使用谷歌找到的不同代码,但似乎没有什么作用。你有一个工作的例子吗? - GuidoG
这是一个测试应用程序,只有一个可见的表单,目前还没有完成任何工作,因此表单和主线程可以响应消息。您看到的代码就是这个测试应用程序中所有的代码。 - GuidoG
@Sertac,我知道我把事情搞砸了。对不起。你的回答详细介绍了“结束任务”的行为,非常有价值。请保留它。点赞(其中一个是我的)证实了这一点。我的点赞是不应该的。抱歉。 - David Heffernan
@David - 我唯一的反对意见是你的回答包括了我已经发布为答案的测试方法的错误,这才是真正重要的事情。问题中包含的代码是有效的,我编写并测试过它,并且与LU RD的评论相关联。点赞不是你的错,也不会影响我。 - Sertac Akyuz
显示剩余12条评论

6
您的代码似乎被完全忽略了,因为您没有进行测试。您是通过任务管理器向其发送“结束任务”,而您发布的代码只在系统关闭时有效。
Windows 8的不同之处似乎在于任务管理器的行为方式。在Windows 8之前,来自任务管理器的“结束任务”将首先尝试优雅地关闭应用程序(发送一个“WM_CLOSE”),您可以使用“OnCloseQuery”处理此操作。但是当应用程序拒绝关闭时,任务管理器将提供强制结束进程的选项。这是您无法处理的。如果您从任务管理器中选择“结束进程”也同样如此。
Windows 8任务管理器不会为强制关闭应用程序提供额外的对话框,但当应用程序拒绝关闭时,它会立即执行此操作。

1
以下是在 Delphi 11.1 Alexandria 中测试过的一些不同情况的解决方案:
当用户或系统关闭应用程序时,Form OnCloseQuery 将被调用。要知道这两个事件中的哪一个被触发,请调用 GetSystemMetrics 并将 SM_SHUTTINGDOWN 作为参数传递。
当存在待处理的系统关闭时,系统度量标准 SM_SHUTTINGDOWN 被设置,否则清除。
如果您只想在系统正在关闭时向用户抑制退出确认消息,则只需使用上述内容即可。
procedure TForm1.FormCloseQuery(Sender: TObject; var CanClose: Boolean);
begin
  if GetSystemMetrics (SM_SHUTTINGDOWN) > 0 then
    Exit;  // app is being closed by a system shutdown

   // app being closed by user, ask user to confirm exit
  if not ConfirmAppExit then
    CanClose := False;
end;

请注意,如果系统正在关闭,则会调用您的Form OnCloseQuery,但不会调用您的Form OnClose,您放置在OnClose中的任何代码都不会执行。因此,如果您希望在系统关闭时执行代码,请勿将代码放置在OnClose中。相反,请使用下面描述的WM_EndSession处理程序。
如果您想要更多,并且能够阻止关闭,请首先编写一个WM_QueryEndSession消息的处理程序:
procedure WMQueryEndSession(var Msg: TWMQueryEndSession); message WM_QueryEndSession;

在这个处理程序中,除了返回消息结果之外,不要做任何事情。该消息被发送到您的应用程序以检查它是否同意关闭系统,这并不意味着系统关机正在进行,因为所有应用程序必须首先同意此消息,但如果只有一个应用程序拒绝此消息(返回False),则关机将不会发生(当真正发生关机时,您将收到WM_EndSession消息)。
在WM_QueryEndSession处理程序中,检查是否有任何必须一次完成的关键运行任务,如果中断将导致数据丢失,则返回False以拒绝系统关机,如下所示:
procedure TForm1.WMQueryEndSession(var Msg: TWMQueryEndSession);
begin
  if CriticalTaskRunning then
    Msg.Result := 0              // Deny system shutdown
  else
    inherited;                       // Agree to system shutdown, Same as Msg.Result := 1
end;

如果您的任务不是关键性的,可以被中断,请不要返回 False,也不要在此时中断您的任务,只需返回 True 并继续运行您的任务,因为一些其他应用可能会拒绝关机并且您中断任务是徒劳的!仅当您收到 WM_EndSession 消息时,也就是所有应用都同意关闭并且系统确实正在关闭时再中断您的任务。

通过返回 False,现在拒绝了关机,使用 ShutdownBlockReasonCreate 在这个时候是多余的,但您可以使用它来向用户(在关机屏幕上)解释为什么您的应用程序正在阻止关机。如果使用这个,请确保在任务完成后调用 ShutdownBlockReasonDestroy。

收到 WM_EndSession 消息时,您现在知道系统确实正在关闭。 如果需要进行清理,中止任何正在运行的任务,保存更改,关闭数据库/文件等,则可以使用 ShutdownBlockReasonCreate 阻止关机,直到完成清理工作后取消关机阻止,如下所示:

procedure WMEndSession (var Msg: TWMEndSession);
begin
  if CleanUpRequired then
    begin
      ShutdownBlockReasonCreate (Handle, 'My app is preparing to close, just a sec...');
      try
        DoCleanUp;
      finally
        ShutdownBlockReasonDestroy (Handle);
      end;
    end;
end;

一些其他的关闭阻塞方法建议您每次启动任务时创建一个关闭阻塞,并在任务完成后销毁关闭阻塞,即使系统没有关闭!我在这里的方法是只在必要时并且只在系统真正关闭时创建关闭阻塞。

希望对某些人有用!


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接