Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / Languages / C++

Owner Drawn CListBox - Version 2

4.50/5 (4 votes)
13 Feb 2011CPOL3 min read 41.4K   3.9K  
An owner drawn CListBox, expands items to look like a CTreeCtrl

Owner_Draw_CListBox/11.png

Introduction

This is a little similar to the previous article whose link is here. The difference is, this CMultiLineListBox class supports dynamic multi-line display. When user clicks or chooses an item in the ListBox, the item will be expanded to show more information. It looks like a CTreeCtrl control.

Implementation

The CMultiLineListBox is derived from CListBox. Important: You must override DrawItem and MeasureItem virtual functions. The two functions complete the main drawing operation. In addition, it handles window messages like WM_ERASEBKGND, WM_KEYDOWN, WM_LBUTTONDOWN, WM_MOUSEMOVE and custom messages MSG_UPDATEITEM. The MSG_UPDATEITEM message is posted when the user clicks an item, drags the mouse or presses a direction key (up / down keys).

In this class, we define an important struct named LISTBOX_INFO. This struct stores information for each item in ListBox. The struct definition is like this:

C++
// This struct store information for each item in ListBox
typedef struct _LISTBOX_INFO_
{
public:
 typedef struct _SUBNODE_INFO_ // Subnode properties
 {
 public:
  CString strText;             // text content, default value is _T("")
  COLORREF fgColor;            // foreground color, default color is black
  COLORREF bgColor;            // background color, default color is white
  _SUBNODE_INFO_()             // constructor
  {
   clean();
  }
  ~_SUBNODE_INFO_()            // destructor
  {
   clean();
  }
 protected:
  inline void clean(void)      // inline function used to initialize member variable
  {
   strText.Empty();
   fgColor = RGB_FOREGROUND;
   bgColor = RGB_BACKGROUND;
  }
 }SUBNODEINFO, *PSUBNODEINFO;
public:
 vector<SUBNODEINFO*> subArray; // Node properties, pre item maybe include many of subnode
 CString strText;               // text content, default value is _T("")
 COLORREF fgColor;              // foreground color, default color is black
 COLORREF bgColor;              // background color, default color is white
 _LISTBOX_INFO_()               // constructor
 {
  clean();
 }
 ~_LISTBOX_INFO_()              // destructor
 {
  clean();
 }
protected:
 inline void clean(void)        // inline function used to initialize member variable
 {
  subArray.clear();
  strText.Empty();
  fgColor = RGB_FOREGROUND;
  bgColor = RGB_BACKGROUND;
 }
}LISTBOXINFO, * PLISTBOXINFO;

In order to use this LISTBOXINFO struct, the custom member functions InsertString, AddString, AddSubString help us to add context to the ListBox.

Using the Code

  • InsertString: Custom member function, used to provide a public interface for external calls. This function has four parameters, insert index, text content and the foreground / background color that you set.
C++
/* Custom member function, Insert string and set foreground 
   and background color for each item in ListBox. The 
   return value is current insert index value. */
int CMultiLineListBox::InsertString
    (int nIndex, LPCTSTR pszText, COLORREF fgColor, COLORREF bgColor)
{
 LISTBOXINFO* pListBox = new LISTBOXINFO;              // new and initialize
 ASSERT(pListBox);
 ASSERT((nIndex >= 0) && (nIndex <= GetCount()));
 pListBox->strText = pszText;
 pListBox->fgColor = fgColor;
 pListBox->bgColor = bgColor;
 m_sArray.insert(m_sArray.begin() + nIndex, pListBox); // insert list
 return CListBox::InsertString(nIndex, pszText);       // call base class InsertString function
}        
  • AddString: Custom member function, used to provide public interface for external call. This function has three parameters, text content and foreground/background color you set.
C++
/* Custom member function, append string and 
   set foreground and background color for each item in ListBox. The 
return value is current insert index value. */
int CMultiLineListBox::AddString(LPCTSTR pszText, COLORREF fgColor, COLORREF bgColor)
{
 LISTBOXINFO* pListBox = new LISTBOXINFO; // new and initialize
 ASSERT(pListBox);
 pListBox->strText = pszText;
 pListBox->fgColor = fgColor;
 pListBox->bgColor = bgColor;
 m_sArray.push_back(pListBox);            // add to list
 return CListBox::AddString(pszText);     // call base class AddString function
}
  • AddSubString: Custom member function, used to provide a public interface for external calls. This function has four parameters, insert index, text content and the foreground / background color that you set.
