Click here to Skip to main content
15,892,965 members
Articles / Programming Languages / C++

Owner Drawn CListBox - Version 2

Rate me:
Please Sign up or sign in to vote.
4.50/5 (4 votes)
13 Feb 2011CPOL3 min read 40.8K   3.9K   26   5
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)


Written By
Software Developer (Senior)
China China
This member has not yet provided a Biography. Assume it's interesting and varied, and probably something to do with programming.

Comments and Discussions

 
GeneralMy vote of 4 Pin
maplewang9-Aug-12 5:12
maplewang9-Aug-12 5:12 
Good, But Need more detail on how to implement subitem draw. and how to make the scroll bar suitable for items and subitems.
GeneralMemory leak? Pin
Jone_liuVC12-Jun-11 17:13
Jone_liuVC12-Jun-11 17:13 
GeneralRe: Memory leak? Pin
Visual-Eleven12-Jun-11 19:44
Visual-Eleven12-Jun-11 19:44 
GeneralDownload files corrupted Pin
Smitha Nishant4-Mar-11 9:44
protectorSmitha Nishant4-Mar-11 9:44 
GeneralRe: Download files corrupted Pin
un11imig5-Apr-11 16:04
un11imig5-Apr-11 16:04 

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.