本节讲述滚动条的复杂使用方法,以便达到更好的效果。Windows操作系统提供了两套机制,一套机制是使用默认的对象属性进行简单的操作,并提供简单便捷的API接口函数。另一套机制是用户可以自定义对象属性,实现自己想要的效果。本节我们将重新定义滚动条对象的属性,以期实现更好的滚动效果。
本节必须掌握的知识点:
滚动条信息
滚动条的范围和位置
第21练:效果更换的滚动条
3.3.1 滚动条信息
滚动条作为一个GUI对象是以一个SCROLLINFO结构体形式存在的。3.2节中对滚动条操作使用的是Windows系统默认的SCROLLINFO结构。如果我们需要对滚动条进行更为细致的操作,就需要重新定义SCROLLINFO结构。
■SCROLLINFO结构
SCROLLINFO结构用于设置和获取滚动条的信息,并包含以下字段:
typedef struct tagSCROLLINFO {
UINT cbSize; // 结构体大小
UINT fMask; // 标志位,指定要设置或获取的信息
int nMin; // 滚动条的最小位置
int nMax; // 滚动条的最大位置
UINT nPage; // 一页的大小
int nPos; // 当前滚动位置
int nTrackPos; // 通过拖动滑块滚动时的跟踪位置
} SCROLLINFO, *PSCROLLINFO;
cbSize:结构体的大小,用于指定结构体的版本。使用时应设置为sizeof(SCROLLINFO),以确保兼容性。
fMask:标志位,指定要设置或获取的信息。可以是以下常数的组合:
SIF_RANGE:设置或获取滚动条的最小和最大位置值。
SIF_PAGE:设置或获取一页的大小。初始大小通常为滚动条可见区域大小的比例。
SIF_POS:设置或获取当前滚动位置。
SIF_TRACKPOS:获取通过拖动滑块滚动时的跟踪位置。
■对滚动条对象进行操作
对滚动条对象进行操作时,可以使用SetScrollInfo和GetScrollInfo函数来设置和获取滚动条的信息。
●SetScrollInfo函数用于设置滚动条的信息。
BOOL SetScrollInfo(
HWND hwnd,
Int nBar,
LPCSCROLLINFO lpsi,
BOOL redraw
);
hwnd:滚动条所属的窗口句柄。
nBar:指定滚动条的类型,可以是SB_HORZ(水平滚动条)或SB_VERT(垂直滚动条)。
lpsi:指向SCROLLINFO结构体的指针,包含了滚动条的信息。
redraw:指定是否立即重绘窗口。
●GetScrollInfo函数用于获取滚动条的信息。
BOOL GetScrollInfo(
HWND hwnd,// 滚动条所属的窗口句柄。
int nBar,// 指定滚动条的类型
LPSCROLLINFO lpsi //指向SCROLLINFO结构体的指针,包含了滚动条的信息。
);
在调用 SetScrollInfo 或 GetScrollInfo 之前,必须将 cbSize 字段设定为该结构的大小:
si.cbSize = sizeof (si) ;
或
si.cbSize = sizeof (SCROLLINFO) ;
当熟悉windows应用程序后,会发现还有其他的一些结构也类似地将第一个字段定义为结构的大小。这样,以后地Windows版本可以扩充结构而同时保持与以前地应用程序兼容。
●fMask字段是一个或多个以SIF为前缀的标志。它们可以用“位或”运算组合在一起。
在SetScrollInfo中指定了SIF_RANGE时,必须在nMin和nMax中指定滚动条的范围。当在GetScrollInfo中指定了SIF_RANGE时,nMin和nMax将返回当前的范围。
SIF_POS与此类似:在调用SetScrollInfo时,须在nPos字段中指定滚动条的位置,而GetScrollInfo则在nPos中返回当前的位置。
SIF_PAGE可用于指定或获取页面大小。在SetScrollInfo中你在nPage中指定页面大小,而GetScrollInfo在nPage中返回页面大小。如果不希望滑块大小发生变化,就不要使用这个标志。
滑块的大小和显示在窗口中的文档多少是成比例的。在窗口中显示的文档数量称为“页面大小”。下面的公式表明了它们的关系:
滑块的大小 页面大小 文档显示的数量
—————— ≈ ———— ≈ ————————
滚动条长度 范围 文档的总大小
SIF_TRACKPOS标志只用在GetScrollInfo中,而且只在处理通知码是SB_TUMBTRACK或SB_THUMBPSOITION的WM_VSCROLL或WM_HSCROLL消息时。在函数返回时,SCROLLINFO结构的nTrackPos字段将返回当前滑块的位置(一个32位的整数值)。
SIF_DISABLENOSCROLL标志只用在SetScrollInfo中。当设定了这个标志后,原来让滚动条不显示的设置,这时将禁用滚动条。
SIF_ALL标志是SIF_RANG、SIF_POS、SIF_PAGE和SIF_TRACKPOS的组合。在处理WM_SIZE消息,该标志可以使得设定滚动条参数更方便。在SetScrollInfo中,SIF_TRACKPOS将被忽略。)同样,处理滚动条消息也变得方便了。
3.3.2 滚动条的范围和位置
默认的滚动条滚动范围是0~100。回顾一下,在实例SYSMETS2.C中,在处理WM_CREATE消息时,我们将滚动条的滚动范围设置为0~NUMLINES – 1,实际的滚动效果是显示全部内容后,结尾存在大量的空白区域。如果要去掉多余的空白窗口就需要准确控制滑块的滚动范围和位置。这就需要我们等到收到WM_SIZE消息时再来确定滚动条的范围:
iVscrollMax = max (0, NUMLINES - cyClient / cyChar) ;
SetScrollRange (hwnd, SB_VERT, 0, iVscrollMax, TRUE) ;
上面两行代码将滚动条的范围设定为从0到25。当滚动条位置是0时,程序显示0到49行,当滚动条位置是1时,显示1到50行。当滚动条位置是25最大值时,显示25到74行。显然我们还需要修改程序的其他地方,但是这是可行的。
新的滚动条函数的另一个好处是:当使用滚动条页面大小时,Windows自动处理了上面的很多事情。结合SCROLLLINFO结构和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显示一个被禁用的滚动条。
3.3.3 第21练:效果更换的滚动条
/*---------------------------------------------------------
SYSMETS.H -- 系统配置信息结构数组(略)
-----------------------------------------------------------*/
/*------------------------------------------------------------------------
021 编程达人win32 API每日一练
第21个例子SYSMETS3.C:获取字体系统配置信息3---改进:适当大小的窗口滚动条
SCROLLINFO 结构
SetScrollInfo函数
GetScrollInfo函数
ScrollWindow函数
(c) www.bcdaren.com 编程达人
-----------------------------------------------------------------------*/
#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 = 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设为cxChar的1.5倍。1表示变宽字体
cxCaps = (tm.tmPitchAndFamily & 1 ? 3 : 2) * cxChar / 2;
//字符行距
cyChar = tm.tmHeight + tm.tmExternalLeading;
ReleaseDC(hwnd, hdc);
// 保存三列文本的宽度,水平方向一页足以显示全部文本,因此会隐藏了水平滚动条
iMaxWidth = 40 * cxChar + 22 * cxCaps;
return 0;
case WM_SIZE:
cxClient = LOWORD(lParam);
cyClient = HIWORD(lParam);
//初始化滚动条参数结构变量
//si.nPos和si.nTrackPos两个参数没有初始化
//因为操作滚动条时才会知道滚动条和滑块的确切位置信息
si.cbSize = sizeof(si); //SCROLLINFO结构的字节数
si.fMask = SIF_RANGE | SIF_PAGE; //指定滚动条的范围和页面大小
si.nMin = 0; //滚动范围的最小值
si.nMax = NUMLINES - 1; //滚动范围的最大值
si.nPage = cyClient / cyChar; //页面大小
// 设置垂直滚动条范围和页面大小
SetScrollInfo(hwnd, SB_VERT, &si, TRUE);
//再次初始化滚动条参数结构变量
si.cbSize = sizeof(si);
//SIF_DISABLENOSCROLL:滚动条不需要时,禁用(不是删除),需要时则可以显示滚动条
//si.fMask = SIF_RANGE | SIF_PAGE | SIF_DISABLENOSCROLL;
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:
// 获取所有垂直滚动条信息之前,先设置下面两个参数
si.cbSize = sizeof(si);
si.fMask = SIF_ALL;//设置或获取所有结构成员
GetScrollInfo(hwnd, SB_VERT, &si);
// 保存位置以便稍后进行比较
iVertPos = si.nPos; //指定当前的滚动框位置
/**************************nPos滚动条位置计算方法*****************************
滑块的大小 页面大小 文档显示的数量
—————— ≈ ———— ≈ ————————
滚动条长度 范围 文档的总大小
SetScrollInfo设定页面大小,滑块大小也因此确定。
滚动条的范围设定在0和NUMLINES-1之间。当滚动条位置是0时,第一行显示在客户区的最上方,而其他行都不可见。
与其在处理WM_CREATE消息时设定滚动条的范围,不如等到收到WM_SIZE消息时再来确定滚动条的范围:
iVscrollMax = max (0, NUMLINES - cyClient / cyChar) ;
SetScrollRange (hwnd, SB_VERT, 0, iVscrollMax, TRUE) ;
//NUMLINES=75,
cyClient / cyChar=50,
iVscrollMax = 75 - 50 = 25
上面两行代码将滚动条的范围设定为从0到25.
当滚动条位置是0时,程序显示0到49行,
当滚动条位置是1时,显示1到50行。
当滚动条位置是25最大值时,显示25到74行。 ******************************************************************************/
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 (si.nPos != iVertPos)
{
//滚动整个客户区的内容,第2个参数为水平滚动多少像素,第3个参数为垂直滚动像素
ScrollWindow(hwnd, 0, cyChar * (iVertPos - si.nPos),
NULL, NULL); //可以下断点测试
UpdateWindow(hwnd); //更新窗口,发送一条WM_PAINT消息并立即重绘窗口
}
return 0;
case WM_HSCROLL:
// 获取所有垂直滚动条信息
si.cbSize = sizeof(si);
si.fMask = SIF_ALL;//设置或获取所有结构成员
// 保存位置以便稍后进行比较
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;
//滚动到绝对的位置,当前位置由nPos决定
//si.nPos = wParam低16位值,si.nTrackPos = wParam高16位值
case SB_THUMBPOSITION:
si.nPos = si.nTrackPos;
break;
default:
break;
}
//设置位置,然后检索。
//由于在Windows中会随时调整窗口的大小,它可能与设置的值不相同。
si.fMask = SIF_POS;//设置或获取滑块的位置
SetScrollInfo(hwnd, SB_HORZ, &si, TRUE);
GetScrollInfo(hwnd, SB_HORZ, &si);
// 如果位置已经改变,滚动窗口
if (si.nPos != iHorzPos)
{
//滚动整个客户区的内容,第2个参数为水平滚动多少像素,第3个参数为垂直滚动像素
//这个API很特殊,不是调用HDC重绘,而是通过移动窗口位置实现滚动,所以不是GDI函数
ScrollWindow(hwnd, cxChar * (iHorzPos - si.nPos), 0,
NULL, NULL);
}
return 0;
case WM_PAINT:
hdc = BeginPaint(hwnd, &ps);//填充PAINTSTRUCT结构变量ps
// 获取垂直滚动条位置
si.cbSize = sizeof(si);
si.fMask = SIF_POS; //指定滚动条的位置
//返回垂直滑块正确位置
GetScrollInfo(hwnd, SB_VERT, &si);
iVertPos = si.nPos; //垂直滚动的位置
// 获取水平滚动条的位置
GetScrollInfo(hwnd, SB_HORZ, &si);
iHorzPos = si.nPos; //水平滚动的位置
// 计算绘制窗口客户区的范围iVertPos + 行数,例如0~49,1~50
//当前客户区顶部显示的行数+iVertPos
iPaintBeg = max(0, iVertPos + ps.rcPaint.top / cyChar);
//当前客户区底部显示的行数+iVertPos
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);
}
/******************************************************************************
SCROLLINFO 结构:包含滚动条参数。由SetScrollInfo函数(或SBM_SETSCROLLINFO消息)设置,
或由GetScrollInfo函数(或SBM_GETSCROLLINFO消息)检索。
typedef struct tagSCROLLINFO {
UINT cbSize; //指定此结构的大小(以字节为单位)。调用者必须将此设置为sizeof(SCROLLINFO)。
UINT fMask; //指定要设置或检索的滚动条参数。
int nMin; //指定最小滚动位置。
int nMax; //指定最大滚动位置。
UINT nPage; //指定页面大小,以设备为单位。滚动条使用此值来确定比例滚动
框的适当大小。
int nPos; //指定滚动框的位置。
int nTrackPos; //指定用户正在拖动的滚动框的立即位置。
//应用程序可以在处理SB_THUMBTRACK请求代码时检索此值。
//应用程序无法设置即时滚动位置;SetScrollInfo函数忽略此成员。
} SCROLLINFO, *LPSCROLLINFO;
*******************************************************************************
SetScrollInfo函数:设置滚动条的参数,包括最小和最大滚动位置,页尺寸,和滚动框(滑块)的位置。如果需要,该功能还会重画滚动条。
int SetScrollInfo(
HWND hwnd, //窗口句柄
int nBar, //滚动条的类型SB_CTL、SB_HORZ或SB_VERT
LPCSCROLLINFO lpsi, //指向SCROLLINFO结构的指针。在调用SetScrollInfo之前,
//将结构的cbSize成员设置 为 sizeof(SCROLLINFO),将
fMask成员设置 为指示要设置的参数
BOOL redraw //指定是否重绘滚动条以反映对滚动条的更改。TRUE or FALSE
);
*******************************************************************************
GetScrollInfo函数:检索滚动条的参数,包括最小和最大滚动位置,页尺寸,和滚动框(滑块)的位置。
BOOL GetScrollInfo(
HWND hwnd, //窗口句柄
int nBar, //滚动条的类型SB_CTL、SB_HORZ或SB_VERT
LPSCROLLINFO lpsi //指向SCROLLINFO结构的指针
);
指向SCROLLINFO结构的指针。在调用GetScrollInfo之前,将cbSize成员设置 为 sizeof(SCROLLINFO),
并设置 fMask成员以指定要检索的滚动条参数。
*******************************************************************************
ScrollWindow函数:滚屏功能滚动指定窗口的客户区的内容。
注意 的滚屏功能提供了向后兼容性。新应用程序应使用ScrollWindowEx函数。
BOOL ScrollWindow(
HWND hWnd, //窗口句柄
int XAmount, //指定水平滚动量(以设备为单位)。
int YAmount, //指定垂直滚动的量(以设备为单位)。
const RECT *lpRect, //指向RECT结构的指针,该结构指定要滚动的客户区域的一部
分。为NULL,则滚动整个客户区
const RECT *lpClipRect //指向包含裁剪矩形坐标的RECT结构的指针 。
//裁剪矩形内的仅设备位会受到影响。从矩形的外部滚动到内部的位被绘制;从矩形内部滚动到外部的位不会绘制。
);
*/
运行结果:
图3-5 效果更好的滚动条
注意
实例在调用CreateWindow函数创建窗口时添加了垂直滚动条和水平滚动条。在窗口过程处理WM_CREATE消息时并没有设置滚动条的位置和范围。而是在处理WM_SIZE消息,改变窗口大小时分别设置了水平和垂直滚动条信息,包括滚动范围和页面大小。
垂直滚动条:
si.nMin = 0; //滚动范围的最小值
si.nMax = NUMLINES - 1; //滚动范围的最大值
si.nPage = cyClient / cyChar; //页面大小
// 设置垂直滚动条范围和页面大小
SetScrollInfo(hwnd, SB_VERT, &si, TRUE);
水平滚动条:
si.nMin = 0;
//客户区宽度 / 平均字符宽度 =列数,即水平滚动条一页的大小
si.nMax = 2 + iMaxWidth / cxChar;
si.nPage = cxClient / cxChar; // 设置水平滚动条范围和页面大小
SetScrollInfo(hwnd, SB_HORZ, &si, TRUE);
【注意】如果想要滚动条信息设置生效,必须先设置SCROLLINFO结构的字节数和指定设置或获取的结构字段。
si.cbSize = sizeof(si); //SCROLLINFO结构的字节数
si.fMask = SIF_RANGE | SIF_PAGE; //指定滚动条的范围和页面大小
窗口过程在处理WM_VSCROLL和WM_HSCROLL消息时,同样先设置SCROLLINFO结构的字节数和指定设置或获取的结构字段。接着调用GetScrollInfo函数获取滚动条对象当前的信息(重要是获取变化前的滑块位置iVertPos)。然后是switch结构根据操作改变滑块的位置,并调用SetScrollInfo函数将其设置生效。接着再次调用GetScrollInfo函数获取新的滚动条滑块位置,和之前滑块位置进行比较,如果发送了变化,则调用ScrollWindow函数滚动窗口客户区。最后调用UpdateWindow函数立即重绘窗口客户区。
【注意】可以从SCROLLINFO结构变量中获取拖动滚动条滑块的实时位置信息。
si.nPos = si.nTrackPos;//当前追踪位置
ScrollWindow函数并不是一个GDI绘图函数,因为它并未引用HDC设备环境句柄,而是通过移动窗口的位置实现的。
窗口过程处理WM_PAINT消息时,还需要准确的绘制出当前客户区显示的文本内容。需要确定显示内容的范围。调用GetScrollInfo函数分别获取水平和垂直滚动条的位置信息,记得别忘了先设置SCROLLINFO结构的字节数和指定获取的滑块位置结构字段。
接着计算显示的文本行:
iHorzPos = si.nPos; //水平滚动的位置
// 计算绘制窗口客户区的范围iVertPos + 行数,例如0~49,1~50
//当前客户区顶部显示的行数+iVertPos
iPaintBeg = max(0, iVertPos + ps.rcPaint.top / cyChar);
//当前客户区底部显示的行数+iVertPos
iPaintEnd = min(NUMLINES - 1,iVertPos + ps.rcPaint.bottom / cyChar);
最后是使用一个for循环语句逐行绘制文本信息。