Introduction
Odometer is a simple class that displays an integer in the style of an automobile
dash odometer. The numbers revolve in the same way. The control consists of a
parent COdom
object with any number of child counter
windows (CCounter
). The size of the odometer
control is determined by the number of counters it contains.
The speed of rotation can be set to any speed you wish.
I have included a slider control so that you can experiment. The images for each
counter are loaded as an array of CImageList
elements: 10 image lists per counter
and 8 images per image list.
Sample image list (ZeroToOne.bmp)
If you wish to resize the odometer control you must:
- Change the
#defines
IMAGE_WIDTH
and/or IMAGE_HEIGHT
.
The numbers are in pixels. - Redo the 10 bitmap image lists so that the width and height correspond to the
#define
s.
After that, the size and placement of the counters within the odometer control will be
automatically calculated by the code.
Classes
COdom, CCounter
#defines
Image widths are in Odom.cpp. Add them to your dialog class as well.
#define IMAGE_WIDTH 16
#define IMAGE_HEIGHT 16
Rotational styles appear in Counter.h:
#define STYLE_ROTATE_FROM_ZERO 0
#define STYLE_ROTATE_WRAPAROUND 1
#define NUM_IMAGE_ARRAYS 10
#define NUM_ARRAY_IMAGES 8
Image arrays
The number of image arrays (zero-to-zone, one-to-two, ..., nine-to-zero) is defined in
Counter.h:
#define NUM_IMAGE_ARRAYS 10
and the arrays are loaded in CCounter::OnCreate ()
:
int CCounter::OnCreate(LPCREATESTRUCT lpCreateStruct)
{
if (CStatic::OnCreate(lpCreateStruct) == -1)
return -1;
BOOL success;
for (int i = 0; i < NUM_IMAGE_ARRAYS; i ++)
{
success = m_ImgArray [i].Create (
IDB_ZERO_TO_ONE + i, 16, 8, MAGENTA);
ASSERT (success);
}
m_LastDigit = 0;
return 0;
}
Note: It is important that the identifiers (IDB_ZERO_TO_ONE
, etc) appear in the
resource.h file in numerical order for the above code to work.
To use
#include
Odom.h and Counter.h and
add Odom.cpp and Counter.cpp to your project. - In your dialog class:
- Declare a variable for the integer to be displayed.
- Declare a variable to hold the number of digits.
- Declare a variable for the odometer control.
int m_nNumber;
int m_NumDigits;
COdom m_Odometer;
Somewhere else, set the number of digits:
m_NumDigits = 8;
The array of counters is contained within the parent window COdom
.
Assuming the control appears in a dialog, do this to create the parent:
CMyDialog::OnInitDialog ()
{
RECT rect;
rect.left = 50;
rect.top = 20;
rect.right = rect.left + (nNumDigits * IMAGE_WIDTH);
rect.bottom = rect.top + IMAGE_HEIGHT;
m_Odometer.SetNumDigits (nNumDigits);
m_Odometer.m_RotationRate = 30;
m_Odometer.m_RotationStyle =
STYLE_ROTATE_FROM_ZERO;
m_Odometer.Create (NULL, SS_BLACKFRAME | WS_VISIBLE
| WS_CHILD, rect, this, IDC_ODOM);
m_Odometer.ShowWindow (SW_SHOW);
}
Then, whenever you update the number, follow it with this:
.
.
.
.
.
.
m_Odometer.m_nNumber = m_nNumber;
m_Odometer.SendMessage (WM_RESET_COUNTERS, 0, 0);
m_Odometer.SendMessage (WM_ADVANCE_COUNTERS, 0, 0);
Where the action is
When the integer is updated by the above code, it is first processed by
ParseNumberArray()
which takes each digit of the integer and creates a
character array, each holding one digit.
int COdom::ParseNumberArray ()
{
TCHAR buf [MAX_DIGITS];
InitDigitsArray ();
wsprintf (buf, "%d", m_nNumber);
int j = 0;
int len = strlen (buf);
for (int i = len - 1; i >= 0; i --)
{
m_DigitsArray [i] = ((int) buf [i]) - 48;
j ++;
}
return len;
}
Then, each element of the array is sent (from right to left)
to the appropriate counter window as an integer:
int j = 0;
for (i = m_nArrayDigits - 1; i >= 0; i --)
{
m_Digits [j].SendMessage (WM_ADVANCE_COUNTER,
m_DigitsArray [i], 0);
j ++;
}
CCounter
In Counter.h:
public:
int m_TimeInterval;
int m_RotationStyle;
private:
int m_nNumber;
int m_LastDigit;
int m_DigitVal;
BOOL m_bAllowRotation;
The counter window receives the integer and immediately invalidates the
window to force a repaint:
LRESULT CCounter::WindowProc(UINT message,
WPARAM wParam, LPARAM lParam)
{
switch (message)
{
case WM_ADVANCE_COUNTER:
m_bAllowRotation = TRUE;
m_DigitVal = (int) wParam;
Invalidate (TRUE);
break;
}
return CStatic::WindowProc(message, wParam, lParam);
}
The real work is done in the counter's paint routine:
void CCounter::OnPaint()
{
CPaintDC dc(this);
int counter;
int i;
POINT pt;
pt.x = 0; pt.y = 0;
if (m_bAllowRotation)
{
switch (m_RotationStyle)
{
case STYLE_ROTATE_FROM_ZERO:
if (m_bInitialize)
m_ImgArray [0].Draw (&dc, 0, pt, ILD_NORMAL);
else
{
m_ImgArray [0].Draw (&dc, 0, pt, ILD_NORMAL);
for (int i = 0; i < m_DigitVal; i ++)
RunImageBmp (i, &dc, pt);
}
m_LastDigit = m_DigitVal;
break;
case STYLE_ROTATE_WRAPAROUND:
m_ImgArray [0].Draw (&dc, 0, pt, ILD_NORMAL);
if (m_DigitVal < 0)
break;
if (m_LastDigit < 0)
m_LastDigit = 0;
if (m_DigitVal == m_LastDigit)
{
m_ImgArray [m_DigitVal].Draw (&dc,
0, pt, ILD_NORMAL);
break;
}
counter = 0;
for (i = m_LastDigit; i < m_DigitVal; i ++)
{
RunImageBmp (i, &dc, pt);
counter ++;
}
if (counter == 0)
{
for (i = m_LastDigit; i < NUM_IMAGE_ARRAYS; i ++)
RunImageBmp (i, &dc, pt);
for (i = 0; i < m_DigitVal; i ++)
RunImageBmp (i, &dc, pt);
}
m_LastDigit = m_DigitVal;
break;
}
m_bInitialize = FALSE;
}
else
{
if (m_LastDigit >= 0)
m_ImgArray [m_LastDigit].Draw (&dc,
0, pt, ILD_NORMAL);
else
m_ImgArray [0].Draw (&dc,
0, pt, ILD_NORMAL);
}
m_bAllowRotation = FALSE;
}
The first image is drawn; then redrawn in RunImageArray()
because it produces
a delay whereby each number "pauses" before going to the next - like an odometer.
Here is where the image array is drawn, image by image:
void CCounter::RunImageBmp (int i,
CPaintDC *pDC, POINT pt)
{
for (int j = 0; j < 8; j ++)
{
m_ImgArray [i].Draw (pDC, j, pt, ILD_NORMAL);
if (m_TimeInterval)
Sleep (m_TimeInterval);
}
}
And now a word about m_bAllowRotation
.
The whole process of rotating the digit counters is dependant on the fact that calling
Invalidate()
in CCounter::WindowProc()
forces a repaint of the
counter window and, therefore, execution of the actual rotation routines. That's all fine
and well, except for one little thing: Any action that causes a repaint at any
time will cause CCounter::OnPaint()
to execute. If the odometer is hidden for any reason,
or moved off the screen, the effected counters will revolve when they regain focus.
That isn't good! So, we have to come up with a way to only update the counter when we
want to, not when Windows decides to. That's where m_bAllowRotation
comes in. It is a flag that is set to TRUE
in CCounter::WindowProc()
and returned to
FALSE
just after the rotation routines in CCounter::OnPaint
are completed.
This ensures that the counter windows are only painted (revolve) when we want them to be.
It isn't elegant, but it works.
void CCounter::OnPaint()
{
if (m_bAllowRotation)
{
.
.
.
}
else
{
if (m_LastDigit >= 0)
m_ImgArray [m_LastDigit].Draw (&dc,
0, pt, ILD_NORMAL);
else
m_ImgArray [0].Draw (&dc, 0, pt, ILD_NORMAL);
}
m_bAllowRotation = FALSE;
}
The "else" code kicks in when the counter windows are invalidated by the operating system
(when m_bAllowRotation
is FALSE
). bAllowRotation
is only
TRUE
when it is specifically set in CCounter::WindowProc()
.
To Review
- Add Odom.h, Odom.cpp, Counter.h and Counter.cpp
to your project.
- Add the image list bitmaps, being careful to make sure that the bitmap ID's are in
numerical order in the resource.h file.
- Add the
#define
s above. - Add
m_nNumber
, m_NumDigits
and m_Odometer
to your dialog class. - Now, whenever your code updates the integer, do this:
m_Odometer.m_nNumber = m_nNumber;
m_Odometer.SendMessage (WM_RESET_COUNTERS, 0, 0);
m_Odometer.SendMessage (WM_ADVANCE_COUNTERS, 0, 0);
- There, you're done!
It may seem like a lot because I went through it step by step. But
really, it's not rocket science.
Considerations
- This control need not be limited to numbers. Letters can be used in the image arrays
(or anything else, for that matter).
This will require lots of time to construct the bitmaps, but it would look interesting.
- Resizing the control requires changing the two
#define
s and completely re-making the
bitmap image arrays. - You may ask: Why two classes? Well, I kept
COdom
and CCounter
separate because I might want to use the counter class alone in another program. - With a little razzle-dazzle, this could easily be converted to a digital clock.
- Yes, the numbers could revolve backwards. But that would not be faithful to an
odometer style.
- The odometer is not limited to dialogs. It should work in any type of window that
follows the normal painting scheme.
Any suggestions or improvements are welcome:
Email to Steve
This member has not yet provided a Biography. Assume it's interesting and varied, and probably something to do with programming.