BeginBufferedPaint: It’s not just for buffered painting any more


I covered the BeginBufferedPaint function in my 2008 PDC presentation, but one thing I didn't mention is that the buffered paint functions are very handy even if you have no intention of painting.

Since the buffered paint functions maintain a cache (provided that you remembed to call Buffered­Paint­Init), you can use Begin­Buffered­Paint to get a temporary bitmap even if you have no intention of actually painting to the screen. You might want a bitmap to do some off-screen composition, or for some other temporary purpose, in which case you can ask Begin­Buffered­Paint to give you a bitmap, use the bitmap for whatever you like, and then pass fUpdateTarget = FALSE when you call End­Buffered­Paint to say "Ha ha, just kidding."

One thing to have to be aware of is that the bitmap provided by Begin­Buffered­Paint is not guaranteed to be exactly the size you requested; it only promises that the bitmap will be at least the size you requested. Most of the time, your code won't care (there are just pixels out there that you aren't using), but if you use the Get­Buffered­Paint­Bits function to obtain direct access to the bits, don't forget to take the stride into account.

Consider this artifical example of a program that uses Create­DIB­Section to create a temporary 32bpp bitmap for the purpose of updating a layered window. Start with the scratch program and make these changes:

BOOL
OnCreate(HWND hwnd, LPCREATESTRUCT lpcs)
{
 BOOL fRc = FALSE;
 HDC hdcWin = GetDC(hwnd);
 if (hdcWin) {
  HDC hdcMem = CreateCompatibleDC(hdcWin);
  if (hdcMem) {
   const int cx = 200;
   const int cy = 200;
   RECT rc = { 0, 0, cx, cy };
   BITMAPINFO bmi = { 0 };
   bmi.bmiHeader.biSize = sizeof(bmi.bmiHeader);
   bmi.bmiHeader.biWidth = cx;
   bmi.bmiHeader.biHeight = cy;
   bmi.bmiHeader.biPlanes = 1;
   bmi.bmiHeader.biBitCount = 32;
   bmi.bmiHeader.biCompression = BI_RGB;
   RGBQUAD *prgbBits;
   HBITMAP hbm = CreateDIBSection(hdcWin, &bmi,
             DIB_RGB_COLORS, &reinterpret_cast<void*&>(prgbBits),
                                                        NULL, 0);
   if (hbm) {
    HBITMAP hbmPrev = SelectBitmap(hdcMem, hbm);

    // Draw a simple picture
    FillRect(hdcMem, &rc,
                     reinterpret_cast<HBRUSH>(COLOR_INFOBK + 1));
    rc.left = cx / 4;
    rc.right -= rc.left;
    rc.top = cy / 4;
    rc.bottom -= rc.top;
    FillRect(hdcMem, &rc,
                   reinterpret_cast<HBRUSH>(COLOR_INFOTEXT + 1));

    // Apply the alpha channel (and premultiply)
    for (int y = 0; y < cy; y++) {
     for (int x = 0; x < cx; x++) {
      RGBQUAD *prgb = &prgbBits[y * cx + x];
      BYTE bAlpha = static_cast<BYTE>(cx * x / cx);
      prgb->rgbRed = static_cast<BYTE>(prgb->rgbRed * bAlpha / 255);
      prgb->rgbBlue = static_cast<BYTE>(prgb->rgbBlue * bAlpha / 255);
      prgb->rgbGreen = static_cast<BYTE>(prgb->rgbGreen * bAlpha / 255);
      prgb->rgbReserved = bAlpha;
     }
    }

    // update the layered window
    POINT ptZero = { 0, 0 };
    SIZE siz = { cx, cy };
    BLENDFUNCTION bf =  { AC_SRC_OVER, 0, 255, AC_SRC_ALPHA };
    fRc = UpdateLayeredWindow(hwnd, NULL, &ptZero, &siz, hdcMem,
                              &ptZero, 0, &bf, ULW_ALPHA);
    SelectBitmap(hdcMem, hbmPrev);
    DeleteObject(hbm);
   }
   DeleteDC(hdcMem);
  }
  ReleaseDC(hwnd, hdcWin);
 }
 return fRc;
}

Pretty standard stuff. But let's convert this to use the buffered paint functions to take advantage of the buffered paint bitmap cache.

