Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

Working with Custom Background Bitmaps in Dialog Boxes and Property Sheets

0.00/5 (No votes)
21 Nov 2010 1  
This article shows how to deal with controls that do not properly paint their background when using custom background bitmaps.

Introduction

A lot of people are having trouble with the gradient bitmaps of the XP property sheet pages (or custom-drawn background bitmaps, respectively) and some standard controls (check boxes, radio buttons, etc.) that do not properly paint their background. Playing around with the (extended) window styles, e.g. WS_EX_TRANSPARENT, does not help. This brief article shows how to solve this issue completely.

Background

Custom background bitmaps are normally drawn by either handling WM_ERASEBKGND (Win32) or CWnd::OnEraseBkgnd (MFC). This applies to both dialog boxes and property sheet pages. On Windows XP with theming activated, the property sheet page background is e.g. a gradient bitmap loaded from the shellstyle.dll.

A second important Windows message (or message set) is WM_CTLCOLORBTN, WM_CTLCOLORDLG, WM_CTLCOLOREDIT, WM_CTLCOLORLISTBOX, WM_CTLCOLORSCROLLBAR, and WM_CTLCOLORSTATIC (Win32) or CWnd::OnCtlColor (MFC). A (good) control sends this message to its parent (dialog box or property sheet page) to ask for the background brush to be used to paint the background of the control.

Real Transparent Controls

The following code snippets depend on MFC but also work if you are using plain Win32 code. The easiest solution you can find in the Web is the following code snippet for the handler CWnd::OnCtlColor:

HBRUSH CMyDialogClass::OnCtlColor(CDC* pDC, CWnd* pWnd, UINT nCtlColor)
{
  if (CTLCOLOR_STATIC == nCtlColor)
  {
    pDC->SetBkMode(TRANSPARENT);
    return (HBRUSH)GetStockObject(NULL_BRUSH);
  }
  return CTheBaseClass::OnCtlColor(pDC, pWnd, nCtlColor);  
}

Yes, this does work for some controls but not for all of them, e.g. radio buttons and check boxes behave different. They just ignore the NULL brush returned by the code snippet above and fill their background with the standard dialog box background color - very annoying!

The Complete Solution

How can you ensure that all controls draw their background according to your custom background bitmap (e.g. a gradient bitmap)? Just follow the guideline in the following subsections.

Step 1: Basic Stuff

Please provide your own implementations for CWnd::OnSize, CWnd::OnEraseBkgnd, and CWnd::OnCtlColor. Add the following attributes to your class:

  HBITMAP m_hBackBitmap;
  LPVOID  m_pvBackBits;
  int     m_iBackBmpWidth;
  int     m_iBackBmpHeight;

In the constructor of your class, initialize the first two attributes with NULL and the last two attributes with -1. In the destructor, add something like this:

  if (NULL!=m_hBackBitmap)
    DeleteObject(m_hBackBitmap);

In the method CWnd::OnSize create a DIB section for the background bitmap and fill it according to your needs, e.g.

void CMyDialogClass::OnSize(UINT nType, int cx, int cy)
{
  CTheBaseClass::OnSize(nType, cx, cy);
  
  if ((m_iBackBmpWidth!=cx) || (m_iBackBmpHeight!=cy) || (NULL==m_hBackBitmap))
  {
    if (NULL!=m_hBackBitmap)
      DeleteObject(m_hBackBitmap), m_hBackBitmap = NULL, m_pvBackBits = NULL;
	  
    m_iBackBmpWidth  = cx;
    m_iBackBmpHeight = cy;

    HDC hDC = GetDC(NULL);

    if (NULL!=hDC)
    {
      BITMAPINFO bi;

      memset(&bi,0,sizeof(bi));
      bi.bmiHeader.biBitCount    = 32;
      bi.bmiHeader.biCompression = BI_RGB;
      bi.bmiHeader.biWidth       = m_iBackBmpWidth;
      bi.bmiHeader.biHeight      = m_iBackBmpHeight;
      bi.bmiHeader.biPlanes      = 1;
      bi.bmiHeader.biSize        = sizeof(BITMAPINFOHEADER);
      bi.bmiHeader.biSizeImage   = m_iBackBmpWidth*m_iBackBmpHeight*4;
      
      m_hBackBitmap = CreateDIBSection(hDC,&bi,DIB_RGB_COLORS,&m_pvBackBits,NULL,0);
      if (NULL==m_hBackBitmap)
      {
        ReleaseDC(NULL,hDC);
        return;
      }
      if (IsBadWritePtr(m_pvBackBits,m_iBackBmpWidth*m_iBackBmpHeight*4))
      {
        DeleteObject(m_hBackBitmap), m_hBackBitmap = NULL, m_pvBackBits = NULL;
        ReleaseDC(NULL,hDC);
        return;
      }
      ReleaseDC(NULL,hDC);

      // Paint your background bitmap in the memory region pointed to by m_pvBackBits
      // It is an A-R-G-B bitmap (set A to 0xFF), the four components are stored in
      // reverse order, i.e. B,G,R,A in memory.
      // Keep in mind that bitmaps are stored bottom-up, i.e. m_iBackBmpHeight-1 is
      // the topmost line of the bitmap.
  
      [... Place your code here ...]
    }
  }
}