C++
/* Custom member function, append subnode string and 
   set foreground and background color for each item in ListBox. 
The return value is current insert index value. */
void CMultiLineListBox::AddSubString
(int nIndex, LPCTSTR pszText, COLORREF fgColor, COLORREF bgColor)
{
 ASSERT((nIndex >=0) && (nIndex < GetCount()));
 
 ASSERT(!m_sArray.empty());
 LISTBOXINFO* pListBox = m_sArray.at(nIndex);
 ASSERT(pListBox);
 LISTBOXINFO::SUBNODEINFO* pSubNode = new LISTBOXINFO::SUBNODEINFO; // new and initialize
 ASSERT(pSubNode);
 pSubNode->strText = pszText;
 pSubNode->fgColor = fgColor;
 pSubNode->bgColor = bgColor;
 pListBox->subArray.push_back(pSubNode); // add to subnode list
}
  • DrawItem: Override virtual function, used to draw text and set foreground / background color for each item in the ListBox.
C++
/* DrawItem virtual function, draw text and color for each item and subnode. */
void CMultiLineListBox::DrawItem(LPDRAWITEMSTRUCT lpDrawItemStruct)
{
 // TODO:  Add your code to draw the specified item
 ASSERT(lpDrawItemStruct->CtlType == ODT_LISTBOX);
 int nIndex = lpDrawItemStruct->itemID;
 if((!m_sArray.empty())  && (nIndex < static_cast<int>(m_sArray.size())))
 {
  CDC dc;
  dc.Attach(lpDrawItemStruct->hDC);
  // Save these value to restore them when done drawing.
  COLORREF crOldTextColor = dc.GetTextColor();
  COLORREF crOldBkColor = dc.GetBkColor();
  // If this item is selected, set the background color 
  // and the text color to appropriate values. Also, erase
  // rect by filling it with the background color.
  CRect rc(lpDrawItemStruct->rcItem);
  
  LISTBOXINFO* pListBox = m_sArray.at(nIndex);
  ASSERT(pListBox);
  if ((lpDrawItemStruct->itemAction | ODA_SELECT) &&
   (lpDrawItemStruct->itemState & ODS_SELECTED))
  {
   dc.SetTextColor(pListBox->bgColor);
   dc.SetBkColor(pListBox->fgColor);
   dc.FillSolidRect(&rc, pListBox->fgColor);
   // Draw item the text.
   CRect rect(rc);
   int nItemCount = 1;
   nItemCount += static_cast<int>(pListBox->subArray.size());
   int nItemHeight = rc.Height() / nItemCount;
   rect.bottom = rect.top + nItemHeight;
   dc.DrawText(pListBox->strText, 
               pListBox->strText.GetLength(), CRect(rect.left + 5, rect.top, 
rect.right, rect.bottom), DT_SINGLELINE | DT_VCENTER);
   
   // Draw subitem the text.
   CRect rcItem;
   rcItem.SetRectEmpty();
   rcItem.top = rect.bottom;
   rcItem.left = rect.left;
   rcItem.right = rect.right;
   rcItem.bottom = rcItem.top + nItemHeight;
   
   vector<LISTBOXINFO::SUBNODEINFO*>::const_iterator iter = pListBox->subArray.begin();
   for(; iter != pListBox->subArray.end(); ++iter)
   {
    LISTBOXINFO::SUBNODEINFO* pSubNode = *iter;
     dc.SetTextColor(pSubNode->fgColor);
     dc.SetBkColor(pSubNode->bgColor);
     dc.FillSolidRect(&rcItem, pSubNode->bgColor);
    CRect rectItem(rcItem);
    rectItem.left += 22;
    dc.DrawText(pSubNode->strText, pSubNode->strText.GetLength(), &rectItem, 
DT_SINGLELINE | DT_VCENTER);
    
    rcItem.top = rcItem.bottom;
    rcItem.bottom = rcItem.top + nItemHeight;
   }
   dc.DrawFocusRect(rc); // Draw focus rect
  }
  else
  {
   dc.SetTextColor(pListBox->fgColor);
   dc.SetBkColor(pListBox->bgColor);
   dc.FillSolidRect(&rc, pListBox->bgColor);
   // Draw the text.
   CRect rect(rc);
   rect.left += 5;
   dc.DrawText(pListBox->strText, pListBox->strText.GetLength(), &rect, DT_SINGLELINE | 
DT_VCENTER);
  }
  // Reset the background color and the text color back to their
  // original values.
  dc.SetTextColor(crOldTextColor);
  dc.SetBkColor(crOldBkColor);
  dc.Detach();
 }
}
  • MeasureItem: Override virtual function, used to calculate current text height for each item in the ListBox.
