1. 上个例子的不足:
首先是滑块的大小不能根据窗口的大小按比例调整使界面显得不友好,其次是滑块位置由wParam的高16位给出,因此滑块的返回就被限制在了16位整数之内,不能利用Win32编程的优势(不能使用32位表示滑块的范围)
一般计算合理的滑块大小的公式为:
2. 使用Win32新的滚条API:
SetScrollRange、GetScrollRange、SetScrollPos、GetScrollPos这四个函数太老了,和前一个例子配合使用的套路局限性太大,上面提到过
现引入新的滚条管理函数:SetScrollInfo和GetScrollInfo函数,它们共同维护一个ScrollInfo结构,设置滑块参数时可以将ScrollInfo中的值传给Set函数进行设置,获取滑块信息时可以通过Get函数将信息传递到ScrollInfo结构中:
typedef struct tagSCROLLINFO {
UINT cbSize; // 即保存本结构的大小,一般初始化为cbSize = sizeof( SCROLLINFO )
// 还有很多结构类似,第一个字段都是本结构的大小
// 最主要是为了日后对程序拓展时(比如加大该结构的信息)可以与老版本兼容
UINT fMask; // 宏,设置或获取的值,以SIF作为前缀,表示Scroll Info
int nMin; // 滚条范围,单位是位置
int nMax;
UINT nPage; // 页面大小,位置的个数
int nPos; // 当前位置
int nTrackPos; // 拖动时的实时位置
} SCROLLINFO, *PSCROLLINFO;
关于fMask:
SIF_RANGE:在set中需要指定nMin和nMax以设定范围,在get中获取该范围
SIF_POS:设定和获取nPos时使用
SIF_PAGE:设定和获取nPage
SIF_TRACKPOS:只用在get中,并且只用在处理SB_THUMBTRACK和SB_THUMBPOSITION通知码中
SIF_ALL:是以上四个的组合,一般用在响应WM_SIZE中,但是set的话将忽略里面的SIF_TRACKPOS的作用!
两个函数的原型:
int SetScrollInfo( HWND hwnd, int fnBar, LPCSCROLLINFO lpsi, BOOL fRedraw );
BOOL GetScrollInfo( HWND hwnd, int fnBar, LPSCROLLINFO lpsi );
可以看到就是通过第三个参数,即ScrollInfo结构来设置和获取信息的,重画滚条的选项只有在Set中有,fnBar就是用来指定是垂直滚条还是水平滚条(SB_VERT、SB_HORZ)
3. 滚条范围的合理取值:
其实当滑块沉底时没必要让最后一行数据显示在客户区顶部(意味着只有第一行有数据,后面都是空白,显得特别不友好),最好是当滑块沉底时最后一行数据刚好显示在客户区的最后一行,这样就只要设定range为[ 0. max( 0, NUMLINES - cyClient / cyChar ) ]就行了,其实0就是nMin,NUMLINES - 1就是nMax,而cyClient / cyChar就是nPage,其实Windows很聪明,只要你设定了nMin, nMax, nPage,Windows就会把实际可以移动的范围设置成[ nMin, max( nMin, nMax - nPage + 1 ) ]了,而用户不需要再自己计算一次了,非常方便,也就是说nMin,nMax只是一个逻辑值,实际的范围由上述自动算出,维护在Windows内部,显示也是这样显示的,也就是实际的范围
!还有更聪明的地方,如果发现nPage大于或等于实际范围,则Windows将自动隐藏滚条了,但是如果你还是想强行显示被隐藏的滚条则可以在set中设定SIF_DISABLENOSCROLL选项,还有就是滑块大小会自动根据ScrollInfo中的值按照之前提到的公式计算并调整!
4. 滚条大小可变并加入水平滚条的效率更高的SysMets3:
// sysmets3.c
#include <windows.h>
#include "sysmets.h"
LRESULT CALLBACK WndProc( HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam );
int WINAPI WinMain(
HINSTANCE hInstance,
HINSTANCE hPrevInstance,
LPSTR lpszCmdLine,
int nCmdShow
)
{
static TCHAR szAppName[] = TEXT("sysmets3");
WNDCLASS wndclass;
HWND hWnd;
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)GetStockObject( WHITE_BRUSH );
wndclass.lpszMenuName = NULL;
wndclass.lpszClassName = szAppName;
RegisterClass( &wndclass );
hWnd = CreateWindow(
szAppName,
TEXT("Get System Metrics Version 3"),
WS_OVERLAPPEDWINDOW | WS_HSCROLL | WS_VSCROLL, // 同时加上水平和垂直的滚条
CW_USEDEFAULT, CW_USEDEFAULT,
CW_USEDEFAULT, CW_USEDEFAULT,
NULL, NULL, hInstance, NULL
);
ShowWindow( hWnd, nCmdShow );
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;
static int cxClient, cyClient; // 客户区的宽度和高度
static int nxMax; // 水平方向最多有多少个位置
TCHAR szBuffer[30];
HDC hDC;
PAINTSTRUCT ps;
TEXTMETRIC tm;
SCROLLINFO si; // 滚条信息结构
int iPreVertPos, iPreHorzPos; // 之前的滑块所处的垂直位置和水平位置
int iVertPos, iHorzPos; // 滑块当前位置
int iPaintBegin, iPaintEnd; // 重绘时在无效矩形内的文本的起始行号和结束行号
int i, x, y;
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;
// 位置总数大概就是一行的字符数在预一两个字符的位置即可
nxMax = 2 + ( 22 * cxCaps + 40 * cxChar ) / cxChar;
ReleaseDC( hWnd, hDC );
return 0;
case WM_SIZE: // 在窗口大小和性状改变时就要相应调整滑块的大小
cxClient = LOWORD( lParam ); // 由于滑块是跟窗口大小成比例的
cyClient = HIWORD( lParam ); // 因此要先获得当前窗口的尺寸
si.cbSize = sizeof( SCROLLINFO ); // 在每次Set前都要设置一下该字段
// 这是好习惯
// Get前就不需要了
si.fMask = SIF_RANGE | SIF_PAGE; // 滑块大小就是由位置范围和每页有多少个位置计算而得
si.nMin = 0;
si.nMax = NUMLINES - 1;
si.nPage = cyClient / cyChar;
SetScrollInfo( hWnd, SB_VERT, &si, TRUE ); // 设置后滑块大小会自动改变
// !不需要设置nPos,因为要保留窗口大小改变之前滑块的位置!
si.cbSize = sizeof( SCROLLINFO );
si.fMask = SIF_RANGE | SIF_PAGE;
si.nMin = 0;
si.nMax = nxMax;
si.nPage = cxClient / cxChar;
SetScrollInfo( hWnd, SB_HORZ, &si, TRUE ); // 同理设置水平滑块的大小
return 0;
// 收到滚条消息后需要获得尽可能多的滚条信息对滚条最终位置进行设置
// 当用户对滚条操作时是无法改变其实际位置nPos的,必须在程序中设定才行
// 即先收到消息,再做出响应,Windows不会自动响应滚条位置改变的消息
case WM_VSCROLL:
si.fMask = SIF_ALL; // 因此要获得尽可能多的通知码
GetScrollInfo( hWnd, SB_VERT, &si );
iPreVertPos = si.nPos; // 记录之前的位置
switch ( LOWORD( wParam ) )
{
case SB_TOP: // 触顶,触顶和触底都是只屏幕上滑块显示的位置而不是实际位置
si.nPos = si.nMin;
break;
case SB_BOTTOM: // 触底,这两条消息只在键盘接口后才有用,只用于响应键盘消息!
si.nPos = si.nMax;
break;
// 对于SB_LEFT和SB_RIGHT也一样
case SB_LINEUP:
si.nPos--;
break;
case SB_LINEDOWN:
si.nPos++;
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;
}
si.cbSize = sizeof( SCROLLINFO );
si.fMask = SIF_POS;
SetScrollInfo( hWnd, SB_VERT, &si, TRUE ); // 设置新的位置
// 但是必须要在Get的一次,因为Get前后的si.nPos可能不一样
// 因为此处用户不需要进行越界检查,越界维护有Windows自动进行
// 如果Set的时候nPos是-1,则Windows会自动将其设为0传进去
// 因此在这种情况下会使得nPos在Set前后会不一样,因此需要用
// Get在获得一次
GetScrollInfo( hWnd, SB_VERT, &si );
if ( si.nPos != iPreVertPos ) // 只有在实际位置改变的情况下对客户区内容进行滚动
{
// InvalidateRect( hWnd, NULL, TRUE ); // 这种方法效率太低
// UpdateWindow( hWnd );
// 使用高级的ScrollWindow函数,该函数不是基于重绘WM_PAINT的,因此不会产生
// WM_PAINT消息,而是直接对客户区进行滚动,但是效果是和重绘一模一样的!
ScrollWindow( hWnd, 0, cyChar * ( iPreVertPos - si.nPos ), NULL, NULL );
// 第二个参数和第三个参数分别表示水平方向上和垂直方向上内容滚动的距离
// 这个距离指的是像素点数量
// 由于内容滚动的方向和滑块滚动的方向正好相反
// 因此填的就是滑块位移的相反数
// 最后两个参数表示滚动的客户区范围和剪裁矩形,NULL就表示对整个客户区进行滚动
}
return 0;
case WM_HSCROLL: // 同理响应垂直滚条消息
si.cbSize = sizeof( SCROLLINFO );
si.fMask = SIF_ALL;
GetScrollInfo( hWnd, SB_HORZ, &si );
iPreHorzPos = si.nPos;
switch ( LOWORD( wParam ) )
{
case SB_LEFT:
si.nPos = si.nMin;
break;
case SB_RIGHT:
si.nPos = si.nMax;
break;
case SB_LINELEFT:
si.nPos--;
break;
case SB_LINERIGHT:
si.nPos++;
break;
case SB_PAGELEFT:
si.nPos -= si.nPage;
break;
case SB_PAGERIGHT:
si.nPos += si.nPage;
break;
case SB_THUMBTRACK:
si.nPos = si.nTrackPos;
break;
}
si.cbSize = sizeof( SCROLLINFO );
si.fMask = SIF_POS;
SetScrollInfo( hWnd, SB_HORZ, &si, TRUE );
GetScrollInfo( hWnd, SB_HORZ, &si );
if ( iPreHorzPos != si.nPos )
{
ScrollWindow( hWnd, cxChar * ( iPreHorzPos - si.nPos ), 0, NULL, NULL );
}
return 0;
case WM_PAINT:
hDC = BeginPaint( hWnd, &ps );
// 重绘可能是由窗口形状或大小改变
// 这种改变不仅会导致滑块大小以及滚条大小范围的改变
// 滑块的nPos可能也会做出相应的调整
// 由于滑块的位置iVertPos/iHorzPos也具有当前是第几行/第几个
// 位于也的最前端的意思,可以根据这个基准点只重绘无效矩形区域中的内容
// 而避免对整个区域的重绘,从而加大效率
si.fMask = SIF_POS;
GetScrollInfo( hWnd, SB_VERT, &si );
iVertPos = si.nPos;
GetScrollInfo( hWnd, SB_HORZ, &si );
iHorzPos = si.nPos;
iPaintBegin = max( 0, iVertPos + ps.rcPaint.top / cyChar ); // 重绘的起始行号
iPaintEnd = min( NUMLINES - 1, iVertPos + ps.rcPaint.bottom / cyChar ); // 重绘的结束行号
for ( i = iPaintBegin; i <= iPaintEnd; i++ )
{
// 水平方向无法逐字输出,因此还是和sysmets2一样依赖Windows对客户区外的部分剪裁
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 );
}