滚动栏是Windows中重要的UI元素,它有两种类型:标准滚动栏、滚动栏控制。二者比较起来,标准滚动栏简单但功能有限制。它只在窗口客户区的边界上,作为窗口的一个部分不含有独立的窗口句柄,消息由所在窗口的WndProc处理。滚动栏控制功能更强大一点,相对复杂一些。它可以在窗口的任何地方,是独立的子窗口有自己的窗口句柄,消息由自己处理。
本文的目的是展示滚动栏的消息处理方法,不涉及复杂的应用,所以例子比较简单,是对msdn上滚动栏示例的简化:只含有垂直滚动栏,不含有水平滚动栏。
下面是这个例子的运行截图:
对右侧的滚动栏进行交互,客户区的文本可以正确地显示出来。这个示例程序代码非常简单:
// Scrollable.cpp // 2012-12-04 by btwsmile #include <Windows.h> #include <tchar.h> // WndProc pre-declaration LRESULT CALLBACK WndProc(HWND, UINT, WPARAM, LPARAM);
因为要调用Windows API函数,所以需包含Windows.h头文件。tchar.h是提供通用字符串处理的,程序中调用了_tcslen()函数计算字符串的长度,为了支持Unicode这样做是很有必要的。WndProc是窗口过程函数的声明式,其实现被放在WinMain后。
int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, PSTR szCmdLine, int iCmdShow) { static TCHAR szAppName[] = TEXT("Scrollable"); WNDCLASS wndclass; MSG msg; 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)(COLOR_WINDOW+1); wndclass.lpszMenuName = NULL; wndclass.lpszClassName = szAppName; if( !::RegisterClass(&wndclass) ) { MessageBox(NULL, TEXT("Register wndclass failed!"), TEXT("ERROR"), MB_OK); return 1; } HWND hWnd = ::CreateWindow(szAppName, szAppName, WS_OVERLAPPEDWINDOW | WS_VSCROLL, 200, 200, 320, 240, NULL, NULL, hInstance, NULL); ::ShowWindow(hWnd, iCmdShow); ::UpdateWindow(hWnd); while( ::GetMessage(&msg, NULL, 0, 0) ) { ::TranslateMessage(&msg); ::DispatchMessage(&msg); } return msg.wParam; }
遵循了窗口的一般创建过程,即:
a. 定义窗口类wndclass;
b. 注册窗口类;
c. 创建窗口并获得句柄hWnd;
d. 显示和更新窗口;
e. 启动消息循环。
需要特别注意的是,在调用CreateWindow函数创建窗口时,窗口风格需指定WS_VSCROLL,这样系统才会在窗口的右侧增加一个滚动栏。
WndProc有点长,但容易理解,主要分成两个部分:变量声明,消息处理。
// WndProc definition LRESULT CALLBACK WndProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam) { HDC hdc; PAINTSTRUCT ps; TEXTMETRIC tm; SCROLLINFO si; static int yClient; // height of client area static int yChar; // vertical scrolling unit static int yPos; // current vertical scrolling position // Create an array of lines to display. #define LINES 28 static TCHAR *abc[] = { TEXT("anteater"), TEXT("bear"), TEXT("cougar"), TEXT("dingo"), TEXT("elephant"), TEXT("falcon"), TEXT("gazelle"), TEXT("hyena"), TEXT("iguana"), TEXT("jackal"), TEXT("kangaroo"), TEXT("llama"), TEXT("moose"), TEXT("newt"), TEXT("octopus"), TEXT("penguin"), TEXT("quail"), TEXT("rat"), TEXT("squid"), TEXT("tortoise"), TEXT("urus"), TEXT("vole"), TEXT("walrus"), TEXT("xylophone"), TEXT("yak"), TEXT("zebra"), TEXT("This line contains words, but no character. Go figure."), TEXT("") };
hdc是设备描述表句柄,想要在客户区输出文本或图形都需要hdc;ps是绘制结构,在处理WM_PAINT消息的时候使用,其rcPaint成员表示无效矩形区域,在计算TextOut的垂直坐标时会用到;tm表示字体的尺寸信息;si表示滚动栏信息。yClient表示客户区的高度,yChar表示每一行的高度,yPos表示当前垂直滚动栏的位置。LINES是常量宏,表示abc所含字符串数量。abc数组保存着将在窗口中显示字符串,每行一个字符串。
switch (uMsg) { case WM_CREATE : hdc = GetDC (hwnd); GetTextMetrics (hdc, &tm); // get text metric info yChar = tm.tmHeight + tm.tmExternalLeading; ReleaseDC (hwnd, hdc); return 0; case WM_SIZE: yClient = HIWORD (lParam); si.cbSize = sizeof(si); si.fMask = SIF_RANGE | SIF_PAGE; si.nMin = 0; si.nMax = LINES - 1; si.nPage = yClient / yChar; SetScrollInfo(hwnd, SB_VERT, &si, TRUE); // Set the vertical scrolling range and page size return 0; case WM_VSCROLL: si.cbSize = sizeof (si); si.fMask = SIF_ALL; GetScrollInfo (hwnd, SB_VERT, &si); yPos = 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; } si.fMask = SIF_POS; SetScrollInfo (hwnd, SB_VERT, &si, TRUE); GetScrollInfo (hwnd, SB_VERT, &si); // If the position has changed, scroll window and update it if (si.nPos != yPos) { ScrollWindow(hwnd, 0, yChar * (yPos - si.nPos), NULL, NULL); UpdateWindow (hwnd); } return 0; case WM_PAINT : hdc = BeginPaint (hwnd, &ps); si.cbSize = sizeof (si); si.fMask = SIF_POS; GetScrollInfo (hwnd, SB_VERT, &si); yPos = si.nPos; { // Find painting limits int iFirstLine = max (0, yPos + ps.rcPaint.top / yChar); int iLastLine = min (LINES - 1, yPos + ps.rcPaint.bottom / yChar); for (int i = iFirstLine; i <= iLastLine; i++) ::TextOut(hdc, 0, ps.rcPaint.top + (i-iFirstLine)*yChar, abc[i], _tcslen(abc[i])); } EndPaint (hwnd, &ps); return 0; case WM_DESTROY : PostQuitMessage (0); return 0; } return DefWindowProc (hwnd, uMsg, wParam, lParam); }
在处理WM_CREATE消息时,调用GetTextMetrics函数获取字体尺寸信息,yChar等于tmHeight与tmExternalLeading之和。
在处理WM_SIZE消息时,调用SetScrollInfo函数设置滚动栏的range和page size,可以看出是以“行”为单位的,而非像素。
接下来处理WM_VSCROLL消息,当用户与右侧滚动栏交互会触发这一消息,其wParam参数的低16位表示交互请求类型,有8种,这里只处理了其中的7种,没处理的一种是SB_THUMBPOSITION,这种消息其实是当用户拖动滚动块松开鼠标后发出的。
处理WM_VSCROLL消息分成三大步:一是更新si.nPos,二是调用SetScrollInfo设置滚动栏的信息,三是判断是否发生了变更,若变更则调用ScrollWindow滚动窗口并调用UpdateWindow里面更新无效的矩形区域。更新的实质是直接向WndProc发送WM_PAINT消息,从而可以看出WndProc在返回前被重入的。
接着处理WM_PAINT消息,对无效的矩形区域进行绘制。注意TextOut附近的代码被专门放入一个语句块中,用大括号标识出来,不这么做编译器会报错,这与作用域有关。
需要特别注意TextOut输出字符串时所使用的坐标,水平方向上总是0,垂直方向上是ps.rcPaint.top + (i-iFirstLine)*yChar,即以无效矩形区域的上侧(top)为基准,偏移i-iFirstLine行后的起始坐标值。
最后是WM_DESTROY消息,当用户关闭程序,窗口被销毁后就会抛出此消息。这时Post退出消息号0,消息循环获取它,正好可以退出循环,整个程序运行结束。