nythrix
nythrix

Reputation: 81

Correct (and flicker-free) way of setting window pixels?

I'm struggling to figure out the proper way of dumping an array of plain RGBA values into the client area of a Win32 window during WM_PAINT. I have the following code but it already seems convoluted and I'm not even finished:

case WM_ERASEBKGND:
  return 1;
case WM_PAINT:
{
  PAINTSTRUCT paintInfo{};
  HDC device = BeginPaint(window, &paintInfo);
  if (device == nullptr)
    throw runtime_error(RG_LOCATION());
  ScopeExit endPaint([&] { EndPaint(window, &paintInfo); });

  HDC offscreenDevice = CreateCompatibleDC(device);
  ScopeExit deleteOffscreenDevice([&] { DeleteDC(offscreenDevice); });
  HBITMAP offscreenBitmap = CreateCompatibleBitmap(device, Distance(paintInfo.rcPaint.left, paintInfo.rcPaint.right),
                                                   Distance(paintInfo.rcPaint.top, paintInfo.rcPaint.bottom));
  ScopeExit deleteOffscreenBitmap([&] { DeleteObject(offscreenBitmap); });
  HBITMAP previousBitmap = reinterpret_cast<HBITMAP>(SelectObject(offscreenDevice, offscreenBitmap));

  // now I need to blit the available pixel data...
  vector<array<uint8_t, 4>> mypixels;
  // ...onto the client area of the window.

  // What do I do next?
  // CreateDIBSection ?
  // BitBlt ?

  return 0;
}

I have some wiggle room with regards to the source "image" memory format so I can make that match whatever the target requires.

Am I doing this correctly? Is there a better way?

P.S.: Obviously I would be storing and not recreating most of the objects each time a WM_PAINT comes along. This is just an example/WIP.

Edit: Added handling of WM_ERASEBKGND.

Edit 2: Ok, it seems I need to be more specific. I am not looking for actual issues with the code I posted. It is only an example of what I have so far in terms of workflow. That means I have the window HDC, an offscreen HDC, an offscreen HBITMAP and a pointer to my pixels which are in, let's say, a hypothetical R8G8B8A8 memory layout. What do I do with these objects? Do I create another HBITMAP via CreateDIBSection and write my pixels into that? What do I do with it after?

Edit 3: See answer from Barmak Shemirani for proper solution (my example code has issues). Check also Paul Sanders' answer for some modern WinAPI tips.

Thanks all!

Upvotes: 6

Views: 776

Answers (2)

Barmak Shemirani
Barmak Shemirani

Reputation: 31659

To print mypixels vector use SetDIBitsToDevice to draw to device context. Or use SetDIBits to create a new HBITMAP object.

For simplicity, this example draw directly in to HDC. But you can use CreateCompatibleDC for buffering, or use the buffer method shown in the other answer.

case WM_PAINT:
{
    //int w = width of source bitmap
    //int h = height of source bitmap
    //optional: make sure width and height are correct
    assert(mypixels.size() == w * h);

    PAINTSTRUCT ps;
    auto hdc = BeginPaint(hwnd, &ps);

    BITMAPINFOHEADER bi{ sizeof(bi) };
    bi.biWidth = w;
    bi.biHeight = h;
    bi.biPlanes = 1;
    bi.biBitCount = 32;
    bi.biCompression = BI_RGB;

    SetDIBitsToDevice(hdc, 0, 0, w, h, 0, 0, 0, h, &mypixels[0],
        (BITMAPINFO*)&bi, DIB_RGB_COLORS);

    EndPaint(hwnd, &ps);

    return 0;
}

Using memory dc:

case WM_PAINT:
{
    RECT rc;
    GetClientRect(hwnd, &rc);
    int canvas_width = rc.right;
    int canvas_height = rc.bottom;

    PAINTSTRUCT ps;
    auto hdc = BeginPaint(hwnd, &ps);

    //create memory dc:
    auto memdc = CreateCompatibleDC(hdc);
    auto hbmp = CreateCompatibleBitmap(hdc, canvas_width, canvas_height);
    auto oldbmp = SelectObject(memdc, hbmp); //<- memdc is ready

    //draw on memory dc:
    BITMAPINFOHEADER bi{ sizeof(bi), w, h, 1, 32, BI_RGB };
    SetDIBitsToDevice(memdc, 0, 0, w, h, 0, 0, 0, h, mypixels.data(),
        (BITMAPINFO*)&bi, DIB_RGB_COLORS);

    //draw on actual dc:
    BitBlt(hdc, 0, 0, canvas_width, canvas_height, memdc, 0, 0, SRCCOPY);

    //clean up:
    SelectObject(memdc, oldbmp);
    DeleteObject(hbmp);
    DeleteDC(memdc);
    EndPaint(hwnd, &ps);

    return 0;
}

Upvotes: 7

catnip
catnip

Reputation: 25408

With regard to drawing flicker-free, Vista and later have double buffering support built into the Win32 API. I have adapted the code below from this article. More info at MSDN. Barmak's answer shows you how to draw your pixels.

Initialisation (per thread):

BufferedPaintInit();

Termination (per thread):

BufferedPaintUnInit();

In your WndProc:

case WM_PAINT:
{
    // Set things up in the usual way
    PAINTSTRUCT ps;     
    HDC hDC = BeginPaint (hWnd, &ps);

    RECT rc;
    GetClientRect (hWnd, &rc);

    // Try to use buffered painting (may fail, so they say)
    HDC hBufferedDC;
    HPAINTBUFFER hBufferedPaint = BeginBufferedPaint (hDC, &rc, BPBF_COMPATIBLEBITMAP, NULL, &hBufferedDC);

    if (hBufferedPaint)
        hDC = hBufferedDC;

    // Draw stuff into hDC

    // Clean up    
    if (hBufferedPaint)
        EndBufferedPaint (hBufferedPaint, TRUE);

    // Finished
    EndPaint (hWnd, &ps);
    break;
}

Nothing to it, really.

Upvotes: 5

Related Questions