Drawing a monochrome bitmap with transparency

Raymond Chen

Last time, I left you with a brief puzzle. Here are two approaches. I am not a GDI expert, so there may be even better solutions out there. To emphasize the transparency, I’ll change the window background color to the application workspace color.

 BOOL WinRegisterClass(WNDCLASS *pwc)
 {
  pwc->hbrBackground = (HBRUSH)(COLOR_APPWORKSPACE + 1);
  return __super::WinRegisterClass(pwc);
 }

Method 1: A big MaskBlt.

void RootWindow::PaintContent(PAINTSTRUCT *pps)
{
 HDC hdcMem = CreateCompatibleDC(pps->hdc);
 if (hdcMem) {
  int cxCheck = GetSystemMetrics(SM_CXMENUCHECK);
  int cyCheck = GetSystemMetrics(SM_CYMENUCHECK);
  HBITMAP hbmMono = CreateBitmap(cxCheck, cyCheck, 1, 1, NULL);
  if (hbmMono) {
   HBITMAP hbmPrev = SelectBitmap(hdcMem, hbmMono);
   if (hbmPrev) {
    RECT rc = { 0, 0, cxCheck, cyCheck };
    DrawFrameControl(hdcMem, &rc, DFC_MENU, DFCS_MENUCHECK);
    COLORREF clrTextPrev = SetTextColor(pps->hdc,
                                     GetSysColor(COLOR_MENUTEXT));
    // COLORREF clrBkPrev = SetBkColor(pps->hdc,
    //                                  GetSysColor(COLOR_MENU));
    MaskBlt(pps->hdc, 0, 0, cxCheck, cyCheck,
           hdcMem, 0, 0, hbmMono, 0, 0
           MAKEROP4(0x00AA0029, SRCCOPY));
    // SetBkColor(pps->hdc, clrBkPrev);
    SetTextColor(pps->hdc, clrTextPrev);
    SelectBitmap(hdcMem, hbmPrev);
   }
   DeleteObject(hbmMono);
  }
  DeleteDC(hdcMem);
 }
}

This has the least amount of typing but feels like overkill to me, using a quaternary raster operation as if were a ternary, just because I didn’t want to create a pattern brush. (The raster operation 0x00AA0029 is the NOP operator; it leaves the destination alone. I didn’t have this memorized; I looked it up in the documentation.) The MAKEROP4 says that for each white pixel in the mask, do nothing (NOP), and for each black pixel, do a SRCCOPY.

Notice that the background color is never used (since it’s supposed to be transparent); consequently, we can delete the code that sets and restores the DC’s background color.

Method 2: The traditional two-step.

void RootWindow::PaintContent(PAINTSTRUCT *pps)
{
 HDC hdcMem = CreateCompatibleDC(pps->hdc);
 if (hdcMem) {
  int cxCheck = GetSystemMetrics(SM_CXMENUCHECK);
  int cyCheck = GetSystemMetrics(SM_CYMENUCHECK);
  HBITMAP hbmMono = CreateBitmap(cxCheck, cyCheck, 1, 1, NULL);
  if (hbmMono) {
   HBITMAP hbmPrev = SelectBitmap(hdcMem, hbmMono);
   if (hbmPrev) {
    RECT rc = { 0, 0, cxCheck, cyCheck };
    DrawFrameControl(hdcMem, &rc, DFC_MENU, DFCS_MENUCHECK);
    COLORREF clrTextPrev = SetTextColor(pps->hdc, RGB(0,0,0));
    COLORREF clrBkPrev = SetBkColor(pps->hdc, RGB(255,255,255));
    BitBlt(pps->hdc, cxCheck, 0, cxCheck, cyCheck,
           hdcMem, 0, 0, SRCAND);
    SetTextColor(pps->hdc, GetSysColor(COLOR_MENUTEXT));
    SetBkColor(pps->hdc, RGB(0,0,0));
    BitBlt(pps->hdc, cxCheck, 0, cxCheck, cyCheck,
           hdcMem, 0, 0, SRCPAINT);
    SetBkColor(pps->hdc, clrBkPrev);
    SetTextColor(pps->hdc, clrTextPrev);
    SelectBitmap(hdcMem, hbmPrev);
   }
   DeleteObject(hbmMono);
  }
  DeleteDC(hdcMem);
 }
}

This is the traditional two-step blit. The first erases the pixels that are about to be overwritten by setting the foreground to black and background to white, then using SRCAND. This has the effect of erasing all the foreground pixels to zero while leaving the background intact. The second blit does the same, but with SRCPAINT. This means that the background pixels need to be treated as black, so that when they are “or”d with the destination, the destination pixels are unchanged. The foreground pixels get the desired foreground color.

This method can be shortened by negating the first blit, reversing the sense of foreground and background, so that the color black doesn’t have to move between the background color and the text color.

void RootWindow::PaintContent(PAINTSTRUCT *pps)
{
 HDC hdcMem = CreateCompatibleDC(pps->hdc);
 if (hdcMem) {
  int cxCheck = GetSystemMetrics(SM_CXMENUCHECK);
  int cyCheck = GetSystemMetrics(SM_CYMENUCHECK);
  HBITMAP hbmMono = CreateBitmap(cxCheck, cyCheck, 1, 1, NULL);
  if (hbmMono) {
   HBITMAP hbmPrev = SelectBitmap(hdcMem, hbmMono);
   if (hbmPrev) {
    RECT rc = { 0, 0, cxCheck, cyCheck };
    DrawFrameControl(hdcMem, &rc, DFC_MENU, DFCS_MENUCHECK);
    COLORREF clrTextPrev = SetTextColor(pps->hdc, RGB(255,255,255));
    COLORREF clrBkPrev = SetBkColor(pps->hdc, RGB(0,0,0));
    BitBlt(pps->hdc, cxCheck, 0, cxCheck, cyCheck,
           hdcMem, 0, 0, 0x00220326); // DSna
    SetTextColor(pps->hdc, GetSysColor(COLOR_MENUTEXT));
    BitBlt(pps->hdc, cxCheck, 0, cxCheck, cyCheck,
           hdcMem, 0, 0, SRCPAINT);
    SetBkColor(pps->hdc, clrBkPrev);
    SetTextColor(pps->hdc, clrTextPrev);
    SelectBitmap(hdcMem, hbmPrev);
   }
   DeleteObject(hbmMono);
  }
  DeleteDC(hdcMem);
 }
}

Whether this shortening is actually an overall improvement is difficult to tell. It’s possible that some display drivers have a highly optimized SRCAND handler whereas they are less likely to have an optimized 0x00220326 handler.

(Exercise: Why can’t you instead reverse the second blit, converting it to a MERGEPAINT?)

0 comments

Discussion is closed.

Feedback usabilla icon