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:
typedef struct _LISTBOX_INFO_
{
public:
typedef struct _SUBNODE_INFO_ {
public:
CString strText; COLORREF fgColor; COLORREF bgColor; _SUBNODE_INFO_() {
clean();
}
~_SUBNODE_INFO_() {
clean();
}
protected:
inline void clean(void) {
strText.Empty();
fgColor = RGB_FOREGROUND;
bgColor = RGB_BACKGROUND;
}
}SUBNODEINFO, *PSUBNODEINFO;
public:
vector<SUBNODEINFO*> subArray; CString strText; COLORREF fgColor; COLORREF bgColor; _LISTBOX_INFO_() {
clean();
}
~_LISTBOX_INFO_() {
clean();
}
protected:
inline void clean(void) {
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.
int CMultiLineListBox::InsertString
(int nIndex, LPCTSTR pszText, COLORREF fgColor, COLORREF bgColor)
{
LISTBOXINFO* pListBox = new LISTBOXINFO; ASSERT(pListBox);
ASSERT((nIndex >= 0) && (nIndex <= GetCount()));
pListBox->strText = pszText;
pListBox->fgColor = fgColor;
pListBox->bgColor = bgColor;
m_sArray.insert(m_sArray.begin() + nIndex, pListBox); return CListBox::InsertString(nIndex, pszText); }
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.
int CMultiLineListBox::AddString(LPCTSTR pszText, COLORREF fgColor, COLORREF bgColor)
{
LISTBOXINFO* pListBox = new LISTBOXINFO; ASSERT(pListBox);
pListBox->strText = pszText;
pListBox->fgColor = fgColor;
pListBox->bgColor = bgColor;
m_sArray.push_back(pListBox); return CListBox::AddString(pszText); }
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.
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; ASSERT(pSubNode);
pSubNode->strText = pszText;
pSubNode->fgColor = fgColor;
pSubNode->bgColor = bgColor;
pListBox->subArray.push_back(pSubNode); }
DrawItem
: Override virtual function, used to draw text and set foreground / background color for each item in the ListBox
.
void CMultiLineListBox::DrawItem(LPDRAWITEMSTRUCT lpDrawItemStruct)
{
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);
COLORREF crOldTextColor = dc.GetTextColor();
COLORREF crOldBkColor = dc.GetBkColor();
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);
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);
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); }
else
{
dc.SetTextColor(pListBox->fgColor);
dc.SetBkColor(pListBox->bgColor);
dc.FillSolidRect(&rc, pListBox->bgColor);
CRect rect(rc);
rect.left += 5;
dc.DrawText(pListBox->strText, pListBox->strText.GetLength(), &rect, DT_SINGLELINE |
DT_VCENTER);
}
dc.SetTextColor(crOldTextColor);
dc.SetBkColor(crOldBkColor);
dc.Detach();
}
}
MeasureItem
: Override virtual function, used to calculate current text height for each item in the ListBox
.
void CMultiLineListBox::MeasureItem(LPMEASUREITEMSTRUCT lpMeasureItemStruct)
{
ASSERT(lpMeasureItemStruct->CtlType == ODT_LISTBOX);
lpMeasureItemStruct->itemHeight = ITEM_HEIGHT;
}
OnEraseBkgnd
: WM_ERASEBKGND
message handler function, draws background color in the ListBox
.
BOOL CMultiLineListBox::OnEraseBkgnd(CDC* pDC)
{
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); 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)
{
CListBox::OnKeyDown(nChar, nRepCnt, nFlags);
UpdateItem();
}
OnLButtonDown
: WM_LBUTTONDOWN
message handler function, when user click item.
void CMultiLineListBox::OnLButtonDown(UINT nFlags, CPoint point)
{
CListBox::OnLButtonDown(nFlags, point);
UpdateItem();
}
OnMouseMove
: WM_MOUSEMOVE
message handler function, when user drag mouse.
void CMultiLineListBox::OnMouseMove(UINT nFlags, CPoint point)
{
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
.
void CMultiLineListBox::UpdateItem()
{
int nIndex = GetCurSel();
if((CB_ERR != nIndex) && (m_nFocusIndex != nIndex))
{
PostMessage(MSG_UPDATEITEM, (WPARAM)m_nFocusIndex, (LPARAM)nIndex);
m_nFocusIndex = nIndex; }
}
OnUpdateItem
: Custom message handler function, used to handler cutom message MSG_UPDATEITEM
to refresh item status.
LRESULT CMultiLineListBox::OnUpdateItem(WPARAM wParam, LPARAM lParam)
{
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(); 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.
...
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++) {
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++) {
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
...
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