News | Articles | Libraries | Developer Tools | Books | Forum Links | Search   
Sections:
 

QA: How can I change the background color of a Tree Control?

By Joao Paulo Figueira, August 28, 2003.
Print version

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:

Discuss

Discuss this article. Here you can write your comments and read comments of other developers.
Rate this article:     Poor Excellent    
 12345 
© 2001-2005 Pocket PC Developer Network, a division of Spb Software House