
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:
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 windwos 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. 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.
...
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 ...
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!