Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

CWinListBox - A CWin derived custom listbox control

0.00/5 (No votes)
16 Aug 2004 1  
Step-by-step creation of a custom ListBox control from a generic CWin.

Introduction

You are here because you consider yourself a rookie and because you want to get a few pointers on how to create a custom control from a generic CWin. If you have already built a control from scratch, there will be little or nothing of interest here. The contents of this article are introductory and purely didactic. The only goal is to address the question "How on Earth could one begin doing something like that?"

That said, now follows a warning. It is often difficult to justify replacing an existing control for one of your own making. CListBox, for instance, comes packed with functionality, and I recommend anyone interested in customization to subclass it, adding and/or removing functionality as needed. The Code Project has plenty of articles that will allow you to include icons, have multi-line text, provide checkboxes and tooltips, you name it. Pretty much anything one might dream of is already here. Perhaps missing are hacks to draw or place bitmaps in the background, and a way to retain the standard scrollbars while modifying their look. If you know of these, please post them.

Direct acknowledgment must be given to Chris Maunder for his article "Creating Custom Controls" as one must go over the same first steps when creating a control from scratch, and I have followed his here because they are explained with pristine clarity. I have also borrowed and modified code for the gradient background from norm.net's article "Extended Use of CStatic Class - CLabel 1.6". Over time, I have read and learned from many of the contributions posted at The Code Project, so I now take the opportunity to acknowledge you all here, thank you people.

What Is to Be Accomplished

The target is a horizontal listbox with single selection functionality that allows both default and individual coloring for each item. The border color is to be user-defined and the background is to be drawn using a color gradient. The scrollbar is to have four button-like divisions and a center display: shift to the leftmost column, shift one column to the left, display the number of elements in the listbox, shift one column to the right, and shift to the rightmost column.

Only those items that are visible will be drawn in the listbox, thus making it possible to add an unlimited number of items (memory permitting). This will reduce display time for large amounts of data (CListbox can appear to hang if there are many items), and will bypass, in the process, the - granted - huge limit that CListbox has regarding the maximum number of items that can be added to it.

On with the coding.

Step-by-Step Procedure

Steps 1 through 8 carry out the basic steps to get a custom control up and running, nothing more. Once compiled, the application will run but nothing will be visible in the custom control area.

Step #1. Create a new class derived from CWnd:

  • Class Type: MFC Class.
  • Class Information/Name: CCWinListBox.
  • Class Information/Base class: generic CWnd.

(All else left to defaults.)

Step #2. We will now register the new window class we intend to create. First, add the following line to "CWinListBox.h":

#define C_CUSTOMLISTBOX_CLASSNAME _T("MFC_CCWinListBox")

Step #3. Then, add the following protected method declaration to "CWinListBox.h":

BOOL RegisterWindowClass();

Step #4. Last, add the implementation of RegisterWindowClass (below) to "CWinListBox.cpp" and call it from the constructor. Note that the cursor IDC_CURSOR1 - the "hand" cursor in this example project - has to be added as a resource. You can substitute it for exactly whatever calls your fancy. Alternatively, you can set the field wndcls.hCursor to NULL and the standard cursor will be used instead.

BOOL CCWinListBox::RegisterWindowClass()
{
    WNDCLASS  wndcls;
    HINSTANCE hInst = AfxGetInstanceHandle();

    if( !( ::GetClassInfo( hInst, C_CWINLISTBOX_CLASSNAME, 
                      &wndcls ) ) ) // Already registered?

    {
        wndcls.style            = CS_DBLCLKS | CS_HREDRAW | CS_VREDRAW;
        wndcls.lpfnWndProc      = ::DefWindowProc;
        wndcls.cbClsExtra       = wndcls.cbWndExtra = 0;
        wndcls.hInstance        = hInst;
        wndcls.hIcon            = NULL;
        wndcls.hCursor          = AfxGetApp()->LoadCursor( IDC_CURSOR1 );
        wndcls.hbrBackground    = (HBRUSH) (COLOR_3DFACE + 1);
        wndcls.lpszMenuName     = NULL;
        wndcls.lpszClassName    = C_CWINLISTBOX_CLASSNAME;

        if( !AfxRegisterClass( &wndcls ) )
        {
            AfxThrowResourceException();
            return FALSE;
        }
    }

    return TRUE;
}

