c++winapimfcscrollviewgdi

Issues with BitBlt in MFC when scrolling: Not drawing newly visible regions


I'm working on implementing double buffering using a memory DC in an MFC project that utilizes CScrollView. My custom view class is derived from CScrollView.

However, I encounter an issue when scrolling down: the newly visible areas are not drawn.

I attempted to reset the viewport origin with memDC.SetViewportOrg(0, 0); but this did not resolve the issue. Moreover, the previously visible areas are redrawn incorrectly after scrolling — they do not align with the amount I've scrolled.

Surprisingly, I found a workaround that contradicts the official MSDN description of the BitBlt parameters.

Could someone help clarify why the observed behavior differs from the MSDN documentation? What might I be missing in handling scrolling and redrawing correctly in a CScrollView with double buffering?

Here's the code without all the miscellaneous bits:

void CMyScrollView::OnDraw(CDC* pDC)
{
    CDocument* pDoc = GetDocument();
    ASSERT_VALID(pDoc);
    if (!pDoc)
        return;

    CRect rect;
    GetClientRect(&rect);

    CDC memDC;
    memDC.CreateCompatibleDC(pDC);

    CBitmap bitmap;
    bitmap.CreateCompatibleBitmap(pDC, rect.Width(), rect.Height());
    CBitmap* pOldBitmap = memDC.SelectObject(&bitmap);

    // set the viewport origin to reflect the mouse scroll.
    CPoint scrollPos = GetDeviceScrollPosition();
    memDC.SetViewportOrg(-scrollPos.x, -scrollPos.y);

    // custom drawing function.
    myDrawingFunction(&memDC); 

    // reset the viewport origin.
    // memDC.SetViewportOrg(0, 0);

    pDC->BitBlt(0, 0, rect.Width(), rect.Height(), &memDC, 0, 0, SRCCOPY);

    // This works, but seems quite hacky and it doesn't seem to match the MSDN description of the parameters.
    // pDC->BitBlt(0, 0, rect.Width()+scrollPos.x, rect.Height()+scrollPos.y, &memDC, 0, 0, SRCCOPY)

    memDC.SelectObject(pOldBitmap);
}

Solution

  • I found other solutions and these seem legit, and figured out why my original hacky solution worked.

        // (In real code, we'd better check the return values of these functions.)
    
        // Coordinate modification.
        //  - Both below methods work in my device, but using logical units might be better 
        //    since `BitBlt` uses logical units.
        //    (BitBlt/GetScrollPosition/GetDeviceScrollPosition use logical/logical/device units, respectively.)
        //  - method 1.
        CPoint scrPos1 = GetScrollPosition();
        memDC.SetWindowOrg(scrPos1.x, scrPos1.y);
        //  - method 2.
        //CPoint scrPos2 = GetDeviceScrollPosition();
        //memDC.SetViewportOrg(-scrPos2.x, -scrPos2.y);
    
    
        // draw whatever eg. myDrawingFunction(&memDC); ...
        // then call BitBlt.
    
    
        // solution 1
        pDC->BitBlt(scrPos1.x, scrPos1.y, cliRect.Width(), cliRect.Height(), &memDC, scrPos1.x, scrPos1.y, SRCCOPY);
        
        // solution 2_1 (when using `SetViewportOrg`)
        //  However, I'm not sure this would work in general, because
        //  on my device now, the units of the `GetScrollPosition` and `GetDeviceScrollPosition` return values ​​seem to be the same.
        memDC.SetViewportOrg(0, 0);
        pDC->BitBlt(scrPos2.x, scrPos2.y, cliRect.Width(), cliRect.Height(), &memDC, 0, 0, SRCCOPY);
        
        // solution 2_2 (when using `SetWindowOrg`)
        memDC.SetWindowOrg(0, 0);
        pDC->BitBlt(scrPos1.x, scrPos1.y, cliRect.Width(), cliRect.Height(), &memDC, 0, 0, SRCCOPY);
    

    Also, if you want, you can use CDC::GetClipBox instead of CWnd::GetClientRect

        // solution 3 (using CDC::GetClipBox instead of CWnd::GetClientRect)
        //  - `GetClipBox` just shifts the coordinates of the CRect as scroll amount, 
        //    so this is the same as solution 1.
        CRect clipRect;
        pDC->GetClipBox(&clipRect);
        // ...create bitmap with clipRect's Width() and Height() and then draw whatever eg. myDrawingFunction(&memDC); ...
        // then call BitBlt.
        pDC->BitBlt(clipRect.left, clipRect.top, clipRect.Width(), clipRect.Height(), &memDC, clipRect.left, clipRect.top, SRCCOPY);
    

    Now we can understand why my original (hacky) solution worked, but I think it's not using the BitBlt function as its intention. So I'm thinking to use one of the above solutions.


    Added:

    Just for future reference, since this is an answer to myself)

    My original question and the answer(example codes right above) were about drawing on the bitmap whose size is just the same as the visible client area. So, the bitmap has to be redrawn whenever the mouse scrolling happens.

    To fix this, one solution would be drawing a larger area (eg. entire scrollable area) on the bitmap once, and just outputting only the visible part of it when mouse scrolls.

    Right now I don't have time to do this, but in the future, if I wanna make this efficient, I should consider separating custom drawing function (which actually draws on the in-memory bitmap - eg. calling LineTo many times) from the OnDraw since OnDraw is called many times here and there in the project, eg: OnDraw gets called whenever OnMouseWheel is executed even if I don't explicitly call Invalidate().

    So, in the separate drawing function, draw the entire area (not only the visible client area) on the bitmap so that the graphics don't have to be redrawn on the bitmap every time the mouse scrolls. Of course, for this, the size of the bitmap should be large enough, eg. the same as the size of the entire scrollable area.

    In the OnDraw, just output the (already finished) drawing on the bitmap to the display(monitor) using BitBlt, but only the part of the bitmap that corresponds to the visible client area using GetClipBox or GetScrollPosition or GetDeviceScrollPosition.

    So... in this method, the actual drawing (on the in-memory bitmap though) is done by that custom drawing function, and OnDraw merely outputs some part of the bitmap to the display(monitor).