Using a CallHandler and Caller Object to Simplify Function Calling Between Anonymous Objects

Published March 26, 2003 by John Hattan, posted by Myopic Rhino
Do you see issues with this article? Let us know.
Advertisement
Graphical User Interface-bits (controls, widgets, whatever) are a natural for object-oriented programming. Dialog boxes are containers of controls. Controls can have their draw or mouse-click behaviors overridden for various custom behaviors. If there's something in the world that seems like a natural for object-orientation, it's GUI controls.

A problem, though, comes when you have a specialized behavior that is too trivial to deserve its own object. Suppose for example that you are building a scrolling listbox object. You want a couple of buttons in the corner of the box that will scroll the contents. If you build a straight set of GUI objects that require special behaviors be taken care of via inheritance, you'll need to derive some kind of "listbox-scrollbutton" class that has the ability to instruct the listbox to scroll its contents. While it is an object-oriented solution to the problem, it's also an unruly solution to the problem. Using the standard approach, you'll find yourself building plenty of trivial GUI objects that do almost nothing.

For example, let's take a look at some standard GUI objects. Suppose we have three objects, Control, Button, and ListBox. These objects are greatly simplified for illustrative purposes, so don't think this is anything even close to a complete GUI here.

class Control // abstract base class for controls
{
public:
Control(DialogBox &rParent, const Rectangle &rR);
virtualvoid MouseButtonDown(const Point &rPos)
{ } // called by the control when someone clicks in the control's rect
protected:
virtual ~Control();
};

class Button : public Control
{
public:
Button(DialogBox &rParent, const Rectangle &rR);
virtual ~Button();
virtual void MouseButtonDown(const Point &rPos);
};

void Button::MouseButtonDown(const Point &rPos)
{
// draw the button as down, etc.
}
Simple enough. Now let's make a listbox, and we'll give it a couple of buttons in the corners to scroll the contents up and down.

class ListBox : public Control
{
public:
ListBox(DialogBox &rParent, const Rectangle &rR);
virtual ~ListBox();
virtual void MouseButtonDown(const Point &rPos);
protected:
Button ScrollUpButton, ScrollDownButton;
};

void ListBox::MouseButtonDown(const Point &rPos)
{
// change the selected item in the listbox
}
It looks great, but we've got a small problem. What we would like to happen would be for the Button objects to call a function in the ListBox when they've been clicked so the ListBox can scroll itself up or down accordingly. With our current scheme outlined above, we'd have to do this:

class ListBoxScrollButton : public Button
{
public:
ListBoxScrollButton (DialogBox &rParent, const Rectangle &rR,
ListBox &rBoxToScroll, const bool &rUp);
virtual ~ListBoxScrollButton();
virtual void MouseButtonDown(const Point &rPos);
private:
ListBox &rMyBox // the "owner" box to be scrolled
bool Up; // am I the "up" button or the "down" button?
};

void ListBoxScrollButton::MouseButtonDown(const Point &rPos)
{
Button::MouseButtonDown(rPos); // show the button as down from the parent class
if (Up)
rMyBox.ScrollUp();
else
rMyBox.ScrollDown();
}
Then the ListBox, instead of containing two Button objects, would contain ListBoxScrollButton objects that have the specialized task of telling the ListBox to scroll itself. While this does work, it's a bit of a pain. You need to implement a new class derived from Button just to do a little if-then statement and call a function in ListBox. Also, it makes your design more of an object-sprawl, as you're now adding ListBox-specific functionality to objects that aren't the ListBox.

A solution to this problem is to make an interface in objects to allow them to call functions in anonymous objects. This can be done with function pointers, but there are a couple of stumbling blocks with that approach:
  1. In order to call a function in another object, you need a pointer to the object itself, so you'll have to store that too.
  2. Type safety is a problem. You'll either need to derive all call-able objects from a common root or use some kind of RTTI mechanism to properly convert a pointer to the object you want to call. Things can get kludgy fast.
  3. The C++ function pointer interface is unruly.
The two small classes presented below handle these problems. The meat of it is a Caller class that stores object instances and function pointers so your object doesn't have to know about the object in which it's going to call a function. It's got a standard interface so you can pass info to and from the object being called. Finally, it cleans up the function pointer interface a bit with a macro that does the casting for you.

The interface consists of three items total:
  1. An empty CallHandler object to maintain type-safety. If you want a function in your object to be called, derive it from CallHandler.
  2. A Caller object. This object is the meat of the system. It contains a pointer to the object and function to be called. It's also got several inline maintenance functions so you can easily assign, reassign, or compare your function pointers.
  3. A MakeCaller macro. This macro is what you'll use to construct Caller objects. You could ignore the macro, but then you'll have to do all the casting yourself when you want to create a Caller object.
