EWin Tutorial - Window Lifespan

Before we get to the next topic that I actually care about, can you tell me about Create() and Destroy()?

Certainly. It is very convenient that you chose to ask about this now, because the next topic, that of creating buttons or other windows on-the-fly, requires understanding this.

Well, a window and a TWin aren't the same thing. A TWin is an object that is supposed to represent the window. But a TWin object usually gets created before the actual window it represents gets made. So, say you have a TWin. Before it appears on the screen and exists, you have to create the window associated with the TWin. You do this by calling Create(). Similarly, you usually want to destroy the window object before the TWin goes away. If the TWin goes away before the window goes away, then the window will just hang out, no longer under your control. So normally TWin objects need to have their Destroy() function called before they themselves are destroyed. Destroy() is a bit heavy-handed for everyday use, though; it's the slay-this-window command. if you want to close a window in response to a user's actions, then you don't typically call Destroy() on the main window directly; instead you call Close(), which calls Destroy() if everything checks out right.

So when you make a TWin, Create() needs to be called on it after its constructor has finished, and Destroy() needs to get called on it before its destructor starts. "But," you say... "Your previous examples don't call Create() or Destroy()."

Well, they did. But indirectly. See, EWin tries to automatically call these functions whenever possible. It tries to hide this menial task. So here's what happens: When a TWin is created, it goes through the list of TWins that it owns. It sees if any of these TWins want to have their Create() called for them automatically. It checks this by calling TWin::bAutoCreate(). If bAutoCreate() returns true, then that window's Create() function is called. If bAutoCreate() returns false, then nothing is done to that window.

(All windows by default have bAutoCreate() return true, except for dialogs, as I mentioned above. If you want to avoid having your controls automatically created, you can call SetAutoCreateOff() to turn this flag off. You will have to call SetAutoCreateOff() for each control you want to remove this flag from.) So there you have it. You don't normally need to call Create() because I take care of this chore for you. When the parent is created, the children get created. And how does this chain get started? Well, the EWinMain() macro actually directly calls Create() on the main window. That starts the chain reaction.

But if you create children after the main window has been created, then it's too late to take advantage of the auto-create feature. If you create a control or dialog or whatever outside of the main window's constructor, it is generally too late -- by the time that function gets called, the main window will have already been created. Thus if you wanted to make a new button in your OnControlCmd() function, you will allocate it with "new TBtn", and then you will need to call Create() on it yourself, since nobody is going to do it for you.

Another thing: if you are ever not sure whether a window is being auto-created or not, you can just call its Create() function yourself. I made sure that it is always safe to repeatedly call Create(). So if this whole automatically-calling-create thing is upsetting you, then you don't have to use it -- call Create() for each of your children, in your main window's OnInit().

The return-value of Create() is usually not very useful, but there is one exception: modal dialogs. Modal dialogs are a very strange beast indeed. See the previous question about dialogs for more information. That takes care of when you call Create(), but when do you need to call Destroy() and when is it called for you automatically? Well, when a window is closed by the user, Destroy() is automatically closed. Also, calling Close() causes Destroy() to be called. Finally, when a window is Destroy()ed, it Destroy()s all of its children.

So normally you don't need to call Destroy(), because your main window will destroy its children when you call Close(), or when the user closes your program. It becomes a chain reaction of closed windows.

You must take a little extra care with heap-allocated (or "dynamically allocated" -- that is, created with new instead of being declared on the stack). Somebody has to delete these windows, after all. But you mustn't go deleting a TWin that still has an attached window. Instead, you should call Destroy() on it first. In fact, that's a good rule of thumb: before you call "delete" on a TWin, call Destroy() on it first. And don't worry about calling Destroy() too much -- like Create(), it has been safeguarded so that you can call it repeatedly without anything bad happening.