...

CCWinListBox::CCWinListBox()
{
    RegisterWindowClass();    // Register custom control

}

Step #5. Having done this, we can now include the header of our CWin derived class ("CWinListBox.h") in the dialog class where the custom control is going to be used, namely, "CustomListBoxDlg.h":

#include "CWinListBox.h"

Step #6. Add a protected member to "CustomListBoxDlg.h" (or the dialog where the control is going to be hosted):

CCWinListBox m_CWindListBox;

Step #7. We have everything we need to actually place a custom control in our dialog via the resource editor. So, place it on the dialog, size it to your heart's content, and then right-click on the grey square to select Properties. Modify the fields as follows:

  • ID: IDC_CUSTOM1
  • Caption: (delete the caption)
  • Class: MFC_CCWinListBox

(All else left to defaults.)

Step #8. Add the following line to DoDataExchange in "CustomListBoxDlg.cpp" (or the appropriate hosting dialog). If you changed the ID in the step before, make sure it agrees with the one below:

DDX_Control(pDX, IDC_CUSTOM1, m_CWindListBox);

Step #9. The code can already be compiled and run. Of course, nothing will be displayed in the custom control area as no implementation for the OnPaint method has been coded yet. This is just a so-far-so-good checkpoint.

Steps 10 through 13 will go over the code to draw borders and background, implementing, in the process, methods to allow the hosting dialog to set the font and colors to be used. After these five steps, the custom control will be visible when running the application.

Step #10. Add public methods to allow the hosting dialog to set the colors and font the control will use (implementation of these methods essentially involves populating member variables). The declarations look like this:

virtual CCWinListBox& SetTextFont ( LONG nHeight, bool bBold, 
                          bool bItalic, CString csFaceName );

virtual CCWinListBox& SetBorderColor ( COLORREF crBorder1, 
                                        COLORREF crBorder2 );

virtual CCWinListBox& SetBkColor ( COLORREF crBkFrom, COLORREF crBkTo );

virtual CCWinListBox& SetDefaultTextColor ( COLORREF crText );

virtual CCWinListBox& SetSelectedColors ( COLORREF crSelText, 
                  COLORREF crSelTextBk,, COLORREF crSelBorder1, 
                  COLORREF crSelBorder2 );

Step #11. The elucidation of an optimal combination of colors that better suits one's dignified complexion - a palette surely conceived by the never quite dormant loony in us - often requires the high performance supercomputing of a Cray machine. Whatever the intimate fashion demons at work, however, in order to initialize the look of the custom listbox, one only has to pass the preferred colors and font info from OnInitDialog in "CustomListBoxDlg.cpp" (or the appropriate hosting dialog). Forgetting myself, I propose:

m_CWindListBox
    .SetTextFont        ( 8, TRUE, FALSE, _T("Verdana") );
    .SetBorderColor     ( RGB(0,77,113), RGB(255,255,255 ) )
    .SetBkColor         ( RGB(183,235, 255 ), RGB(255,255,11 ) )
    .SetDefaultTextColor( RGB(0,77,113) )
    .SetSelectedColors  ( RGB(255,255,255 ), RGB(70,136,136), 
                               RGB(199,226,226), RGB(23,47,47) )

Step #12. The raving madness over, we are ready to do some drawing. We'll paint off-screen to reduce flickering. First, add a message handler for WM_PAINT via ClassWizard. Having done that, include the implementation below; at this point, only the background (gradient, in this case) and borders will be drawn. Compile and run to check what the animal looks like as it grows.

