三、Windows编程之滚动条—滚动条卷动范围
在SYSMETS2中,卷动范围设置最小为0,最大为NUMLINES-1。当滚动条位置是0时,第一行信息显示在显示区域的顶部;当滚动条的位置是NUMLINES-1时,最后一行显示在显示区域的顶部,并且看不见其它行。
可以说SYSMETS2卷动范围太大。事实上只需把信息最后一行显示在显示区域的底部而不是顶部即可。我们可以对SYSMETS2作出一些修改以达到此点。当处理WM_CREATE消息时不设置滚动条范围,而是等到接收到WM_SIZE消息后再做此工作:
iVscrollMax = max (0, NUMLINES - cyClient / cyChar) ; SetScrollRange (hwnd, SB_VERT, 0, iVscrollMax, TRUE) ;
假定NUMLINES等于75,并假定特定窗口大小是:50(cyChar除以cyClient)。换句话说,我们有75行信息但只有50行可以显示在显示区域中。使用上面的两行程序代码,把范围设置最小为0,最大为25。当滚动条位置等于0时,程序显示0到49行。当滚动条位置等于1时,程序显示1到50行;并且当滚动条位置等于25(最大值)时,程序显示25到74行。很明显需要对程序的其它部分做出修改,但这是可行的。
新滚动条函数的一个好的功能是当使用与滚动条范围一样大的页面时,它已经为您做掉了一大堆杂事。可以像下面的程序代码一样使用SCROLLINFO结构和SetScrollInfo:
si.cbSize = sizeof (SCROLLINFO) ; si.cbMask = SIF_RANGE | SIF_PAGE ; si.nMin = 0 ; si.nMax = NUMLINES - 1 ; si.nPage = cyClient / cyChar ; SetScrollInfo (hwnd, SB_VERT, &si, TRUE) ;
这样做之后,Windows会把最大的滚动条位置限制为si.nMax - si.nPage +1而不是si.nMax。像前面那样做出假设:NUMLINES等于75 (所以si.nMax等于74),si.nPage等于50。这意味着最大的滚动条位置限制为74 - 50 + 1,即25。这正是我们想要的。
当页面大小与滚动条范围一样大时,会发生什么情况呢?在这个例子中,就是nPage等于75或更大的情况。Windows通常隐藏滚动条,因为它并不需要。如果不想隐藏滚动条,可在呼叫SetScrollInfo时使用SIF_DISABLENOSCROLL,Windows只是让那个滚动条不能被使用,而不隐藏它。
新SYSMETS
SYSMETS3-此章中最后的SYSMETS程序版本-显示在程序4-4中。此版本使用SetScrollInfo和GetScrollInfo函数,添加左右卷动的水平滚动条,并能更有效地重画显示区域。
SYSMETS3.C /*------------------------------------------------------------------ SYSMETS3.C -- System Metrics Display Program No. 3 (c) Charles Petzold, 1998 ----------------------------------------------------------------*/ #include <windows.h> #include "sysmets.h" LRESULT CALLBACK WndProc (HWND, UINT, WPARAM, LPARAM) ; int WINAPI WinMain (HINSTANCE hInstance, HINSTANCE hPrevInstance, PSTR szCmdLine, int iCmdShow) { static TCHAR szAppName[] = TEXT ("SysMets3") ; HWND hwnd ; MSG msg ; WNDCLASS wndclass ; wndclass.style = CS_HREDRAW | CS_VREDRAW ; wndclass.lpfnWndProc= WndProc ; wndclass.cbClsExtra = 0 ; wndclass.cbWndExtra = 0 ; wndclass.hInstance = hInstance ; wndclass.hIcon = LoadIcon (NULL, IDI_APPLICATION) ; wndclass.hCursor = LoadCursor (NULL, IDC_ARROW) ; wndclass.hbrBackground= (HBRUSH) GetStockObject (WHITE_BRUSH) ; wndclass.lpszMenuName = NULL ; wndclass.lpszClassName = szAppName ; if (!RegisterClass (&wndclass)) { MessageBox (NULL, TEXT ("Program requires Windows NT!"), szAppName, MB_ICONERROR) ; return 0 ; } hwnd = CreateWindow (szAppName, TEXT ("Get System Metrics No. 3"), WS_OVERLAPPEDWINDOW | WS_VSCROLL | WS_HSCROLL, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, NULL, NULL, hInstance, NULL) ; ShowWindow (hwnd, iCmdShow) ; UpdateWindow (hwnd) ; while (GetMessage (&msg, NULL, 0, 0)) { TranslateMessage (&msg) ; DispatchMessage (&msg) ; } return msg.wParam ; } LRESULT CALLBACK WndProc (HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam) { static int cxChar, cxCaps, cyChar, cxClient, cyClient, iMaxWidth ; HDC hdc ; int i, x, y, iVertPos, iHorzPos, iPaintBeg, iPaintEnd ; PAINTSTRUCT ps ; SCROLLINFO si ; TCHAR szBuffer[10] ; TEXTMETRIC tm ; switch (message) { case WM_CREATE: hdc = GetDC (hwnd) ; GetTextMetrics (hdc, &tm) ; cxChar = tm.tmAveCharWidth ; cxCaps = (tm.tmPitchAndFamily & 1 ? 3 : 2) * cxChar / 2 ; cyChar = tm.tmHeight + tm.tmExternalLeading ; ReleaseDC (hwnd, hdc) ; // Save the width of the three columns iMaxWidth = 40 * cxChar + 22 * cxCaps ; return 0 ; case WM_SIZE: cxClient = LOWORD (lParam) ; cyClient = HIWORD (lParam) ; // Set vertical scroll bar range and page size si.cbSize = sizeof (si) ; si.fMask = SIF_RANGE | SIF_PAGE ; si.nMin = 0 ; si.nMax = NUMLINES - 1 ; si.nPage = cyClient / cyChar ; SetScrollInfo (hwnd, SB_VERT, &si, TRUE) ; // Set horizontal scroll bar range and page size si.cbSize = sizeof (si) ; si.fMask = SIF_RANGE | SIF_PAGE ; si.nMin = 0 ; si.nMax = 2 + iMaxWidth / cxChar ; si.nPage = cxClient / cxChar ; SetScrollInfo (hwnd, SB_HORZ, &si, TRUE) ; return 0 ; case WM_VSCROLL: // Get all the vertical scroll bar information si.cbSize = sizeof (si) ; si.fMask = SIF_ALL ; GetScrollInfo (hwnd, SB_VERT, &si) ; // Save the position for comparison later on iVertPos = si.nPos ; switch (LOWORD (wParam)) { case SB_TOP: si.nPos = si.nMin ; break ; case SB_BOTTOM: si.nPos = si.nMax ; break ; case SB_LINEUP: si.nPos - = 1 ; break ; case SB_LINEDOWN: si.nPos += 1 ; break ; case SB_PAGEUP: si.nPos -= si.nPage ; break ; case SB_PAGEDOWN: si.nPos += si.nPage ; break ; case SB_THUMBTRACK: si.nPos = si.nTrackPos ; break ; default: break ; } // Set the position and then retrieve it. Due to adjustments // by Windows it may not be the same as the value set. si.fMask = SIF_POS ; SetScrollInfo (hwnd, SB_VERT, &si, TRUE) ; GetScrollInfo (hwnd, SB_VERT, &si) ; // If the position has changed, scroll the window and update it if (si.nPos != iVertPos) { ScrollWindow (hwnd, 0, cyChar * (iVertPos - si.nPos), NULL, NULL) ; UpdateWindow (hwnd) ; } return 0 ; case WM_HSCROLL: // Get all the vertical scroll bar information si.cbSize = sizeof (si) ; si.fMask = SIF_ALL ; // Save the position for comparison later on GetScrollInfo (hwnd, SB_HORZ, &si) ; iHorzPos = si.nPos ; switch (LOWORD (wParam)) { case SB_LINELEFT: si.nPos -= 1 ; break ; case SB_LINERIGHT: si.nPos += 1 ; break ; case SB_PAGELEFT: si.nPos -= si.nPage ; break ; case SB_PAGERIGHT: si.nPos += si.nPage ; break ; case SB_THUMBPOSITION: si.nPos = si.nTrackPos ; break ; default : break ; } // Set the position and then retrieve it. Due to adjustments // by Windows it may not be the same as the value set. si.fMask = SIF_POS ; SetScrollInfo (hwnd, SB_HORZ, &si, TRUE) ; GetScrollInfo (hwnd, SB_HORZ, &si) ; // If the position has changed, scroll the window if (si.nPos != iHorzPos) { ScrollWindow (hwnd, cxChar * (iHorzPos - si.nPos), 0, NULL, NULL) ; } return 0 ; case WM_PAINT : hdc = BeginPaint (hwnd, &ps) ; // Get vertical scroll bar position si.cbSize = sizeof (si) ; si.fMask = SIF_POS ; GetScrollInfo (hwnd, SB_VERT, &si) ; iVertPos = si.nPos ; // Get horizontal scroll bar position GetScrollInfo (hwnd, SB_HORZ, &si) ; iHorzPos = si.nPos ; // Find painting limits iPaintBeg = max (0, iVertPos + ps.rcPaint.top / cyChar) ; iPaintEnd = min ( NUMLINES - 1, iVertPos + ps.rcPaint.bottom / cyChar) ; for (i = iPaintBeg ; i <= iPaintEnd ; i++) { x = cxChar * (1 - iHorzPos) ; y = cyChar * (i - iVertPos) ; TextOut (hdc, x, y, sysmetrics[i].szLabel, lstrlen (sysmetrics[i].szLabel)) ; TextOut (hdc, x + 22 * cxCaps, y, sysmetrics[i].szDesc, lstrlen (sysmetrics[i].szDesc)) ; SetTextAlign (hdc, TA_RIGHT | TA_TOP) ; TextOut (hdc, x + 22 * cxCaps + 40 * cxChar, y, szBuffer, wsprintf (szBuffer, TEXT ("%5d"), GetSystemMetrics (sysmetrics[i].iIndex))) ; SetTextAlign (hdc, TA_LEFT | TA_TOP) ; } EndPaint (hwnd, &ps) ; return 0 ; case WM_DESTROY : PostQuitMessage (0) ; return 0 ; } return DefWindowProc (hwnd, message, wParam, lParam) ; }
这个版本的程序仰赖Windows保存滚动条信息并做边界检查。在WM_VSCROLL和WM_HSCROLL处理的开始,它取得所有的滚动条信息,根据通知码调整位置,然后呼叫SetScrollInfo设置其位置。程序然后呼叫GetScrollInfo。如果该位置超出了SetScrollInfo呼叫的范围,则由Windows来纠正该位置并且在GetScrollInfo呼叫中传回正确的值。
SYSMETS3使用ScrollWindow函数在窗口的显示区域中卷动信息而不是重画它。虽然该函数很复杂(在新版本的Windows中已被更复杂的ScrollWindowEx所替代),SYSMETS3仍以相当简单的方式使用它。函数的第二个参数给出了水平卷动显示区域的数值,第三个参数是垂直卷动显示区域的数值,单位都是图素。
ScrollWindow的最后两个参数设定为NULL,这指出了要卷动整个显示区域。Windows自动把显示区域中未被卷动操作覆盖的矩形设为无效。这会产生WM_PAINT消息。再也不需要InvalidateRect了。注意ScrollWindow不是GDI函数,因为它不需设备内容句柄。它是少数几个非GDI的Windows函数之一,它可以改变窗口的显示区域外观。很特殊但不方便,它是随滚动条函数一起记载在文件中。
WM_HSCROLL处理拦截SB_THUMBPOSITION通知码并忽略SB_THUMBTRACK。因而,如果使用者在水平滚动条上拖动卷动方块,在使用者释放鼠标按钮之前,程序不会水平卷动窗口的内容。
WM_VSCROLL的方法与之不同:程序拦截SB_THUMBTRACK消息并忽略SB_THUMBPOSITION。因而,程序随使用者在垂直滚动条上拖动卷动方块而垂直地滚动内容。这种想法很好,但应注意:一旦使用者发现程序会立即响应拖动的卷动方块,他们就会不断地来回拖动卷动方块。幸运的是现在的PC快得可以胜任这种严酷的测试。但是在较慢的机器上,可以考虑为GetSystemMetrics使用SB_SLOWMACHINE参数来替代这种处理。
加快WM_PAINT处理的一个方法由SYSMETS3展示:WM_PAINT处理程序确定无效区域中的文字行并仅仅重画这些行。当然,程序代码复杂一些,但速度很快。