Search code examples
delphidelphi-xe4

pmAuto ModalPopupMode proper use or bug workaround


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)

Solution

  • 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:

    1. 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;
      
    2. 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().

    3. 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.