Owner Drawn CListBox - Version 2
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. Its look 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:
// 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.
/* 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.
/* 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.
/* 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 theListBox
.
/* 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 theListBox
.
// 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 theListBox
.
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.
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.
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.
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 windwos messageWM_LBUTTONDOWN
/WM_KEYDOWN
/WM_MOUSEMOVE
handler function call this custom function to update item in ListBox.
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 messageMSG_UPDATEITEM
to refresh item status.
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. The One is 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.
// 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.
#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 add 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!