Mikołaj
Mikołaj

Reputation: 93

Off-screen drawing GDI+

I have a problem - I need to draw two png files, one on the other. When I do it usual way, there is a "blinking" effect (first image overdraws the second one for small time period). I use GDI+ library and my WM_PAINT handling looks like this:

case WM_PAINT:
{
    PAINTSTRUCT ps; 
    HDC hdc = BeginPaint( hwnd, & ps );
    displayImage(firstImage, hwnd);
    displayImage(secondImage, hwnd);
    EndPaint( hwnd, & ps );
    break;
}

displayImage function:

void displayImage(HBITMAP mBmp, HWND mHwnd)
{
    RECT myRect;
    BITMAP bm;
    HDC screenDC, memDC;
    HBITMAP oldBmp;
    BLENDFUNCTION bf;

    GetObject(mBmp, sizeof(bm), &bm);

    bf.BlendOp = AC_SRC_OVER;
    bf.BlendFlags = 0;
    bf.SourceConstantAlpha = 0xff;

    bf.AlphaFormat = AC_SRC_ALPHA;

    screenDC = GetDC(mHwnd);
    GetClientRect(mHwnd, &myRect);

    if (mBmp == NULL)
        FillRect(screenDC, &myRect, WHITE_BRUSH);

    else
    {
        memDC = CreateCompatibleDC(screenDC);
        oldBmp = (HBITMAP)SelectObject(memDC, mBmp);
        AlphaBlend (screenDC, 0, 0, myRect.right,myRect.bottom, memDC, 0, 0, bm.bmWidth,bm.bmHeight, bf);
        SelectObject(memDC, oldBmp);
        DeleteDC(memDC);
        ReleaseDC(mHwnd, screenDC);
    }
}

Loading files to variables:

HBITMAP mLoadImg(WCHAR *szFilename)
{
   HBITMAP result=NULL;

   Gdiplus::Bitmap* bitmap = new Gdiplus::Bitmap(szFilename,false);
   bitmap->GetHBITMAP(NULL, &result);
   delete bitmap;
   return result;
}


firstImage = mLoadImg(L"data\\img\\screen.png");
secondImage = mLoadImg(L"data\\img\\screen2.png");

I've heard that I should do a off-screen drawing. How should that look like?

Upvotes: 5

Views: 2402

Answers (3)

Adrian McCarthy
Adrian McCarthy

Reputation: 48019

First, change displayImage to take the HDC and RECT from the caller instead of the HWND.

void displayImage(HBITMAP mBmp, HDC hdc, const RECT &myRect)
{
    if (mBmp == NULL)
        FillRect(screenDC, &myRect, WHITE_BRUSH);
    else
    {
        BITMAP bm;
        GetObject(mBmp, sizeof(bm), &bm);

        HDC memDC = CreateCompatibleDC(screenDC);
        HBITMAP oldBmp = (HBITMAP)SelectObject(memDC, mBmp);

        BLENDFUNCTION bf;
        bf.BlendOp = AC_SRC_OVER;
        bf.BlendFlags = 0;
        bf.SourceConstantAlpha = 0xff;
        bf.AlphaFormat = AC_SRC_ALPHA;

        AlphaBlend(hdc, 0, 0, myRect.right, myRect.bottom, memDC, 0, 0, bm.bmWidth, bm.bmHeight, bf);

        SelectObject(memDC, oldBmp);
        DeleteDC(memDC);
    }
}

Then, in the caller create a compatible DC and bitmap. These are your off-screen space for doing the compositing. Make the calls to displayImage with this new DC. This will compose the PNGs offscreen. Finally, blit the composed result to the actual window DC in one go.

