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);
[... 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);
if (CTLCOLOR_STATIC == nCtlColor) {
if ((NULL!=pCtrl) && (pCtrl->m_hWnd==pWnd->m_hWnd)) 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