ModalResult 不在组件内部起作用

Posted

技术标签:

【中文标题】ModalResult 不在组件内部起作用【英文标题】:ModalResult not acting inside of a component 【发布时间】:2017-03-12 10:48:16 【问题描述】:

您可以在下面看到一个组件的代码,该组件在 TPersistent 类中允许我分配一些 TCustomButtons(TButton 或 TBitBtn)。

我将我的组件放在一个模态表单上,并分配了 2 个按钮(确定和取消)。 通常,当我按下任何这些按钮时,我的表单应该会关闭。

我的问题是为什么表单没有关闭?

type
  TMyComp = class;
  TButtons = class;

  TMyComp = class(TComponent)
  private
    FButtons: TButtons;
    procedure SetButtons(Value: TButtons);
  public
    constructor Create(AOwner: TComponent); override;
    destructor Destroy; override;
 published 
    property Buttons: TButtons read FButtons write SetButtons;
 end;

 TButtons = class(TPersistent)
 private
   FOwner: TMyComp;
   FBtnOk: TCustomButton;
   FBtnCancel: TCustomButton;
   procedure SetCustomButton(Index: Integer; Value: TCustomButton);
   procedure BtnOkOnClick(Sender: TObject);
   procedure BtnCancelOnClick(Sender: TObject);
 protected
 public
   constructor Create(AOwner: TMyComp);  virtual;
   procedure Assign(Source: TPersistent); override;
 published
   property BtnOk: TCustomButton index 0 read FBtnOk write SetCustomButton;
   property BtnCancel: TCustomButton index 1 read FBtnCancel write SetCustomButton;
 end;

implementation

constructor TMyComp.Create(AOwner: TComponent);
begin
  inherited;
  FButtons:= TButtons.Create(Self);
end;

destructor TMyComp.Destroy;
begin
  FButtons.Free;
  inherited;
end;

//------- TButtons ---------
constructor TButtons.Create(AOwner: TMyComp);
begin
  inherited Create;
  FOwner:= AOwner;
end;

procedure TButtons.Assign(Source: TPersistent);
begin
  if Source is TButtons then
    begin
      FBtnOk:= TButtons(Source).BtnOk;
      FBtnCancel:= TButtons(Source).BtnCancel;
    end
  else
    inherited Assign(Source);
end;

procedure TButtons.SetCustomButton(Index: Integer; Value: TCustomButton);
begin
  case Index of
    0: if FBtnOk <> Value then
      begin
        FBtnOk:= Value;

        if Assigned(FBtnOk) then
          begin
            //TBitBtn
            if (FBtnOk is TBitBtn) then
              (FBtnOk as TBitBtn).OnClick:= BtnOkOnClick;

            //TButton
            if (FBtnOk is TButton) then
              (FBtnOk as TButton).OnClick:= BtnOkOnClick;
          end;
      end;

    1: if FBtnCancel <> Value then
      begin
        FBtnCancel:= Value;

        if Assigned(FBtnCancel) then
          begin
            //TBitBtn
            if (FBtnCancel is TBitBtn) then
              (FBtnCancel as TBitBtn).OnClick:= BtnCancelOnClick;

            //TButton
            if (FBtnCancel is TButton) then
              (FBtnCancel as TButton).OnClick:= BtnCancelOnClick;
          end;
      end;
  end;

  if Assigned(Value) then Value.FreeNotification(FOwner);
end;

procedure TButtons.BtnCancelOnClick(Sender: TObject);
begin
  showmessage('Cancel pressed!');

  if Sender is TButton then
    (Sender as TButton).ModalResult:= mrCancel;

  if Sender is TBitBtn then
    (Sender as TBitBtn).ModalResult:= mrCancel;
end;

procedure TButtons.BtnOkOnClick(Sender: TObject);
begin
  //do some input validations here...
  showmessage('Ok pressed!');

  if Sender is TButton then
    (Sender as TButton).ModalResult:= mrOk;

  if Sender is TBitBtn then
    (Sender as TBitBtn).ModalResult:= mrOk;
end;

【问题讨论】:

MCVE 意味着我们知道您的代码是什么。为什么要为按钮设置事件处理程序。为什么不设置 ModalResult。不过,这一切看起来有点粗略。 【参考方案1】:

其他答案/cmets 已经解释了为什么代码不起作用 - 您设置按钮的 ModalResult 为时已晚,因此当您期望它不会传播到表单的 ModalResult 时。

我想建议一个替代实现,它也包含一个解决方案,并解决您的代码缺少的其他一些问题:

type
  TButtons = class;

  TMyComp = class(TComponent)
  private
    FButtons: TButtons;
    procedure SetButtons(Value: TButtons);
  protected
    procedure Notification(AComponent: TComponent; Operation: TOperation); override;
  public
    constructor Create(AOwner: TComponent); override;
    destructor Destroy; override;
  published 
    property Buttons: TButtons read FButtons write SetButtons;
  end;

  TButtons = class(TPersistent)
  private
    FOwner: TMyComp;
    FButtons: array[0..1] of TCustomButton;
    FClickEvents: array[0..1] of TNotifyEvent;
    function GetCustomButton(Index: Integer): TCustomButton;
    procedure SetCustomButton(Index: Integer; Value: TCustomButton);
    procedure BtnOkOnClick(Sender: TObject);
    procedure BtnCancelOnClick(Sender: TObject);
  public
    constructor Create(AOwner: TMyComp);
    destructor Destroy; override;
    procedure Assign(Source: TPersistent); override;
  published
    property BtnOk: TCustomButton index 0 read GetCustomButton write SetCustomButton;
    property BtnCancel: TCustomButton index 1 read GetCustomButton write SetCustomButton;
  end;

implementation

//------- TMyComp ---------
constructor TMyComp.Create(AOwner: TComponent);
begin
  inherited;
  FButtons := TButtons.Create(Self);
end;

destructor TMyComp.Destroy;
begin
  FButtons.Free;
  inherited;
end;

procedure TMyComp.Notification(AComponent: TComponent; Operation: TOperation);
var
  i: Index;
begin
  inherited;
  if Operation = opRemove then
  begin
    for i := Low(FButtons.FButtons) to High(FButtons.FButtons) do
    begin
      if AComponent = FButtons.FButtons[i] then
      begin
        FButtons.FButtons[i] := nil;
        FButtons.FClickEvents[i] := nil;
        Exit;
      end;
    end;
  end;      
end;

//------- TButtons ---------
constructor TButtons.Create(AOwner: TMyComp);
begin
  inherited Create;
  FOwner := AOwner;
end;

constructor TButtons.Destroy;
begin
  Assign(nil);
  inherited;
end;

procedure TButtons.Assign(Source: TPersistent);
var
  i: Integer;
begin
  if Source = nil then
  begin
    for i to Low(FButtons) to High(FButtons) do
      SetCustomButton(i, nil);
  end
  else if Source is TButtons then
  begin
    for i to Low(FButtons) to High(FButtons) do
      SetCustomButton(i, TButtons(Source).FButtons[i]);
  end
  else
    inherited Assign(Source);
end;

function TButtons.GetCustomButton(Index: Integer): TCustomButton;
begin
  Result := FButtons[Index];
end;

type
  TCustomButtonAccess = class(TCustomButton)
  end;

procedure TButtons.SetCustomButton(Index: Integer; Value: TCustomButton);
begin
  if FButtons[Index] <> Value then
  begin
    if Assigned(FButtons[Index]) then
    begin
      TCustomButtonAccess(Value).OnClick := FClickEvents[Index];
      FClickEvents[Index] := nil;
      FButtons[Index].RemoveFreeNotification(FOwner);
    end;
    FButtons[Index] := Value;
    if Assigned(Value) then
    begin
      Value.FreeNotification(FOwner);
      FClickEvents[Index] := TCustomButtonAccess(Value).OnClick;
      case Index of
        0: TCustomButtonAccess(Value).OnClick := BtnOkOnClick;
        1: TCustomButtonAccess(Value).OnClick := BtnCancelOnClick;
      end;
    end;
  end;
end;

procedure TButtons.BtnOkOnClick(Sender: TObject);
var
  Form: TCustomForm;
begin
  //do some input validations here...
  ShowMessage('Ok pressed!');
  Form := GetParentForm(TControl(Sender));
  if Form <> nil then
    Form.ModalResult := mrOk;
  // optional
  if Assigned(FClickEvents[0]) then
    FClickEvents[0](Sender);
end;

procedure TButtons.BtnCancelOnClick(Sender: TObject);
var
  Form: TCustomForm;
begin
  ShowMessage('Cancel pressed!');
  Form := GetParentForm(TControl(Sender));
  if Form <> nil then
    Form.ModalResult := mrCancel;
  // optional
  if Assigned(FClickEvents[1]) then
    FClickEvents[1](Sender);
end;

【讨论】:

GetParentForm(TControl(Sender)) 这就是我要找的……干得好雷米!最初我尝试使用GetParentForm(Self),但编译器要求我提供TControl。所以就像一个解决方案,根据你的回答,我刚刚使用了这个代码:form:= GetParentForm(TControl(Sender)); if form &lt;&gt; nil then form.ModalResult:= mrOk;【参考方案2】:

在您的事件处理程序中,您正在设置按钮的ModalResult。当 该表单的 ModalResult 设置时,模式表单将关闭。

button's ModalResult 的目的是框架将 父表单的 ModalResult 设置为相同的值。

设置按钮组件的 ModalResult 属性是一种使单击按钮关闭模式表单的简单方法。 单击按钮时,其父窗体的 ModalResult 属性设置为与按钮的 ModalResult 属性相同的值。 例如,如果一个对话框有 OK 和 Cancel 按钮,它们的 ModalResult 属性可以在设计时分别设置为 mrOk 和 mrCancel。在运行时,单击 OK 按钮然后将对话框的 ModalResult 属性更改为 mrOk,然后单击 Cancel 按钮将对话框的 ModalResult 属性更改为 mrCancel。 除非需要进一步处理,否则按钮不需要 OnClick 事件处理程序

所以,基本上你做错的是你没有预定义按钮的ModalResult。所以:

单击按钮时,其ModalResult 仍为mrNone。 因此不会更改表单的ModalResult。 (我怀疑如果您再次单击该按钮,它的行为会如您所愿。)

要解决您的问题,您有两种选择:

最好尽快设置按钮的ModalResult。没有理由让“取消”按钮拥有ModalResult &lt;&gt; mrCancel。这就是它通常在 DFM 中流式传输的原因。 如果你绝对必须使用事件处理程序;设置表单的 ModalResult

【讨论】:

【参考方案3】:

一个更简单的复制案例可以是这样的:

创建一个包含两个表单的应用程序,从自动创建的表单列表中删除第二个表单。使用以下点击处理程序在每个表单上添加一个按钮。

在单元1中:

procedure TForm1.Button1Click(Sender: TObject);
var
  F: TForm;
begin
  F := TForm2.Create(nil);
  try
    F.ShowModal;
  finally
    F.Free;
  end;
end;

在单元2中:

procedure TForm2.Button1Click(Sender: TObject);
begin
  Button1.ModalResult := mrOk;
end;

运行应用程序,按下按钮启动模态表单。在第二个表单上按下按钮,表单不会关闭。

表单没有关闭的原因是按钮的点击处理程序来不及设置表单的模态结果。

你可以从'Vcl.StdCtrls.pas'中TCustomButton.Click;的代码中看出为什么为时已晚。评论是我的。

procedure TCustomButton.Click;
var
  Form: TCustomForm;
begin
  Form := GetParentForm(Self);
  if Form <> nil then Form.ModalResult := ModalResult; // this is where modal result is checked
  inherited Click;                                     // this is where your click handler is run
end;

您会注意到第二次单击会关闭表单。那是因为按钮的模态结果在第一次点击后就已经设置好了。

【讨论】:

奇怪的想法是,在过去我用过很多次,它的工作原理!但是代替Button1.ModalResult := mrOk,我一直使用ModalResult := mrOk,它可以工作。 无论如何,当我SetCustomButton 时,我可以为按钮分配一个ModalResult 值。但是我要进行输入验证,如果验证失败,那么我需要保持表单打开......那么我该如何使用我的组件结构来做到这一点? @real - 当然,按钮的模态结果所做的是设置表单的模态结果。您可以随时设置表单的模态结果。您可以在我的回答中的最后一个 sn-p 中查看如何访问父表单。当我注意到您明确询问“为什么表单没有关闭”时,我从我的 answer 中删除了该信息。 你可能想问一个关于测试用例的第二个问题的问题。 您的部分回答让我了解ModalResult 的行为方式,但它并没有解决我的问题。正如我在评论中解释的那样,GetParentForm(Self) 它在组件下不起作用(编译器无法识别自我)。 GetParentForm(TControl(Sender)) 在这种情况下是最终解决方案。

以上是关于ModalResult 不在组件内部起作用的主要内容,如果未能解决你的问题,请参考以下文章

在角度组件之间传递参数不起作用

React 组件在表单标签内不起作用

父组件的类在子组件中不起作用

导航组件在 onActivityResult 内导航不起作用

在刷新页面之前,下拉菜单和模式在 Vue 组件中不起作用

将 CSS 应用于子组件不起作用