白驹过隙,这篇文章距今已有一年以上的历史。技术发展日新月异,文中的观点或代码很可能过时或失效,请自行甄别:)

看p先生的<>的时候,在第四章对ScrollWindow这厮后两个参数lpRect和lpClipRect很不解,便深入了解了下,首先看下MSDN对它的解释:

The ScrollWindow function scrolls the contents of the specified
window's client area.

此函数定义如下:

 BOOL ScrollWindow(
        __in  HWND hWnd,
        __in  int XAmount,
        __in  int YAmount,
        __in  const RECT *lpRect,
        __in  const RECT *lpClipRect
    );

各参数的解释:

HWND:

Handle to the window where the client area is to be scrolled.

XAmout:

Specifies the amount, in device units, of horizontal scrolling. If the window being scrolled has the CS_OWNDC or CS_CLASSDC style, then this parameter uses logical units rather than device units. This parameter must be a negative value to scroll the content of the window to the left.

YAmount:

Specifies the amount, in device units, of vertical scrolling. If the window being scrolled has the CS_OWNDC or CS_CLASSDC style, then this parameter uses logical units rather than device units. This parameter must be a negative value to scroll the content of the window up.

lpRect:

Pointer to the RECT structure specifying the portion of the client area to be scrolled. If this parameter is NULL, the entire client area is scrolled.

lpClipRect:

Pointer to the RECT structure containing the coordinates of the clipping rectangle. Only device bits within the clipping rectangle are affected. Bits scrolled from the outside of the rectangle to the inside are painted; bits scrolled from the inside of the rectangle to the outside are not painted.

对此函数的说明:

If the caret is in the window being scrolled, ScrollWindow
automatically hides the caret to prevent it from being erased and then
restores the caret after the scrolling is finished. The caret position
is adjusted accordingly.

The area uncovered by ScrollWindow is not repainted, but it is
combined into the window's update region. The application eventually
receives a WM_PAINT message notifying it that the region must be
repainted. To repaint the uncovered area at the same time the
scrolling is in action, call the UpdateWindow function immediately
after calling ScrollWindow.

If the lpRect parameter is NULL, the positions of any child windows in
the window are offset by the amount specified by the XAmount and
YAmount parameters; invalid (unpainted) areas in the window are also
offset. ScrollWindow is faster when lpRect is NULL.

If lpRect is not NULL, the positions of child windows are not changed
and invalid areas in the window are not offset. To prevent updating
problems when lpRect is not NULL, call UpdateWindow to repaint the
window before calling ScrollWindow.

MSDN在这里对lpRect参数解释的很详细,但是对lpClipRect却没有过多解释,比如lpClipRect为NULL的时候呢?ScrollWindow是如何运作的呢?

为了解惑,于是做了一系列小测试,下面是代码片段:

int i,temp;
static int cxClient,cyClient,cxChar,cyChar,cyLine,j;
TEXTMETRIC tm;
HDC hdc;
PAINTSTRUCT ps;
static char sp[30];
static RECT rc;

