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;
CScrollBar* pBar;
DWORD dwStyle = GetStyle();
pBar = GetScrollBarCtrl(SB_VERT);
if ((pBar != NULL && !pBar->IsWindowEnabled()) ||
(pBar == NULL && !(dwStyle & WS_VSCROLL)))
{
sizeScroll.cy = 0;
}
pBar = GetScrollBarCtrl(SB_HORZ);
if ((pBar != NULL && !pBar->IsWindowEnabled()) ||
(pBar == NULL && !(dwStyle & WS_HSCROLL)))
{
sizeScroll.cx = 0;
}
xOrig = x = GetScrollPos(SB_HORZ);
int xMax = GetScrollLimit(SB_HORZ);
x += sizeScroll.cx;
if (x < 0)
x = 0;
else if (x > xMax)
x = xMax;
yOrig = y = GetScrollPos(SB_VERT);
int yMax = GetScrollLimit(SB_VERT);
y += sizeScroll.cy;
if (y < 0)
y = 0;
else if (y > yMax)
y = yMax;
if (x == xOrig && y == yOrig)
return FALSE;
if (bDoScroll)
{
int xAmount = -(x-xOrig);
int yAmount = -(y-yOrig);
if (IsWindowVisible() )
{
CRect fromRect;
GetClientRect(fromRect);
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);
}
}
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.