C++
// MeasureItem virtual function, calculate text height, 
// but the height value is fixed value in here.
void CMultiLineListBox::MeasureItem(LPMEASUREITEMSTRUCT lpMeasureItemStruct)
{
 // TODO:  Add your code to determine the size of specified item
 ASSERT(lpMeasureItemStruct->CtlType == ODT_LISTBOX);
 lpMeasureItemStruct->itemHeight = ITEM_HEIGHT;
}
  • OnEraseBkgnd: WM_ERASEBKGND message handler function, draws background color in the ListBox.
C++
BOOL CMultiLineListBox::OnEraseBkgnd(CDC* pDC)
{
 // Set listbox background color
 CRect rc;
 GetClientRect(&rc);
 
 CDC memDC;
 memDC.CreateCompatibleDC(pDC);
 ASSERT(memDC.GetSafeHdc());
 CBitmap bmp;
 bmp.CreateCompatibleBitmap(pDC, rc.Width(), rc.Height());
 ASSERT(bmp.GetSafeHandle());
 CBitmap* pOldbmp = (CBitmap*)memDC.SelectObject(&bmp);
 memDC.FillSolidRect(rc, LISTBOX_BACKGROUND); // Set background color which you want
 pDC->BitBlt(0, 0, rc.Width(), rc.Height(), &memDC, 0, 0, SRCCOPY);
 memDC.SelectObject(pOldbmp);
 bmp.DeleteObject();
 memDC.DeleteDC();
 return TRUE;
}
  • OnKeyDown: WM_KEYDOWN message handler function, when user press direction key.
C++
void CMultiLineListBox::OnKeyDown(UINT nChar, UINT nRepCnt, UINT nFlags)
{
 // TODO: Add your message handler code here and/or call default
 CListBox::OnKeyDown(nChar, nRepCnt, nFlags);
 UpdateItem();
}
  • OnLButtonDown: WM_LBUTTONDOWN message handler function, when user click item.
C++
void CMultiLineListBox::OnLButtonDown(UINT nFlags, CPoint point)
{
 // TODO: Add your message handler code here and/or call default
 CListBox::OnLButtonDown(nFlags, point);
 UpdateItem();
}
  • OnMouseMove: WM_MOUSEMOVE message handler function, when user drag mouse.
C++
void CMultiLineListBox::OnMouseMove(UINT nFlags, CPoint point)
{
 // TODO: Add your message handler code here and/or call default
 CListBox::OnMouseMove(nFlags, point);
 UpdateItem();
}
  • UpdateItem: Custom member function, used to user click item, press direction key or drag mouse. The windows message WM_LBUTTONDOWN /WM_KEYDOWN / WM_MOUSEMOVE handler function call this custom function to update item in ListBox.
C++
void CMultiLineListBox::UpdateItem()
{
 // If per item height not equal, 
 // you must calculate area between the current focus item with last one,
 // otherwise you must calculate area 
 // between the current focus item with previously focus item.
 int nIndex = GetCurSel();
 if((CB_ERR != nIndex) && (m_nFocusIndex != nIndex))
 {
  PostMessage(MSG_UPDATEITEM, (WPARAM)m_nFocusIndex, (LPARAM)nIndex);
  m_nFocusIndex = nIndex; // Set current select focus index
 }
}
  • OnUpdateItem: Custom message handler function, used to handler cutom message MSG_UPDATEITEM to refresh item status.
C++
LRESULT CMultiLineListBox::OnUpdateItem(WPARAM wParam, LPARAM lParam)
{
 // MSG_UPDATEITEM message handler
 int nPreIndex = static_cast<int>(wParam);
 int nCurIndex = static_cast<int>(lParam);
 if(-1 != nPreIndex)
 {
  SetItemHeight(nPreIndex, ITEM_HEIGHT);
 }
 
 if(-1 != nCurIndex)
 {
  int nItemCount = 1;
  LISTBOXINFO* pListBox = m_sArray.at(nCurIndex);
  ASSERT(pListBox);
  nItemCount += static_cast<int>(pListBox->subArray.size());
  SetItemHeight(nCurIndex, ITEM_HEIGHT * nItemCount);
 }
  Invalidate(); // Update item
 return 0;
}

