Menu

How to correctly reference an OWL::TWindow as parent window

Floele
2014-07-21
2014-07-24
  • Floele

    Floele - 2014-07-21

    Hi,

    let's assume I want to call an OWL:TDialog using the following constructor:

    OWL::TDialog(AParent,ResourceId,pMod)
    

    As first argument, I need to pass an OWL::TWindow. Let's further assume that the window which serves as parent is either a real OWL::TWindow or is an external window (eg. created by Win32, WinForms, WPF).

    For simplicty I created a method that returns the parent HWND to use and then wrap an OWL::TWindow around it for reference in the constructor:

    OWL::TDialog(&OWL::TWindow(GetParentHandle(), NULL),ResourceId,pMod)
    

    This works in general, but it creates a problem: The destructor of TWindow is a bit overeager and destroys all child windows of the wrapped window (ForEach(shutDown)), even when the referenced window doesn't "belong" to OWL at all. So if it contains several (OWL) child windows/controls, after having used it as parent window all OWL child controls are destroyed even though the "root" window still exists.

    So shouldn't ForEach(shutDown) also depend on !IsFlagSet(wfAlias)? Or am I doing it wrong?

    Best regards,
    Florian

     

    Last edit: Floele 2014-07-21
  • Vidar Hasfjord

    Vidar Hasfjord - 2014-07-21

    Hi Florian,

    OWL::TDialog(&OWL::TWindow(GetParentHandle(), NULL),ResourceId,pMod)

    The obvious problem here is that you create a temporary object. The temporary TWindow object is destructed at the end of the statement, which is earlier than you intended.

    Also, as an aside, you should in general not take the address of a temporary object. It leaves the pointer dangling at the end of the statement.

    You may try to dynamically allocate the alias instead (new TWindow), and then call Application->CondemnWindow(Parent) in your derived dialog destructor to ensure the alias is eventually deallocated. The TWindow destructor inherited by your derived dialog will call Parent->RemoveChild, which should circumvent the child destruction issue, i.e. when your TWindow alias parent is eventually destructed by the TApplication condemnation mechanism, it should have no children.

    shouldn't ForEach(shutDown) also depend on !IsFlagSet(wfAlias)?

    Actually it does. The implementation of Destroy, called by shutDown, checks for wfAlias, and only calls ::DestroyWindow if the flag is not set. So, shutDown correctly handles children that are aliases. However, it is correct for the parent to destroy its children. By definition, a parent TWindow owns its children, aliases or not.

    Normally, an alias refers to a non-OWL window instance, and will typically not have any TWindow children. By using an alias as a parent, you implicitly insert your derived dialog into the child list of the alias parent. However, this should not pose a problem in correct code, since the child (your derived dialog, in this case) should destruct before the parent, and hence remove itself from the child list of the parent. Consequentially, the child list will be empty when the alias destructor is subsequently called.

    I cannot see a realistic scenario in which you would create an alias, add a TWindow child (which may itself be an alias), and then expect that child to outlive its parent. For a start, it would leave the child without a parent. Perhaps it would be good to add diagnostics and/or exception handling to detect such illicit corner cases.

     
  • Floele

    Floele - 2014-07-22

    Hi,

    thanks for your reply, but it seems to me that I have not yet explained my issue properly.

    The temporary TWindow object is destructed at the end of the statement, which is earlier than you intended.

    Well, not quite. I do expect the TWindow object to be destroyed, because after the TDialog has closed and returns, the TWindow object as wrapper for the parent window is no longer needed.

    TWindow alias parent is eventually destructed by the TApplication condemnation mechanism, it should have no children.

    I don't understand what you mean here. The only point of time where the parent window has no children anymore is then the applications exits. No children are removed at any time during runtime.

    However, it is correct for the parent to destroy its children

    It is, but the parent is not actually destroyed at all because it's an alias. So in my opinion, the children must not be destroyed either (even if they are not aliases).

    Normally, an alias refers to a non-OWL window instance, and will typically not have any TWindow children

    That my be typical, but in my case it's different.

    Consequentially, the child list will be empty

    Nope! The "child" TDialog may be distroyed, but the aliased window still has many more children (not dialogs, but controls).

    and then expect that child to outlive its parent.

    I don't do that. I expect the parent to stay alive, along with all its children (until I close the app). But by using the aliased window, it will destroy its children long before the parent window gets destroyed.

    I cannot see a realistic scenario in which you would create an alias

    Create a WPF (main) window. Insert an OWL control. Now call an OWL dialog with the main window as aliased parent. Close the dialog. Result: Child OWL control is destroyed after opening the dialog, even though the main window is still alive.

     
  • Vidar Hasfjord

    Vidar Hasfjord - 2014-07-22

    Hi Florian,

    We are talking about the OWLNext innards here, so it is easy to get confused. :-)

    I do expect the TWindow object to be destroyed, because after the TDialog has closed and returns, the TWindow object as wrapper for the parent window is no longer needed.

    Can you show actual code? In the statement you showed, the temporary TWindow object will be destructed before you even get to the point of executing the dialog.

    I assumed you create a temporary TWindow object to pass to the TDialog base class constructor as part of the constructor for a derived dialog. Based on that, here is some code demonstrating what I was describing:

    class TDlg : public TDialog
    {
    public:
    
      TDlg(HWND nonOwlParent, TResId resId, TModule* module) 
        : TDialog(new TWindow(nonOwlParent), resId, module)
      {}
    
      ~TDlg()
      {Application->CondemnWindow(Parent);}
    };
    


    By using dynamic allocation and CondemnWindow, TDlg properly controls the lifetime of the temporary TWindow alias used to encapsulate the non-OWL parent. We cannot use "delete" instead of CondemnWindow because that will kill Parent before ~TWindow is called for TDlg.

    The only point of time where the parent window has no children anymore is then the applications exits.

    We may be confusing the Windows interface element parent-child relationships (HWND) with the TWindow parent-child relationships (TWindow::ChildList). As soon as you add TWindow children to a TWindow parent, those C++ instances are owned by the latter. But, TWindow instances can be aliases, so the underlying Windows elements may have independent lifetime.

    Create a WPF (main) window. Insert an OWL control.

    What did you pass for a parent to the control?

    Result: Child OWL control is destroyed after opening the dialog, even though the main window is still alive.

    I don't quite see how your control ended up in the temporary TWindow parent's child list, unless you used the same TWindow parent for your control and dialog — or there is an automatic child snatching mechanism in OWL that I have overlooked.

    Some actual example code would clarify things.

     
  • Floele

    Floele - 2014-07-23

    In the statement you showed, the temporary TWindow object will be destructed before you even get to the point of executing the dialog.

    The code pretty much looks like this. However, the problem is not the life time of the TWindow object.
    The problem is what happens when the TWindow object is destroyed, be it sooner or later.

    By using dynamic allocation and CondemnWindow, TDlg properly controls the lifetime

    This is something I need to check for properly destroying the TWindow object, but if I understand you correctly I would still face the same issue: The TWindow destructor will dispose of all its children.

    What did you pass for a parent to the control?

    The parent of the control is the same window that serves as parent (HWND) for the dialog, so

    unless you used the same TWindow parent for your control and dialog

    That's exactly what I do.

    Does this properly explain my issue? I can't think of any specific code to explain the situation but I can draw a window layout/diagram/whatever for you if that helps.

     
  • Vidar Hasfjord

    Vidar Hasfjord - 2014-07-23

    the problem is not the life time of the TWindow object.

    I very much suspect that this is exactly the problem. The rule in OWL is that a TWindow parent owns its TWindow children. So if you use a temporary parent you will get temporary children. That's how it is.

    The TWindow destructor will dispose of all its children.

    Exactly. And the destructor for those TWindow children will destroy the underlying Windows interface elements unless the wfAlias flag is set. If wfAlias is set, however, the underlying interface element will live on.

    I can't think of any specific code to explain the situation

    Well, let me try then. :-)

    :::C++
    //
    // This example demonstrates a non-OWL main window.
    // OWLNext version: 6.40
    // Build tool: Visual Studio Express 2013
    // Additional dependencies: "$(OWLROOT)/include/owl/inputdia.rc".
    //
    
    #include <owl/applicat.h>
    #include <owl/framewin.h>
    #include <owl/static.h>
    #include <owl/inputdia.h>
    
    // This function creates a non-OWL window for the sake of illustration.
    // In a real application, this may be some external window class, e.g.
    // a WPF form.
    //
    HWND CreateNonOwlForm(HINSTANCE app)
    {
      // Define a callback for our form class.
      // Quits the application when the form is closed.
      //
      struct TLocal
      {
        static LRESULT CALLBACK WndProc(HWND w, UINT msg, WPARAM a1, LPARAM a2)
        {
          if (msg == WM_DESTROY)
          {
            PostQuitMessage(0);
            return 0;
          }
          return DefWindowProcA(w, msg, a1, a2);
        }
      };
    
      //  Register a window class for our form.
      //
      WNDCLASSEX c;
      c.cbSize = sizeof(c);
      c.style = CS_HREDRAW | CS_VREDRAW;
      c.lpfnWndProc = &TLocal::WndProc;
      c.cbClsExtra = c.cbWndExtra = 0;
      c.hInstance = app;
      c.hIcon = LoadIcon(NULL, IDI_APPLICATION);
      c.hCursor = LoadCursor(NULL, IDC_ARROW);
      c.hbrBackground = (HBRUSH) GetStockObject(WHITE_BRUSH);
      c.lpszMenuName = NULL;
      c.lpszClassName = "NonOwlForm";
      c.hIconSm = LoadIcon(NULL, IDI_APPLICATION);
      RegisterClassExA(&c);
    
      // Create and return the form.
      //
      return CreateWindowExA(0, c.lpszClassName,
        "Non-OWL Form Window",
        WS_OVERLAPPEDWINDOW,
        CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT,
        NULL, NULL, app, NULL);
    }
    
    using namespace owl;
    
    class TTest : public TApplication
    {
    protected:
    
      virtual void InitMainWindow() override
      {
        HWND f = CreateNonOwlForm(GetHandle());
        SetMainWindow(new TFrameWindow(f)); // Alias the form.
        GetMainWindow()->ShowWindow(SW_NORMAL);
    
    #if 0 // Buggy code (temporary parent destroys children):
    
        (new TStatic(&TWindow(f), 0, "Static", 4, 4, 50, 20))
          ->Create();
        TInputDialog(&TWindow(f), "Dialog", "Prompt").Execute();
    
    #else // Correct code (parent lifetime extended):
    
        (new TStatic(GetMainWindow(), 0, "Static", 4, 4, 50, 20))
          ->Create();
        TInputDialog(GetMainWindow(), "Dialog", "Prompt").Execute();
    
    #endif
    
      }
    
    };
    
    int OwlMain(int, tchar* [])
    {return TTest().Run();}
    
     
  • Floele

    Floele - 2014-07-23

    Heh, you simplify the situation a bit so that the problem is easy to solve ;)

    What you are esstentially doing here is instead of creating a new TWindow wrapper using the existing and never deleted reference to the application's main window. I agree that it will work in this situation.

    However, it's not as simple as that. What if I have two (any number of) windows that work like the main window? In your code, OWL knows about the main window, it knows when it's created and it knows when it is to be destroyed. In my application, the OWL knows about the main window but is not aware of any other windows being created. Let me try to explain it with your code (just untested pseudo code though):

    The OWL control would be created like this:

    (new TStatic(new OWL::TFrameWindow(HandleOfSpecialWindowWhichJustHostsThisControl), 0, "Static", 4, 4, 50, 20))
      ->Create();
    

    The window (a control to be precise) which passes the HandleOfSpecialWindowWhichJustHostsThisControl to TStatic is inside one of the application's main windows (the containing main window may change at runtime!)

    Now let's say the static wants to show a message box when you click on it. As parent it needs to choose the current root window

    ParentHandle = GetAncestor(GetHandle(), GA_ROOT);
    

    Now we have the problem: We have a handle, but no TWindow. If the current root is the main window, we can use the existing TWindow instance, but what can we do if it's not?

    ParentHandle = GetAncestor(GetHandle(), GA_ROOT);
    if (ParentHandle == MainWindowHandle)
    {
        // We know the main window and have a TWindow reference of it.
        TInputDialog(GetMainWindow());
    }
    else
    {
        // What now?
        TInputDialog(...);
    }
    
     
  • Vidar Hasfjord

    Vidar Hasfjord - 2014-07-23

    in my opinion, the children [of an alias] must not be destroyed either (even if they are not aliases).

    Such a change would break existing code. It would also be very dangerous. Consider:

    :::C++
    new TStatic(&TWindow(f), 0, "Static", 4, 4, 50, 20);
    


    Here, if the temporary TWindow parent didn't delete the TStatic child, the child would be left parent-less, and the Parent pointer within the child would be dangling, causing undefined behaviour.

     
  • Vidar Hasfjord

    Vidar Hasfjord - 2014-07-23

    If the current root is the main window, we can use the existing TWindow instance, but what can we do if it's not?

    You can keep a registry of discovered non-OWL parents and clean it up at application exit.

    In any case, any TWindow children you create must have TWindow parent that outlives them. That's the problem you must solve.

     
  • Vidar Hasfjord

    Vidar Hasfjord - 2014-07-23

    Now let's say the static wants to show a message box when you click on it. As parent it needs to choose the current root window

    This particular scenario is not a problem. Since the message box is temporary, the parent alias can be as well.

    :::C++
    HWND p = GetAncestor(GetHandle(), GA_ROOT);
    TInputDialog(&TWindow(p), "Dialog", "Prompt").Execute();
    


    To be clear, the latter statement is not a problem, since the TInputDialog lifetime is over by the end of the statement. It is equivalent to:

    :::C++
    {
      TWindow w(p);
      TInputDialog d(&w, "Dialog", "Prompt");
      d.Execute();
    }
    


    And it will not cause any other controls you created to be destroyed. The temporary TWindow parent created here only knows about the TInputDialog child, not any other HWND children that the aliased parent window may have, nor any other TWindow aliases to the same window, or their children, if any.

    By the way, you can use owl::GetWindowPtr to retrieve the TWindow pointer for a HWND handle, if any exists. For example,

    :::C++
    TWindow* TMyApplication::GetAlias(HWND h)
    {
      TWindow* w = GetWindowPtr(h);
      if (!w)
      {
        w = new TWindow(h);
        AliasRegistry.push_back(w);
      }
      return w;  
    }
    
     

    Last edit: Vidar Hasfjord 2014-07-23
  • Floele

    Floele - 2014-07-23

    And it will not cause any other controls you created to be destroyed.

    Uhm...but that is exactly my problem. It does destroy other controls. So if I understand you correctly the question is why this temporary TWindow object knows about further children?

     
  • Vidar Hasfjord

    Vidar Hasfjord - 2014-07-23

    hm...but that is exactly my problem. It does destroy other controls.

    Can you demonstrate this in my example? I tested for this explicitly and found no such thing. For example, this works perfectly fine:

    :::C++
    (new TStatic(GetMainWindow(), 0, "Static", 4, 4, 50, 20))
      ->Create();
    TInputDialog(&TWindow(f), "Dialog", "Prompt").Execute();
    


    Here, even though we use a temporary alias as the parent to the TInputDialog, it does not affect the lifetime of the TStatic, even though they have parents that encapsulate the same HWND window. The parents (TWindow instances) are separate and independent, with separate child lists (TWindow::ChildList). In particular, TInputDialog's parent knows nothing about the TStatic.

     

    Last edit: Vidar Hasfjord 2014-07-24
  • Floele

    Floele - 2014-07-24

    OK, thanks for the insight so far. I did some more debugging to find out where the children come from and noticed that it works like this:

    1. Dialog is running.

      TInputDialog(&TWindow(f), "Dialog", "Prompt").Execute();

    2. After the dialog has been created, the main window receives and processes further window messages. One of theses messages creates further OWL controls inside the main window. It appears that these child controls then add themselves to the child list of the temporary window object that is being created as dialog parent.

    3. When the dialog closes, the temporary window instance is deleted along with all children that have been added while the dialog is open.

    So it looks like I need to make sure that these children are added either after the dialog closes or before it opens...which is a bit tricky in this case to achieve.

     
  • Vidar Hasfjord

    Vidar Hasfjord - 2014-07-24

    Hi Florian, good to hear you have got to the root of the problem. I hope you find a nice and simple solution.

    So it looks like I need to make sure that these children are added either after the dialog closes or before it opens

    Alternatively, find a way to alias the non-OWL windows throughout their lifetime, so that you can avoid the temporary aliases. E.g. using my GetAlias function posted earlier:

    :::C++
    TInputDialog(app->GetAlias(f), "Dialog", "Prompt").Execute();
    


    To detect when the non-OWL windows close and do proper clean up, you can make a TWindow-derived class and use that instead of TWindow within the implementation of GetAlias.

    :::C++
    class TAlias : public TWindow
    {
    public:
    
      TAlias(HWND h) : TWindow(h)
      {GetApp().AddAlias(this);}
    
      virtual ~TAlias()
      {GetApp().RemoveAlias(this);}
    
      TApp& GetApp() const
      {return dynamic_cast<TApp&>(*GetApplication());}
    
    protected:
    
      virtual void CleanupWindow() override
      {
        TWindow::CleanupWindow();
        GetApp().Condemn(this); // App will delete later.
      }
    
    };
    
    TWindow* TApp::GetAlias(HWND h)
    {
      TWindow* w = GetWindowPtr(h);
      return w ? w : new TAlias(h);
    }
    
    void TApp::AddAlias(TAlias* w) {/*...*/}
    void TApp::RemoveAlias(TAlias* w) {/*...*/}
    


    This particular solution might not be feasible in your case, but hopefully you can find a way that achieves the same goal; i.e. that avoids the brittle temporary aliases.

     
  • Floele

    Floele - 2014-07-24

    Thanks. I think I found a working solution by initialising the OWL controls a bit sooner than necessary (and thus before displaying the dialog).

     

Anonymous
Anonymous

Add attachments
Cancel





MongoDB Logo MongoDB