问题
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:
- Form1(MainForm) and Form3 are permanent forms. (Created in the dpr)
- Form2 is created when needed and freed afterward.
- Form3 contains a TComboBox with X items.
Sequence of actions :
- Form1 create and show Form2 modal.
- Form2 show form3 modal.
- Close Form3
- Close and free Form2
- Show Form3 <---- The TComboBox now contains 0 items.
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.
- Is it a known fact that one shouldn't mix permanent and temporary form when ModalPopupMode is pmAuto?
- If not, is there any known workaround for this problem?
- If it's a bug, is this fixed in more recent version of Delphi? (I'm using XE4)
回答1:
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()
andTWinControl.DestroyHandle()
directly. They are bothprotected
, 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 alsoprotected
, 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 subsequentShowModal()
.send the
TComboBox
window aCM_DESTROYHANDLE
message and letTWinControl
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 byTWinControl.DestroyHandle()
when destroying child windows. When aTWinControl
component receives that message, it callsUpdateRecreatingFlag()
andDestroyHandle()
on itself.
回答2:
Based on Remy's excellent answer I implemented something that fixes these issues in the whole application. You will need to descend all your modal forms from a custom TForm descendant - TMyModalForm in my example (which, IMO, is always a good practice anyway). All modal forms in my application descend from this. Please notice that I also set PopupMode to pmAuto in CreateParams() before calling the inherited method. This prevents the z-order problem when showing modal windows, but also causes the window handle problem described in your question. Also, I just broadcast the CM_DESTROYHANDLE if action is caHide. This skips an unnecessary notification for MDI child windows and modal windows which are destroyed on close. BTW, for future reference, this issue still exists in 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;
来源:https://stackoverflow.com/questions/32099167/pmauto-modalpopupmode-proper-use-or-bug-workaround