The ability to easily use attractive custom controls is, I believe, one of the factors that has given Microsoft's Visual Basic such a devoted following (as will Visual C++ soon have). But it is best not to forget that custom controls are relatively easy to create on ObjectWindows. Most of the hard work being handled by the base classes of the OWL framework.
The custom control that I will create will be used to represent a slide control, of the kind found on old TV sets, Graphic Equalisers or Fader controls on Audio Mixing decks. I assume that before reading this article you are at least familiar with the basics of ObjectWindows.
The first stage is to create the bitmaps for our controls. These are called SLIDBASE.BMP for background of the slider and SLIDBUTT.BMP for the button. Both bitmaps being created using resource workshop.
The button is intended to traverse the dark area in the centre of
the base. The bottom of the control represents 0%, the top 100%.
Before I start on a description of the stages needed to animate these simple bitmaps and create a reusable custom control, I need to define some constants as follows:
const int MAXPOINT = 19;
const int SLIDERHEIGHT = 112;
const int SLIDERWIDTH = 40;
const int BUTTONWIDTH = 20;
const int BUTTONHEIGHT = 10;
const int XOFFSET = SLIDERWIDTH/2 - BUTTONWIDTH/2;
const int ARROWWIDTH = 16;
const int ARROWHEIGHT = 16;
const int DOWNX = 3;
const int DOWNY = 94;
const int UPX = 22;
const int UPY = 94;
const int TEXTPOSX = 4;
const int TEXTPOSY = 4;
We need a number of include files, OWL.H for the ObjectWindows system, CONTROL.H to supply the base class from which our custom control is derived, BWINDOW.H, to supply a Borland Window type (TBWindow) into which the position of the control as a percentage of maximum is displayed. STRSTREAM.H and IOMANIP.H are required to format the numbers displayed in the percentage window.
The name of the new custom control is to be TMyScroll. For those not yet totally au fait with ObjectWindows nomenclature, the leading letter 'T' is used to denote that this is a Type of MyScroll. At this point we also use a macro (_CLASSDEF). This is used to typedef references and pointers to the class. In this case the statement:
_CLASSDEF(TMyScroll)
generates the following statements:
class _CLASSTYPE TMyScroll;
typedef TMyScroll FAR * PTMyScroll;
typedef TMyScroll _FAR & RTMyScroll;
typedef TMyScroll _FAR * _FAR & RPTMyScroll;
typedef const TMyScroll _FAR * PCTMyScroll;
typedef const TMyScroll _FAR & RCTMyScroll;
This is a convenient way of generating all the typedefs you may require for the class.
Our TMyScroll class is publicly derived from TControl, and responds to the mouse left button being pressed and released, and the mouse being moved while over the custom control. We therefore need to capture these events and process them. This is done using the DDVT (Dynamic Dispatch Virtual Tables) of Object Windows. The DDVT uses a Borland specific extension to the C++ language in order to semi-automatically allow us to tap into the message processing loop. This is achieved with the declaration:
virtual void MyOispatchHandler(RTMessage) =
[DispatchValue];
The only windows message, in addition to the mouse ones already mentioned, that we are required to handle is the paint message. Each time windows wants the control to be drawn, it will send a WMPAINT message, this is trapped by using the Dispatch value of WM_FIRST+WM_PAINT
All the messages not specifically catered for are handled by the base class (TControl) so we need not worry about them.
We also need a number of values retained by the class, these include the number of units by which the control moves for one click of the arrow buttons (Delta), the values represented by the top and bottom of the slidebar range, (I've conveniently fixed them to 0% and 100%, but they could take other ranges). A series of Boolean values used to indicate if buttons have been pressed (and therefore require shading ), the physical position of the thumbnail and the value represented by the thumbnail position (in the range of BottomOfRange to TopOfRange).
// class for the new scroll bar
class TMyScroll : public TControl
{
protected:
int Delta; // The amount by which the thumbnail moves
// for each click of the arrow buttons
int BottomOfRange,
TopOfRange;
BOOL DownButton, // logicals used to decide which
UpButton, // buttons need to be inverted
SliderButton;
int CurrentValue, // The current value of the control
// in the range BottomOfRange to TopOfRange
ThumbnailPosition; // Physical position of thumbnail in window
virtual LPSTR GetClassName() { return "TMyScroll";}
virtual void SetupWindow();
public:
TMyScroll(PTWindowsObject AParent, int AnID,
int X, int Y, PTModule AModule = NULL);
virtual void SetRange(int LoVal, int HiVal);
virtual void GetRange(int& LoVal, int& HiVal);
virtual int GetPos();
virtual void SetPos(int ThumbnailPosition);
virtual WORD Transfer(Pvoid DataPtr, WORD TransferFlag);
// DDVT response functions
virtual void WMLButtonDown(RTMessage)
= [WM_FIRST + WM_LBUTTONDOWN];
virtual void WMLButtonUp(RTMessage)
= [WM_FIRST + WM_LBUTTONUP];
virtual void WMMouseMove(RTMessage)
= [WM_FIRST + WM_MOUSEMOVE];
virtual void WMPaint(RTMessage)
= [WM_FIRST + WM_PAINT];
};
The constructor simply sets the start position of the control.
TMyScroll::TMyScroll(PTWindowsObject AParent, int AnID,
int X, int Y, PTModule AModule)
: TControl(AParent, AnID, NULL, X, Y, 40, 112, AModule)
{
Delta =1; // Amount to add when the user presses an arrow
ThumbnailPosition = ZEROPOINT; // start position for thumbnail
CurrentValue = 0; // initial value of control
DownButton = FALSE; // set all buttons off
UpButton = FALSE;
SliderButton = FALSE;
}
SetupWindow is used to perform any post-constructor initialisation tasks. In this case, sets the range of values the control will report. In this case between zero and 100. The call to the base class's SetupWindow must not be forgotten, in this case the order in which it is called is not significant.
// sets up the range 0..100
void TMyScroll::SetupWindow()
{
SetRange(0,100);
TControl::SetupWindow();
}
The SetRange member function is not too exciting either, it is used to represent the minimum and maximum values that can be expressed by the control.
void TMyScroll::SetRange(int LoVal, int HiVal)
{
BottomOfRange = LoVal;
TopOfRange = HiVal;
}
The GetRange member function returns (as reference) the values set up with SetRange.
void TMyScroll::GetRange(int& LoVal, int& HiVal)
{
LoVal = BottomOfRange;
HiVal = TopOfRange;
}
The GetPos member function returns the current setting of the
thumbnail in the range set by SetRange (not in percent).
int TMyScroll::GetPos()
{
return CurrentValue;
}
The SetPos member function (like GetPos) sets the current setting in
the range (and units) used in SetRange. If the position set is
different from the current position, the physical position of the
slider is recalculated and the control is invalidated, thus redrawing
the complete control.
void TMyScroll::SetPos(int NewPos)
{
int LoVal, HiVal;
// get the current control range
GetRange(LoVal, HiVal);
// see if the new value is in range
if (NewPos > HiVal)
NewPos = HiVal;
else if (NewPos < LoVal)
NewPos = LoVal;
if (CurrentValue != NewPos) // if the position has changed
{
CurrentValue = NewPos;
ThumbnailPosition = ZEROPOINT -
((CurrentValue - BottomOfRange)
* (ZEROPOINT -MAXPOINT)
/ (TopOfRange - BottomOfRange));
// Redraw the control
InvalidateRect(HWindow, NULL, FALSE);
}
}
The Transfer member function is not used by this control, but is provided in case you wish to use the control in conjunction with a Transfer buffer.
WORD TMyScroll::Transfer(Pvoid DataPtr, WORD TransferFlag)
{
// transfer the thumb position
int* NewPtr = (int*) DataPtr;
if (TransferFlag == TF_GETDATA)
*NewPtr = CurrentValue;
else
SetPos(*NewPtr);
return sizeof(int);
}
The WMMoveMouse member function is used to detect the new position of the mouse, and to use that information to decide on the position of the slide button, or if the mouse has moved outside the immediate area of the button.
void TMyScroll::WMMouseMove(RTMessage Msg)
{
if (SliderButton &&
(Msg.LP.Lo<XOFFSET ||
Msg.LP.Lo>XOFFSET+BUTTONWIDTH))
SliderButton = FALSE;
if (SliderButton && Msg.LP.Hi>=MAXPOINT &&
Msg.LP.Hi<=ZEROPOINT)
{
int LoVal, HiVal;
// get the current control range
GetRange(LoVal, HiVal);
// Redraw the control
SetPos((ZEROPOINT-Msg.LP.Hi)*
(TppOfRange-BottomOfRange)/
(ZEROPOINT-MAXPOINT));
}
}
The WMLButtonDown member function determines if the mouse button was
pressed in either of the arrow buttons or on the slider button. If this
is the case, the appropriate flag is set.
void TMyScroll::WMLButtonDown(RTMessage Msg)
{
RECT rRect;
RECT lRect;
RECT sRect;
// Set the area of the right button
SetRect(&rRect, UPX, UPY, UPX + ARROWWIDTH, UPY +
ARROWHEIGHT);
// Set the area of the left button
SetRect(&lRect, DOWNX, DOWNY, DOWNX + ARROWWIDTH,
DOWNY + ARROWHEIGHT);
// Set the area of the slider button
SetRect(&sRect, XOFFSET, ThumbnailPosition, XOFFSET +
BUTTONWIDTH, ThumbnailPosition + BUTTONHEIGHT);
// Check for up arrow
if (PtInRect(&rRect,MAKEPOINT(Msg.LP)))
{
UpButton = TRUE;
SetPos(GetPos() + Delta);
}
// check for down arrow
else if (PtInRect(&lRect,MAKEPOINT(Msg.LP)))
{
DownButton = TRUE;
SetPos(GetPos() - Delta);
}
else if (PtInRect(&sRect,MAKEPOINT(Msg.LP)))
{
SliderButton = TRUE;
}
}
The WMLButton member function sets all the buttons to inactive and
invalidates the control to redraw.
void TMyScroll::WMLButtonUp(RTMessage Msg)
{
DownButton = FALSE;
UpButton = FALSE;
SliderButton = FALSE;
InvalidateRect(HWindow, NULL, FALSE);
}
This is the big one! The WMPaint member function is responsible for
drawing the custom control. Firstly the backdrop is redrawn and
the slider button deposited in the appropriate position. The percentage
value is written to the little window on the top of the control and
then, depending on the flags set, one or other of the arrow buttons is
inverted to show that it is being pressed.
void TMyScroll::WMPaint(RTMessage)
{
HDC hDC, MemDC;
HBITMAP OldBitmap;
HBRUSH OldBrush;
RECT Rect;
PAINTSTRUCT ps;
int BMHeight;
memset(&ps,0x00,sizeof(PAINTSTRUCT));
hDC = BeginPaint(HWindow, &ps);
// draw the up arrow
GetClientRect(HWindow,&Rect);
MemDC = CreateCompatibleDC(hDC);
OldBitmap = (HBITMAP)SelectObject(MemDC,
LoadBitmap( GetApplication()->hlnstance,
"SLIDBASE"));
StretchBlt(hDC, 0, 0, Rect.right, Rect.bottom,
MemDC,0,0,39,111,
SRCCOPY);
DeleteObject( SelectObject( MemDC, LoadBitmap(
GetApplication()->hInstance, "SLIDBUTT")));
BitBlt(hDC, XOFFSET, ThumbnailPosition, 20, 10,
MemDC,0,0,SRCCOPY);
DeleteObject(SelectObject(MemDC, OldBitmap)); DeleteDC(MemDC);
// display the number
char buffer[6];
ostrstream num(buffer,sizeof(buffer));
num << setw(3) << GetPos() << "%" << ends;
HFONT hFont = CreateFont(14,7,0,0,400,0,0,0,
OEM__CHARSET, OUT_DEFAULT_PRECIS,
CLIP_DEFAULT_PRECIS, DEFAULT_QUALITY,
DEFAULT_PITCH|FF_DONTCARE, "system");
SelectObject(hDC,hFont);
DWORD BkGrnd = SetBkColor(hDC,
GetPixel(hDC,TEXTPOSX,TEXTPOSY));
TextOut(hDC,TEXTPOSX,TEXTPOSY,buffer,4);
DeleteObject(hFont);
SetBkColor(hDC,BkGrnd);
RECT rRect;
RECT lRect;
SetRect(&rRect, UPX, UPY, UPX + ARROWWIDTH, UPY +
ARROWHEIGHT);
SetRect(&lRect, DOWNX, DOWNY, DOWNX + ARROWWIDTH,
DOWNY + ARROWHEIGHT);
// Check for up arrow
if (UpButton)
{
InvertRect(hDC,&rRect);
}
// check for down arrow
else if (DownButton)
{
InvertRect(hDC,&lRect);
}
EndPaint(HWindow, &ps);
}
All that is now left is to create the main window (in this case a BWCC gray window) and to instantiate several Sliders to demonstrate their use, and to create the necessary Application class and the WinMain function.
class MyWindow : public TBWindow
{
public:
TMyScroll* TheScroller[4];
MyWindow(PTWindowsObject AParent, LPSTR ATitle);
};
MyWindow::MyWindow(PTWindowsObject AParent, LPSTR ATitle)
: TBWindow (AParent, ATitle)
{
for (int i = 0; i < 4; i++)
{
TheScroller[i] = new TMyScroll(this,
200 + i, 10 + 100 * (i % 2),
10 + 100 * (i % 2));
}
}
class MyApp : public TApplication
{
public:
MyApp(LPSTR ApName, HANDLE Inst, HANDLE Prevlnst,
LPSTR CmdLine, int CmdShow)
: TApplication( ApName, Inst, PrevInst, CmdLine, CmdShow) {}
virtual void InitMainWindow();
};
void MyApp::InitMainWindow()
{
MainWindow = new MyWindow(NULL, "Custom Control");
}
int PASCAL WinMain (HANDLE Inst, HANDLE PrevInst,
LPSTR CmdLine, int CmdShow )
{
MyApp X("Custom Control App",
Inst,PrevInst,CmdLine,CmdShow);
X.Run();
return X.Status;
}