0

设想:

  • 一个 TActionManager、一个 TAction 和一个 TButton(与此操作相关联)
  • ActionManager 在其 OnUpdate 事件处理程序中不断启用 Action
  • 动作事件处理程序中的代码使用 ShellExecAndWait 方法启动外部程序(使用 Jedi 代码库 JCL)
  • 要求:应用程序不应允许通过再次快速单击按钮来启动应用程序两次

问题:

  • ShellExecAndWait 不会阻塞应用程序消息循环,因此用户可以在外部应用程序仍处于打开状态时单击
  • 如果 Action 处理程序方法在调用 ShellExecAndWait 之前禁用 Action,Update 方法将立即重新启用它

所以我可以这样写

procedure TMyForm.OnMyAction(Sender: TObject);
begin
  try  
    // notify Action Manager that the Action is temporarily disabled
    SomeGlobalFlag := True;

    // disable the action 
    (Sender as TAction).Enabled := False;

    // do the call
    ShellExecAndWait( ... );

  finally

    // enable the action 
    (Sender as TAction).Enabled := True;

    // allow ActionManager to control the action again
    SomeGlobalFlag := False; 

  end;
end;

有没有更简单的方法?正如这个问题的标题所说 - 我可以阻止输入以执行外部应用程序吗?

4

4 回答 4

6

这取决于你希望你的程序对用户有多友好。

问题中显示的方法就足够了,但它可能会让用户想知道为什么按钮显示为禁用。如果您的程序使按钮保持启用状态,但在单击时改变了其行为,您的程序可能会更有帮助。它可以通知用户之前的程序仍在运行,而不是启动程序的另一个副本,甚至可能提供将焦点设置到该程序。

问题标题询问如何禁用表单上的所有控件。(表单是否是模态无关紧要;模态处理禁用表单,而不是模态表单本身。)Mjn 的答案是通过暂停一个动作列表来做到这一点。这不会禁用与操作无关的控件,也不会禁用与不同操作列表关联的控件。它还可以禁用与同一操作列表关联的其他表单上的控件。

Marck 的回答隐含地禁用了所有控件,但可能会使用户感到困惑,因为没有任何控件看起来被禁用。这似乎类似于 Sertac在评论中显然提到的想法,以禁用整个表单。

要禁用表单上的所有控件并使其显示为禁用,您可以使用如下递归函数:

procedure EnableControls(Parent: TWinControl; Enabled: Boolean);
var
  i: Integer;
  Ctl: TControl;
begin
  for i := 0 to Pred(Parent.ControlCount) do begin
    Ctl := Parent.Controls[i];
    Ctl.Enabled := Enabled;
    if Ctl is TWinControl then
      EnableControls(TWinControl(Ctl), Enabled);
  end;
end;

像这样使用它:

procedure TMyForm.OnMyAction(Sender: TObject);
begin
  EnableControls(Self, False);
  try
    ShellExecAndWait(...);
  finally
    EnableControls(Self, True);
  end;
end;

由于我们直接修改Enabled控件的属性,这些属性将Enabled与任何关联操作的属性分离。这解决了当前的需求,但有一个不希望的副作用,即对操作Enabled属性的进一步修改不会影响此表单上的控件。

动作可以与多个控件相关联,并且控件可以驻留在多个表单上。由于更新的是操作,而不是直接更新控件,因此实际上没有一种方法可以使用操作仅在一个表单上禁用控件。


现在我们来讨论禁用表单上的所有控件是否真的是引发这个问题的问题的正确解决方案。关于目标和建议的解决方案,这个问题有点分散。建议的解决方案是严厉的(禁用表单上的所有内容),因为实际上只需要阻止调用单个命令。并且不应该调用的命令与表单无关;无论有多少控件与任意数量的表单上的操作相关联,都不应调用该命令。因此,我们应该禁用该操作,并隐式禁用与之关联的任何控件,或者我们应该修改OnExecute事件处理程序来检测重新进入。

问题中显示的解决方案是禁用操作的方法。设置一个标志以指示该操作正在执行,并在执行完成时清除它。检查OnUpdate事件处理程序中的标志。不过,无需手动禁用OnExecute处理程序中的操作;Execute动作已经在他们的方法中更新了自己。所以我们有这个代码:

var
  ActionIsExecuting: Boolean = False;

