Odometer-style Counter Control






4.67/5 (7 votes)
Feb 26, 2002
4 min read

118770

1907
Create an odometer-style counter, with revolving digits,
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/orIMAGE_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:
// each digit always starts from zero #define STYLE_ROTATE_FROM_ZERO 0 // each digit starts rotating from where it left off #define STYLE_ROTATE_WRAPAROUND 1 // Also, put them in the dialog class // which holds the odometer. //In Counter.h: // number of CImageList objects per counter #define NUM_IMAGE_ARRAYS 10 // num images in each image list #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; // Load 10 image arrays BOOL success; // used for debugging 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.
//In your dialog class 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; //or STYLE_ROTATE_WRAPAROUND 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:
. . // some code . // m_nNumber has been updated by your code . . // some code . 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 () { // Take the integer to be shown in // the odometer and parse // it to fill digits array -- one integer // per element. TCHAR buf [MAX_DIGITS]; InitDigitsArray (); // set elts to zero 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:
// Send a message to each counter 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: // delay between each number image int m_TimeInterval; // start from zero or wrap around int m_RotationStyle; private: // number to be advanced to in counter int m_nNumber; // current number being shown, before updating int m_LastDigit; // digit to be shown - counter // will scroll to this digit. int m_DigitVal; BOOL m_bAllowRotation; // this is set to prevent invalid rotation // when Windows repaints the control // due to hiding or minimizing.
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; //the window now knows what integer to show m_DigitVal = (int) wParam; // force a repaint because thats where // the rotation takes place Invalidate (TRUE); break; } return CStatic::WindowProc(message, wParam, lParam); }
The real work is done in the counter's paint routine:
void CCounter::OnPaint() { // device context for painting CPaintDC dc(this); int counter; int i; POINT pt; pt.x = 0; pt.y = 0; if (m_bAllowRotation) { // start each counter from zero every time switch (m_RotationStyle) { case STYLE_ROTATE_FROM_ZERO: // First time thru -- set the counter to zero if (m_bInitialize) m_ImgArray [0].Draw (&dc, 0, pt, ILD_NORMAL); else { // draw first image m_ImgArray [0].Draw (&dc, 0, pt, ILD_NORMAL); for (int i = 0; i < m_DigitVal; i ++) // run through the image array RunImageBmp (i, &dc, pt); } // save for next time through m_LastDigit = m_DigitVal; break; // continue scrolling from existing number case STYLE_ROTATE_WRAPAROUND: // start each counter from current num and wrap around m_ImgArray [0].Draw (&dc, 0, pt, ILD_NORMAL); // just in case if (m_DigitVal < 0) break; // make sure last digit is not negative if (m_LastDigit < 0) m_LastDigit = 0; // there was no change; draw last digit and exit if (m_DigitVal == m_LastDigit) { m_ImgArray [m_DigitVal].Draw (&dc, 0, pt, ILD_NORMAL); break; } // Run as far as it can go. If the new // digit is less than the current // digit, then the loop will not be // entered and counter will be zero. counter = 0; for (i = m_LastDigit; i < m_DigitVal; i ++) { RunImageBmp (i, &dc, pt); counter ++; } // wrap around if necessary // new digit is less than old digit if (counter == 0) { // run up to zero for (i = m_LastDigit; i < NUM_IMAGE_ARRAYS; i ++) RunImageBmp (i, &dc, pt); // then run up to new digit for (i = 0; i < m_DigitVal; i ++) RunImageBmp (i, &dc, pt); } // save for next time through m_LastDigit = m_DigitVal; break; } m_bInitialize = FALSE; } else // this is here in case the window // is repainted because it was hidden { 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; // Do not call CStatic::OnPaint() // for painting messages }
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) { // loops through a CImageList array - // in this case, 8 images. for (int j = 0; j < 8; j ++) { m_ImgArray [i].Draw (pDC, j, pt, ILD_NORMAL); if (m_TimeInterval) // if a delay is set, do it 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) { . . // rotation routines . } else { // redraw the current digit if (m_LastDigit >= 0) m_ImgArray [m_LastDigit].Draw (&dc, 0, pt, ILD_NORMAL); else // draw a zero 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
andm_Odometer
to your dialog class. - Now, whenever your code updates the integer, do this:
- There, you're done!
m_Odometer.m_nNumber = m_nNumber; m_Odometer.SendMessage (WM_RESET_COUNTERS, 0, 0); m_Odometer.SendMessage (WM_ADVANCE_COUNTERS, 0, 0);
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
andCCounter
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