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