如何以非模态形式禁用所有组件?

mjn*_*mjn 0 delphi vcl modal-dialog tactionmanager

场景:

  • 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;
Run Code Online (Sandbox Code Playgroud)

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

Rob*_*edy 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;
Run Code Online (Sandbox Code Playgroud)

像这样使用它:

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

由于我们直接修改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;
Run Code Online (Sandbox Code Playgroud)

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

对于更加独立的解决方案,我们可以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;
Run Code Online (Sandbox Code Playgroud)

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;
Run Code Online (Sandbox Code Playgroud)

像这样使用它:

begin
  TemporaryFlag(@ActionIsExecuting);
  ShellExecAndWait(...);
end;
Run Code Online (Sandbox Code Playgroud)

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