void CCWinListBox::OnPaint() 
{
    CPaintDC dc(this); // device context for painting

    
    // Paint off-screen

    CDC*     pDCMem      = new CDC;
    CBitmap* pPrevBitmap = NULL;
    CBitmap  bmpCanvas;
    CRect    rArea;
    
    GetClientRect( rArea );
    
    pDCMem->CreateCompatibleDC( &dc );
    
    bmpCanvas.CreateCompatibleBitmap( &dc, rArea.Width(), rArea.Height() );
    
    pPrevBitmap = pDCMem->SelectObject( &bmpCanvas );
    
    // DRAWING BEGINS --------------------------------


    CRect rClient( rArea );

    // Leave room for the border

    rClient.DeflateRect( 1, 1, 1, 1 );
    
    // Draw gradient background

    DrawGradientBackground( pDCMem, rClient, m_crBkFrom, m_crBkTo );
    
    // Draw the border

    pDCMem->Draw3dRect( rArea, m_crBorder1, m_crBorder2 );

    // DRAWING ENDS --------------------------------


    // Copy from memory to the screen

    dc.BitBlt( 0, 0, rArea.Width(), rArea.Height(), pDCMem, 0, 0, SRCCOPY );

    pDCMem->SelectObject( pPrevBitmap );

    delete pDCMem;
}

Step #13. You may still notice some flickering. Add a message handler for WM_ERASEBKGND - via ClassWizard - and simply return TRUE to formally notify Mr. Gates that this custom control will not need background erasing. Compile and run, the flickering should be all gone.

BOOL CCWinListBox::OnEraseBkgnd(CDC* pDC) 
{
    return TRUE;
}

Steps 14 through 20 will go over the code to draw the listbox items, implementing, in the process, methods to allow the hosting dialog to add, change, insert, and remove items. After these seven steps, it will be possible to see items within the area of the custom control if these are added from the hosting dialog.

Step #14. Indeed, let's turn the control into a listbox. A CTypedPtrArray will collect information for each listbox entry. Create the protected class CListBoxItem within the CCWinListBox class and declare a typed pointer array member variable over it. Don't forget to include the header "afxtempl.h" to be able to use the array template:

#include <afxtempl.h>


// Item class

class CListBoxItem : public CObject
{
public:
    CListBoxItem()
    {
        csLabel.Empty();
        rItem.SetRectEmpty();
        bSelected = FALSE;
    }
    
    virtual ~CListBoxItem()
    {
        csLabel.Empty();
    }

    CString  csLabel;
    COLORREF crFace;
    CRect    rItem;
    BOOL     bSelected;
};

// Listbox item storage

CTypedPtrArray < CObArray, CListBoxItem* > m_tpaItems;
int                                        m_iTotalItems;

Step #15. Initialize the number of items to zero in the constructor and remove all items in the destructor.

m_iTotalItems = 0;      // in the constructor


RemoveAll( FALSE );     // in the destructor

Step #16. Of course, we need to declare and implement a RemoveAll method. It should be public so that it can also be called from the hosting dialog (in which case, the control should be redrawn). Its implementation deletes each CListBoxItem object before removing all pointers from the array. Don't forget to reset the total number of elements - m_iTotalItems - to 0.

void CCWinListBox::RemoveAll( BOOL bInvalidate /* = TRUE  */ )
{
    for( int iIndex = 0; iIndex < m_iTotalItems; iIndex++ )
        delete m_tpaItems[ iIndex ];

    m_tpaItems.RemoveAll();
    m_iTotalItems = 0;

    if( bInvalidate )
        Invalidate();
}

Step #17. Now that we have a structure to store the listbox items, that is properly initialized and destroyed, we are ready to write public methods to add, change, insert, and remove items. Their implementation should be self-explanatory:

void CCWinListBox::AddString( CString csLabel )
{
    AddStringWithColor( csLabel, m_crText );
}

void CCWinListBox::AddStringWithColor( CString csLabel, COLORREF crFace )
{
    m_tpaItems.SetAtGrow( m_iTotalItems, new CListBoxItem() );

    m_tpaItems[ m_iTotalItems ]->csLabel = csLabel;
    m_tpaItems[ m_iTotalItems ]->crFace  = crFace;

    m_iTotalItems++;

    Invalidate();
}

void CCWinListBox::ChangeStringAt( int iIndex, CString csLabel )
{
    ChangeStringAndColorAt( iIndex, csLabel, m_tpaItems[ iIndex ]->crFace );
}

void CCWinListBox::ChangeColorAt( int iIndex, COLORREF crFace )
{
    ChangeStringAndColorAt( iIndex, m_tpaItems[ iIndex ]->csLabel, crFace );
}

