I'm having problems using TApplication.ModalPopupMode=pmAuto and I was wondering if my problems were caused by my usage of pmAuto or a bug in delphi.
Simple use case:
Sequence of actions :
I use ComboBox as an example, but I guess any controls that saves information in the DestroyWnd procedure and restore it in the CreateWnd procedure isn't working right. I tested TListBox and it displays the same behavior too.
It is not really a bug, just a quirk in how the various windows interact with each other when dealing with modality.
When Form3
is first created, TComboBox.CreateWnd()
is called during DFM streaming. When Form3.ShowModal()
is called for the first time, Form3
calls RecreateWnd()
on itself if its PopupMode
is pmNone
and Application.ModalPopupMode
is not pmNone
. OK, so TComboBox.DestroyWnd()
gets called, saving the items, then TComboBox.CreateWnd()
gets called, restoring the items. Recreating the TComboBox
's window during ShowModal()
is not ideal, but it works this time.
When Form3.ShowModal()
is called the second time, TComboBox.CreateWnd()
is called again without a previous call to TComboBox.DestroyWnd()
! Since the items have not been saved, they cannot be restored. That is why the TComboBox
is empty.
But why does this happen? When Form2
is freed, Form3
's window is still associated with Form2
's window. The first call to Form3.ShowModal
set Form2
's window as Form3
's parent/owner window. When you close a TForm
, it is merely hidden, its window still exists. So, when Form2
and Form3
are closed, they still exist and are linked together, and then when Form2
is destroyed, all of its child and owned windows get destroyed. TComboBox
receives a WM_NCDESTROY
message, resetting its Handle
to 0 without notifying the rest of its code that the window is being destroyed. Thus, TComboBox
does not have a chance to save its current items because DestroyWnd()
is not called. DestroyWnd()
is called only when the VCL itself is destroying the window, not when the OS destroys it.
Now, how can you fix this? You will have to destroy the TComboBox
's window, triggering its DestroyWnd()
method, before freeing Form2
. The trick is that TComboBox.DestroyWnd()
will save the items only if the csRecreating
flag is enabled in the TComboBox.ControlState
property. There are a few different ways you can accomplish that:
call TWinControl.UpdateRecreatingFlag()
and TWinControl.DestroyHandle()
directly. They are both protected
, so you can use an accessor class to reach them:
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;
call TWinControl.RecreateWnd()
directly. It is also protected
, so you can use an accessor class to reach it:
type
TComboBoxAccess = class(TComboBox)
end;
Form2 := TForm2.Create(nil);
try
Form2.ShowModal;
finally
TComboBoxAccess(Form3.ComboBox1).RecreateWnd;
Frm.Free;
end;
Form3.ShowModal;
The TComboBox
window is not actually be created until the next time it is needed, in the subsequent ShowModal()
.
send the TComboBox
window a CM_DESTROYHANDLE
message and let TWinControl
handle everything for you:
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
is used internally by TWinControl.DestroyHandle()
when destroying child windows. When a TWinControl
component receives that message, it calls UpdateRecreatingFlag()
and DestroyHandle()
on itself.