pmAuto ModalPopupMode 正确使用或错误解决方法

pmAuto ModalPopupMode proper use or bug workaround

我在使用 TApplication.ModalPopupMode=pmAuto 时遇到问题,我想知道我的问题是由于我使用 pmAuto 还是 delphi.

中的错误引起的

简单用例:

操作顺序:

我以 ComboBox 为例,但我猜任何在 DestroyWnd 过程中保存信息并在 CreateWnd 过程中恢复信息的控件都无法正常工作。我测试了 TListBox,它也显示了相同的行为。

这并不是真正的错误,只是各种 windows 在处理模态时如何相互交互的一个怪癖。

首次创建Form3时,在DFM流式传输期间调用TComboBox.CreateWnd()。当 Form3.ShowModal() 第一次被调用时,如果 PopupModepmNoneApplication.ModalPopupMode 不是 [=19],Form3 会调用自身 RecreateWnd() =].好的,所以 TComboBox.DestroyWnd() 被调用,保存项目,然后 TComboBox.CreateWnd() 被调用,恢复项目。在 ShowModal() 期间重新创建 TComboBox 的 window 并不理想,但这次有效。

第二次调用 Form3.ShowModal() 时,再次调用 TComboBox.CreateWnd() 而没有 先前调用 TComboBox.DestroyWnd()!由于项目尚未保存,因此无法恢复。这就是 TComboBox 为空的原因。

但是为什么会这样呢?当 Form2 被释放时,Form3 的 window 仍然与 Form2 的 window 关联。第一次调用 Form3.ShowModalForm2 的 window 设置为 Form3 的 parent/owner window。当你关闭一个 TForm 时,它只是被隐藏了,它的 window 仍然存在。所以,当 Form2Form3 关闭时,它们仍然存在并链接在一起,然后当 Form2 被销毁时,其所有 child 和拥有的 windows 被摧毁。 TComboBox 收到一条 WM_NCDESTROY 消息,将其 Handle 重置为 0,而不通知其其余代码 window 正在被销毁。因此,TComboBox 没有机会保存它的当前项目,因为 DestroyWnd() 没有被调用。 DestroyWnd() 仅在 VCL 本身正在销毁 window 时调用,而不是在 OS 销毁它时调用。

现在,你该如何解决这个问题?在释放 Form2 之前,您必须销毁 TComboBox 的 window,触发其 DestroyWnd() 方法。诀窍是只有在 TComboBox.ControlState 属性 中启用 csRecreating 标志时 TComboBox.DestroyWnd() 才会保存项目。您可以通过几种不同的方式完成此操作:

  1. 直接调用TWinControl.UpdateRecreatingFlag()TWinControl.DestroyHandle()。它们都是 protected,因此您可以使用访问器 class 来访问它们:

    type
      TComboBoxAccess = class(TComboBox)
      end;
    
    Form2 := TForm2.Create(nil);
    try
      Form2.ShowModal;
    finally
      with TComboBoxAccess(Form3.ComboBox1) do
      begin
        UpdateRecreatingFlag(True);
        DestroyHandle;
        UpdateRecreatingFlag(False);
      end;
      Frm.Free;
    end;
    Form3.ShowModal;
    
  2. 直接调用TWinControl.RecreateWnd()。它也是 protected,因此您可以使用访问器 class 来访问它:

    type
      TComboBoxAccess = class(TComboBox)
      end;
    
    Form2 := TForm2.Create(nil);
    try
      Form2.ShowModal;
    finally
      TComboBoxAccess(Form3.ComboBox1).RecreateWnd;
      Frm.Free;
    end;
    Form3.ShowModal;
    

    TComboBox window 直到下一次需要时才真正创建,在随后的 ShowModal().

  3. TComboBoxwindow发送一条CM_DESTROYHANDLE消息,让TWinControl为您处理一切:

    Form2 := TForm2.Create(nil);
    try
      Form2.ShowModal;
    finally
      if Form3.ComboBox1.HandleAllocated then
        SendMessage(Form3.ComboBox1.Handle, CM_DESTROYHANDLE, 1, 0);
      Frm.Free;
    end;
    Form3.ShowModal;
    

    CM_DESTROYHANDLE在销毁childwindows时被TWinControl.DestroyHandle()内部使用。当 TWinControl 组件收到该消息时,它会自行调用 UpdateRecreatingFlag()DestroyHandle()

根据雷米的出色回答,我实施了一些措施来修复整个应用程序中的这些问题。您将需要从自定义 TForm 后代-我的示例中的 TMyModalForm 继承所有模态表单(无论如何,IMO 始终是一个好习惯)。我的应用程序中的所有模态形式都源于此。请注意,在调用继承方法之前,我还在 CreateParams() 中将 PopupMode 设置为 pmAuto。这可以防止显示模态 windows 时出现 z 顺序问题,但也会导致问题中描述的 window 句柄问题。另外,如果操作是 caHide,我只是广播 CM_DESTROYHANDLE。这会跳过 MDI 子 windows 和模态 windows 的不必要通知,它们在关闭时被销毁。 顺便说一句,为了将来参考,这个问题在 Delphi 10.2.3 Tokyo.

中仍然存在
type
  TMyModalForm = class(TForm)
  protected
    procedure DoClose(var Action: TCloseAction); override;
    procedure CreateParams(var Params: TCreateParams); override;
  end;

procedure TMyModalForm.DoClose(var Action: TCloseAction);
var
  Msg: TMessage;
begin
  inherited DoClose(Action);
  if Action = caHide then
  begin
    FillChar(Msg, SizeOf(Msg), 0);
    Msg.Msg := CM_DESTROYHANDLE;
    Msg.WParam := 1;
    Broadcast(Msg);
  end;
end;

procedure TMyModalForm.CreateParams(var Params: TCreateParams);
begin
  PopupMode := pmAuto;
  inherited;
end;

end;