case WM_PAINT:
{
    PAINTSTRUCT ps; 
    HDC hdc = BeginPaint(hwnd, &ps);
    RECT myRect;
    GetClientRect(hwnd, &myRect);

    // Create an off-screen DC for composing the images.
    HDC hdcMem = CreateCompatibleDC(hdc);
    HBITMAP hbmpMem = CreateCompatibleBitmap(hdc, myRect.right, myRect.bottom);
    HBITMAP hbmpOld = (HBITMAP) SelectObject(hdcMem, hbmpMem);

    // Compose the images to the offscreen bitmap.
    displayImage(firstImage, hdcMem, myRect);
    displayImage(secondImage, hdcMem, myRect);

    // Blit the resulting composition to the window DC.
    BitBlt(hdc, 0, 0, myRect.right, myRect.bottom,
           hdcMem, 0, 0, SRCCOPY);

    // Clean up the offscreen stuff.
    SelectObject(hdcMem, hbmpOld);
    DeleteObject(hbmpMem);
    DeleteDC(hdcMem);

    EndPaint(hwnd, &ps);
    break;
}

Finally, if you're still seeing a flash of the background color, see Pavan Chandaka's answer.

Upvotes: 3

Barmak Shemirani
Barmak Shemirani

Reputation: 31659

You don't need all of that. You can use GDI+ directly:

static Gdiplus::Image *firstImage;
static Gdiplus::Image *secondImage;

case WM_CREATE: // or WM_INITDIALOG if it's dialog
{
    firstImage = new Gdiplus::Image(L"data\\img\\screen.png");
    secondImage = new Gdiplus::Image(L"data\\img\\screen2.png");
    return 0;
}

case WM_PAINT:
{
    PAINTSTRUCT ps = { 0 };
    HDC hdc = BeginPaint(hwnd, &ps);

    Gdiplus::Graphics gr(hdc);
    gr.DrawImage(firstImage, 0, 0);
    gr.DrawImage(secondImage, 0, 0);//<== this will draw transparently

    EndPaint(hwnd, &ps);

    return 0;
}

However, this code is still drawing 2 images back to back with possible flicker (like your original code). Use double-buffering in WM_PAINT so that only one BltBlt is done. Simply change to:

if (msg == WM_PAINT)
{
    PAINTSTRUCT ps = { 0 };
    HDC hdc = BeginPaint(hwnd, &ps);

    RECT rc;
    GetClientRect(hwnd, &rc);
    HDC memdc = CreateCompatibleDC(hdc);
    HBITMAP hbitmap = CreateCompatibleBitmap(hdc, rc.right, rc.bottom);
    HGDIOBJ oldbmp = SelectObject(memdc, hbitmap);

    FillRect(memdc, &rc, WHITE_BRUSH);
    Gdiplus::Graphics gr(memdc);
    gr.DrawImage(firstImage, 0, 0);
    gr.DrawImage(secondImage, 0, 0);

    BitBlt(hdc, 0, 0, rc.right, rc.bottom, memdc, 0, 0, SRCCOPY);

    SelectObject(memdc, oldbmp);
    DeleteObject(hbitmap);
    DeleteDC(memdc);

    EndPaint(hwnd, &ps);

    return 0;
}

As for the original code:

void displayImage(HBITMAP mBmp, HWND mHwnd)
{
HDC hdc = GetDC(mHwnd);
...
}

You should change the function declaration to void displayImage(HBITMAP mBmp, HWND mHwnd, HDC hdc) then you can pass the hdc directly from WM_PAINT

Upvotes: 6

Pavan Chandaka
Pavan Chandaka

Reputation: 12821

Handle "WM_ERASEBKGND" message by your self.

Actually before loading the second image two things happen.

  1. WM_ERASEBKGND is triggered first to fill the image area with, whatever current windows background color is.
  2. WM_PAINT to render action.

Documentation says to avoid blink/Flickr, provide a default handler for "WM_ERASEBKGND".

Below is the link, go to "A Control That Doesn't Flicker". You have an example too.

https://msdn.microsoft.com/en-us/library/ms969905.aspx

Upvotes: 0

Related Questions