void CCWinListBox::ChangeStringAndColorAt( int iIndex, 
                        CString csLabel, COLORREF crFace )
{
    if( iIndex >= 0 && iIndex < m_iTotalItems )
    {
        m_tpaItems[ iIndex ]->csLabel   = csLabel;
        m_tpaItems[ iIndex ]->crFace    = crFace;

        Invalidate();
    }
}

void CCWinListBox::InsertString( int iIndex, CString csLabel )
{
    InsertStringWithColor( iIndex, csLabel, m_crText );
}

void CCWinListBox::InsertStringWithColor( int iIndex, 
                            CString csLabel, COLORREF crFace )
{
    // iIndex == m_ITotalItems is ok, will be appended in last position

    if( iIndex >= 0 && iIndex <= m_iTotalItems )
    {
       m_tpaItems.InsertAt( iIndex, new CListBoxItem() );

        m_tpaItems[ iIndex ]->csLabel = csLabel;
        m_tpaItems[ iIndex ]->crFace  = crFace;

        m_iTotalItems++;

        Invalidate();
    }
}

void CCWinListBox::RemoveAt( int iIndex )
{
    if( iIndex >= 0 && iIndex < m_iTotalItems )
    {
        m_tpaItems.RemoveAt( iIndex );

        m_iTotalItems--;

        Invalidate();
    }
}

Step #18. Let's also provide public methods to retrieve the total number of items currently in the listbox as well as the label and color of a given item (by means of its index position).

int CCWinListBox::GetCount()
{
    return m_iTotalItems;
}

CString CCWinListBox::GetStringAt( int iIndex )
{
    if( iIndex >= 0 && iIndex < m_iTotalItems )
        return m_tpaItems[ iIndex ]->csLabel;
    else
        return "";
}

COLORREF CCWinListBox::GetColorAt( int iIndex )
{
    if( iIndex >= 0 && iIndex < m_iTotalItems )
        return m_tpaItems[ iIndex ]->crFace;
    else
        return m_crText;    // m_crText acts as default

}

Step #19. Since this will be a horizontal listbox, we need to decide how wide the columns will be. An appropriate public method SetColumnWidth is therefore declared and implemented. Note that the column width can be made variable without much complication so that, for example, the width of each depends on the size of the largest item in it.

// declaration

virtual CCWinListBox& SetColumnWidth( int iNumberOfCharacters, 
                                    BOOL bInvalidate = TRUE );

// implementation

CCWinListBox& CCWinListBox::SetColumnWidth( int iNumberOfCharacters, 
                                     BOOL bInvalidate /* = TRUE  */)
{
    CDC*  pDC        = GetDC();
    CFont *pPrevFont = pDC->SelectObject( &m_Font );

    TCHAR strWidestText[100];
    
    memset( strWidestText, 0, 100 );

    // Let's use a character likely to be wide

    memset( strWidestText, 'X', 
      ( iNumberOfCharacters < 100 )? iNumberOfCharacters:100 );
    
    // Calculate the lenght of the string under

    // this font plus 4 pixel padding

    m_iColumnWidth = ( pDC->GetTextExtent( strWidestText ) ).cx + 4;

    pDC->SelectObject( pPrevFont );
    ReleaseDC( pDC );

    if( bInvalidate )
        Invalidate();

    return *this;
}

Step #20. We are now ready to draw the listbox contents. First of all, are there any? If so, loop through the array while determining the position of the item to be drawn. Stop the drawing once items' rectangles fall out of sight. The code below goes in the OnPaint method implementation, and is placed right after the DrawGradientBackground call and before drawing the borders.

// Are there any items to draw?

