Date: 19 March 1997
Author: Christopher Kohlhoff
The architecture of the Windows interface is based heavily on the concept of an event. An event is sent to an application (or window) when the user performs an action (such as clicking the mouse or selecting a menu item), and in response to system actions.
In programs using the raw Windows API (that is, not using a framework such as OWL) event handling usually involves enormous switch/case statements. Fortunately OWL provides a much simpler mechanism: the response table.
In order to see event handling in action, we will add a menu to the application. Menus are one of the most common ways of adding interaction to an application. To add a menu, do the following:
IDM_MAINMENU MENU
{
POPUP "&File"
{
MENUITEM "&New", CM_FILENEW
MENUITEM "&Open...", CM_FILEOPEN
MENUITEM "&Save", CM_FILESAVE
MENUITEM "Save &As...", CM_FILESAVEAS
MENUITEM SEPARATOR
MENUITEM "E&xit", CM_EXIT
}
POPUP "&Shape"
{
MENUITEM "&Line", CM_SHAPELINE
MENUITEM "&Rectangle", CM_SHAPERECTANGLE
MENUITEM "&Ellipse", CM_SHAPEELLIPSE
MENUITEM SEPARATOR
MENUITEM "&Clear\tCtrl+Del", CM_SHAPECLEAR
}
POPUP "&Help"
{
MENUITEM "&About", CM_HELPABOUT
}
}
IDM_MAINMENU ACCELERATORS
{
VK_DELETE, CM_SHAPECLEAR, VIRTKEY, CONTROL
}
Note that you can create these resources visually using a resource editor such as Resource Workshop.
void TTutorialApp::InitMainWindow()
{
TTutorialClientWindow* client = new TTutorialClientWindow(0);
TFrameWindow* frame = new TFrameWindow(0, GetName(), client);
// Set the menu for the main window.
//
frame->AssignMenu(IDM_MAINMENU);
// Associate a menu accelerator (shortcut keys)
// table with the main window.
//
frame->Attr.AccelTable = IDM_MAINMENU;
SetMainWindow(frame);
}
Now that we have added a menu to the application, we want to handle events, such as responding to the user selecting a menu item. To handle these events we must add response tables to the window and application.
We will add a response table to TTutorialClientWindow so that we can respond when the user clicks and moves the mouse:
class TTutorialClientWindow : public TWindow
{
public:
TTutorialClientWindow(TWindow* parent, TModule* module = 0);
public:
enum TShapeType {
Line = CM_SHAPELINE,
Rectangle = CM_SHAPERECTANGLE,
Ellipse = CM_SHAPEELLIPSE
};
static TShapeType ShapeType;
protected:
void EvLButtonDown(uint modKeys, TPoint& point);
void EvLButtonUp(uint modKeys, TPoint& point);
void EvMouseMove(uint modKeys, TPoint& point);
void CmShapeClear();
DECLARE_RESPONSE_TABLE(TTutorialClientWindow);
};
// Define our window response table.
//
// Note how there is a '1' at the end of macro name. This
// indicates that the class TTutorialClientWindow has one
// base class that also has a response table.
//
// The first parameter to the DEFINE_RESPONSE_TABLEx macro
// is the name of the class that owns the response table.
//
// Subsequent arguments are immediate base classes that
// also have a response table (and should therefore have
// events forwarded to them).
//
DEFINE_RESPONSE_TABLE1(TTutorialClientWindow, TWindow)
// Add a handler for the left mouse-button down event.
// There must be a function called EvLButtonDown to
// handle the message.
//
EV_WM_LBUTTONDOWN,
// Add a handler for the left mouse-button up event.
// There must be a function called EvLButtonUp to
// handle the message.
//
EV_WM_LBUTTONUP,
// Add a handler for the mouse move event. There
// must be a function called EvLButtonDown to handle
// the message.
//
EV_WM_MOUSEMOVE,
// Associate the menu item Shape|Clear with the
// function CmShapeClear. The first parameter is
// the id of the menu item (see the menu resource).
// The second parameter is the corresponding member
// function in the TTutorialClientWindow class.
//
EV_COMMAND(CM_SHAPECLEAR, CmShapeClear),
END_RESPONSE_TABLE;
For the events in the response table called EV_WM_LBUTTONDOWN, EV_WM_LBUTTONUP and EV_WM_MOUSEMOVE the member functions must be called EvLButtonDown, EvLButtonUp and EvMouseMove respectively. The first parameter in each indicates if the user was holding a key down (such as Shift or Control) when the mouse button was pressed. The second parameter is the coordinates of the mouse click.
void TTutorialClientWindow::EvLButtonDown(uint /*modKeys*/, TPoint& /*point*/)
{
}
void TTutorialClientWindow::EvLButtonUp(uint /*modKeys*/, TPoint& /*point*/)
{
}
void TTutorialClientWindow::EvMouseMove(uint /*modKeys*/, TPoint& /*point*/)
{
}
For the event which responds to the Shape|Clear menu item, we define a member function called CmShapeClear (the name matches the second parameter to the EV_COMMAND macro):
void TTutorialClientWindow::CmShapeClear()
{
MessageBox("Clear Shapes");
}
We will now modify the TTutorialApp class to display a message box when the user selects Help|About. We will also make it so that only one shape (Line, Rectangle or Ellipse) can be selected, and that selection will be indicated by a check against the menu item.
class TTutorialApp : public TApplication
{
// ...
protected:
void CmHelpAbout();
void CmShapeSelect(uint id);
void CeShapeSelect(TCommandEnabler& tce);
DECLARE_RESPONSE_TABLE(TTutorialApp);
};
// Define our application response table.
//
DEFINE_RESPONSE_TABLE1(TTutorialApp, TApplication)
// Associate the Help|About menu command
// with the function CmHelpAbout.
//
EV_COMMAND(CM_HELPABOUT, CmHelpAbout),
// Add handlers so that one and only one of
// Shape|Line, Shape|Rectangle or Shape|Ellipse
// will be selected at any given time.
//
EV_COMMAND_AND_ID(CM_SHAPELINE, CmShapeSelect),
EV_COMMAND_AND_ID(CM_SHAPERECTANGLE, CmShapeSelect),
EV_COMMAND_AND_ID(CM_SHAPEELLIPSE, CmShapeSelect),
EV_COMMAND_ENABLE(CM_SHAPELINE, CeShapeSelect),
EV_COMMAND_ENABLE(CM_SHAPERECTANGLE, CeShapeSelect),
EV_COMMAND_ENABLE(CM_SHAPEELLIPSE, CeShapeSelect),
END_RESPONSE_TABLE;
void TTutorialApp::CmHelpAbout()
{
MessageBox(0, "About", "Help", MB_OK);
}
void TTutorialApp::CmShapeSelect(uint id)
{
TTutorialClientWindow::ShapeType = (TTutorialClientWindow::TShapeType) id;
}
void TTutorialApp::CeShapeSelect(TCommandEnabler& tce)
{
// Set the check of the menu item depending on whether
// or not it was the item most recently selected.
//
tce.SetCheck(TTutorialClientWindow::ShapeType == tce.GetId() ?
TCommandEnabler::Checked :
TCommandEnabler::Unchecked);
}
When you compile and run this application, you will note that all of the menu items are greyed out except for File|Exit and Help|About. What these both have in common is that there are functions that resond to them (File|Exit is handled by the TApplication class). But how does OWL know to grey the other items out? And when is the CeShapeSelect function called?
OWL uses a process called Command Enabling. Basically it works by performing the following steps for each menu item when a menu is about to appear, and when the application is doing nothing. Note that this explanation is much simpler than what actually happens.
The need to be able to call a base class's response table is why the
DEFINE_RESPONSE_TABLEx macro can take more than one parameter. If we
were to change the line
DEFINE_RESPONSE_TABLE1(TTutorialApp, TApplication)
to be
DEFINE_RESPONSE_TABLE(TTutorialApp)
the menu item File|Exit would no longer be enabled (try it!).
The command enabling sequence above has the additional feature that if
we add our own CmExit to TTutorialApp and add the line
EV_COMMAND(CM_EXIT, CmExit),
to the response table, then TTutorialApp::CmExit would be called, but
TApplication::CmExit would not. This allows us to override the event-handling
functionality of base classes.