Erase the Background with Your Custom Bitmap

In CWnd::OnEraseBkgnd, perform the following:

BOOL CMyDialogClass::OnEraseBkgnd(CDC* pDC)
{
  if (NULL!=m_hBackBitmap)
  {
    HDC hDC = pDC->m_hDC;
    HDC hDCMem = CreateCompatibleDC(hDC);
    RECT rcClient;
    GetClientRect(&rcClient);
  }
	
  if ((rcClient.right==m_iBackBmpWidth) &&
      (rcClient.bottom==m_iBackBmpHeight))
  {
    HGDIOBJ hOldBmp = SelectObject(hDCMem,m_hBackBitmap);
    BitBlt(hDC,0,0,m_iBackBmpWidth,m_iBackBmpHeight,hDCMem,0,0,SRCCOPY);
    SelectObject(hDCMem,hOldBmp);
    DeleteDC(hDCMem);
    return TRUE;
  }

  return CTheBaseClass::OnEraseBkgnd(pDC);
}

Handle All Controls That Do Not Properly Paint their Background

For each control that does not paint properly, we will provide a bitmap brush that fits the position and size of the control. Add the following attribute to your class (for each control):

  HBRUSH m_hCtrlBrush;

In the constructor, initialize the attribute with NULL. For each control, add the following lines to the destructor of your dialog box class:

  if (NULL!=m_hCtrlBrush)
    DeleteObject(m_hCtrlBrush);

In OnInitDialog, determine the size and position of the control and create a bitmap brush (brush generation code follows below):

  RECT rcControl;
  
  GetDlgItem(IDC_CONTROLxxx)->GetWindowRect(&rcControl);
  ScreenToClient(&rcControl);
  m_hCtrlBrush = CreateDIBBrush(rcControl.left,
	rcControl.top,rcControl.right-rcControl.left,rcControl.bottom-rcControl.top);

In CWnd::OnCtlColor, perform the following:

HBRUSH CMyDialogClass::OnCtlColor(CDC* pDC, CWnd* pWnd, UINT nCtlColor)
{
  CWnd *pCtrl = GetDlgItem(IDC_CONTROLxxx); // add the control ID here
  
  if (CTLCOLOR_STATIC == nCtlColor) 	// in this example, 
				// a radio button or check box is assumed
  {
    if ((NULL!=pCtrl) && (pCtrl->m_hWnd==pWnd->m_hWnd)) // match, this is an 
				// "evil" control, for which we have a bitmap brush
      return m_hCtrlBrush;
  }
  return CTheBaseClass::OnCtlColor(pDC, pWnd, nCtlColor);
}

The CreateDIBBrush Method

One method is missing: CreateDIBBrush. It creates a bitmap brush using the GDI function CreateBrushIndirect. CreateBrushIndirect can create pattern brushes as well as bitmap brushes. The following code creates a bitmap brush for a control that is displayed at the coordinates x,y having the width cx and the height cy. It just extracts a small "window" of the background bitmap that is suitable for the area covered by the control.

HBRUSH CMyDialogClass::CreateDIBrush ( int x, int y, int cx, int cy )
{
  if ((x<0) || (y<0) || 
      (0==cx) || (0==cy) ||
      ((x+cx)>m_iBackBmpWidth) || ((y+cy)>m_iBackBmpHeight) ||
      (NULL==m_pvBackBits))
    return NULL;

  HGLOBAL hDIB = GlobalAlloc(GHND,sizeof(BITMAPINFOHEADER)+cx*cy*4);

  if (NULL==hDIB)
    return NULL;

  LPVOID lpvBits = GlobalLock(hDIB);

  if (NULL==lpvBits)
  {
    GlobalFree(hDIB);
    return NULL;
  }

  BITMAPINFOHEADER *bih = (BITMAPINFOHEADER*)lpvBits;

  bih->biBitCount    = 32;
  bih->biCompression = BI_RGB;
  bih->biWidth       = cx;
  bih->biHeight      = cy;
  bih->biPlanes      = 1;
  bih->biSize        = sizeof(BITMAPINFOHEADER);
  bih->biSizeImage   = cx*cy*4;
    
  PDWORD pdwData = (PDWORD)(bih+1);

  for (int y=0;y<cy;y++)
  {
    PDWORD pdwDst = pdwData+(cy-1-y)*cx;
    PDWORD pdwSrc = ((PDWORD)m_pvBackBits)+(m_cy-1-y-y)*m_cx+x;
    memcpy(pdwDst,pdwSrc,cx<<2);
  }

  GlobalUnlock(hDIB);

  LOGBRUSH lb;

  lb.lbStyle = BS_DIBPATTERN;
  lb.lbColor = DIB_RGB_COLORS;
  lb.lbHatch = (LONG)hDIB;

  HBRUSH hBrush = CreateBrushIndirect(&lb);

  if (NULL==hBrush)
  GlobalFree(hDIB);

  return hBrush;
}

Conclusion

It is not straight forward to deal with dialog boxes and property sheet pages whose controls paint their background according to a custom background. This is because some static controls (check boxes, radio buttons) ignore the NULL brush that is returned by the modified CWnd::OnEraseBkgnd method. This article demonstrated how to solve this problem.

History

  • 21st November, 2010: Initial post

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here