How to Use the Control

To integrate MultiLineListBox into your own project, you first need to add the following files to your project:

  • MultiLineListBox.h
  • MultiLineListBox.cpp

Two methods to use this control class. One is a static associate, the other is dynamic create.
First, you will also need add ListBox control to dialog template. Next, include header file MultiLineListBox.h in dialog's h file, and create a CMultiLineListBox variable (or use Class Wizard to generate a variable for CListBox object, but revised CListBox to CMultiLineListBox in .h and .cpp files).

Note: This ListBox must have styles: Owner draw: variable, Selection: Single, Has strings: TRUE, Sort: FALSE.
Finally, add the following code to OnInitDialog function in dialog.cpp file.

C++
// OnInitDialog
...
COLORREF clr[][2] = 
 {
  {RGB(53, 0, 27), RGB(236, 255, 236)},
  {RGB(66, 0, 33), RGB(233, 255, 233)},
  {RGB(85, 0, 43), RGB(204, 255, 204)},
  {RGB(106, 0, 53), RGB(191, 255, 191)},
  {RGB(119, 0, 60), RGB(9, 255, 9)},
  {RGB(136, 0, 68), RGB(0, 236, 0)},
  {RGB(155, 0, 78), RGB(0, 225, 0)},
  {RGB(168, 0, 84), RGB(0, 204, 0)},
  {RGB(170, 0, 85), RGB(0, 185, 0)},
  {RGB(187, 0, 94), RGB(0, 170, 0)},
  {RGB(206, 0, 103), RGB(0, 151, 0)},
  {RGB(211, 0, 111), RGB(0, 136, 0)},
  {RGB(236, 0, 118), RGB(0, 117, 0)},
  {RGB(255, 108, 182), RGB(0, 98, 0)},
  {RGB(255, 121, 188), RGB(0, 89, 0)},
  {RGB(255, 138, 197), RGB(0, 70, 0)},
  {RGB(255, 157, 206), RGB(0, 53, 0)},
  {RGB(255, 170, 212), RGB(0, 36, 0)},
  {RGB(255, 193, 224), RGB(0, 21, 0)}
 };
 CString strText(_T(""));
 int nIndex = -1;
 for(int i=0; i<sizeof(clr)/sizeof(clr[0]); i++) // Add item in ListBox
 {
  strText.Format(_T("%02d - Hello, World!"), i+1);
  nIndex = m_listBox.AddString(strText, clr[i][0], clr[i][1]);
  if(i % 2)
  {
   for(int j=0; j<3; j++) // Add subnode to item in ListBox
   {
    strText.Format(_T("%02d.%02d - Hello, World!"), i+1, j+1);
    m_listBox.AddSubString(nIndex, strText, clr[i][1], clr[i][0]);
   }
  }
  else
  {
   for(int j=0; j<2; j++)
   {
    strText.Format(_T("%02d.%02d - Hello, World!"), i+1, j+1);
    m_listBox.AddSubString(nIndex, strText, clr[i][1], clr[i][0]);
   }
  }
 }
...

The other way is dynamic create, use member function Create to generate a CMultiLineListBox object.

Note: You must set LBS_OWNERDRAWVARIABLE and LBS_HASSTRINGS styles in the Create function call.

First, you include header file MultiLineListBox.h in dialog's file. Next, create a CMultiLineListBox variable (or use Class Wizard generate a variable for CListBox object, but revised name CListBox to CMultiLineListBox in .h and .cpp files). Finally, add the following code to OnInitDialog function in dialog.cpp file.

C++
#define IDC_LISTBOX 0x11 // define resource ID
// OnInitDialog
...
CRect rc;
GetClientRect(&rc);
rc.bottom -= 35;
rc.DeflateRect(CSize(10, 10));
m_listBox.Create(WS_CHILD | WS_VISIBLE | WS_BORDER | WS_HSCROLL | WS_VSCROLL |
LBS_OWNERDRAWVARIABLE | LBS_HASSTRINGS, rc, this, IDC_LISTBOX);
...

After adding this code, you can append the above code to add items and subnodes to the ListBox control.

Of course, I believe you can do better than this. Now try it yourself.
Good luck, and thank you!

History

  • 14th February, 2011: Initial version

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)