Introduction
The last article I wrote introduced some of the basic concepts of the Visual Component Framework. Much of it
dealt with some of the techniques I used to get things like the advanced RTTI features to work, as well as other
features of the core of the VCF, the Foundation Kit. In this article we'll see that all of the stuff is actually
there for a reason! We will walk through the steps of putting together a simple scribble application, which will
cover things like drawing, events, customizing VCF component classes, and menus. As I know you are all breathless
with anticipation, and in fact due to the millions of letters of fan mail, and the hundreds of thousands of adoring
fans out there (ha,ha), let's dive right in!!!
Where did WinMain() go ?
Like any application, we need a place to begin, so what better place to start than the infamous main() function.
Whoa there pardner - this is Win32 we're talkin' 'bout here ! There ain't no stinkin main! Bill says were all supposed
to use WinMain()
! Wrongo! Since I wanted the VCF to have a cross platform architecture, and in every other OS out
there programs start in a main(), I had to figure out how to make this work. Thankfully, after scouring through
other people's code (is there any other way!), I found an example of doing just this, yet still making windows
think the app is a Win32 app, not a console app. By altering the linker settings we can replace the default entry
point to point somewhere else, in this case our main() function. If you look at the linker settings of any program
you run in VC++ you'll see the subsystem set to "windows", which is important. If it were set to "console"
a console window comes up first instead of a normal window. You can still create windows like normal, but this
is hardly the kind of behavior for a well-behaved Windows application. Since the subsystem is windows, the default
entry point of the application is also different, which is where WinMain
is usually executed. This is where our
little trick comes in. We can set the entry point symbol to read "mainCRTStartup
", thus bypassing the
default behavior, and running our main() function as the entry point.
OK, OK, enough of the technical blather, let's actually run something! The first step in any VCF program is
to create an instance of an VCF::Application
derived object on the stack. If you do not need to override
any functionality then you can simply use VCF::Application
, otherwise use your VCF::Application
derived class. Once this is done, you simply call the VCF::Application
's static method VCF::Application::appMain()
,
passing in the argc
and argv
parameters that your main()
gets, and
voilà!
You're done! VCF will take care of the rest for your in terms of starting your app and calling the correct run
methods. Let's check this out in code:
int main(int argc, char *argv[])
{
VCF::Application app;
VCF::Application::appMain( argc, argv );
return 0;
}
This illustrates simply using the default VCF::Application
object. The next sample uses a derived
class to create the application object.
class ScribbleApplication : public Application {
public:
ScribbleApplication(){};
virtual ~ScribbleApplication(){};
};
int main(int argc, char *argv[])
{
ScribbleApplication app;
VCF::Application::appMain( argc, argv );
return 0;
}
Now we have supplied our own ScribbleApplication
class, as opposed to relying on the default Application
class. So what is actually happening under the hood ? Basically the VCF runtime system is taking care of a bunch
of start up stuff for you. The constructor of the Application
class initializes the FoundationKit,
the GraphicsKit, and finally the ApplicationKit, which in turn initializes things like UIToolkit
(which
is responsible for doling out the windowing specific Peer classes, like window frames, text widgets, etc), and
registers all the core VCF component classes with the FoundationKit's ClassRegistry
. The next thing
it does (and this is fairly important) is to set itself as the current Application
object for this
process, which can be retrieved by the Application's static method Application::getRunningInstance()
,
which will return a pointer to the app object. It is important to note that you should never create
more than one Application or Application-derived object for you process. Finally it creates a special peer, known
as an ApplicationPeer
object, that handles some of the OS-specific tasks related to running an application.
Once the Application object is created we can move to the appMain()
method, which simply verifies
that we in fact do have a valid running app instance, calls an initialization function on the application's peer
object, which in Win32 does stuff like calling InitCommonControlsEx()
and OleInit()
,
sets the command line for the Application (for later use if necessary), and then calls the first of the Application's
commonly overridden methods, initRunningApplication()
.
Init what?
The initRunningApplication()
call allows the developer to initialize whatever application-specific
startup code he or she wants. For those familiar with MFC, it is similar to CWinApp's
InitInstance()
method. It is typically where the main window of the application is created. The function returns true
or false
, true
indicating
it is OK to continue on, and false
to signal an error. This return
code is used in the appMain()
method to determine whether or not to continue, with a return value
of true
indicating it is safe to continue onwards. A return of false
at this point will cause the Application's
terminateRunningApplication()
method to be called, and then the process will clean up and exit. The terminateRunningApplication()
is another method you can override to ensure any of your application specific data gets cleaned up properly. Once
the application has been initialized the call to the Application
's run()
method is made,
which in turn invokes the peer's run()
method where any OS-specific code is then executed. In our
case (under Win32), we find a typical message loop (something like the following):
MSG msg;
HACCEL hAccelTable = NULL;
while ( GetMessage( &msg, NULL, 0, 0 ) ) {
if (!TranslateAccelerator( msg.hwnd, hAccelTable, &msg ) ) {
TranslateMessage( &msg );
DispatchMessage( &msg );
}
}
When the loop ends (the main window is closed) the Application's terminateRunningApplication()
is
called, allowing you to gracefully exit and cleanup your app-specific data. The peer's terminateApp()
method is called, which does OS-specific shutdown chores. Lastly, the UIToolkit
is told to cleanup,
and then we quit the process. As a developer with VCF you never have to touch this stuff, but it helps to at least
be aware of what's going on. The main things to note are the virtual methods for initialization and termination
of your application via overriding the initRunningApplication()
and terminateRunningApplication()
.
Creating our main window
So now that we've seen how to create our application object and start the ball rolling, how do we create actual
windows? And once this is answered, where is the best place to create them? In general, the best place to create
the application's windows are inside of the initRunningApplication()
method that you override in your
Application-derived class. Actually, creating the windows is cake (MFC developers should hold onto something solid
at this point). All you have to do is create a new instance of VCF::Window
(or a class derived from
it) and presto you have a window you can manipulate. You must also set the Application's main window, so a call
to setMainWindow()
will also have to be made at this point as well.
VCF does not use a two step creation process like some other frameworks for the creation of Controls (of which
a Window is derived from). I believe this greatly simplifies coding, though there are some tweaks that had to happen
in the base classes (but this is hidden from the developer, and irrelevant for deriving classes). When creating
any kind of Component instance in the VCF, always, always do so on the heap using the new operator. So for our
main window, we code the following:
Window* mainWindow = new Window();
NOT
Window mainWindow;
The main window belongs to the Application (which is why we call setMainWindow()
), which will destroy
it when the Application is closed. In turn all components and controls on the Window will in turn be destroyed
when the Window is destroyed. This greatly simplifies development since you don't have to worry about who destroys
what or what the special cases are etc. We talk more of this in a bit when we start to cover adding controls.
Once you have created a Window, you can easily set properties on it, like the bounds (via the setBounds()
method), the caption (via the setCaption()
method), and many others. So let's set our caption to read
"VCF Scribble App", and the left, and right to be at 200, 200, respectively, and the width and height
to be 500, 500, respectively.
mainWindow->setCaption( "VCF Scribble App" );
mainWindow->setBounds( &Rect(200,200,700,700) );
setCaption()
takes a VCF::String
as an argument, where a VCF::String
is nothing more than a typedef around std::basic_string<VCFChar>
. Eventually I would like to
write an actual class that has the same STL interface as std::basic_string
, but can also handle Unicode,
so that all the internals used only Unicode and OS calls were in Unicode as well were possible, but for now this
works pretty well (those interested in developing something like this, your help would be greatly appreciated!!).
Setting the bounds requires a pointer to a Rect
class, which we provide via a temporary stack object.
The Rect
has members for left, top, right, and bottom, so for a rectangle at 200, 200, and a width
and height of 500, 500, we pass in 200, 200, 700, 700, these being the Rect
constructor's arguments
for the left, top, right, and bottom, respectively. Rect
classes store their data internally as doubles
for better accuracy and not having to convert back and forth so much, allowing for fewer rounding errors (hopefully).
In fact if you look at the VCF Application Kit classes, and Graphics Kit classes, all coordinates are handled as
doubles. I have found that many windowing systems (with the exception of Win32) do this and decided to include
this in the VCF.
Customizing the main window
Well, now we can run our app and display a window, but that in and of itself isn't horribly useful, so let's
move on and customize things a bit. We'll derive a new class from VCF::Window
, and add some more controls,
as well as other features as the article progresses. In this first pass, we are going to add two panels to our
window when it is created, one which will be aligned to the right, and another which is aligned to the remaining
client area. So lets see what the code looks like and then we'll cover how it works.
class ScribbleWindow : public Window {
public:
ScribbleWindow(){
panel1 = new Panel();
panel1->setBounds( &Rect(0,0,100,200) );
this->add( panel1, ALIGN_RIGHT );
panel2 = new Panel();
this->add( panel2, ALIGN_CLIENT );
};
virtual ~ScribbleWindow() {};
VCF::Panel* panel1;
VCF::Panel* panel2;
};
As you can see, we have two instances of the VCF::Panel
class, panel1 and panel2, and we create
them using the new
operator. A VCF::Panel
is a simple
control that implement's the VCF::Container
interface class, thus allowing it hold other child controls.
A panel also has a default border that draws a 3D edges on the outsides of the control. In our case we are
going add panel1 to the window and align it to the right, while we will add panel2 and align it to
the remaining client area. Once we have created panel1 we set its bounds, with its width becoming 100. This
is done because when aligned to the right or the left, the control will still keep its original width, but only
change its height. The next step is to add the control, and this is done via the window's add()
method,
where we pass in the control and an enum that specifies the alignment we would like (our choices are ALIGN_LEFT
,
ALIGN_RIGHT
, ALIGN_TOP
, ALIGN_BOTTOM
, ALIGN_CLIENT
, and ALIGN_NONE
for fixed coordinates within the parent control). Like panel1, we create panel2 the same way, and
then simply use the add
method again, this time passing in panel2, and an alignment of ALIGN_CLIENT
.
Note we don't have to bother setting the bounds for panel2; since it is being added with an alignment of
ALIGN_CLIENT
, the parent container (in this case the window) will handle realigning and repositioning
the control. At this point, we now have a window with two child controls, both of which are aligned and will be
automatically resized for us as we resize the parent window. In addition, that pesky old problem of flickering
windows is gone!! Resize to your hearts content and you'll see nary a flicker! This is because VCF will automatically
double buffer for you during repaint messages. However, you can turn this off by simply setting calling the control's
setDoubleBuffer()
method and passing in false
(passing
in true will turn double buffering on again).
Adding controls
Adding controls to other controls is a very useful and handy thing to have, so let's go into a bit more detail
as to how this actually works. A control that can hold other controls is referred to as a container
in the VCF. Any control can be a container so long as it properly implements the methods of the VCF::Container
interface class. To aid in this, there is a class that implements these methods called VCF::AbstractContainer
that you can inherit from to add this functionality to your classes. If we were to look at the declaration of the
VCF::Panel
class we can see it does just this. The main methods of the container you'll use is the
add()
methods and the remove()
method. The add()
methods come in two flavors,
the first being to just add the control, and the container will simply use whatever alignment is specified in the
control's alignment property, and the second takes a control and an alignment type to specify how to add the control
to the container. When you add a control you are also specifying the newly added control's parent, which happens
automatically in the add()
method. You should never set the control's parent directly yourself, as
this can lead to inconsistent behavior. Adding a control to a parent control also means the parent control will
delete its child when the parent is deleted, so you don't have to worry about freeing the memory associated with
the control.
When adding a control you can specify the alignment type, which determines how the container will resize the
child control. Aligning to the left (ALIGN_LEFT
) will resize the control so that its left side will
always be flush to either the extreme left of the client edge of the container control, or the right edge of the
previously added control with left alignment. The control will maintain its width, but the top, left and bottom
will be determined by the container. Similar behavior happens for right, top, or bottom alignment (ALIGN_RIGHT
,
ALIGN_TOP
, ALIGN_BOTTOM
respectively). If the alignment is ALIGN_CLIENT
,
then the control is resized to any remaining client space after the other aligned controls have been positioned.
If the control does not want any alignment rules enforced then its alignment should be set to ALIGN_NONE
,
which is the default alignment for all of the basic controls. ALIGN_NONE
means the coordinates specified
are always respected and are not change during resizing of the container.
Custom controls: the ScribbleView control
So this is all well and good, but lets make this a even more interesting. We now have two panels, one which
will house some buttons, and another that will house a custom control we'll create that will serve as our scribble
drawing surface. Whenever you want to create a custom control in the VCF you have several options on how to proceed.
Your first issue is whether or not you want to start from scratch, or just enhance an already existing control,
such as added beveled text effects to the Label control. If you want to start from scratch, often the best place
to derive from is the VCF::CustomControl
class. The next issue you'll face is whether or not you want
the control to be "light weight" or "heavy weight". A heavy weight control is one that uses
native windowing system resources for displaying and routing events, in other words, under Win32, a control that
has a HWND and an HDC associated with it. A good example of this is a Window, or a Panel control, both of which
are considered heavy weight controls. A light weight control, on the other hand, shares these resources with it's
heavy weight parent (somewhere along the line), thus reducing the number of native resources in use. An example
of this would be the VCF::Label
control. This is extremely useful to have, especially when you are
building controls that are components of another, more complex control (like a header, or toolbar buttons). Those
of you who have used Java will recognize this in the Native heavy weight peer classes, while those from a Delphi
background will see a similarity with the TWndControl
and TGraphicControl
classes. For
those of you from an MFC background there is no comparable class within MFC (sadly), everything ends up being a
window, unless you go to the trouble of writing something similar to light weight controls yourself.
For our purposes we are going to build a lightweight custom control, so we are going to derive it from VCF::CustomControl
.
In order to actually make the control either heavy weight or light weight, we pass a boolean flag into the constructor
of the VCF::CustomControl
, with true
indicating that
the control is a heavyweight control, and false
for light weight
controls (the default value is true
). Lets look at some code:
class ScribbleView: public VCF::CustomControl {
public:
ScribbleView():
VCF::CustomControl( false )
{
}
virtual ~ScribbleView(){}
};
The next thing we are going to want to do is to customize our painting of the control. To do this, we just override
the paint()
method, call the super class if necessary, and then do our own stuff after that. When
implementing a paint()
method, you are passed in a pointer to a GraphicsContext
object,
which is how all drawing is handled. The GraphicsContext
contains all the drawing state info, as well
as a variety of methods for drawing 2D primitives. Before we go much further, let's take a look at the code and
then I'll explain more.
class ScribbleView: public VCF::CustomControl {
public:
ScribbleView():
VCF::CustomControl( false )
{
}
virtual ~ScribbleView(){}
virtual void paint( GraphicsContext* ctx ) {
CustomControl::paint(ctx);
Color color(0.85f,0.85f,0.85f);
ctx->setColor( &color );
Rect r( 5, 5, getWidth() - 5, getHeight()-5 );
ctx->rectangle( &r );
ctx->fillPath();
}
};
The first line of the paint()
method calls the super classes paint()
method first,
then a Color
object is created, and set to a light gray color. The GraphicsContext's
color is then set with a call to the setColor()
method. Doing this causes the GraphicsContext
to use the specified color for all paths that are stroked or filled after this. After this we draw a rectangle
by calling the GraphicsContext's
rectangle()
method, passing a pointer to a Rect
object, and then to actually render the rectangle, we call fillPath()
, which fills any path operations
with the GraphicsContext's
current color.
The GraphicsContext
works by specifying a series of drawing operations à la PostScript,
like moveTo, lineTo, rectangle, ellipse, etc, and then you can either fill or stroke the path(s) generated by the
previous functions. After filling or stroking the path(s), the path operations are deleted, and you continue on.
In addition to these lower level 2D primitive calls, there are a series of high level calls that can be used to
draw more complex geometry, as well as take advantage of the GraphicsContext's
transform matrix for
things like scaling, translation, rotation, and shearing (perhaps this will be covered in another article).
Well now that wasn't too bad, so lets actually add our control to our ScribbleWindow
so we can
see it.
class ScribbleWindow : public Window {
public:
ScribbleWindow(){
panel1 = new Panel();
panel1->setBounds( &Rect(0,0,100,200) );
this->add( panel1, ALIGN_RIGHT );
panel2 = new Panel();
this->add( panel2, ALIGN_CLIENT );
scribView = new ScribbleView();
panel2->add( scribView, ALIGN_CLIENT );
};
virtual ~ScribbleWindow() {};
VCF::Panel* panel1;
VCF::Panel* panel2;
ScribbleView* scribView;
};
Mouse Events & ScribbleView
So now our control displays, but beyond that it doesn't do much. So, since we want to be able draw in it, lets
hook up our event handling for mouse events. A control has three methods which can be overridden for this purpose:
mouseDown()
, mouseMove()
, and mouseUp()
, each of which get passed a pointer
to a MouseEvent
object. The MouseEvent object has information like what mouse button is pressed, as
well as being able to determine if the Alt, Control, or Shift key is currently pressed. By overriding these methods
on our control we can customize the behavior of the control, namely to draw lines whenever the left mouse button
is held down and the mouse is dragged. So lets see this in action!
class ScribbleView: public VCF::CustomControl {
public:
virtual void mouseDown( MouseEvent* event ) {
CustomControl::mouseDown( event );
dragPt = *event->getPoint();
GraphicsContext* ctx = this->getContext();
ctx->moveTo( dragPt.m_x, dragPt.m_y );
}
virtual void mouseMove( MouseEvent* event ) {
CustomControl::mouseMove( event );
if ( event->hasLeftButton() ) {
Point* pt = event->getPoint();
GraphicsContext* ctx = this->getContext();
ctx->moveTo( dragPt.m_x, dragPt.m_y );
dragPt = *pt;
ctx->lineTo( dragPt.m_x, dragPt.m_y );
ctx->setColor( Color::getColor( "red" ) );
ctx->strokePath();
}
}
private:
Point dragPt;
};
The two methods we're going to override are mouseDown()
and mouseMove()
. In both cases
we make sure to call the superclass's methods first, and then do our own stuff. In the mouseDown()
method we get a pointer to a MouseEvent
object which has the current mouse coordinates, retrieved
by a call to the event's getPoint()
method, and store this in a member variable of the class. In the
mouseMove()
method, we verify that the left mouse button is being held down, through a quick call
to the MouseEvent's
hasLeftButton()
method which will return true
if the left button is down, gets the current point, and then draws a red line on the control's GraphicsContext
.
Adding event handlers
Now that we've got our basic ScribbleView
class done, we need to hook up some event handlers to
make our app a bit more useful. What we're going to do is to add two command buttons to the right aligned panel,
one that will be disabled until something is drawn on the ScribbleView
, in which case it will then
be enabled and if the user clicks on it, it will clear the ScribbleView
. The second will exit the
app if clicked. To make things look pretty, we'll also add a Label to the top the right-aligned panel, and set
it's caption to read "Commands". So, without further ado, let's see some code!
class ScribbleWindow : public Window {
public:
ScribbleWindow(){
panel1 = new Panel();
panel1->setBounds( &Rect(0,0,100,200) );
this->add( panel1, ALIGN_RIGHT );
panel2 = new Panel();
this->add( panel2, ALIGN_CLIENT );
scribView = new ScribbleView();
panel2 ->add( scribView, ALIGN_CLIENT );
label1 = new Label();
label1->setCaption( "Commands" );
label1->setBounds( &Rect(10, 10, 80, 35) );
panel1->add( label1, ALIGN_TOP );
btn1->setBounds( &Rect(10, 50, 80, 75) );
panel1->add( btn1 );
btn1->setCaption( "Clear" );
btn1->setEnabled( false );
btn2 = new CommandButton();
btn2->setBounds( &Rect(10, 90, 80, 115) );
panel1->add( btn2 );
btn2->setCaption( "Exit" );
};
virtual ~ScribbleWindow() {};
VCF::Panel* panel1;
VCF::Panel* panel2;
ScribbleView* scribView;
};
As you can see this part is fairly similar to what we've seen previously in other parts of the code we've been
writing. The label, and two buttons (btn1 and btn2) are all created on the heap, and then added to
their appropriate parent control. Now we'll add the event handling by first setting up our functions in our ScribbleWindow
and implementing the functionality we would like to have happen when the functions are called.
class ScribbleWindow : public Window {
public:
ScribbleWindow(){
panel1 = new Panel();
panel1->setBounds( &Rect(0,0,100,200) );
this->add( panel1, ALIGN_RIGHT );
panel2 = new Panel();
this->add( panel2, ALIGN_CLIENT );
scribView = new ScribbleView();
panel2 ->add( scribView, ALIGN_CLIENT );
label1 = new Label();
label1->setCaption( "Commands" );
label1->setBounds( &Rect(10, 10, 80, 35) );
panel1->add( label1, ALIGN_TOP );
btn1->setBounds( &Rect(10, 50, 80, 75) );
panel1->add( btn1 );
btn1->setCaption( "Clear" );
btn1->setEnabled( false );
btn2 = new CommandButton();
btn2->setBounds( &Rect(10, 90, 80, 115) );
panel1->add( btn2 );
btn2->setCaption( "Exit" );
};
virtual ~ScribbleWindow() {};
void onScribbleViewMouseUp( MouseEvent* e ) {
btn1->setEnabled( true );
}
void onBtn2Clicked( ButtonEvent* e ) {
this->close();
}
void onBtn1Clicked( ButtonEvent* e ) {
btn1->setEnabled( false );
scribView->repaint();
}
VCF::Panel* panel1;
VCF::Panel* panel2;
ScribbleView* scribView;
};
We'll look at the functions one at a time. The first function, onScribbleViewMouseUp()
, will be
called when the mouse button is released on the ScribbleView
control. When this happens, the enabled
state of btn1 is set to true
, meaning the button is enabled
(not grayed out) and can accept user input. The next function, onBtn2Clicked()
, is called whenever
btn2 is clicked by the user (or the click()
method is called programmatically), and causes
the window to close, which, since it is set as the Application's main window, will also cause the application to
quit cleanly as well. The final function, onBtn1Clicked()
, is called whenever btn1 is clicked,
and it sets btn1's enabled state to false
, which grays the button
out (or disables it), and calls the ScribbleView
's repaint()
method (a method inherited
from VCF::Control
), which in turn causes the control to clear itself, erasing the contents in the
process.
Now that we've seen what our callback functions do, how do we actually hook them up to the events that the objects
we're interested in fire off? In the VCF this is done through a similar system to Java's Listener
class, which itself is an implementation of the Observer pattern. A Listener, in the VCF, is a C++ interface class
that defines one or more methods that get fired when an event happens. So, for example, the ButtonListener
interface has a method called onButtonClicked()
which will get called when the object we are listening
to (in this case a button) fires off an appropriate event (in the case of the button, this happens when the clicked()
method is invoked. To listen to a particular object we call the appropriate add listener method on the object we
would like to listen to. If we wanted to listen to the button click events on a VCF::CommandButton
object, we would call the button's addButtonListener()
method, and pass in an object that implemented
the ButtonListener
C++ interface. To facilitate this, there are special classes, that are usually
defined in the same header as the listener interface, called handlers, as in the ButtonHandler
class
that implements the ButtonListener
interface. A handler class has a series of variables that are member
function pointers, one for each method that is implemented for the listener interface. In addition, the handler
also has a pointer to an object that is the handler's source, and where the member function pointers point to.
This sounds more complicated than it actually is so lets see some code to hopefully try and make things a bit clearer.
class ScribbleWindow : public Window {
public:
ScribbleWindow(){
ButtonHandler* bh = new ButtonHandler( this );
bh->m_buttonClicked = (OnButtonEvent)ScribbleWindow::onBtn1Clicked;
this->addEventHandler( "ButtonHandler", bh );
btn1->addButtonListener( bh );
bh = new ButtonHandler( this );
bh->m_buttonClicked = (OnButtonEvent)ScribbleWindow::onBtn2Clicked;
this->addEventHandler( "ButtonHandler2", bh );
btn2->addButtonListener( bh );
MouseHandler* mh = new MouseHandler( this );
this->addEventHandler( "MouseHandler", mh );
mh->m_mouseUp = (OnMouseEvent)ScribbleWindow::onScribbleViewMouseUp;
view->addMouseListener( mh );
};
virtual ~ScribbleWindow() {};
void onScribbleViewMouseUp( MouseEvent* e ) {
btn1->setEnabled( true );
}
void onBtn2Clicked( ButtonEvent* e ) {
this->close();
}
void onBtn1Clicked( ButtonEvent* e ) {
btn1->setEnabled( false );
scribView->repaint();
}
VCF::Panel* panel1;
VCF::Panel* panel2;
ScribbleView* scribView;
};
We are going to set up the event handlers for the buttons first, first for btn1, and then for btn2.
We start by creating a new ButtonHandler
object on the heap, using the new
operator, passing in the source object to the constructor, in this case the ScribbleWindow
instance.
The ButtonHandler
's m_buttonClicked
member variable is set to point to the ScribbleWindow's
onBtn1Clicked()
method, and we then add the handler to the ScribbleWindow's
list of event
handlers, which will clean up all the event handler's in it's list for us when it is destroyed. The final step
is to actually add the ButtonHandler
object as a listener to the button (btn1), which is accomplished
by calling btn1's addButtonListener()
method and passing in the button handler object. At this
point we are hooked up and ready to receive events from btn1! The same type of thing is done for the other
two event handlers. You may be asking, "But why not just implement the ButtonHandler
interface
on the ScribbleWindow
class?" Well this works OK if you know ahead of time you will only ever
be listening to one object, but what about when you want to listen to multiple object's that use the same listener
interface? You end up with a nasty series of if statements testing the type of object, doing one thing for this
type of object, another thing for another type of object, and so on. This makes it a little cleaner (in my opinion),
and hopefully scales better, as well as being more direct, so you can "see" that function such and such
will get called by object such and such, for this event.
Fun with Scribble Menus
The last thing we'll discuss in this article is adding menu items to the ScribbleWindow
. There
are two main types of menus: Menus that exist on the Window's frame (usually on the top of the window), and popup
menus that usually are context dependent. The first type of menu is known as a VCF::MenuBar
class,
while the other kind is the VCF::PopupMenu
. Our example will contain both types, a main menu on our
ScribbleWindow,
and a popup menu associated with our ScribbleView
control. Both VCF::MenuBar
and VCF::PopupMenu
have a root menu item that you use to attach other menu items to. The simplest
way to attach menu items is to create a new instance of a DefaultMenuItem
, passing in the string caption
of the menu item, the parent of the menu item, and the menu bar or popup menu for the item, into the constructor
of the new menu item. This will automatically add the newly created menu item to the parent item passed in. Adding
event handlers to the menu items is accomplished just like before, except we use the MenuItemHandler. Lets look
at our last bit of code for the article to see this in action.
class ScribbleWindow : public Window {
public:
ScribbleWindow(){
this->setMenuBar( new MenuBar() );
MenuBar* menuBar = this->getMenuBar();
MenuItem* item = menuBar->getRootMenuItem();
DefaultMenuItem* file = new DefaultMenuItem( "&File", item, menuBar );
DefaultMenuItem* fileExit = new DefaultMenuItem( "&Exit", file, menuBar );
DefaultMenuItem* view = new DefaultMenuItem( "&View", item, menuBar );
viewClear= new DefaultMenuItem( "&Clear", view, menuBar );
MenuItemHandler* menuHandler = new MenuItemHandler( this );
menuHandler->m_menuItemClicked = (OnMenuItemEvent)ScribbleWindow::onFileExitClicked;
fileExit->addMenuItemListener( menuHandler );
this->addEventHandler( "FileExit", menuHandler );
MenuItemHandler* viewClearMenuHandler = new MenuItemHandler( this );
viewClearMenuHandler->m_menuItemClicked = (OnMenuItemEvent)ScribbleWindow::onViewClearClicked;
viewClear->addMenuItemListener( viewClearMenuHandler );
this->addEventHandler( "viewClear", viewClearMenuHandler );
PopupMenu* popup = new PopupMenu( this );
DefaultMenuItem* popupRoot = new DefaultMenuItem( "root", NULL, popup );
DefaultMenuItem* popupEditClear = new DefaultMenuItem( "&Clear", popupRoot, popup );
popupEditClear->addMenuItemListener( viewClearMenuHandler );
popup->setRootMenuItem( popupRoot );
scribbleView->setPopupMenu( popup );
};
virtual ~ScribbleWindow() {};
void onFileExitClicked( MenuItemEvent* e ) {
this->close();
}
void onViewClearClicked( MenuItemEvent* e ) {
btn1->setEnabled( false );
viewClear->setEnabled( false );
scribbleView->repaint();
}
VCF::Panel* panel1;
VCF::Panel* panel2;
ScribbleView* scribView;
DefaultMenuItem* viewClear;
};
To create our main menu we create a new VCF::MenuBar
, and then set the menu bar of our main window.
Once this is done we can start to add menu items to the root as described previously. We create a "File"
and "Exit" menu items, as well as a "View" and "Clear" items. As with the command
buttons, we create an event handler, this time in the form of a VCF::MenuItemHandler
, and assign it's
m_menuItemClicked
to point to the ScribbleWindow's
onViewClearClicked()
method. The popup menu is created in the same way, and it shares a handler with the viewClear menu item, since
it only displays a single command "Clear".
Whew!...That's all folks
So we have our Scribble app, which weighs in at around 200 lines of code, has auto alignment of all it's child
windows, double buffering, allows you do draw/scribble on the left hand side, to clear the screen, exit the app,
and also updates the state of the clear button when ever the drawing's state changes. This also includes event
handling for the various objects we want to listen and respond to when they fire off events. Hopefully I have done
a reasonable job of explaining how all this works and presenting an alternative C++ architecture for developing
Win32 applications.