procedure TMyForm.OnMyAction(Sender: TObject);
begin
  // notify Action Manager that the Action is temporarily disabled
  ActionIsExecuting := True;
  try  
    // do the call
    ShellExecAndWait( ... );
  finally
    // allow ActionManager to control the action again
    ActionIsExecuting := False; 
  end;
end;

procedure TSomeModule.ActionUpdate(Sender: TObject);
begin
  (Sender as TAction).Enabled := not ActionIsExecuting and ...
end;

这需要多个代码部分就如何处理此操作的执行进行合作。动作更新代码需要知道动作需要能够暂时禁用自己。

对于更独立的解决方案,我们可以不理会OnUpdate事件,只保持操作一直启用。相反,我们将在本地跟踪重新进入并通知用户:

procedure TMyForm.OnMyAction(Sender: TObject);
{$J+} // a.k.a. $WRITABLECONST ON
const
  ActionIsExecuting: Boolean = False;
{$J-}
begin
  if ActionIsExecuting then begin
    ShowMessage('The program is still running. Please wait.');
    exit;
  end;

  ActionIsExecuting := True;
  try
    ShellExecAndWait(...);
  finally
    ActionIsExecuting := False;
  end;
end;

Mjn 的回答称用于设置状态和管理 try-finally 块的五行代码是“太多样板”。您可以使用接口对象和辅助函数将其简化为一行:

type
  TTemporaryFlag = class(TInterfacedObject)
  private
    FFlag: PBoolean;
  public
    constructor Create(Flag: PBoolean);
    destructor Destroy; override;
  end;

function TemporaryFlag(Flag: PBoolean): IUnknown;
begin
  Result := TTemporaryFlag.Create(FFlag);
end;

constructor TTemporaryFlag.Create;
begin
  inherited;
  FFlag := Flag;
  FFlag^ := True;
end;

destructor TTemporaryFlag.Destroy;
begin
  FFlag^ := False;
  inherited;
end;

像这样使用它:

begin
  TemporaryFlag(@ActionIsExecuting);
  ShellExecAndWait(...);
end;

该函数返回一个接口引用,编译器将其存储在隐式声明的临时变量中。在代码的最后,该变量被销毁,存储的接口对象被释放,将标志返回到其先前的值。

于 2012-06-01T14:59:46.117 回答
2

此解决方案取决于实际的 Action 或 ActionManager 组件。仍然有太多的“样板”代码。也非常脆弱,因为它假设 Sender 是一个 TAction 实例。

procedure TMyForm.OnMyAction(Sender: TObject);
begin
  try  
    // disable all actions 
    (Sender as TAction).ActionList.State := asSuspended; 


    // do the call
    ShellExecAndWait( ... );

  finally

    // enable all actions 
    (Sender as TAction).ActionList.State := asNormal; 

  end;
end;
于 2012-06-01T10:25:22.933 回答
0

您可以将所有 ui 元素放在面板上,然后禁用面板。您的应用程序仍将响应移动、调整大小、重绘等。

于 2012-06-01T14:00:48.573 回答
0

的实现EnableControls遭受了一些奇怪的事情:

  • 并非表单上的所有组件都同时启用

所以禁用所有(不保存以前的状态)是一个很大的失误。

要正确执行此操作,您需要创建一个组件列表并存储指向该组件和之前启用状态的指针,因此在恢复时您可以恢复之前的状态。

最糟糕的是,这种方式也不完全正确,因为某些代码可能会更改某些组件的启用状态,并且在恢复该组件的启用状态时不能恢复......所以没有简单的方法可以禁用/重新启用。 .. 更不用说代码是否依赖于某些组件的启用状态。

我现在不记得我过去是怎么做的了...但是有一句话可以阻止鼠标和键盘的表单(忽略它们),而另一个可以取消阻止它...这就是我想要找到的当我从谷歌来到这里时......可能与(TMouseActivate)有关,但我记得它也阻止键盘,所以也许这还不够:

procedure TMyForm.FormMouseActivate(Sender: TObject; Button: TMouseButton; Shift: TShiftState; X, Y, HitTest: Integer; var MouseActivate: TMouseActivate);
begin
     if YourCondition
     then beign
               MouseActivate:=maNoActivateAndEat;
          end;
end;

我记得我没有使用以下内容,但它也可以使用:

procedure BlockInput(ABlockInput:Boolean);stdcall;external 'USER32.DLL';
...
procedure TMyForm.MyEvent(Sender:TObject);
begin
     BlockInput(True);
     // Your code, long loop, etc
     BlockInput(False);
end;
于 2016-11-03T09:10:17.390 回答