BOOL
OnCreate(HWND hwnd, LPCREATESTRUCT lpcs)
{
 BOOL fRc = FALSE;
 HDC hdcWin = GetDC(hwnd);
 if (hdcWin) {
  HDC hdcMem;
  // HDC hdcMem = CreateCompatibleDC(hdcWin);
  // if (hdcMem) {
   const int cx = 200;
   const int cy = 200;
   RECT rc = { 0, 0, cx, cy };
   // BITMAPINFO bmi = { 0 };
   // bmi.bmiHeader.biSize = sizeof(bmi.bmiHeader);
   // bmi.bmiHeader.biWidth = cx;
   // bmi.bmiHeader.biHeight = cy;
   // bmi.bmiHeader.biPlanes = 1;
   // bmi.bmiHeader.biBitCount = 32;
   // bmi.bmiHeader.biCompression = BI_RGB;
   RGBQUAD *prgbBits;
   BP_PAINTPARAMS params = { sizeof(params), BPPF_NOCLIP };
   HPAINTBUFFER hbp = BeginBufferedPaint(hdcWin, &rc,
                              BPBF_TOPDOWNDIB, &params, &hdcMem);
   if (hbp) {
    int cxRow;
    if (SUCCEEDED(GetBufferedPaintBits(hpb, &prgbBits, &cxRow))) {
   // HBITMAP hbm = CreateDIBSection(hdcWin, &bmi,
   //        DIB_RGB_COLORS, &reinterpret_cast<void*&>(prgbBits),
   //                                                   NULL, 0);
   // if (hbm) {
    // HBITMAP hbmPrev = SelectBitmap(hdcMem, hbm);

    // Draw a simple picture
    FillRect(hdcMem, &rc,
                     reinterpret_cast<HBRUSH>(COLOR_INFOBK + 1));
    rc.left = cx / 4;
    rc.right -= rc.left;
    rc.top = cy / 4;
    rc.bottom -= rc.top;
    FillRect(hdcMem, &rc,
                   reinterpret_cast<HBRUSH>(COLOR_INFOTEXT + 1));

    // Apply the alpha channel (and premultiply)
    for (int y = 0; y < cy; y++) {
     for (int x = 0; x < cx; x++) {
      RGBQUAD *prgb = &prgbBits[y * cxRow + x];
      BYTE bAlpha = static_cast<BYTE>(cx * x / cx);
      prgb->rgbRed = static_cast<BYTE>(prgb->rgbRed * bAlpha / 255);
      prgb->rgbBlue = static_cast<BYTE>(prgb->rgbBlue * bAlpha / 255);
      prgb->rgbGreen = static_cast<BYTE>(prgb->rgbGreen * bAlpha / 255);
      prgb->rgbReserved = bAlpha;
     }
    }

    // update the layered window
    POINT ptZero = { 0, 0 };
    SIZE siz = { cx, cy };
    BLENDFUNCTION bf =  { AC_SRC_OVER, 0, 255, AC_SRC_ALPHA };
    fRc = UpdateLayeredWindow(hwnd, NULL, &ptZero, &siz, hdcMem,
                              &ptZero, 0, &bf, ULW_ALPHA);
    // SelectBitmap(hdcMem, hbmPrev);
    // DeleteObject(hbm);
   }
   EndBufferedPaint(hpb, FALSE);
   // DeleteDC(hdcMem);
  }
  ReleaseDC(hwnd, hdcWin);
 }
 return fRc;
}

