滚动栏是Windows中重要的UI元素,它有两种类型:标准滚动栏、滚动栏控制。二者比较起来,标准滚动栏简单但功能有限制。它只在窗口客户区的边界上,作为窗口的一个部分不含有独立的窗口句柄,消息由所在窗口的WndProc处理。滚动栏控制功能更强大一点,相对复杂一些。它可以在窗口的任何地方,是独立的子窗口有自己的窗口句柄,消息由自己处理。
本文的目的是展示滚动栏的消息处理方法,不涉及复杂的应用,所以例子比较简单,是对msdn上滚动栏示例的简化:只含有垂直滚动栏,不含有水平滚动栏。
下面是这个例子的运行截图:
对右侧的滚动栏进行交互,客户区的文本可以正确地显示出来。这个示例程序代码非常简单:
1. 头文件和声明
// 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后。
2. 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,这样系统才会在窗口的右侧增加一个滚动栏。
3. WndProc的实现
WndProc有点长,但容易理解,主要分成两个部分:变量声明,消息处理。
3.1 变量声明
// 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数组保存着将在窗口中显示字符串,每行一个字符串。
3.2 消息处理
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,消息循环获取它,正好可以退出循环,整个程序运行结束。