if( m_iTotalItems > 0 )
{
    CFont* pPrevFont = pDCMem->SelectObject( &m_Font );

    if( m_iItemHeight == - 1 )
        m_iItemHeight = 
        pDCMem->GetTextExtent( m_tpaItems[ 0 ]->csLabel ).cy + 4;

    // A default value of 10 characters

    // in case no column width was set

    if( m_iColumnWidth == -1 )
        SetColumnWidth( 10, FALSE );

    CRect rItem;

    rItem.left   = rClient.left + 2; // left padding

    rItem.top    = rClient.top;
    rItem.right  = rItem.left + m_iColumnWidth;
    rItem.bottom = rItem.top + m_iItemHeight;

    int       iPrevMode  = pDCMem->SetBkMode( TRANSPARENT );
    COLORREF  crPrevText = pDCMem->SetTextColor( m_crText );

    BOOL bStopDrawing = FALSE;

    // Loop through items and draw them

    for( int iIndex = 0; iIndex < m_iTotalItems; iIndex++ )
    {
        // next column?

        if( rItem.bottom > rClient.bottom )
        {
            rItem.left   += m_iColumnWidth + 2;
            rItem.top    = rClient.top;
            rItem.right  = rItem.left + m_iColumnWidth;
            rItem.bottom = rItem.top + m_iItemHeight;

            if( rItem.right > rClient.right )      // Just prettier

                rItem.right = rClient.right - 3;

            // out of sight?

            if( rItem.left > rClient.right )
                bStopDrawing = TRUE;
        }

        if( !bStopDrawing )
        {
            // Is the item selected?

            if( m_tpaItems[ iIndex ]->bSelected )
            {
                pDCMem->FillSolidRect( rItem.left, rItem.top + 2,
                                       rItem.Width(), rItem.Height() - 4,
                                       m_crSelTextBk );
                pDCMem->Draw3dRect ( rItem.left, rItem.top + 2, 
                        rItem.Width(), rItem.Height() - 4,
                        m_crSelBorder1, m_crSelBorder2 );
                           
                pDCMem->SetTextColor( m_crSelText );
            }
            else
                pDCMem->SetTextColor( m_tpaItems[ iIndex ]->crFace );

            // Now the text

            pDCMem->DrawText( m_tpaItems[ iIndex ]->csLabel,
                              CRect( rItem.left + 2, rItem.top, 
                              rItem.right, rItem.bottom ),
                              DT_LEFT | DT_SINGLELINE | DT_VCENTER );

            // Record item rectangle

            m_tpaItems[ iIndex ]->rItem = rItem;
        }
        else
            m_tpaItems[ iIndex ]->rItem.SetRectEmpty();

        // Next item rectangle

        rItem.top    = rItem.bottom;
        rItem.bottom = rItem.top + m_iItemHeight;
    }
    
    pDCMem->SelectObject( pPrevFont );
    pDCMem->SetBkMode( iPrevMode );
    pDCMem->SetTextColor( crPrevText );
}

Steps 21 and 22 will go over the code to detect mouse clicks over the control area. After these two steps, it will be possible to select an item and see its background toggle.

Step #21. Adding a WM_LBUTTONDOWN message handler - via ClassWizard - will allow us to detect if the user has selected an item by clicking on it with the mouse. The implementation loops through the array of items, finds the one that has been clicked on, and deselects all others in the process.

void CCWinListBox::OnLButtonDown(UINT nFlags, CPoint point) 
{
    for( int iIndex = 0; iIndex < m_iTotalItems; iIndex++ )
      if( m_tpaItems[ iIndex ]->rItem.PtInRect( point ) )
        m_tpaItems[ iIndex ]->bSelected = !m_tpaItems[ iIndex ]->bSelected;
      else
        m_tpaItems[ iIndex ]->bSelected = FALSE;

    Invalidate();
    
    CWnd::OnLButtonDown(nFlags, point);
}

Step #22. It will be of utmost kindness to allow the hosting dialog to find out which item is selected, if any. Below is the implementation of a public method that returns the index of the selected item or -1 if none is selected.

int CCWinListBox::GetSelectedItem()
{
    int iSelectedIndex = -1;

    for( int iIndex = 0; iIndex < m_iTotalItems 
                         && iSelectedIndex == -1; iIndex++ )
        if( m_tpaItems[ iIndex ]->bSelected )
            iSelectedIndex = iIndex;

    return iSelectedIndex;
}

Steps 23 through 27 will go over the code to implement the scrollbar. Modifications will be made to the implementation of the OnPaint and OnLButtonDown methods. After these five steps, the contents of the listbox will shift in response to mouse clicks.

