Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

A solution to get rid of the flicker of controls on scroll window(WINCE/WM)

0.00/5 (No votes)
7 Nov 2010 1  

Problem Description

A simple MFC application which has only one control(button, editbox or whatever) on a scroll window flickers when the scroll bar is dragged up and down.
 

Reason of flicker

Common process of scrolling is described as follows:
 
1. The scroll bar control notifies its owner window, which is scroll window, when the scroll bar is dragged.
2. After receiving the notification, the scroll window calculates the vertical and horizontal movement and also the rectangle to be scrolled. Suppose the dimension(x,y,w,h) of the client area of scroll window is ( 0, 0, 240, 320), and we scroll the window from top to down with 50 pixel vertical movement.
3. All pixels in rectangle (0, 50, 240 270) will be copied to (0, 0, 240, 270) using bitblt().
4. All controls on scroll window move upwards with 50 pixels offset.
5. Invalidates the rectangle area ( 0, 270, 240, 320 )
6. A WM_PAINT message will be sent to make the invalid area to be painted, as well as all the controls on the scroll window
 
The flicker problem is caused by clipping mechanism. As we known, the clipping mechanism is a way to paint window effectively, so the area occupied by child window will not be painting when the window paints the client area, and then the child window would paint this area by itself. However, this is not good for scrolling because it causes flicker. Suppose there's a button on coordinate (0, 50) with 100 pixels width, 30 pixels height. The rectangle area (0, 50, 100, 30) occupied by this button is not copied and painted in step 3 because it is clipped. And this area will be painted with background color and content in step 6 because it is invalidated. Finally, the area, whose dimension is (0, 0, 100, 30), that the button would be moved to would be painted.
 
So there are totally 4 times painting on the screen.
 
1. bitblt() copies the pixels from (0, 50, 240 270) to (0, 0, 240, 270) except the pixel in area (0, 50, 100, 30)
2. window erases the invalid area with background color, including (0, 50, 100, 30)
3. window prints the content on invalid area, also including (0, 50, 100, 30)
4. The button is printed on its new position which is (0, 0, 100, 30)
 
All the changes will be seen by our eyes although the time of painting is very short. We can't see the whole painting process clearly but flicker.
 

Solution

The simplest solution to solve the flicker is by removing the style WS_CLIPCHILDREN. The clipping does not work without this style so the whole area will be copied, the change 2, 3 will be cancelled. The change 4 will still be done but have no effect on eyes. This solution works in WIN32.
 
Unfortunately, this solution does not work in WinCE/WM because the removing of Windows style WS_CLIPCHILDREN is forbidden on CE platform. MSDN says "All windows implicitly have the WS_CLIPSIBLINGS and WS_CLIPCHILDREN styles." So we can't create the scroll window without WS_CLIPCHILDREN.
 
A workable solution is using another scroll function instead of the system's scroll function. Suppose you are using CScrollView as scroll window:
 
1. Overrides CScrollview::OnScrollBy, the core part of scrolling is implemented by this method.
2. Use the following code as your implementation of OnScrollBy:
 
BOOL MyScrollView::OnScrollBy(CSize sizeScroll, BOOL bDoScroll)
{
    int xOrig, x;
    int yOrig, y;
 
    // don't scroll if there is no valid scroll range (ie. no scroll bar)
    CScrollBar* pBar;
    DWORD dwStyle = GetStyle();
    pBar = GetScrollBarCtrl(SB_VERT);
    if ((pBar != NULL && !pBar->IsWindowEnabled()) ||
        (pBar == NULL && !(dwStyle & WS_VSCROLL)))
    {
        // vertical scroll bar not enabled
        sizeScroll.cy = 0;
    }
    pBar = GetScrollBarCtrl(SB_HORZ);
    if ((pBar != NULL && !pBar->IsWindowEnabled()) ||
        (pBar == NULL && !(dwStyle & WS_HSCROLL)))
    {
        // horizontal scroll bar not enabled
        sizeScroll.cx = 0;
    }
 
    // adjust current x position
    xOrig = x = GetScrollPos(SB_HORZ);
    int xMax = GetScrollLimit(SB_HORZ);
    x += sizeScroll.cx;
    if (x < 0)
        x = 0;
    else if (x > xMax)
        x = xMax;
 
    // adjust current y position
    yOrig = y = GetScrollPos(SB_VERT);
    int yMax = GetScrollLimit(SB_VERT);
    y += sizeScroll.cy;
    if (y < 0)
        y = 0;
    else if (y > yMax)
        y = yMax;
 
    // did anything change?
    if (x == xOrig && y == yOrig)
        return FALSE;
 
    if (bDoScroll)
    {
        //Note: following is different from the original implementation of MFC
        int xAmount = -(x-xOrig);
        int yAmount = -(y-yOrig);
        if (IsWindowVisible() )
        {
            CRect fromRect;
            GetClientRect(fromRect);
            // When visible, let Windows do the scrolling
            if( yAmount > 0 ){
                fromRect.DeflateRect( 0, 0, 0, yAmount );
            } else {
                fromRect.DeflateRect( 0, -yAmount, 0, 0 );
            }
            if( xAmount > 0 ){
                fromRect.DeflateRect( xAmount, 0, 0, 0 );
            } else {
                fromRect.DeflateRect( 0, 0, -xAmount, 0 );
            }
 
            CDC* dc = GetDCEx( NULL, DCX_CACHE | DCX_WINDOW| DCX_CLIPSIBLINGS );
            CRect toRect = fromRect;
            toRect.OffsetRect( 0, yAmount );
            dc->BitBlt( toRect.left, toRect.top, toRect.Width(), toRect.Height(),
                dc, fromRect.left, fromRect.top,
                SRCCOPY);
            CRect invalidRect;
            GetClientRect( &invalidRect );
            invalidRect.SubtractRect( invalidRect, toRect );
            InvalidateRect( &invalidRect, TRUE );
        }
 
        HWND hWndChild = ::GetWindow(m_hWnd, GW_CHILD);
        if (hWndChild != NULL)
        {
            for (; hWndChild != NULL;
                hWndChild = ::GetNextWindow(hWndChild, GW_HWNDNEXT))
            {
                CRect rect;
                ::GetWindowRect(hWndChild, &rect);
                ScreenToClient(&rect);
                ::SetWindowPos(hWndChild, NULL,
                    rect.left+xAmount, rect.top+yAmount, 0, 0,
                    SWP_NOSIZE|SWP_NOACTIVATE|SWP_NOZORDER);
            }
        }
        //Note end

        if (x != xOrig)
            SetScrollPos(SB_HORZ, x);
        if (y != yOrig)
            SetScrollPos(SB_VERT, y);
    }
    return TRUE;
}
 
The principal of the code is using a non-clipped DC instead of clipped DC to do scrolling, GetDCEx() is used to get such non-clipped DC. The source and destination are calculated, bitblt() works according to the calculation result. Then calculates the invalid rect of the window and invalidates it, the system will automatically repaint this area later. Finally scrolls all child windows.
 
I've test it on the emulator and my O2 mobile phone, it works fine so far. I have to say it's not a perfect solution but it's the only way I could find till now.
 
This solution supposes you are using CScrollView as scroll window, CView derived class works fine too. The code cannot be used on CWnd derived class directly because there's no OnScrollBy methond in CWnd. However, it's easy to use the principal of the code to make any type of scroll window to get rid of flicker of controls.
 
Please let me know without any hesitation if you have any problem or suggestion.

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here