pmAuto ModalPopupMode 正确使用或错误解决方法
pmAuto ModalPopupMode proper use or bug workaround
我在使用 TApplication.ModalPopupMode=pmAuto 时遇到问题,我想知道我的问题是由于我使用 pmAuto 还是 delphi.
中的错误引起的
简单用例:
- Form1(MainForm) 和 Form3 是永久窗体。 (在 dpr 中创建)
- Form2 在需要时创建,并且
之后获释。
- Form3 包含一个带有 X 项的 TComboBox。
操作顺序:
- Form1 创建并显示 Form2 模式。
- Form2 显示 form3 模态。
- 关闭 Form3
- 关闭并释放 Form2
- 显示 Form3 <---- TComboBox 现在包含 0 个项目。
我以 ComboBox 为例,但我猜任何在 DestroyWnd 过程中保存信息并在 CreateWnd 过程中恢复信息的控件都无法正常工作。我测试了 TListBox,它也显示了相同的行为。
- 当 ModalPopupMode 为 pmAuto 时,不应混合使用永久和临时表单,这是否已为人所知?
- 如果没有,是否有任何已知的解决此问题的方法?
- 如果这是一个错误,是否已在更新的 Delphi 版本中修复? (我正在使用 XE4)
这并不是真正的错误,只是各种 windows 在处理模态时如何相互交互的一个怪癖。
首次创建Form3
时,在DFM流式传输期间调用TComboBox.CreateWnd()
。当 Form3.ShowModal()
第一次被调用时,如果 PopupMode
是 pmNone
而 Application.ModalPopupMode
不是 [=19],Form3
会调用自身 RecreateWnd()
=].好的,所以 TComboBox.DestroyWnd()
被调用,保存项目,然后 TComboBox.CreateWnd()
被调用,恢复项目。在 ShowModal()
期间重新创建 TComboBox
的 window 并不理想,但这次有效。
第二次调用 Form3.ShowModal()
时,再次调用 TComboBox.CreateWnd()
而没有 先前调用 TComboBox.DestroyWnd()
!由于项目尚未保存,因此无法恢复。这就是 TComboBox
为空的原因。
但是为什么会这样呢?当 Form2
被释放时,Form3
的 window 仍然与 Form2
的 window 关联。第一次调用 Form3.ShowModal
将 Form2
的 window 设置为 Form3
的 parent/owner window。当你关闭一个 TForm
时,它只是被隐藏了,它的 window 仍然存在。所以,当 Form2
和 Form3
关闭时,它们仍然存在并链接在一起,然后当 Form2
被销毁时,其所有 child 和拥有的 windows 被摧毁。 TComboBox
收到一条 WM_NCDESTROY
消息,将其 Handle
重置为 0,而不通知其其余代码 window 正在被销毁。因此,TComboBox
没有机会保存它的当前项目,因为 DestroyWnd()
没有被调用。 DestroyWnd()
仅在 VCL 本身正在销毁 window 时调用,而不是在 OS 销毁它时调用。
现在,你该如何解决这个问题?在释放 Form2
之前,您必须销毁 TComboBox
的 window,触发其 DestroyWnd()
方法。诀窍是只有在 TComboBox.ControlState
属性 中启用 csRecreating
标志时 TComboBox.DestroyWnd()
才会保存项目。您可以通过几种不同的方式完成此操作:
直接调用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;
直接调用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()
.
向TComboBox
window发送一条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;
我在使用 TApplication.ModalPopupMode=pmAuto 时遇到问题,我想知道我的问题是由于我使用 pmAuto 还是 delphi.
中的错误引起的简单用例:
- Form1(MainForm) 和 Form3 是永久窗体。 (在 dpr 中创建)
- Form2 在需要时创建,并且 之后获释。
- Form3 包含一个带有 X 项的 TComboBox。
操作顺序:
- Form1 创建并显示 Form2 模式。
- Form2 显示 form3 模态。
- 关闭 Form3
- 关闭并释放 Form2
- 显示 Form3 <---- TComboBox 现在包含 0 个项目。
我以 ComboBox 为例,但我猜任何在 DestroyWnd 过程中保存信息并在 CreateWnd 过程中恢复信息的控件都无法正常工作。我测试了 TListBox,它也显示了相同的行为。
- 当 ModalPopupMode 为 pmAuto 时,不应混合使用永久和临时表单,这是否已为人所知?
- 如果没有,是否有任何已知的解决此问题的方法?
- 如果这是一个错误,是否已在更新的 Delphi 版本中修复? (我正在使用 XE4)
这并不是真正的错误,只是各种 windows 在处理模态时如何相互交互的一个怪癖。
首次创建Form3
时,在DFM流式传输期间调用TComboBox.CreateWnd()
。当 Form3.ShowModal()
第一次被调用时,如果 PopupMode
是 pmNone
而 Application.ModalPopupMode
不是 [=19],Form3
会调用自身 RecreateWnd()
=].好的,所以 TComboBox.DestroyWnd()
被调用,保存项目,然后 TComboBox.CreateWnd()
被调用,恢复项目。在 ShowModal()
期间重新创建 TComboBox
的 window 并不理想,但这次有效。
第二次调用 Form3.ShowModal()
时,再次调用 TComboBox.CreateWnd()
而没有 先前调用 TComboBox.DestroyWnd()
!由于项目尚未保存,因此无法恢复。这就是 TComboBox
为空的原因。
但是为什么会这样呢?当 Form2
被释放时,Form3
的 window 仍然与 Form2
的 window 关联。第一次调用 Form3.ShowModal
将 Form2
的 window 设置为 Form3
的 parent/owner window。当你关闭一个 TForm
时,它只是被隐藏了,它的 window 仍然存在。所以,当 Form2
和 Form3
关闭时,它们仍然存在并链接在一起,然后当 Form2
被销毁时,其所有 child 和拥有的 windows 被摧毁。 TComboBox
收到一条 WM_NCDESTROY
消息,将其 Handle
重置为 0,而不通知其其余代码 window 正在被销毁。因此,TComboBox
没有机会保存它的当前项目,因为 DestroyWnd()
没有被调用。 DestroyWnd()
仅在 VCL 本身正在销毁 window 时调用,而不是在 OS 销毁它时调用。
现在,你该如何解决这个问题?在释放 Form2
之前,您必须销毁 TComboBox
的 window,触发其 DestroyWnd()
方法。诀窍是只有在 TComboBox.ControlState
属性 中启用 csRecreating
标志时 TComboBox.DestroyWnd()
才会保存项目。您可以通过几种不同的方式完成此操作:
直接调用
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;
直接调用
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()
.向
TComboBox
window发送一条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;