Click here to Skip to main content
15,867,308 members
Articles / Desktop Programming / MFC
Article

Odometer-style Counter Control

Rate me:
Please Sign up or sign in to vote.
4.67/5 (7 votes)
25 Feb 20024 min read 117.1K   1.9K   33   12
Create an odometer-style counter, with revolving digits,

Sample Image

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)

Sample Image

If you wish to resize the odometer control you must:

  1. Change the #defines IMAGE_WIDTH and/or IMAGE_HEIGHT. The numbers are in pixels.
  2. Redo the 10 bitmap image lists so that the width and height correspond to the #defines.

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

  1. #include Odom.h and Counter.h and add Odom.cpp and Counter.cpp to your project.
  2. In your dialog class:
    1. Declare a variable for the integer to be displayed.
    2. Declare a variable to hold the number of digits.
    3. 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

  1. Add Odom.h, Odom.cpp, Counter.h and Counter.cpp to your project.
  2. Add the image list bitmaps, being careful to make sure that the bitmap ID's are in numerical order in the resource.h file.
  3. Add the #defines above.
  4. Add m_nNumber, m_NumDigits and m_Odometer to your dialog class.
  5. Now, whenever your code updates the integer, do this:
  6. m_Odometer.m_nNumber = m_nNumber;
    
    m_Odometer.SendMessage (WM_RESET_COUNTERS, 0, 0);
    m_Odometer.SendMessage (WM_ADVANCE_COUNTERS, 0, 0);
  7. 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

  1. 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.
  2. Resizing the control requires changing the two #defines and completely re-making the bitmap image arrays.
  3. 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.
  4. With a little razzle-dazzle, this could easily be converted to a digital clock.
  5. Yes, the numbers could revolve backwards. But that would not be faithful to an odometer style.
  6. 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

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here


Written By
Web Developer
United States United States
This member has not yet provided a Biography. Assume it's interesting and varied, and probably something to do with programming.

Comments and Discussions

 
GeneralSomething went really wrong Pin
Jörgen Sigvardsson27-Feb-02 1:03
Jörgen Sigvardsson27-Feb-02 1:03 
GeneralRe: Something went really wrong Pin
Nish Nishant27-Feb-02 1:45
sitebuilderNish Nishant27-Feb-02 1:45 
GeneralRe: Something went really wrong Pin
Chris Maunder27-Feb-02 3:22
cofounderChris Maunder27-Feb-02 3:22 
GeneralRe: Something went really wrong Pin
Nish Nishant27-Feb-02 3:46
sitebuilderNish Nishant27-Feb-02 3:46 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.