[MORE] A special case is when you call TWin::SetAutoDeleteWhenDestroyed(true). This tells the TWin to automatically call delete on itself when it gets destroyed, either by you calling Destroy() on it, or by the user closing it, or by somebody else destroying it. This is useful when you want to create a utility or informational window and then let it do its thing without supervision. When the user closes the window, it will clean itself up! Just be sure never to call SetAutoDeleteWhenDestroyed() on a TWin that was created on the stack (instead of being created with new) -- the TWin will still call delete on itself, even though it wasn't new'ed, and your program will crash. We'll go over a typical use of SetAutoDeleteWhenDestroyed() later in this section.


What if I wanted to make a new button on the fly?

By this I assume you mean to dynamically allocate a TBtn via the new operator. (We use a button just because it's convenient in the example code... more likely you would want to make an entire modeless dialog on the fly, not just a lousy button.)

But before we go further, let's think about why you'd want to use this technique. Most of the time, you don't need to dynamically allocate windows. In the example below, it's overkill -- you could just create the TBtn in the class definition of the base window, and use Hide() and Show() to make it seem to appear and disappear. That is what I recommend you do, whenever possible. You could also use Hide() and Show()on dialogs. Or Create() and Destroy(), if that's more convenient, as it usually is. To repeat: you don't need to dynamically allocate any window to use it in the common way.

So when do you want to dynamically allocate a window? When you don't know how many windows you'll need beforehand. If, for instance, you need to create a window for each document the user has open, you can't pre-declare them -- you don't know how many you will need.

So anyway, on with the example.

You will have to use "new" to dynamically allocate your button, and "delete" to clean it up. Also, you will need to call its Create() and Destroy() functions yourself.

So, assuming that the whole Create() and Destroy() thing is clear to you after reading the previous topic, and you know something about menus, we can just jump right into the code:

 
#include <win.h>
#include <btn.h>


class TMyWin : public TWin
{
  TBtn *pMyButton;
  public:
  TMyWin(TWin *PParent, const TWinAttr &Changes)
    : TWin(PParent, Changes)
    {
      pMyButton = NULL;
    }

  virtual void OnDone()
    {
      if (pMyButton != NULL)
      {
        pMyButton->Destroy();
        delete pMyButton;
        pMyButton = NULL; //this is not actually needed, it's "antibugging"
      }
    }

  virtual bool OnCmdExecute(UINT ID)
    {
      if (ID == ID_MAKE_ME_A_BUTTON)
      {
        if (pMyButton == NULL)
        {
          pMyButton = new TBtn(this,
                          TWinAttr().X(50).Y(50).W(50).H(100).Text("Hi"));
          pMyButton->Create();
        }
        return true;
      }
      else
        return false;
    }
};

Note that I keep a pointer to the button as a member variable of the class. See the previous discussion on why I can't just create a temporary TBtn and let that TBtn go out of scope when OnCmdExecute() ends (the button would also go away when the function ended!).

Next let's look at it in a more complete context.


What if I wanted to make a new button each time the user chose a menu item?

There are two ways to do this. First, the "obvious" way: you have an array of pointers to these TBtns, and you add entries to the array as need be. We will use a TDynamicArray of TBtn *'s. A TDynamicArray is just like a regular array, except that the number of entries in the array can be grown or shrunk at run-time. It is similar to the "matrix" template in STL, and if you are comfortable with STL, you can just use that instead. But I will use the TDynamicArray because it is used internally by EWin all the time, and I am more comfortable with it. I think it is marginally simpler to use. The TDynamicArray class, like all EWin templates, is in "template.h". So here we go:

 
class TMyWin : public TWin
{
  public:
  TDynamicArray< TBtn * > Btns;
  TMyWin(TWin *PParent, const TWinAttr &Changes)
    : TWin(PParent, Changes)
    {
    }

  virtual void OnDone()
    {
      for (int loop = 0; loop < Btns.Count(); loop++)
      {
        Btns[ loop ]->Destroy();
        delete Btns[ loop ];
      }
      Btns.Clear(); //remove all entries from the array
    }

  virtual bool OnCmdExecute(UINT ID)
    {
      if (ID == ID_MAKE_ME_A_BUTTON)
      {
        int temp = Btns.Count() * 5;
        TBtn *p = new TBtn(this,
                      TWinAttr().X(temp).Y(temp).W(50).H(100).Text("Hi"));
        p->Create();
        Btns.Add(p);
        return true;
      }
      else
        return false;
    }
};

This code will make a new button each time you choose the "make me a button" menu choice. It will allocate a TBtn and store it in the TDynamicArray so that you can delete it later.

Initially, I have an array of zero TBtn pointers. To add items to my array, I call TDynamicArray::Add(). To remove items, I would call Remove(), or to remove them all at once, RemoveAll() or Clear(). To find out how many things are in the array, use Count(), and to get or set one of the items in the array, just use the [] syntax like I would for a regular C array. If I wanted an array of ints:

 
TDynamicArray<int> MyArray;
for (int loop = 0; loop < 100; loop++)
  MyArray.Add(loop);
MyArray[ 0 ] = 88;

Now the 0th entry in MyArray is 88; entry 66 in the array is 66; entry 99 is 99. Etc.

So there you go. But it may seem silly to you to keep all those pointers around just so that you can delete them. Can't somebody do that for you? Well, when the owning window (the main window) dies, it will automatically call Destroy() on these windows, since they are children of the main window. But it can't delete the buttons, since it doesn't know they were dynamically allocated. So you have to call delete on them yourself, and since you can't call delete without first calling Destroy(), you have to call Destroy() and then delete it.

But there is another trick you can use. You can tell TWins to "automatically delete myself when Destroy() is called." To do this, you call SetAutoDeleteWhenDestroyed( true ):

 
[in OnCmdExecute:]
p->SetAutoDeleteWhenDestroyed( true );

Then the object will clean itself up, and you don't need to do any clean up in the main window's OnDone(). And since you no longer use the dynamic array for anything, you could remove it entirely.

In complex programs, you usually need to keep a pointer to the objects around for some reason other than just deleting them, so you typically end up needing the dynamic array anyway.

(If you only wanted to have a fixed maximum number of buttons, say ten, then you wouldn't necessarily need a TDynamicArray -- you could instead use a regular array and a counter variable.)

[MORE] TDynamicArray::Remove() removes items in the most efficient manner possible -- it removes the item in question, then, to fill the hole that has been made in the array, it grabs the last item in the array and stuffs it into the hole. This means that if you have an array with [0,1,2,3,4,5] in it, and you delete the "0", you will get [5,1,2,3,4]! I made this optimization because I very often don't care about the order of things in the array. But if you *do* care, then you shouldn't use Remove(). Instead, there is a function called RemoveWithoutMessingUpOrder(). I know this distinction is going to catch some people unaware. I am trying to find a cleaner, more elegant solution to this problem, that doesn't break everybody's existing code. Also, I find the name "RemoveWithoutMessingUpOrder" just as stupid-sounding as you do.


Hey, I just had a thought: what happens if I call Create() on a TWin, then I Destroy() it, and then call Create() again?

The window will be born, die, and be reborn again. This is sometimes a very handy way of implementing things, especially modeless dialogs. Let's walk through a typical use of this idea.

 
class TMyWin : public TWin
{
  public:
  TMyHappyModelessDlg MyHappyDlg;
  TMyWin(TWin *PParent, const TWinAttr &Changes) : TWin(PParent, Changes)
    { }

  virtual bool OnCmdExecute(UINT ID)
    {
      if (ID == MAKE_MY_MODELESS_DLG)
        MyHappyDlg.Create();
      else if (ID == CLOSE_MY_MODELESS_DLG)
        MyHappyDlg.Close();
      return true;
    }
};

You declare a TMyHappyModelessDlg in the class declaration of your main window, and, because it is a dialog, it won't get automatically Create()ed. So, initially it is just an object. Then your user uses a menu item or something and you want that modeless dialog to appear. So you call Create() on it.

[Remember, modeless dialogs are the ones that *don't* wait for the user to finish. In a modAL dialog, the Create() call won't return until the dialog is ended. But sometimes that isn't appropriate. For instance, if you want a tool palette, that would be a modeless dialog, because you wouldn't want the user to have to close the tool palette before they could continue using your program!]

So now that you have called Create(), the modeless dialog's OnInit() will get called, and then the dialog will be there, sitting atop your main window. The user will be able to do other things, choose different menu items, etc., without interacting with the dialog. The dialog will wait patiently for them. But say they choose the "Hide modeless dialog" menu option, and so your code calls MyHappyModelessDlg.Close() to close the window. Or alternatively, suppose the modeless dialog has a close-button. Pushing this button also causes Close() to get called. In both situations, Close() will call Destroy(), and OnDone() will get called, and the dialog window itself will be dead.

But the TMyHappyModelessDlg *won't* be dead. If you stored away useful data in variables, you can still access that data, etc. And if the user should choose the "show this modeless dialog" option again, then lo and behold the dialog will be born again!

Now, there are a few tricks to making this work smoothly. The first is to make sure that your OnInit() and OnDone() functions are going to work right if you call them repeatedly. In the example above, OnInit() and OnDone() could get called an indefinite number of times. Oh, there is another function available to be overloaded if you need it: OnReinit(). This gets called after OnDone() has been called, but before the window has been re-created. You may want to do some preparatory work here. Usually, though, you can do everything you want to in OnInit() and OnDone() by means of carefully thinking through how things will work. Let me continue with the example. Say I have a modeless dialog template named IDD_HAPPY, and the only thing on this dialog is a slider bar, where you are supposed to type in how happy you are, as a number from 1 to 10. The edit box has an ID of, oh, say, ID_HAPPYVALUE.

 
#include <dlg.h>
#include <trackbar.h>

class TMyHappyModelessDlg : public TModelessDlg
{
  protected:
  int Happiness; //this is where we store how happy they are
  TTrackBar Bar;

  public:
  TMyHappyModelessDlg(TWin *PParent)
    : TModelessDlg(PParent, IDD_HAPPY),
      Bar(this, TWinAttr().ID(ID_HAPPYVALUE))
    {
      Happiness = 5; //initialize this to medium-happy
    }

  virtual void OnInit()
    {
      //okay, set up the track bar's range
      Bar.SetRange(1, 10);

      //set the trackbar to the current happiness value. This will be 5
      //the first time the window is created, but the second time the
      //window is created, Happiness will be whatever value the user
      //last typed in.
      Bar.SetPos(Happiness);
    }

  virtual void OnDone()
    {
      //okay, window is dying! Store off the happiness in case the parent
      //window calls GetHappiness() [below] while the window is destroyed.
      Happiness = Bar.GetPos();
    }

  //here is a utility function that the owner of this window can call
  //whenever it needs to know how happy the user is. It can call this
  //function whether the window is Create()d or not -- the function works
  //any time.
  int GetHappiness()
    {
      //if the window is created, then get the most up-to-date happiness
      //from the trackbar. (If the window isn't alive, then we'll just
      //return the last stored happiness value.)
      if (bAlive())
        Happiness = Bar.GetPos();

      return Happiness;
    }
};

So now the modeless dialog can be Create()d and Close()d as many times as needed. [Remember, Close() is the "friendly" version of Destroy().] And at any time, the owner of this modeless dialog can call GetHappiness() to get the most current happiness value.


So when do I call Close() and when do I call Destroy()?

There is a simple answer to this, but first I will bore you with details.

Close() will usually end up causing Destroy() to get called. But first, it checks with the object itself, to see if the window "wants" to die. If the window is okay with dying, then Destroy() is called. Otherwise, the window doesn't die after all.

Close() asks the TWin whether it wants to die or not by calling TWin::QueryEnd(). If QueryEnd() returns true, then the window is okay with dying. So your code can do some checks by overloading this function. You can make sure things are safe for closing. The most common thing to do here is to bring up an "Are you sure you want to close this document?" message box.

So, generally speaking, you should call Close() if the *user* told you to close the window: if you are responding to a user event that directs you to close the window, then call Close(). The window may have a good reason for not wanting to obey the user's instructions. But if you MUST have the window die NOW, such as when the parent window itself is closing, then call Destroy(). There's no point in asking the window -- it has to die now regardless of what it wants.

 

 

 

 (go on to the next page in the tutorial...)