在Windows应用程序开发中,经常需要为对话框和属性页设置自定义背景,同时确保其中的控件能够正确地绘制其背景。然而,有些标准控件(如复选框、单选按钮等)在绘制背景时并不总是按照预期的方式进行。即使尝试使用扩展窗口样式(如WS_EX_TRANSPARENT),问题依然存在。本文将展示如何彻底解决这一问题。
自定义背景位图通常通过处理WM_ERASEBKGND(Win32)或CWnd::OnEraseBkgnd(MFC)消息来绘制,这适用于对话框和属性页。在启用了主题的Windows XP上,属性页背景可能是从shellstyle.dll加载的渐变位图。
另一个重要的Windows消息集是WM_CTLCOLORBTN、WM_CTLCOLORDLG、WM_CTLCOLOREDIT、WM_CTLCOLORLISTBOX、WM_CTLCOLORSCROLLBAR和WM_CTLCOLORSTATIC(Win32)或CWnd::OnCtlColor(MFC)。一个好的控件会向其父窗口(对话框或属性页)发送这些消息,请求用于绘制控件背景的画刷。
以下代码片段依赖于MFC,但也适用于使用纯Win32代码的情况。在Web上可以找到的最简单的解决方案是以下代码片段,用于处理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);
}
是的,这确实适用于某些控件,但并非全部,例如单选按钮和复选框的行为就不同。它们会忽略上述代码片段返回的NULL画刷,并用标准对话框背景色填充其背景——这非常烦人!
如何确保所有控件都根据自定义背景位图(例如渐变位图)绘制其背景?只需按照以下小节中的指南操作。
请为类提供自己的实现CWnd::OnSize、CWnd::OnEraseBkgnd和CWnd::OnCtlColor。在类中添加以下属性:
HBITMAP m_hBackBitmap;
LPVOID m_pvBackBits;
int m_iBackBmpWidth;
int m_iBackBmpHeight;
在类的构造函数中,将前两个属性初始化为NULL,将最后两个属性初始化为-1。在析构函数中,添加如下代码:
if (NULL != m_hBackBitmap)
DeleteObject(m_hBackBitmap);
在CWnd::OnSize方法中,为背景位图创建一个DIB节,并根据需要进行填充:
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 ...]
}
}
}
在CWnd::OnEraseBkgnd中执行以下操作:
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);
}
处理所有未正确绘制背景的控件:
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);
}
创建DIB画刷的方法:
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;
}