// changes to WinMain
 if (SUCCEEDED(BufferedPaintInit())) {
 // if (SUCCEEDED(CoInitialize(NULL))) {/* In case we use COM */
  hwnd = CreateWindowEx(WS_EX_LAYERED,
  // hwnd = CreateWindow(
  ...
  BufferedPaintUnInit();
  // CoUninitialize();
  ...

We're using the buffered paint API not for buffered painting but just as a convenient way to get a bitmap and a DC at one shot. It saves some typing (you don't have to create the bitmap and the DC and select the bitmap in and out), and when you return the paint buffer to the cache, some other window that calls Begin­Buffered­Paint may be able to re-use that bitmap.

There are a few tricky parts here. First, if you're going to be accessing the bits directly, you need to call Get­Buffered­Paint­Bits and use the cxRow to determine the bitmap stride. Next, when we're done, we pass FALSE to End­Buffered­Paint to say, "Yeah, um, thanks for the bitmap, but don't Bit­Blt the results back into the DC we passed to Begin­Buffered­Paint. Sorry for the confusion."

A less obvious trick is that we used BPPF_NOCLIP to get a full bitmap. By default, Begin­Buffered­Paint returns you a bitmap which is clipped to the DC you pass as the first parameter. This is an optimization to avoid allocating memory for pixels that can't be seen anyway when End­Buffered­Paint goes to copy the bits back to the original DC. We don't want this optimization, however, since we have no intention of blitting the results back to the original DC. The clip region of the original DC is irrelevant to us because we just want a temporary bitmap for some internal calculations.

Anyway, there you have it, an example of using Begin­Buffered­Paint to obtain a temporary bitmap. It doesn't win much in this example (since we call it only once, at window creation time), but if you have code which creates a lot of DIB sections for temporary use, you can use this trick to take advantage of the buffered paint cache and reduce the overhead of bitmap creation and deletion.

Pre-emptive snarky comment: "How dare you show us an alternative method that isn't available on Windows 2000!"

Comments (10)
  1. SimonRev says:

    While I certainly wouldn't say "How dare you show us an alternative method that isn't available on Windows 2000!", I do have to admit that while very interesting, this isn't practical for many of us who do have to write software that still runs on Windows XP.

    Certainly, the day our Product Management gives us the go ahead to drop XP, stuff like this will be useful.  But since we barely dropped Win95 support 5 years ago, I doubt that day is coming any time soon. (Which I find sad, since there is a ton of stuff in Vista and Win 7 that would make my life easier if I could use it).

    Anyway, thanks for the tip, I hope I can use it someday.

  2. GrumpyYoungMan says:

    @SimonRev

    Amazing.  Raymond goes out of his way to preempt such comments and you go ahead and do it anyway.  Any sympathy I might have had for you was erased instantly.  

  3. Ben Voigt says:

    Can pre-emptive snarky comments get stars?  One is surely deserved here!

  4. Joshua says:

    Microsoft has indeed been making improvements to APIs of Windows. Case in point: GetNamedPipeClientSessionId.

  5. Bradley says:

    Why would you want to do this in the real world? It seems to go out of it's way to get a bitmap that may or may not work on the OS, and one that might not be the size you asked for. This sounds like a "clever hack" that is just going to cause problems, when it would be FAR easier and safer to just create a bitmap the "normal" way.

    [Consider an app with 500 UI widgets, and when some event occurs, each widget needs to redraw its cached image. You could create and destroy 500 temporary bitmaps or use the same temporary bitmap 500 times. -Raymond]
  6. SimonRev says:

    @GrumpyYoungMan

    My point was in no way to complain.  I hope that nothing in my original post could be construed as "snarky".  

    The fact is I appreciate having these little tidbits pointed out.  Unfortunately, XP is victim of its own success and shows no sign disappearing any time soon, leaving 3rd party developers (and in fairness, many teams internal to Microsoft as well) more or less stuck using decade old APIs until XP is finally completely end of lifed.

    That said, I am glad to know of one more tool that I hope I could use one day.

  7. Worf says:

    Guess I don't get the snark. I use Windows 2000, but I don't see the problem – Microsoft doesn't support Win2k since July 2010, so… XP however is 2014.

    Of course, if this article was written in 2008, I suppose it would've been appropriate then. In which case it appears Raymond's queue is around 3 years deep. Which means Raymond's new pre-emptive snarks about XP will probably be misunderstood as XP is unsupported when we see them in 2014.

    Looks like an interesting API and someone had the foresight to include a "oops" parameter to let it be used in interesting ways.

  8. ulric says:

    it's not necessary to do something like call GdiFlush before using the bits of the bitmap?

    [All the standard rules apply. I don't see where I suggested otherwise. -Raymond]
  9. ulric says:

    Isn't FillRect function drawing on the the bitmap bits that are being directly accessed right after, or did I misunderstand the sample?

    If so, isn't syncing with GDIflush() required at that point, if not, why?

    [Yes, it is necessary. That's a bug. But it also has nothing to do with the conversion to use BeginBufferedPaint. -Raymond]
  10. ulric says:

    it would be best to update the sample for posterity; it's already spending many lines on premultiplying alpha blending; might as well make it 100% correct

Comments are closed.