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 ) ) )
{
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();
}
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);
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 );
CRect rClient( rArea );
rClient.DeflateRect( 1, 1, 1, 1 );
DrawGradientBackground( pDCMem, rClient, m_crBkFrom, m_crBkTo );
pDCMem->Draw3dRect( rArea, m_crBorder1, m_crBorder2 );
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>
class CListBoxItem : public CObject
{
public:
CListBoxItem()
{
csLabel.Empty();
rItem.SetRectEmpty();
bSelected = FALSE;
}
virtual ~CListBoxItem()
{
csLabel.Empty();
}
CString csLabel;
COLORREF crFace;
CRect rItem;
BOOL bSelected;
};
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;
RemoveAll( FALSE );
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 )
{
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 )
{
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;
}
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.
virtual CCWinListBox& SetColumnWidth( int iNumberOfCharacters,
BOOL bInvalidate = TRUE );
CCWinListBox& CCWinListBox::SetColumnWidth( int iNumberOfCharacters,
BOOL bInvalidate )
{
CDC* pDC = GetDC();
CFont *pPrevFont = pDC->SelectObject( &m_Font );
TCHAR strWidestText[100];
memset( strWidestText, 0, 100 );
memset( strWidestText, 'X',
( iNumberOfCharacters < 100 )? iNumberOfCharacters:100 );
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.
if( m_iTotalItems > 0 )
{
CFont* pPrevFont = pDCMem->SelectObject( &m_Font );
if( m_iItemHeight == - 1 )
m_iItemHeight =
pDCMem->GetTextExtent( m_tpaItems[ 0 ]->csLabel ).cy + 4;
if( m_iColumnWidth == -1 )
SetColumnWidth( 10, FALSE );
CRect rItem;
rItem.left = rClient.left + 2;
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;
for( int iIndex = 0; iIndex < m_iTotalItems; iIndex++ )
{
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 )
rItem.right = rClient.right - 3;
if( rItem.left > rClient.right )
bStopDrawing = TRUE;
}
if( !bStopDrawing )
{
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 );
pDCMem->DrawText( m_tpaItems[ iIndex ]->csLabel,
CRect( rItem.left + 2, rItem.top,
rItem.right, rItem.bottom ),
DT_LEFT | DT_SINGLELINE | DT_VCENTER );
m_tpaItems[ iIndex ]->rItem = rItem;
}
else
m_tpaItems[ iIndex ]->rItem.SetRectEmpty();
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).
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;
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:
m_tpaSB[ 2 ]->rItem.right += iAddum;
break;
case 3:
m_tpaSB[ 3 ]->rItem.OffsetRect( iAddum, 0 );
break;
case 4:
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++ )
{
CRect rItem( m_tpaSB[ i ]->rItem );
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( 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 );
break;
case 3:
m_iDisplayColumn +=
( m_iDisplayColumn == m_iMaxColumn )? 0:1;
if( m_iDisplayColumn == m_iMaxColumn )
KillTimer( 1 );
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.
if( m_iTotalItems > 0 )
{
CFont* pPrevFont = pDCMem->SelectObject( &m_Font );
if( m_iItemHeight == - 1 )
m_iItemHeight =
pDCMem->GetTextExtent( m_tpaItems[ 0 ]->csLabel ).cy + 4;
CRect rSB( rClient );
rSB.top = rSB.bottom - m_iItemHeight;
rClient.bottom -= m_iItemHeight + 2;
if( m_bCalculateSB_Div )
CalculateSBDivisions( rSB );
int iPrevMode = pDCMem->SetBkMode( TRANSPARENT );
COLORREF crPrevText = pDCMem->SetTextColor( m_crText );
DrawSB_ShiftColumn( pDCMem, rSB );
if( m_iColumnWidth == -1 )
SetColumnWidth( 10, FALSE );
CRect rItem;
rItem.left = rClient.left + 2;
rItem.top = rClient.top;
rItem.right = rItem.left + m_iColumnWidth;
rItem.bottom = rItem.top + m_iItemHeight;
int iCurrentColumn = 0;
BOOL bStopDrawing = FALSE;
for( int iIndex = 0; iIndex < m_iTotalItems; iIndex++ )
{
if( rItem.bottom > rClient.bottom )
{
iCurrentColumn++;
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 )
rItem.right = rClient.right - 3;
if( rItem.left > rClient.right )
bStopDrawing = TRUE;
}
if( iCurrentColumn >= m_iDisplayColumn && !bStopDrawing )
{
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 );
pDCMem->DrawText( m_tpaItems[ iIndex ]->csLabel,
CRect( rItem.left + 2, rItem.top,
rItem.right, rItem.bottom ),
DT_LEFT | DT_SINGLELINE | DT_VCENTER );
m_tpaItems[ iIndex ]->rItem = rItem;
}
else
m_tpaItems[ iIndex ]->rItem.SetRectEmpty();
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.