Step #23. As proposed, the scrollbar will have four button-like divisions and a center display: shift to the leftmost column, shift to the left one column, display the total number of elements currently in the listbox, shift to the right one column, and shift to the rightmost column.

In order to remember where the divisions lay and whether they are clicked or not, a typed pointer array is declared over the class CScrollBarDiv (declared and implemented as shown below). The array contains the positions of the rectangles for each button as well as their corresponding labels. If clicked on, the appropriate division will be flagged for painting - and to carry out the scroll operation - in the OnPaint method. The flag m_bCalculateSB_Div is provided so that the coordinates of each division are calculated only once (in the OnPaint method) and can also be useful if the custom control is hosted by one of those naughty resizable dialogs (in which case, setting m_bCalculateSB_Div to TRUE will get the scrollbar divisions recalculated and resized dynamically).

// Scrollbar divisions

class CScrollBarDiv : public CObject
{
public:
    CScrollBarDiv()
    {
        csLabel.Empty();
        rItem.SetRectEmpty();
        bPressed = FALSE;
    }
    
    virtual ~CScrollBarDiv()
    {
        csLabel.Empty();
    }
    
    CString csLabel;
    CRect   rItem;
    BOOL    bPressed;
};

CTypedPtrArray<CObArray, CScrollBarDiv*> m_tpaSB;
BOOL                                     m_bCalculateSB_Div;

Step #24. The methods OnLButtonDown and OnLButtonUp loop through the scrollbar array to select and deselect the division (button) where the mouse click occurred. The central section (division #2) is reserved to display a count of the items in the listbox, so this division is not subject to mouse clicking. The modification to OnLButtonDown is shown below. For OnLButtonUp, add a message handler for WM_LBUTTONUP - via ClassWizard - and populate it with the code below. Also, add a message handler for WM_TIMER to allow the listbox to continue to scroll if the mouse button remains pressed.

void CCWinListBox::OnLButtonDown(UINT nFlags, CPoint point) 
{
    if( point.y > m_tpaSB[ 0 ]->rItem.top )
    {
        for( int i = 0; i < 5; i++ )
            if( i != 2 && m_tpaSB[ i ]->rItem.PtInRect( point ) )
            {
                m_tpaSB[ i ]->bPressed = TRUE;

                if( i == 1 || i == 3 )
                    SetTimer( 1, 250, NULL );

                break;
           }
    }
    else
    {
        for( int iIndex = 0; iIndex < m_iTotalItems; iIndex++ )
            if( m_tpaItems[ iIndex ]->rItem.PtInRect( point ) )
                m_tpaItems[ iIndex ]->bSelected = 
                              !m_tpaItems[ iIndex ]->bSelected;
            else
                m_tpaItems[ iIndex ]->bSelected = FALSE;
    }

    Invalidate();
    
    CWnd::OnLButtonDown(nFlags, point);
}

void CCWinListBox::OnLButtonUp(UINT nFlags, CPoint point) 
{
    for( int i = 0; i < 5; i++ )
        if( m_tpaSB[ i ]->bPressed )
        {
            m_tpaSB[ i ]->bPressed = FALSE;

            Invalidate();

            if( i == 1 || i == 3 )
                KillTimer( 1 );

            break;
        }
    
    CWnd::OnLButtonUp(nFlags, point);
}

void CCWinListBox::OnTimer(UINT nIDEvent) 
{
    switch( nIDEvent )
    {
    case 1:
        Invalidate();
    break;
    }
    
    CWnd::OnTimer(nIDEvent);
}

Step #25. In order to simplify the appearance and readability of the code in the onPaint method, two helper protected methods have been implemented: CalculateSBDivisions and DrawSB_ShiftColumn. The first simply sections the scrollbar area, calculating and storing the location of each division. The second draws the border of each division swapping colors to display the click/idle toggle. Most importantly, however, it modifies the global m_iDisplayColumn attribute according to which division, if any, is currently being clicked on.