Here's the code. There's not all that much of it.

class CallHandler
{
};

typedef int (CallHandler::*PFN)(void *pCaller);

class Caller
{
public:
Caller() : pOb(NULL), pFn(NULL)
{ }
Caller(CallHandler *_pOb, PFN _pFn) : pOb(_pOb), pFn(_pFn)
{ }
Caller(const Caller &rC)
{ pOb = rC.pOb; pFn = rC.pFn; }
Caller& operator=(const Caller &rC)
{ pOb = rC.pOb; pFn = rC.pFn; return *this; }

bool operator==(const Caller &rC) const
{ return (pOb == rC.pOb) && (pFn == rC.pFn); }
bool operator!=(const Caller &rC) const
{ return (pOb != rC.pOb) || (pFn != rC.pFn); }
int Call(void *pC) // actually do the function call
{ return (pOb && pFn) ? (pOb->*pFn)(pC) : 0; }
private:
CallHandler *pOb;
PFN pFn;
};

#define MakeCaller(pObject, Class, pFunction) Caller((CallHandler*)(void*)pObject, \
(PFN)(int (Class::*)(void*)) &Class::pFunction)
There's no real magic to the CallHandler code. It's been implemented similarly in a dozen different projects. This just happens to be my favorite way of doing it, because it can preserve the object-oriented nature of your code, and it is very lightweight. I've seen more elaborate versions that either use the underlying system's message queue, implement their own asychronous message queue, or use "functors" (C++ objects that look like functions). While they all work, this method seems to have the best balance between size and portability.

Let's look at how a CallHandler can help out our problem with building specialized objects for the ListBox object. First, let's change our Button object a bit:

class Button : public Control
{
public:
Button(DialogBox &rParent, const Rectangle &rR);
virtual ~Button();
virtual void MouseButtonDown(const Point &rPos);
void SetMouseButtonDownCaller(const Caller &rC)
{ MyMouseButtonDownCaller = rC; }
private:
Caller MyMouseButtonDownCaller;
};

void Button::MouseButtonDownHdl(const Point &rPos)
{
// draw the button down, etc.
MyMouseButtonDownCaller.Call(this); // call the handler if another object has set one
}
Now our Button object contains a click handler in addition to its existing virtual MouseButtonDown() function. If you need to handle a click, you now have the choice of deriving from Button as we originally did or overloading the button's click handler. The choice of which to do depends on the job you're planning. If you just need a function to be called when the button is clicked, as we do in this example, use the handler. If you need to override several behaviors, it's probably better to derive a new object from Button.

Don't worry if you don't need to set a click handler. The Caller object by default sets its members to NULL, and if you do invoke the Call() member on a Caller object that has not been overridden, it will just return without doing anything.

In any case, we no longer need to make a special object derived from Button. We can go back to our original ListBox object, but with a couple of lines of code to tell the Button object that we want to be called.

class ListBox : public Control, CallHandler // note that we're now derived from CallHandler
{
public:
ListBox(DialogBox &rParent, const Rectangle &rR);
virtual ~ListBox();
virtual void MouseButtonDownHdl(const Point &rPos);
int ButtonHandler(Button *pButton); // this will be called by the buttons
protected:
Button ScrollUpButton, ScrollDownButton; // we can just use standard button objects now
};

ListBox::ListBox(DialogBox &rParent, const Rectangle &rR)
{
// do the standard member-initialization here
ScrollUpButton.SetMouseButtonDownCaller(MakeCaller(this, ListBox, ButtonHandler));
ScrollDownButton.SetMouseButtonDownCaller(MakeCaller(this, ListBox, ButtonHandler));
}

void ListBox::MouseButtonDownHdl(const Point &rPos)
{
// change the selected item in the listbox
}

int ListBox::ButtonHandler(Button *pButton)
{
if (pButton == &ScrollUpButton)
{
// the up button was clicked, scroll the list up
}
else
{
// the down button was clicked, scroll the list down
}
return 0; // you can use this to return info to the button
}
Now a special ListBoxScrollButton object that handles the clicks is not necessary. In the constructor, we told the Button object that when it is clicked it should call the ButtonHandler function. The ButtonHandler checks to see which button is clicked (done for brevity's sake. We could have made two functions if we wanted), then does the appropriate scrolling. It's a fairly simple and portable way to keep from polluting your namespace with dozens of little objects that do trivial things in dialog boxes and elsewhere.

In short, this is one of my favorite hacks. It's simple and can make your programs simpler. It's certainly not limited to writing GUI's, but that's a place where the need for such a thing crops up repeatedly.
Cancel Save
0 Likes 0 Comments

Comments

Nobody has left a comment. You can be the first!
You must log in to join the conversation.
Don't have a GameDev.net account? Sign up!
Advertisement