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.
}
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
}
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();
}
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:
- 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.
- 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.
- The C++ function pointer interface is unruly.
The interface consists of three items total:
- An empty CallHandler object to maintain type-safety. If you want a function in your object to be called, derive it from CallHandler.
- 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.
- 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.
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)
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
}
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
}
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.