void CCWinListBox::CalculateSBDivisions( CRect rSBRect )
{
    m_bCalculateSB_Div = FALSE;

    int iWidth = rSBRect.Width() / 5;
    int iAddum = rSBRect.Width() % 5; // extra pixels?


    for( int i = 0; i < 5; i++ )
    {
        m_tpaSB[ i ]->rItem = CRect( rSBRect.left + ( i * iWidth ), 
                              rSBRect.top, 1 + ( i + 1 ) * iWidth, 
                              rSBRect.bottom );

        switch ( i )
        {
            case 2:
                 // add extra pixels to the center division

                m_tpaSB[ 2 ]->rItem.right += iAddum;
                break;
            case 3:
                 // shift position to account for extra pixels

                m_tpaSB[ 3 ]->rItem.OffsetRect( iAddum, 0 );
                break;
            case 4:
                 // shift position to account for extra pixels

                m_tpaSB[ 4 ]->rItem.OffsetRect( iAddum, 0 );
                break;
        }
    }
}

void CCWinListBox::DrawSB_ShiftColumn( CDC *pDC, CRect rSBArea )
{
    pDC->FillSolidRect( rSBArea, m_crSB_Bk );

    for( int i = 0; i < 5; i++ ) // draw the scrollbar

    {
        CRect rItem( m_tpaSB[ i ]->rItem );

        // display or button section

        if( i == 2 )
        {
            pDC->Draw3dRect( rItem, m_crSB_Border1, m_crSB_Border1 );
            rItem.DeflateRect( 2, 2, 2, 2 );
            pDC->Draw3dRect( rItem, m_crSB_Border2, m_crSB_Border2 );
        }
        else
            pDC->Draw3dRect( rItem,
                 ( m_tpaSB[ i ]->bPressed )? m_crSB_Border1:m_crSB_Border2,
                 ( m_tpaSB[ i ]->bPressed )? m_crSB_Border2:m_crSB_Border1 );

        pDC->SetTextColor( m_crSB_Text );
        pDC->DrawText( m_tpaSB[ i ]->csLabel, rItem, 
                       DT_CENTER | DT_SINGLELINE | DT_VCENTER );

        // if a scrollbar division is currently clicked on,

        // modify the display column

        if( m_tpaSB[ i ]->bPressed )
            switch ( i )
            {
                case 0:
                    m_iDisplayColumn = 0;
                    break;
                case 1:
                    m_iDisplayColumn -= ( m_iDisplayColumn == 0 )? 0:1;

                    if( m_iDisplayColumn == 0 )
                        KillTimer( 1 ); // no need to continue repainting


                    break;
                case 3:
                    m_iDisplayColumn += 
                               ( m_iDisplayColumn == m_iMaxColumn )? 0:1;

                    if( m_iDisplayColumn == m_iMaxColumn )
                        KillTimer( 1 ); // no need to continue repainting


                    break;
                case 4:
                    m_iDisplayColumn = m_iMaxColumn;
                    break;
            }
    }
}

Step #26. In order to be of any use, the scrollbar center display needs to reflect (real-time) any change in the number of items in the listbox. One could include the code to do this in DrawSB_ShiftColumn, or in those methods that alter the number of items in the listbox, namely, AddStringWithColor, InsertStringWithColor, RemoveAt, and RemoveAll. Either way, all that is required is to include the following line:

m_tpaSB[ 2 ]->csLabel.Format( _T("%d"), m_iTotalItems );

Step #27. Let's now modify the code in the onPaint method. The important bits are inside the for loop: first, the field rItem.left continues to be reset to rClient.left + 2 (the left margin) until we reach the correct column to display; second, drawing is prevented until - again - the correct column to display has been reached.

Note that the methods CalculateSBDivisions and DrawSB_ShiftColumn are called before the items are drawn. This is important because the attribute m_iDisplayColumn is modified - to adjust to the shift caused by mouse clicking - inside DrawSB_ShiftColumn. The animal now looks like this.

// Are there any items to draw?

