QA: How can I change the background color of a Tree Control?
Joao Paulo Figueira (joao.fig@mail.telepac.pt), August 28, 2003.
Question
How can I change the background color of a Tree Control?
Answer
The tree view control allows you to paint both the window background and the tree item background. To give users
the impression of a solid background color, you should paint both in the same color. This is achieved through a two
step strategy (similar to the one used for header controls): overriding the WM_ERASEBKGND message processing
for the window background and using the custom draw service for the tree items. The following code snippets show you
how this can be done.
// CColorTreeCtrl::OnCustomDraw
//
// Handles custom draw
//
void CColorTreeCtrl::OnCustomDraw(NMHDR* pNMHDR, LRESULT* pResult)
{
NMTVCUSTOMDRAW* pCD = (NMTVCUSTOMDRAW*)pNMHDR;
DWORD dwDrawStage;
*pResult = CDRF_DODEFAULT;
dwDrawStage = pCD->nmcd.dwDrawStage;
if(dwDrawStage == CDDS_PREPAINT)
{
*pResult = CDRF_NOTIFYITEMDRAW;
}
else if(dwDrawStage == CDDS_ITEMPREPAINT)
{
if(!(pCD->nmcd.uItemState & (CDIS_FOCUS | CDIS_SELECTED)))
{
pCD->clrText = m_crText;
pCD->clrTextBk = m_crBack;
*pResult = CDRF_NEWFONT;
}
}
}
// CColorTreeCtrl::OnEraseBkgnd
//
// This is where we specify the background color of the header
//
BOOL CColorTreeCtrl::OnEraseBkgnd(CDC* pDC)
{
CRect rc;
GetClientRect(&rc);
//
// Paint the client in the specified background color
//
pDC->FillSolidRect(&rc, m_crBack);
return TRUE;
}
The test in the CDDS_ITEMPREPAINT phase allows the tree control to paint the selected item. This looks
great, but has one major flaw: it does not handle buttons nor lines. As a matter of fact, these will always
be painted with a white brush and, as far as I have tested, there is no way to change it. Selecting a new brush
into pCD->nmcd.hdc doesn't work, neither does superclassing the tree's class and setting a new window
background brush. The window procedure will insist in doing the job the way it thinks is best, with a white brush.
So, how can we make the tree paint those backgrounds the way we want it to? We don't. We paint them ourselves.
Instead of just specifying the text and background colors and asking for the custom draw service to paint the
item, we will take over the whole process. This implies drawing the tree items under all possible conditions and
states. As we will see, it is not an impossible task. It is even fun, because we will get a chance of custom
painting anything we want in the tree item. So, this QA does not answer the above question only. A more appropriate
title would be:
Question
How do I custom paint a Tree Control?
Answer
The same principles apply: we have to handle the WM_ERASEBKGBD message and the NM_CUSTOMDRAW
notification. The main difference is on how we handle the custom draw. Instead of customizing little things,
we take over and do the whole thing.
Painting an Item
A tree item occupies one single line in the tree control client area, so the vertical position of the item is
easy to manage (it is reported by the tree in pCD->nmcd.rc). The horizontal position is much harder to
determine. The horizontal position is dependent on the item's ancestry, i.e., on how many parents the item has.
Also, we have to take into account the specified style bits, namely TVS_HASLINES, TVS_LINESATROOT
and TVS_HASBUTTONS. The solution I chose is the simplest I could find: a recursive function.
// CColorTreeCtrl::PaintParentLine
//
// This method recursively paints the parent item lines (vertical).
// The lines are drawn with the pen and brush selected into the HDC.
//
void CColorTreeCtrl::PaintParentLine(HTREEITEM hParent, HDC hDC, RECT &rc)
{
HTREEITEM hGrand;
//
// Check if the parent has a parent itself and process it
//
hGrand = GetParentItem(hParent);
if(hGrand)
PaintParentLine(hGrand, hDC, rc);
//
// Check if the parent has a sibling. If so, draw the vertical line
//
if(GetNextSiblingItem(hParent) && (m_dwStyle & TVS_HASLINES))
{
//
// Now, check if this is a root item. If it is, we have to make
// sure that TVS_LINESATROOT is enabled
//
if(!hGrand)
{
if(m_dwStyle & TVS_LINESATROOT)
LineVert(hDC, m_nIndent / 2 + rc.left, rc.top, rc.bottom);
}
else
LineVert(hDC, m_nIndent / 2 + rc.left, rc.top, rc.bottom);
}
//
// Advance the drawing position
//
rc.left += m_nIndent;
}
This method receives as parameters the handle to the current item's parent, the HDC used for painting and
a reference to the drawing RECT. The rc.left member is updated as we go from parent to child. When
the method returns, it has drawn the vertical lines (if any) to the left of the item text and advanced the
drawing position appropriately, meaning that the item will be properly indented. Drawing a vertical line
depends on whether the parent item has a next sibling and line drawing is enabled.
After painting the lines, we have to paint the item's open / close button and the connecting lines. This is
done in the following methods:
// CColorTreeCtrl::PaintItemLines
//
// Paints the lines of an item
//
void CColorTreeCtrl::PaintItemLines(HTREEITEM hItem, HTREEITEM hParent, HDC hDC, RECT &rc)
{
int x,
y,
xm = m_nIndent / 2,
ym = (rc.bottom - rc.top) / 2;
x = rc.left + m_nIndent / 2;
y = rc.top;
if(GetPrevSiblingItem(hItem) || hParent)
{
if(!hParent) // Root node?
{
if(m_dwStyle & TVS_LINESATROOT) // Lines at root?
LineVert(hDC, x, y, y + ym); // Connect to prev / parent
}
else
LineVert(hDC, x, y, y + ym); // Connect to prev / parent
}
y += ym;
LineHorz(hDC, x, x + xm + 1, y); // Connect to text / icon
if(GetNextSiblingItem(hItem))
{
if(!hParent) // Root node?
{
if(m_dwStyle & TVS_LINESATROOT) // Lines at root?
LineVert(hDC, x, y, y + ym); // Connect to next
}
else
LineVert(hDC, x, y, y + ym); // Connect to next
}
}
// CColorTreeCtrl::PaintButton
//
// Draws the open / close button
//
void CColorTreeCtrl::PaintButton(HDC hDC, RECT &rc, BOOL bExpanded)
{
if(m_hIconBtn[0])
{
::DrawIconEx(hDC,
rc.left + (m_nIndent - 16) / 2,
rc.top,
m_hIconBtn[bExpanded ? 1 : 0],
16,
16,
0,
NULL,
DI_NORMAL);
}
else
{
HPEN hBoxPen,
hMrkPen,
hOldPen;
HBRUSH hNewBrush,
hOldBrush;
int h = rc.bottom - rc.top,
x = rc.left + (m_nIndent - 9) / 2,
y = rc.top + (h - 9) / 2 + 1;
hBoxPen = ::CreatePen(PS_SOLID, 1, m_crLine);
hMrkPen = ::CreatePen(PS_SOLID, 1, RGB( 0, 0, 0));
hNewBrush = ::CreateSolidBrush(RGB(255, 255, 255));
hOldPen = (HPEN) ::SelectObject(hDC, hBoxPen);
hOldBrush = (HBRUSH) ::SelectObject(hDC, hNewBrush);
//
// Draw the box
//
::Rectangle(hDC, x, y, x+9, y+9);
//
// Now, the - or + sign
//
::SelectObject(hDC, hMrkPen);
LineHorz(hDC, x + 2, x + 7, y + 4); // '-'
if(!bExpanded)
LineVert(hDC, x + 4, y + 2, y + 7); // '+'
::SelectObject(hDC, hOldPen);
::SelectObject(hDC, hOldBrush);
::DeleteObject(hMrkPen);
::DeleteObject(hBoxPen);
::DeleteObject(hNewBrush);
}
}
Note that the PaintButton method allows you to draw the classical buttons (9x9 with white background
and '-' and '+' symbols) or to render an icon in its place. I chose to use icons because they conveniently
paint themselves using a transparent mask, but this can be changed to suit your own needs. If you want to
use icons check the ones in the sample code for alignement information.
Also note that all the drawing methods use straight API code. I preferred this approach because the custom
draw service uses API drawing objects and I wanted to avoid the overhead of converting to and using the MFC
objects.
Finally, let's have a look at the workhorse method for handling item painting. It is quite self-explanatory:
// CColorTreeCtrl::OnCustomDraw
//
// Handles custom draw
//
void CColorTreeCtrl::OnCustomDraw(NMHDR* pNMHDR, LRESULT* pResult)
{
NMTVCUSTOMDRAW* pCD = (NMTVCUSTOMDRAW*)pNMHDR;
DWORD dwDrawStage;
*pResult = CDRF_DODEFAULT;
dwDrawStage = pCD->nmcd.dwDrawStage;
if(dwDrawStage == CDDS_PREPAINT)
{
//
// This is the beginning of the drawing phase.
// Cache some properties for later.
//
m_nIndent = GetIndent();
m_dwStyle = GetStyle();
m_hImgList = TreeView_GetImageList(m_hWnd, TVSIL_NORMAL);
*pResult = CDRF_NOTIFYITEMDRAW;
}
else if(dwDrawStage == CDDS_ITEMPREPAINT)
{
HDC hDC = pCD->nmcd.hdc;
HPEN hLinPen,
hOldPen;
HBRUSH hBackBrush,
hOldBrush;
HTREEITEM hItem = (HTREEITEM)pCD->nmcd.dwItemSpec,
hParent = GetParentItem(hItem);
RECT rc = pCD->nmcd.rc;
TV_DISPINFO tvdi;
RECT rcText;
TCHAR szText[1024];
hLinPen = ::CreatePen(PS_SOLID, 1, m_crLine);
hOldPen = (HPEN) ::SelectObject(hDC, hLinPen);
hOldBrush = (HBRUSH) ::SelectObject(hDC, m_brush);
//
// Draw the parent lines, if any
//
if(hParent)
PaintParentLine(hParent, hDC, rc);
//
// Draw the lines connecting to the previous and next items, if any
//
if(m_dwStyle & TVS_HASLINES)
PaintItemLines(hItem, hParent, hDC, rc);
//
// Get the item information to draw the current item
//
tvdi.item.mask = TVIF_CHILDREN | TVIF_HANDLE | TVIF_IMAGE |
TVIF_SELECTEDIMAGE |
TVIF_STATE | TVIF_TEXT;
tvdi.item.hItem = hItem;
tvdi.item.pszText = szText;
tvdi.item.cchTextMax = 1024;
if(!GetItem(&tvdi.item))
goto error_exit; // Exit silently (should never happen, though)
//
// Now, check for callback items
//
if(tvdi.item.iImage == I_IMAGECALLBACK ||
tvdi.item.iSelectedImage == I_IMAGECALLBACK ||
tvdi.item.pszText == LPSTR_TEXTCALLBACK )
{
HWND hWndParent;
hWndParent = ::GetParent(m_hWnd);
if(hWndParent)
{
tvdi.hdr.hwndFrom = m_hWnd;
tvdi.hdr.idFrom = ::GetDlgCtrlID(m_hWnd);
tvdi.hdr.code = TVN_GETDISPINFO;
::SendMessage(hWndParent, WM_NOTIFY, tvdi.hdr.idFrom,
(LPARAM)&tvdi);
}
}
//
// Paint the buttons, if any
//
if(m_dwStyle & TVS_HASBUTTONS)
{
if(tvdi.item.cChildren == 1)
PaintButton(hDC, rc, tvdi.item.state & TVIS_EXPANDED);
else if(tvdi.item.cChildren == I_CHILDRENCALLBACK)
PaintButton(hDC, rc, FALSE);
}
//
// If we have buttons or line, we must make room for them
//
if(m_dwStyle & (TVS_HASBUTTONS | TVS_HASLINES))
rc.left += m_nIndent;
//
// Check if we have any normal icons to draw
//
if(m_hImgList)
{
int iImage,
cx, cy;
if(pCD->nmcd.uItemState & CDIS_SELECTED)
iImage = tvdi.item.iSelectedImage;
else
iImage = tvdi.item.iImage;
ImageList_Draw(m_hImgList, iImage, hDC, rc.left, rc.top, ILD_NORMAL);
ImageList_GetIconSize(m_hImgList, &cx, &cy);
rc.left += cx;
}
rc.left += 4;
//
// Calculate the text drawing rectangle
//
rcText = rc;
::DrawText(hDC, szText, -1, &rcText, DT_LEFT | DT_NOPREFIX |
DT_SINGLELINE | DT_VCENTER | DT_CALCRECT);
rcText.left -= 2;
rcText.right += 2;
rcText.bottom += 2;
//
// Clear the background
//
if(pCD->nmcd.uItemState & CDIS_FOCUS)
{
hBackBrush = ::CreateSolidBrush(RGB( 0, 0, 156));
::SetTextColor (hDC, RGB(255, 255, 255));
::SetBkColor (hDC, RGB( 0, 0, 156));
}
else
{
hBackBrush = ::CreateSolidBrush(m_crBack);
::SetTextColor (hDC, m_crText);
::SetBkColor (hDC, m_crBack);
}
::FillRect(hDC, &rcText, hBackBrush);
::DeleteObject(hBackBrush);
//
// Now, draw the text
//
::DrawText(hDC, szText, -1, &rc, DT_LEFT | DT_NOPREFIX | DT_SINGLELINE | DT_VCENTER);
//
// Draw the focus rect
//
if(pCD->nmcd.uItemState & CDIS_FOCUS)
::DrawFocusRect(hDC, &rcText);
error_exit:
//
// Clean up
//
::SelectObject(hDC, hOldBrush);
::SelectObject(hDC, hOldPen);
::DeleteObject(hLinPen);
*pResult = CDRF_SKIPDEFAULT;
}
}
The sample application displays a simple tree and allows you to change a number of settings, namely the tree
styles, indentation, button appearance and colors (text, line and background - after all this is what this QA was
all about...). There are a number of odd ends that are not covered yet in the code, but it is a good starting
point for your own tree. Missing are: dotted lines and overlay images.
Sample
You can download a sample here - TreeColor.zip (190K).
And that's it.
Related resources:
-
http://www.pocketpcdn.com/articles/buttoncolor.html
QA: How to create a colored button?
-
http://www.pocketpcdn.com/articles/checktree.html
QA: How can I create a tree control with checkboxes?
-
http://www.pocketpcdn.com/articles/transparent_static.html
QA: How to make static controls transparent?
- http://www.pocketpcdn.com/articles/hdr_color.html
Article: How can I change the background color of the header of a List Control
- http://www.codeproject.com/ce/transparent_controls.asp
Article: Transparent Controls with custom image backgrounds on PocketPC