switch(iMsg)
{
case WM_CREATE:
for(i=0;i<30;i++)
{
sp[i]='0'+i;
}
j=0;
hdc=GetDC(hwnd);
GetTextMetrics(hdc,&tm);
cxChar=tm.tmAveCharWidth;
cyChar=tm.tmHeight+tm.tmExternalLeading;
ReleaseDC(hwnd,hdc);
break;
case WM_SIZE:
cxClient=LOWORD(lParam);
cyClient=HIWORD(lParam);
cyLine=cyClient/cyChar;
break;
case WM_LBUTTONDOWN:
GetClientRect(hwnd,&rc);
rc.top=cyChar;
rc.bottom=5*cyChar;
ScrollWindow(hwnd,0,-cyChar,NULL,NULL);
j-=1;
break;
case WM_PAINT:
hdc=BeginPaint(hwnd,&ps);

for(i=j,temp=0;temp<30;temp++,i++)
{
TextOutA(hdc,0,i*cyChar+cyChar,&sp[temp],sizeof(char));
}
TextOut(hdc,0,0,TEXT("hello world"),lstrlen(TEXT("hello world")));
EndPaint(hwnd,&ps);
break;

首先窗口中依次从窗口第二行开始按照ASCII表顺序依次输出0及其之后的100个字符.并且在窗口最上面一行输出经典的各位程序猿最熟悉的"hello world".每当点击鼠标左键一次时出"hello world"外的字符便向上滚动一行.

当首次运行时如下效果:

Capture.PNG

恩,是我们要的效果,好了,按一下鼠标左键一次,问题出现了,经典的"hello world"居然消失不见了!!!

Capture.PNG

为什么呢?先想一下,我们在ScrollWindow的第三个和第四个参数都设置为了NULL,意思就是说滚动区域和剪切位置都是整个客户区,按照我们的预想,先把整个客户区向上一行,然后再在第一行输出"hello world",为什么实际上去没有更新呢?好了,这次我们在WM_PAINT处加个断点监视下各个变量的值,我们知道在BeginPaint的第二个参数ps中有一个RECT结构,这个结构就是重绘区域(invalid region).我们来看下这个结构是什么呢?,首次运行后点击鼠标一次,运行到WM_PAINT,可以看到ps的rcPaint的数值如下:

Capture.PNG

可以看到更新区域居然是最后一行,也就是说点击鼠标左键之后重绘区域并没有包括"hello world",所以向上移动一行后"hello world"便被覆盖掉了.soga...

于是猜想,要是我将"hello world"显示的位置移到窗口最后一行呢,是不是就是hello world就会重绘了呢?好了,代码改下:

case WM_PAINT:
hdc=BeginPaint(hwnd,&ps);

for(i=j,temp=0;temp<50;temp++,i++)
{
TextOutA(hdc,0,i*cyChar+cyChar,&sp[temp],sizeof(char));
}
TextOut(hdc,0,ps.rcPaint.top,TEXT("hello world"),lstrlen(TEXT("hello world")));
EndPaint(hwnd,&ps);
break;

运行,按鼠标左键,出现下图:

Capture.PNG

恩,猜想正确,再按一次鼠标左键,咦,怎么回事?

Capture.PNG

hello world出现了两次,怪哉,按理说倒数第二行应该出现字母"N"才对,经过google后知道原来ScrollWindow的原理是运用了BitBlt,原来如此.继续研究...

明白了上面的原理后我们将滚动的范围和区域改为窗口第二行开始到屏幕底端,同时将"hello world"的显示移回窗口第一行,改后部分代码如下:

case WM_LBUTTONDOWN:
GetClientRect(hwnd,&rc);
rc.top=cyChar;
ScrollWindow(hwnd,0,-cyChar,&rc,&rc);
j-=1;
break;

先把断点去掉,运行,点击鼠标左键:

Capture.PNG

perfect,完全按照我们的预想在work,但是又有了疑惑,如果我将将剪切区域设置为&rc,而滚动区域为NULL即整个窗口滚动呢?会出现什么呢?

改代码,测试:

Capture.PNG

怪哉,为什么我将滚动区域设为整个客户区了可是"hello world"依旧还存在呢?MSDN上也说了当lpRect为NULL时,整个客户区也应该滚动啊?不解...,在WM_PAINT处断点测试,也同样发现无效区域也是最后一行

Capture.PNG看来在这种情况下windows同时将lpRect限定在了lpClipRect的范围内了...

接下来换一下,我将rcClipRect设为NULL,而将rcRect设为&rc,测试如下:

Capture.PNG

我去,hello world怎么又没了??!!我明明将滚动区域限制在了rc里了,为什么我的第一行仍然给覆盖了呢?再次在WM_PAINT处断点,观察到重绘区域依旧是最后一行:

Capture.PNG看来当lpRect为NULL而lpClipRect不为NULL时,windows自动将lpRect设为lpClipRect值了.

到此,对ScrollWindow的探索也就有了结果,看来关键的还是lpClipRect这个参数,哪怕是lpRect为NULL,滚动范围依旧为lpClipRect,而lpClipRect为NULL,不管lpRect为何值,滚动范围依旧是lpClipRect,不信可以将lpRect设置为另外一个区域试试,这里就不举例了:)

网上搜索了良久都没有看到对此API的详细解释,遂一番研究后有了此文,希望能够帮到正在看此文的你,当然如果我的理解有误还望指出,不甚感激:-)