if( m_iTotalItems > 0 )
{
    CFont* pPrevFont = pDCMem->SelectObject( &m_Font );
    
    if( m_iItemHeight == - 1 )
        m_iItemHeight = 
           pDCMem->GetTextExtent( m_tpaItems[ 0 ]->csLabel ).cy + 4;

    // Deal with the scrollbar first

    CRect rSB( rClient );

    rSB.top        = rSB.bottom - m_iItemHeight;
    rClient.bottom -= m_iItemHeight + 2;
    // adjust client area so that items

    // are not drawn over the scrollbar area


    if( m_bCalculateSB_Div ) // need to calculate scrollbar divisions?

        CalculateSBDivisions( rSB );

    int      iPrevMode  = pDCMem->SetBkMode( TRANSPARENT );
    COLORREF crPrevText = pDCMem->SetTextColor( m_crText );

    DrawSB_ShiftColumn( pDCMem, rSB ); // adjust shift and draw scrollbar


    // A default value of 10 characters in case no column width was set

    if( m_iColumnWidth == -1 )
        SetColumnWidth( 10, FALSE );

    // Now draw the contents of the listbox

    CRect rItem;

    rItem.left   = rClient.left + 2; // left padding

    rItem.top    = rClient.top;
    rItem.right  = rItem.left + m_iColumnWidth;
    rItem.bottom = rItem.top + m_iItemHeight;

    int  iCurrentColumn = 0;
    BOOL bStopDrawing   = FALSE;

    // Loop through items and draw them

    for( int iIndex = 0; iIndex < m_iTotalItems; iIndex++ )
    {
        // next column?

        if( rItem.bottom > rClient.bottom )
        {
            iCurrentColumn++;

            // Once the column to display has been reached

            // positions are advanced as usual

            // otherwise, reset the left position each time

            if( iCurrentColumn > m_iDisplayColumn )
                rItem.left += m_iColumnWidth + 2;
            else
                rItem.left = rClient.left + 2;
            
            rItem.top    = rClient.top;
            rItem.right  = rItem.left + m_iColumnWidth;
            rItem.bottom = rItem.top + m_iItemHeight;

            if( rItem.right > rClient.right ) // Just prettier

                rItem.right = rClient.right - 3;

            // out of sight?

            if( rItem.left > rClient.right )
                bStopDrawing = TRUE;
        }

        // Only draw the items that can be seen

        // otherwise set the items' rectangles to 0

        if( iCurrentColumn >= m_iDisplayColumn && !bStopDrawing )
        {
            // Is the item selected?

            if( m_tpaItems[ iIndex ]->bSelected )
            {
                pDCMem->FillSolidRect( rItem.left, rItem.top + 2,
                                       rItem.Width(), rItem.Height() - 4,
                                       m_crSelTextBk );
                pDCMem->Draw3dRect ( rItem.left, rItem.top + 2, 
                        rItem.Width(), rItem.Height() - 4,
                        m_crSelBorder1, m_crSelBorder2 );
                            
                pDCMem->SetTextColor( m_crSelText );
            }
            else
                pDCMem->SetTextColor( m_tpaItems[ iIndex ]->crFace );

            // Now the text

            pDCMem->DrawText( m_tpaItems[ iIndex ]->csLabel,
                              CRect( rItem.left + 2, rItem.top, 
                              rItem.right, rItem.bottom ),
                              DT_LEFT | DT_SINGLELINE | DT_VCENTER );

            // Record item rectangle

            m_tpaItems[ iIndex ]->rItem = rItem;
        }
        else
            m_tpaItems[ iIndex ]->rItem.SetRectEmpty();

        // Next item's rectangle

        rItem.top    = rItem.bottom;
        rItem.bottom = rItem.top + m_iItemHeight;
    }

    pDCMem->SelectObject( pPrevFont );
    pDCMem->SetBkMode( iPrevMode );
    pDCMem->SetTextColor( crPrevText );

    m_iMaxColumn = iCurrentColumn;
}

Step #28. Compile and run the application. Having realized the target functionality, we are, thus, finished.

The custom listbox is nicer to look at than it is useful. Nonetheless, I hope that you have picked up something relevant from its assembly and can now tackle more interesting and complex ventures.

Evolution

I am keen on this. My only intention is to provide an example that is coded clearly, as simple to understand and follow as possible. I am sure that there are finer solutions to the functionality I have implemented here. Any suggestions that improve, simplify, or better explain the code are welcome.

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