dustland

dustball in dustland

win32程序设计-chapter4 设备无关代码

windows SDK chapter 4 设备无关代码

绘制和重绘

为什么需要重绘?

在控制台编程时,输出均以纯文本打印到终端,终端会永远保存输出,自然不需要重绘

但是窗口编程的时候,一个窗口可能被移动到屏幕边界之外或者被其他窗口遮盖,此时这个窗口上的文字就消失了,

系统不会一直记着(操作系统有可能临时记住)窗口上画过什么,在哪里画的.当用户再次将这个窗口置于顶层的时候,操作系统就要求程序,将刚才抹去的文字重新打印.这个过程当然也是在消息循环中处理的.

操作系统怎么发出重绘的要求呢?

使用WM_PAINT消息

WM_PAINT

操作系统什么时候会发出这个WM_PAINT要求呢?

用户移动窗口,导致原来被遮盖的部分暴露,或者关闭了覆盖该窗口的对话框

用户调整窗口大小

滚动条滚动

程序自己显式调用InvalidateRect或者InvalidateRgn,生成WM_PAINT消息

下拉菜单后收回

只要是程序需要更新客户区的时候,应该自动或者强制发出WM_PAINT消息,绕一个弯集中从事件函数中处理

操作系统不能记住被覆盖的部分吗?

少数情况下可以,比如鼠标指针造成的小面积覆盖或者在客户区内拖动图表

有效矩形和无效矩形

需要重新绘制的部分成为无效矩形

什么部分需要重新绘制?先前被遮盖现在需要至于顶层的窗口部分,或者先前移出屏幕现在移入屏幕的部分

windows操作系统会为每个打开的窗口维护一个数据结构,该数据结构用来记录该窗口的最小无效区域范围,如果在该窗口重绘之前,在其上又有新的无效矩形则windows取所有无效区域的最小覆盖.如果原来该窗口进程的消息队列中没有有WM_PAINT消息,则发送一条该消息.如果已经有过WM_PAINT则只需要更新操作系统维护的数据结构

什么数据结构?目前未知,需要学习核心编程后获得原理

立刻重绘

各种WM消息是有优先级之分的

WM_PAINT重绘消息的优先级就比较低,WM_VSCROLL垂直滚动条消息就比较高

即使消息队列中已经存在了WM_PAINT消息,后来加入一个WM_VSCROLL消息,这个WM_VSCROLL消息也会先被处理.如此优先队列有个好处是,WM_PAINT消息只会存在一条,多次改变之后重绘和每次改变后的重绘,对于人类来说几乎观察不出来,因此WM_PAINT不需要太多

这就有可能导致,程序一直在忙活高优先级的消息处理,WM_PAINT这个消息一直不处理,比如一个有上亿行的数据库,不停拖动滚动条改变垂直视角的时候程序可能一直忙于处理WM_VSCROLL.那么我们看到的窗口就会迟滞.

但是有那种关系户性质的WM_PAINT消息,它不用排队等待被处理,而是被操作系统亲自安排给程序"给我立刻处理WM_PAINT"

调用UpdateWindow(hwnd)函数即可做到这一点

如果调用UpdateWindow函数时,整个客户区都是有效的,那么调用了一个寂寞

否则,即客户区存在无效矩形,UpdateWindow让窗口立刻收到WM_PAINT,由windows操作系统调用窗口过程WndProc函数

当WM_PAINT处理完后,UpdateWindow将控制还给调用者

这就解释了为啥在主函数创建窗口实例,ShowWindow之后,进入消息循环之前,有一次UpdateWindow

1
2
ShowWindow(hwnd, iCmdShow);
UpdateWindow(hwnd);

即使这里不写UpdateWindow(hwnd),通常也能立刻显式窗口.但是就怕WM_PAINT一直排队的情况

GDI

graphics device interface,图形设备接口,负责系统与绘图程序的信息交换

图形设备,如打印机,屏幕

GDI负责封装硬件设备,并且向上层提供接口,在代码层面如果需要和图形设备交互,只需要调用接口.

类似于系统调用接口,GDI也允许程序直接访问物理硬件,只能通过"设备环境"抽象接口访问硬件

举一个GDI的例子

1
2
WINGDIAPI WINBOOL WINAPI TextOutA(HDC hdc,int x,int y,LPCSTR lpString,int c);
WINGDIAPI WINBOOL WINAPI TextOutW(HDC hdc,int x,int y,LPCWSTR lpString,int c);

TextOut函数的作用是向窗口客户区输出一个字符串,

参数 意义
hdc 设备环境句柄,几乎所有的GDI函数都以hdc作为第一个参数
x 开始绘制的客户区相对横坐标
y --纵坐标
lpString 需要输出到客户区的字符串
c lpString的长度

设备环境

设备环境DC是内核维护的数据结构

一个设备环境与一个特定你的显式设备相关联

设备环境的属性包括文本颜色,文本背景色,字体等等

GDI函数可以根据相对于设备环境分成两类,一类是使用HDC真正绘图的函数,另一类是修改HDC的函数

如果以在纸上画画类比的话,

设备环境就相当于图纸,画笔,图纸下面的桌子,灯光,以及谁来画(决定字体)

真正输出的内容相当于这个特定的人主观画出的东西

设备环境是一个大结构体,怎么引用他呢?使用指针?不是,使用HDC类型的句柄.

实际上这个句柄也是一个整数,可能是内核维护的设备环境表的下标,反正一个设备环境对应唯一的一个编号,作为句柄索引该设备环境

获取设备环境句柄

BeginPaint/EndPaint

这一对只应用于处理WM_PAINT消息

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
  PAINTSTRUCT ps;
RECT rect;
HDC hdc;
static TCHAR szBuffer[128] = TEXT("default");
switch (message)
{
...
case WM_PAINT:
hdc = BeginPaint(hwnd, &ps);//获取设备环境hdc,并且第二个参数ps中也有相同的hdc
GetClientRect(hwnd, &rect);//获取当前客户区矩形范围,通过第二个参数返回
DrawText(hdc, szBuffer, -1, &rect, DT_SINGLELINE | DT_CENTER | DT_VCENTER);
//使用hdc设备环境,向rect矩形范围内绘制一个单行上下左右都居中的字符串"default"
EndPaint(hwnd, &ps);//释放ps中保存的hdc(也就是BeginPaint创建的hdc)
return 0;
...
}
1
2
3
4
5
6
7
8
9
HDC BeginPaint(
[in] HWND hWnd, //窗口实例句柄
[out] LPPAINTSTRUCT lpPaint //
);

BOOL EndPaint(
[in] HWND hWnd,
[in] const LPPAINTSTRUCT lpPaint
);

两个函数的第一个参数均为HWND窗口句柄

hwnd是啥?

1
2
3
4
5
6
7
8
9
10
11
12
13
hwnd = CreateWindow(classType1,
TEXT("first class first instance"),
WS_OVERLAPPEDWINDOW,
CW_USEDEFAULT,
CW_USEDEFAULT,
CW_USEDEFAULT,
CW_USEDEFAULT,
NULL,
NULL,
hInstance,
NULL);
ShowWindow(hwnd, iCmdShow);

窗口实例的句柄

第二个参数均为LPPAINTSTRUCT类型

struct PAINTSTRUCT

1
2
3
4
5
6
7
8
typedef struct tagPAINTSTRUCT {
HDC hdc; //设备环境句柄
WINBOOL fErase; //表明背景是否应该被擦除,0为是
RECT rcPaint; //指明需要绘制的矩形区域
WINBOOL fRestore; //尚未使用,保留
WINBOOL fIncUpdate; //尚未使用
BYTE rgbReserved[32]; //尚未使用
} PAINTSTRUCT,*PPAINTSTRUCT,*NPPAINTSTRUCT,*LPPAINTSTRUCT;

第一个成员就是hdc,这和BeginPaint返回的hdc是同一个值,实际上相当于BeginPaint以通过引用返回了一个包含ps的结构体,然后又单独通过返回值返回了hdc句柄.多此一举,只是为了用起来方便

第二个成员表明Windows在返回该结构体的BeginPaint中,是否擦除了无效区域背景,如果擦除了则fErase=0.默认情况下是会自动擦除的.自定以擦除过程可以处理WM_ERASEBKGND消息

注册窗口类WNDCLASS时指定了一个画刷wndclass.hbrBackground=(HBRUSH)GetStockObject(WHITE_BRUSH),windows将会使用这里指定的画刷擦除无效背景

第三个参数rcPaint定义了无效矩形边界,以像素为单位,相对于客户区左上角的距离

image-20220807094042735

该矩形区域就是需要重绘的区域,也就是调用BeginPaint之后重绘的区域

BeginPaint如果调用成功则返回HDC句

EndPaint释放一个HDC句柄,调用成功则返回TRUE

绘图GDI函数的调用要夹在两个函数之间

如果只写这两个函数

1
2
3
  	PAINTSTRUCT ps;        
hdc = BeginPaint(hwnd, &ps);//获取设备环境hdc,并且第二个参数ps中也有相同的hdc
EndPaint(hwnd, &ps);//释放ps中保存的hdc(也就是BeginPaint创建的hdc)

BeginPaint只会重绘无效矩形部分,也就是第二个参数ps中rcPaint指定的矩形

考虑到客户区有可能是一个圆形或者其他非矩形形状,一般不管三七二十一将整个客户区重绘一遍.

这就需要先调用InvalidateRect使得整个客户区失效,然后再调用BeingPaint重绘

1
2
3
4
5
BOOL InvalidateRect(
[in] HWND hWnd,
[in] const RECT *lpRect,
[in] BOOL bErase
);

第一个参数是窗口句柄

第二个参数是需要无效化的矩形范围,如果是NULL则表示整个客户区

第三个参数决定BeginPaint是否擦除原背景,当bErase=TRUE则擦除原有背景.否则不擦除

GetDC/ReleaseDC

处理非WM_PAINT消息时,如果需要绘制客户区,就要调用这一对.

或者只是需要设备环境信息,不需要绘图时,可以调用这一对.

1
2
3
4
5
6
7
8
HDC GetDC(
[in] HWND hWnd //窗口实例句柄
);

int ReleaseDC(
[in] HWND hWnd,
[in] HDC hDC //需要释放的hDC句柄,一定是最近的GetDC获取的那个句柄,两个函数配套使用
);

两个函数的使用一定是在处理同一条消息时配对完成的

GetDC返回的设备句柄可以在整个客户区绘制,但是BeginPaint只允许在失效矩形绘制

GetDC不会将无效矩形有效化.如有需要,显式调用ValidateRect(hwnd,rect),当rect为NULL则表示整个客户区

GetWindowDC/ReleaseWindowDC

GetDC绘制的只有客户区,对于窗口标题栏这种非客户区无能为力

GetWindowDC则可以绘制包括客户区,非客户区在内该窗口的所有区域

相应的消息是WM_NCPAINT

GDI绘制函数

TEXTOUT

1
2
3
4
5
6
7
BOOL TextOutA(
[in] HDC hdc,//GetDC或者BeginPaint返回的hdc句柄
[in] int x,//输出在客户区的起始水平位置
[in] int y,
[in] LPCSTR lpString,//需要输出的字符串
[in] int c//输出字符串长度
);

关于x,y坐标

image-20220807101513222
GetTextMatrics获取字体尺寸

系统字体取决于分辨率和字号大小,不能假设字体大小,而是调用函数获取信息,编写设备无关代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
  typedef struct tagTEXTMETRICA {
LONG tmHeight;//字体总高度
LONG tmAscent;//字体基线以上高度
LONG tmDescent;//字体基线以下高度
LONG tmInternalLeading;//字体重音符号高度
LONG tmExternalLeading;//行间距
LONG tmAveCharWidth;//消息字符加权宽度
LONG tmMaxCharWidth;//最宽字符宽度
LONG tmWeight;//字体粗细
LONG tmOverhang;//为字符串加粗或者斜体额外宽度
LONG tmDigitizedAspectX;
LONG tmDigitizedAspectY;
BYTE tmFirstChar;该字体第一个字符的编号
BYTE tmLastChar;
BYTE tmDefaultChar;
BYTE tmBreakChar;
BYTE tmItalic;
BYTE tmUnderlined;
BYTE tmStruckOut;
BYTE tmPitchAndFamily;//决定字体是否是等宽字体,最低位为1则变宽,为0则等宽
BYTE tmCharSet;
} TEXTMETRICA,*PTEXTMETRICA,*NPTEXTMETRICA,*LPTEXTMETRICA;

BOOL GetTextMetricsW(
[in] HDC hdc,
[out] LPTEXTMETRICW lptm
);

GetTextMetricsW第一个参数是一个hdc设备环境句柄,该函数就是返回该hdc设备环境中的字体属性

第二个参数是一个TEXTMETRICA类型的引用,用来承载返回值

该结构体类型有20个成员,成员意义:

image-20220807102446433
文本格式化

GetTextMetrics应该什么时候调用呢?

windows运行时,系统字体不会改变,因此GetTextMetircs只需要调用一次,保存下字体属性,一劳永逸

那么啥时候办这个"一劳"呢?在处理WM_CREATE消息时.

为啥在这个时候?WM_CREATE一定是窗口收到的第一条消息,此时窗口还没有输出到桌面,文字都还没打印.机不可失

1
2
3
4
5
6
7
8
9
10
11
hwnd = CreateWindow(classType1,
TEXT("first class first instance"),
WS_OVERLAPPEDWINDOW,
CW_USEDEFAULT,
CW_USEDEFAULT,
CW_USEDEFAULT,
CW_USEDEFAULT,
NULL,
NULL,
hInstance,
NULL);

在创建窗口实例时就会发送WM_CREATE消息

举个例子,假如要打印多行字符,需要设置好字间距和行间距

字间距好说,就是tmAveCharWidth

行间距(这里的行间距指的是上一行字体的顶和下一行字体的顶的距离,包括纯空白的高度)麻烦点,需要tmHeight+tmExternalLeading

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
LRESULT CALLBACK WndProc(HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam)
{

HDC hdc;//设备环境句柄
PAINTSTRUCT ps;
RECT rect;
TEXTMETRICA tm;//字体属性结构
static int cxChar,cyChar;
static TCHAR szBuffer[128] = TEXT("default");
switch (message)
{
case WM_CREATE:

hdc=GetDC(hwnd);
GetTextMetrics(hdc,&tm);//获取当前系统字体属性,用tm承载
cxChar=tm.tmAveCharWidth;//cxChar为平均小写字母宽度
cyChar=tm.tmHeight+tm.tmExternalLeading;//cyChar为字体高度加上一个行间距
ReleaseDC(hwnd,hdc);
return 0;
case ...
return DefWindowProc(hwnd, message, wParam, lParam);
}
}
wsprintf格式化字符串

不管是TextOut还是DrawText,决定往屏幕输出的参数都是字符串类型,不能是结构体或者整数或者浮点数

想要类似使用cout<<5这种直接打印整数到窗口是不可能的

那么怎么打印整数呢?用wsprintf先把要打印的所有东西,格式化到一个字符中,然后输出这个字符串

比如统计鼠标左键在客户区按下的次数

1
2
3
4
5
6
7
8
9
10
case WM_LBUTTONDOWN:
static int cLBUTTONDOWN;//铁打的静态变量只会定义一次
++cLBUTTONDOWN;
TCHAR szBuffer[100];//流水的局部变量
wsprintf(szBuffer,TEXT("this is the %d time you click the left button"),cLBUTTONDOWN);//格式化字符串到szBuffer
hdc = GetDC(hwnd);
GetClientRect(hwnd, &rect);
DrawText(hdc, szBuffer, -1, &rect, DT_SINGLELINE | DT_CENTER | DT_VCENTER);
ReleaseDC(hwnd, hdc);
return 0;
image-20220807110305547

每次按下鼠标左键都会更新cLBUTTONDOWN这个统计数字

显式多行

windows32编程上给出的例子是打印SystemMetrics所有的系统参数

效果如图

image-20220807145300545

已经有一个窗口程序的雏形了

使用GetSystemMetrics获取系统参数

1
2
3
int GetSystemMetrics(
[in] int nIndex
);

该函数使用一个下标作为参数,意思是查询SystemMetrics表的第nIndex个元素

这个表啥样呢?

Value Meaning
SM_ARRANGE56 The flags that specify how the system arranged minimized windows. For more information, see the Remarks section in this topic.
SM_CLEANBOOT67 The value that specifies how the system is started:0 Normal boot1 Fail-safe boot2 Fail-safe with network bootA fail-safe boot (also called SafeBoot, Safe Mode, or Clean Boot) bypasses the user startup files.
SM_CMONITORS80 The number of display monitors on a desktop. For more information, see the Remarks section in this topic.
.... ...

Value为枚举类型的下标,从0开始编号,微软给出的文档并没有按照Value递增的顺序,而是按照枚举值字符串的字典序递增排列的SM_ARRANGE<SM_CLEANBOOT

为了方便观察,书上先建立了一个头文件sysmets.h,里面只有一个结构体数组struct sysmetrics[],每个结构体数组有三项,下标,枚举值字符串,意义.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
struct
{
int Index;
const TCHAR* szLabel;//此处需要用const修饰字符串常量,否则在virsual studio上会报告指针指向常量的错误
const TCHAR* szDesc;
}sysmetrics[] =
{
SM_CXSCREEN, //初始化本结构体数组第一个元素的第一个成员
TEXT("SM_CXSCREEN"),//sysmetrics[0].szLabel=TEXT("SM_CXSCREEN");
TEXT("Screen width in pixels"),//初始化本结构体数组第一个元素的第三个成员szDesc

SM_CYSCREEN, TEXT("SM_CYSCREEN"),
TEXT("Screen height in pixels"),

...
}

GetSystemMetrics(Index)将会和systemtrics[Index]配套使用

然后主程序包含该头文件,相当于直接在主程序中定义了这么一个结构体数组

这个数组还有GetSystemMetrics函数在啥时候发挥的作用呢?

在WndProc消息处理函数中,下面炎鸠以下这个回调函数干了啥

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
LRESULT CALLBACK WndProc(HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam)
{
static int cxChar, cxCaps, cyChar;//静态的小写,大写字体宽度,行间距
HDC hdc;//设备环境句柄
int i;//遍历sysmetrics数组时使用的循环变量i
PAINTSTRUCT ps;//
TCHAR szBuffer[10];//缓冲区,用于整合一行需要打印的字符
TEXTMETRIC tm;//字体属性结构体,用于承载GetTextMetrics返回值
switch (message)
{
case WM_CREATE:
hdc = GetDC(hwnd);
GetTextMetrics(hdc, &tm);//tm获取当前字体属性
cxChar = tm.tmAveCharWidth;//获取小写字体宽度
cxCaps = (tm.tmPitchAndFamily & 1 ? 3 : 2) * cxChar / 2;
//获取大写字体宽度,变宽字体中,该值应该为小写字体宽度的1.5倍,等宽字体中大小写字母的宽度应该一样
//由于的最低为是等宽变宽标志,因此与1按位与之后的结果,
//如果为1说明字体变宽,cxCaps先等于三倍的cxChar然后除以2就是1.5倍的小写字母宽度
cyChar = tm.tmHeight + tm.tmExternalLeading;//行间距,上一行字的本行字顶的距离
ReleaseDC(hwnd, hdc);//释放设备环境
return 0;
case WM_PAINT:
hdc = BeginPaint(hwnd, &ps);//重绘无效区,获得设备环境句柄
for (i = 0; i < NUMLINES; i++)//SystemMetrics有NUMLINES项,因此下标遍历0到NUMLINES
{
//每行打印三项,枚举类型字符串,意义,参数值
TextOut(hdc, 0, cyChar * i,//第二个参数中cyChar就起了作用,规定在哪里开始绘制,因为前面i行都已经绘制完毕,因此本行应该在第i+1行位置绘制
sysmetrics[i].szLabel,
lstrlen(sysmetrics[i].szLabel));
TextOut(hdc, 22 * cxCaps, cyChar * i,//x坐标也有意义了,因为szLabel已经占用了从x=0开始的一些字节,因此szDesc从x=22开始绘制
sysmetrics[i].szDesc,
lstrlen(sysmetrics[i].szDesc));
SetTextAlign(hdc, TA_RIGHT | TA_TOP);//设置文本右上角对齐,作用于hdc指向的设备环境
TextOut(hdc, 22 * cxCaps + 40 * cxChar, cyChar * i, szBuffer,
wsprintf(szBuffer, TEXT("%5d"),

GetSystemMetrics(sysmetrics[i].Index)));//此处压行了,szBuffer的构造放在了最后一个参数的求值中
SetTextAlign(hdc, TA_LEFT | TA_TOP);//设置回左对齐,好借好还,再借不难
}
EndPaint(hwnd, &ps);//结束绘制,释放ps中的hdc设备环境
return 0;
case WM_DESTROY:
PostQuitMessage(0);
return 0;
}
return DefWindowProc(hwnd, message, wParam, lParam);
}

打印不开问题

由于心胸狭隘

如果电脑屏幕比较小,那么可以显式的行数就少,可能一个屏幕没法全部打印出来.甚至屏幕窄了一行都显式不全,比如:

image-20220807161039836

这是因为本程序没有考虑客户区的大小,它只管打印它的,不管人能不能看见

怎么获取客户区的大小呢?

GetClientRect
1
2
3
4
BOOL GetClientRect(
[in] HWND hWnd,
[out] LPRECT lpRect
);

第一个参数是设备环境句柄

第二个参数是返回值,用一个RECT结构体引用承载当前客户区信息

这个结构体啥样呢?

1
2
3
4
5
6
typedef struct tagRECT {
LONG left;
LONG top;
LONG right;
LONG bottom;
} RECT, *PRECT, *NPRECT, *LPRECT;

GetClientRect返回的矩形中,左和上坐标都是0,即以客户区的左上角为基准

右和下坐标是实际大小(像素)

处理WM_SIZE消息

更好的方法是处理WM_SIZE消息

每当窗口大小发生变化(位置变化不算),Windows就会向窗口过程发送一条WM_SIZE消息,此时传递给WndProc处理函数的lParam参数就有实际意义了,高16位是新的窗口高度,低16位是新的窗口宽度,可以使用两个静态变量来承载保存这个两个值,静态的好处是只会定义一次,那么在本次处理WM_SIZE消息时修改这两个值,就可以在处理下一条消息比如WM_PAINT时使用刚才保存的值

1
2
3
4
5
6
7
8
9
10
11
12
WndProc(...):
static int cxClient, cyClient;
...
switch (message) {
case ...
case WM_SIZE:
cxClient = LOWORD(lParam);
cyClient = HIWORD(lParam);

return 0;
case ...
}

其中LOWORD和HIWORD是定义在WINDEF.H中的两个宏

1
2
3
4
#define LOWORD(l)           ((WORD)(((DWORD_PTR)(l)) & 0xffff))
#define HIWORD(l) ((WORD)((((DWORD_PTR)(l)) >> 16) & 0xffff))
#define LOBYTE(w) ((BYTE)(((DWORD_PTR)(w)) & 0xff))
#define HIBYTE(w) ((BYTE)((((DWORD_PTR)(w)) >> 8) & 0xff))

LOWORD(l)就是不管l啥类型,对l取双字然后和0xFFFF全一的字按位与,即保留低字

在处理WM_SIZE消息时只负责计算出新的客户区宽高,重绘它是一点儿也不用管,因为一般WM_SIZE消息之后会有一个WM_PAINT消息接踵而至,重绘的工作在WM_PAINT中完成

这样看一切都合理了,处理WM_SIZE相当于为处理WM_PAINT预处理,计算客户区大小

但是窗口第一次显式出来的时候呢?WM_SIZE不是说得在窗口大小发生变化时才会收到吗?

事实上WM_CREATE之后不会立刻传递WM_PAINT进行绘制,而是会先传递WM_SIZE,可以理解为窗口从无到有也包含了客户区的尺寸变化

这样改进之后的WM_PAINT处理过程:

1
2
3
4
5
6
7
8
9
MAXLINES = cyClient / cyChar;
for (i = 0; i < min(NUMLINES,MAXLINES); ++i) {
TextOut(hdc, 10, i * cyChar, sysmetrics[i].szLabel,lstrlen(sysmetrics[i].szLabel));
TextOut(hdc, 40 * cxCaps, i * cyChar, sysmetrics[i].szDesc, lstrlen(sysmetrics[i].szDesc));
wsprintf(szBuffer, TEXT("%5d"), GetSystemMetrics(sysmetrics[i].Index));
SetTextAlign(hdc, TA_LEFT);
TextOut(hdc, 80 * cxCaps, i * cyChar, szBuffer, lstrlen(szBuffer));
SetTextAlign(hdc, TA_LEFT);
}

首先MAXLINES计算得到当前客户区最大能够容纳的完整行数,然后下面打印的时候取现有行数和能够打印的最大完整行数最小值.

这样就不会在客户区最底下打印出短斤少两的半行了

image-20220807165837263

但是这样没有解决不能显式完全的问题,甚至说只能解决边界上的显式好不好看问题,真是吹毛求疵

要用有限的屏幕空间浏览长于一个屏幕的信息,最好的方法就是添加一个滚动条,拖到下面看下面,拖到上面看上面,

比如任务管理器的滚动条

image-20220807170100222

滚动条

滚动条的效果就类似拿着放大镜看一个巨大的报纸,但是放大镜固定不动,动的是报纸

下滑滚动条就是报纸往彼方移动,相当于放大镜往己方移动

与其说是放大镜,不如说是一张扣了个方框的不透明纸压在报纸上,透过这个方框看报纸

添加滚动条

滚动条属于窗口实例的风格,只需要在实例化窗口对象的时候往窗口对象风格标识符上按位与上滚动条特性WS_VSCROLL|WS_HSCROLL垂直滚动条|水平滚动条

比如:

1
2
3
4
5
hwnd = CreateWindow(szAppName, TEXT("Get System Metrics No. 1"),
WS_OVERLAPPEDWINDOW| WS_VSCROLL | WS_HSCROLL,//此处加入滚动条属性
CW_USEDEFAULT, CW_USEDEFAULT,
CW_USEDEFAULT, CW_USEDEFAULT,
NULL, NULL, hInstance, NULL);

之后的效果如图:

image-20220807171154754

确实窗口右侧和下方都出现了滚动条

但是这时候拖动滚动条是没有效果的,它只是个摆设

Windows操作系统负责处理滚动条上的鼠标动作,但是不负责键盘接口.如果想要通过键盘控制滚动条,需要显式给出对应关系,但这是后话,目前的任务是给滚动条加上管理范围,让它不是摆设

Windows操作系统和应用程序

位置和范围

范围是一对整数,表明滚动条的最小值和最大值

位置是滑块实时在这个范围中的值,位置永远属于是范围这个整数集合

默认滚动条的范围是[0,100)

范围

可以调用setScrollRange修改其范围

1
2
3
4
5
6
7
BOOL SetScrollRange(
[in] HWND hWnd,
[in] int nBar,
[in] int nMinPos,
[in] int nMaxPos,
[in] BOOL bRedraw
);
参数 意义
hwnd 窗口实例的句柄
nBar 标志滚动条的类型,是水平还是垂直
nMinPos 滚动条最小范围
nMaxPos 滚动条最大范围
bRedraw 规定是否重绘滚动条,可以在下一次显示前,所有对滚动条的修改完成之后,再规定为TRUE表明重绘
设置滚动条范围

当滚动条范围是[0,NUMLINES-1],当滑块位置是0,此时第一条文本显示在客户区最上方,下面顺次是第2条文本,第3条...这样是合理的

当滑块位置是NUMLINES-1,此时最后一条文本显示在客户区最上方,下面全都是空白.显然着这不合理.

怎么样让滑块最远到达的位置,使得最后一条文本显示 在客户区的最下方?

假设客户区大小100,需要打印1000行,当滑块在0时可以显示第1到100行,当滑块在1时可以显示第2到101行,当滑块在900时,可以显示第901到1000行

因此只需要NUMLINES-cyClient就可以实现

啥时候更新滚动条范围呢?NUMLINES是一个常数,cyClient客户区垂直行数是一个变量,因此滚动条范围上界可以跟随cyClient变化

都在处理WM_SIZE时修改

1
2
3
4
5
6
case WM_SIZE:
cxClient = LOWORD(lParam);
cyClient = HIWORD(lParam);
iVscrollMax = max(0, NUMLINES - cyClient / cyChar);//非负
SetScrollRange(hwnd, SB_VERT, 0, iVscrollMax, TRUE);//修改范围
return 0;

位置

滑块的位置是一个整数,比如当滚动条范围是[0,1024)

那么滑块的位置就有0,1,2,...,1022,1023,总共1024种情况

必须使用SetScrollPos函数修改滑块的位置

1
int SetScrollPos(HWN hWnd,int nBar,int nPos,BOOL bRedraw);
参数 意义
hwnd 窗口实例的句柄
nBar 标志滚动条的类型,是水平还是垂直
nPos 滑块新位置,该值需要介于滚动条的范围内
bRedraw 规定是否重绘滚动条,同SetScrollRange中的bRedraw参数

程序和windows操作系统合作处理滚动条上的动作

windows操作系统:

处理滚动条中的所有鼠标信息,

当鼠标拖动滑块的时候高亮

当用户拖动滑块的时候,在滚动条内移动滑块

向拥有滚动条的窗口过程发送滚动条消息

程序:

初始化滚动条的范围和位置

处理传送给窗口过程的滚动条信息

更新滑块的位置

根据滚动条的变化更新客户区的内容

处理滚动条消息

单击滚动条或者拖动滑块会导致windows向窗口过程发送WM_VSCROLL或者WM_HSCROLL消息

此时wParam和lParam参数就有实际意义了.其中lParam用于滚动条是子窗口.wParam用于滚动条是窗口的一部分,目前只使用wParam就足够了

wParam是一个无符号32位双字

1
2
typedef _W64 unsigned int UINT_PTR, *PUINT_PTR;
typedef UINT_PTR WPARAM;

其低位字代表了鼠标在滚动条的动作,又称为"通知码",低位字的枚举值在winuser.h中有定义

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#define SB_LINEUP 0
#define SB_LINELEFT 0
#define SB_LINEDOWN 1
#define SB_LINERIGHT 1
#define SB_PAGEUP 2
#define SB_PAGELEFT 2
#define SB_PAGEDOWN 3
#define SB_PAGERIGHT 3
#define SB_THUMBPOSITION 4
#define SB_THUMBTRACK 5
#define SB_TOP 6
#define SB_LEFT 6
#define SB_BOTTOM 7
#define SB_RIGHT 7
#define SB_ENDSCROLL 8

各个枚举值的对应效果

image-20220808081546006

wParam的高位字表示滑块位置

使用滚动条

加入滚动条处理的WndProc函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
LRESULT CALLBACK WndProc(HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam) {


static int iVscrollPos;//记录滑块位置
static int cxChar, cxCaps, cyChar;
static int cxClient, cyClient;
TEXTMETRIC tm;
HDC hdc;
PAINTSTRUCT ps;
int i,y;
TCHAR szBuffer[10];
RECT rect;

switch (message) {
case WM_CREATE:
hdc = GetDC(hwnd);
GetTextMetrics(hdc, &tm);
cxChar = tm.tmAveCharWidth;
cxCaps = cxChar;
if (tm.tmPitchAndFamily & 1) {
cxCaps = cxChar * 3 / 2;
}
cyChar = tm.tmHeight + tm.tmExternalLeading;

ReleaseDC(hwnd, hdc);
return 0;
case WM_SIZE:
cxClient = LOWORD(lParam);
cyClient = HIWORD(lParam);

return 0;

case WM_VSCROLL:
switch (LOWORD(wParam)) {
case SB_LINEUP://点击滚动条顶端的箭头导致滑块垂直位置上移一个单元
iVscrollPos -= 1;
break;
case SB_LINEDOWN:
iVscrollPos += 1;
break;
case SB_PAGEUP://点击滑块的滑道导致滑块垂直位移一页
iVscrollPos -= cyClient / cyChar;
break;
case SB_PAGEDOWN:
iVscrollPos += cyClient / cyChar;
break;
case SB_THUMBPOSITION://拖动滑块,其位置应该由wParam的高位字决定
iVscrollPos = HIWORD(wParam);
break;
default:
break;
}
iVscrollPos = max(0, min(iVscrollPos, NUMLINES - 1));//iVscrollPos不允许小于0,不允许比NUMLINES行大
if (iVscrollPos != GetScrollPos(hwnd, SB_VERT)) {//测试一下本次滚动之后和上一次的位置是否相同,相同则不做修改
SetScrollPos(hwnd, SB_VERT, iVscrollPos, TRUE);
InvalidateRect(hwnd, NULL, TRUE);//本次滚动条动作处理完毕,整个窗口无效化,强制产生WM_PAINT消息,进行重绘处理
}
return 0;

case WM_PAINT:
hdc = BeginPaint(hwnd,&ps);
for (i = 0; i < NUMLINES; ++i) {//i最大不能超过现有行数
y = cyChar * (i - iVscrollPos);//原来的第i行应该在客户区的第i-iVescrollPos行打印
TextOut(hdc, 10, y, sysmetrics[i].szLabel,lstrlen(sysmetrics[i].szLabel));
TextOut(hdc, 40 * cxCaps, y, sysmetrics[i].szDesc, lstrlen(sysmetrics[i].szDesc));
wsprintf(szBuffer, TEXT("%5d"), GetSystemMetrics(sysmetrics[i].Index));
SetTextAlign(hdc, TA_LEFT);
TextOut(hdc, 80 * cxCaps, y, szBuffer, lstrlen(szBuffer));
SetTextAlign(hdc, TA_LEFT);
}
EndPaint(hwnd, &ps);

return 0;
case WM_LBUTTONDOWN:
GetClientRect(hwnd, &rect);
wsprintf(szBuffer, TEXT("%d"),cxClient);
MessageBox(NULL, szBuffer,TEXT("notice"), NULL);

return 0;


case WM_DESTROY:
PostQuitMessage(0);
return 0;

}
return DefWindowProc(hwnd, message, wParam, lParam);
}

关于绘图时的处理y = cyChar * (i - iVscrollPos);实际上干了一个将第i条作为客户区的第i-iVscrollPos行打印

image-20220808084718857

立刻重绘

处理WM_VSCROLL时并没有立刻重绘客户区,而是调用InvalidateRect将这个皮球踢到下一次消息处理,WM_PAINT的处理.

WM_PAINT这个消息的优先级最低,当窗口过程的消息队列中有多种消息(比如多种对窗口有改动的消息)时,会首先处理其他消息,最后才会处理WM_PAINT消息.

这就好比市领导要求某中学视察,苦逼老师们提前一周就得造假材料,补完教案.等这一些都忙活完了,到领导视察当天,把所有材料一汇总,呈递给领导审阅

造材料就相当于处理其他消息

处理WM_PAINT就是呈递的临门一脚

如果要让WM_PAINT立刻被处理,需要在InvalidateRect之后立刻UpdateWindow

1
2
3
4
5
if (iVscrollPos != GetScrollPos(hwnd, SB_VERT)) {
SetScrollPos(hwnd, SB_VERT, iVscrollPos, TRUE);
InvalidateRect(hwnd, NULL, TRUE);
UpdateWindow(hwnd);
}

Get/SetScrollInfo

先前的窗口中,滑块的大小是固定的,那么一小点都点不到.

而人家的滑块似乎是和总行数挂钩的,总行数越少滑块越大

image-20220808091255112
image-20220808091242529

确实如此,理论上可以得到一个公式 \[ \frac{滑块大小}{滚动条长度}=\frac{页面大小}{范围}=\frac{文档显示数量}{文档总大小} \]

我们先进的Set/GetScrollInfo函数已经超过了老式的Set/GetScrollRange,Set/GetScrollPos函数Set/GetScrollInfo就可以考虑这一点了

使用Set/GetScrollInfo完全可以做到先前的老式函数.

这俩函数可以设置/获得滚动条的所有信息

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
typedef struct tagSCROLLINFO {//SCROLLINFO结构体
UINT cbSize;//sizeof(SCROLLINFO)
UINT fMask;//要设置或者获取的值
int nMin;//范围最小值
int nMax;//范围最大值
UINT nPage;//页面大小
int nPos;//当前位置
int nTrackPos;//当前追踪位置
} SCROLLINFO, *LPSCROLLINFO;


int SetScrollInfo(
[in] HWND hwnd,
[in] int nBar,//要么是SB_VERT,要么是SB_HORZ,作用是决定滑块是水平还是垂直的
[in] LPCSCROLLINFO lpsi,//lpsi规定了ScrollInfo结构体应该被设置成什么状态
[in] BOOL redraw//表示是否重绘滚动条
);

BOOL GetScrollInfo(
[in] HWND hwnd,
[in] int nBar,
[in, out] LPSCROLLINFO lpsi//返回值用引用承载
);

奇怪的是,结构体的第一个成员是自己的大小,这不随便用sizeof一算就有了吗?

windows程序设计给出的解释是,方便以后扩充结构使用

使用该结构体之前需要ixan将cbSize字段填充

1
2
SCROLLINFO si;
si.cbSize=sizeof(SCROLLINFO);

真™抽象

fMask有效值:

1
2
3
4
5
6
#define SIF_RANGE 0x0001//范围掩码,获取SCROLLINFO.nMIn,.mMax
#define SIF_PAGE 0x0002//页面大小掩码,获取SCROLLINFO.nPage
#define SIF_POS 0x0004//滑块位置掩码,获取SCROLLINFO.nPos
#define SIF_DISABLENOSCROLL 0x0008
#define SIF_TRACKPOS 0x0010
#define SIF_ALL (SIF_RANGE | SIF_PAGE | SIF_POS | SIF_TRACKPOS)

不管是使用Set还是GetScrollInfo方法,引用传递的lpsi参数都要指定fMask这个成员

对于GetScrollInfo方法,指定了fMask=SIF_POS ,那么函数执行后lpsi引用的nPos就是有效值,其他成员无效

对于SetScrollInfo方法,指定了fMask=SIF_POS,那么函数根据lpsi的nPos值修改滚动条参数.其他成员不予理睬

修改滑块位置

使用SetScrollInfo改进的WM_CREATE处理函数

1
2
3
4
5
6
7
8
9
static SCROLLINFO si;
...

if (iVscrollPos != GetScrollPos(hwnd, SB_VERT)) {
si.fMask = SIF_POS;//设置掩码,只改变nPos参数
SetScrollInfo(hwnd, SB_VERT, &si, FALSE);//不立刻重绘
InvalidateRect(hwnd, NULL, TRUE);
UpdateWindow(hwnd);
}
设置滚动条范围
1
2
3
4
5
6
7
8
9
10
11
case WM_SIZE:
cxClient = LOWORD(lParam);
cyClient = HIWORD(lParam);
si.fMask = SIF_RANGE | SIF_PAGE;
si.nMin = 0;
si.nMax = NUMLINES - 1;
si.nPage = cyClient / cyChar;
SetScrollInfo(hwnd, SB_VERT, &si, TRUE);
//iVscrollMax = max(0, NUMLINES - cyClient / cyChar);老方法
//SetScrollRange(hwnd, SB_VERT, 0, iVscrollMax, TRUE);
return 0;

虽然我们传递的si.nMax=NUMLINES-1但是Windows操作系统会自动将滚动条范围最大值设置为si.nMax-si.nPage+1,这就是SetScrollInfo函数相对于SetScrollRange的好处

更完善的滑动效果

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
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;//循环变量,绘制起点x,绘制起点y,竖直滑块位置,水平滑块位置,需要开始绘制的行号,需要结束绘制的行号
PAINTSTRUCT ps;
SCROLLINFO si;
TCHAR szBuffer[10];
TEXTMETRIC tm;
RECT rect;

switch (message) {
case WM_CREATE:

hdc = GetDC(hwnd);
GetTextMetrics(hdc, &tm);//获取系统字体属性
cxChar = tm.tmAveCharWidth;
cxCaps = cxChar;
if (tm.tmPitchAndFamily & 1) {
cxCaps = cxChar * 3 / 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.cbSize = sizeof(si);//初始化si结构体
si.fMask = SIF_RANGE | SIF_PAGE;//设置访问掩码
si.nMin = 0;//设置滚动条范围上限为0
si.nMax = NUMLINES - 1;//设置滚动条下限为NUMLINES-1
si.nPage = cyClient / cyChar;//设置滚动条滚一页(一页=客户区一屏)的行数,客户区高度/一行的高度=客户区行数
SetScrollInfo(hwnd, SB_VERT, &si, TRUE);//注册滚动条信息

//设置水平滚动条属性
si.cbSize = sizeof(si);//初始化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:
si.cbSize = sizeof(si);
si.fMask = SIF_ALL;//设置访问掩码
GetScrollInfo(hwnd, SB_VERT, &si);
iVertPos = si.nPos;//iVertPos记录修改之前的si.nPos滑块位置
switch (LOWORD(wParam)) {
case SB_TOP:
si.nPos = si.nMax;
break;
case SB_BOTTOM:
si.nPos = si.nMin;
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://TRACK会一直跟踪鼠标位置
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) {//比较本次修改前后si.nPos是否发生了变化
//如果发生了变化则立刻通知重绘
ScrollWindow(hwnd, 0, cyChar * (iVertPos - si.nPos), NULL, NULL);
UpdateWindow(hwnd);
}
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_LINERIGHT:
si.nPos += 1;
break;
case SB_LINELEFT:
si.nPos -= 1;
break;
case SB_PAGELEFT:
si.nPos -= si.nPage;
break;
case SB_PAGERIGHT:
si.nPos += si.nPage;
break;
case SB_THUMBPOSITION://POSITION只会在鼠标松开时才更新位置
si.nPos = si.nTrackPos;
break;

default:
break;
}
si.fMask = SIF_POS;
SetScrollInfo(hwnd, SB_HORZ, &si, TRUE);
GetScrollInfo(hwnd, SB_HORZ, &si);
if (si.nPos != iHorzPos) {
ScrollWindow(hwnd, cxChar * (iHorzPos - si.nPos), 0, NULL, NULL);
UpdateWindow(hwnd);
}



case WM_PAINT:
hdc = BeginPaint(hwnd,&ps);//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;
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);
wsprintf(szBuffer, TEXT("%5d"), GetSystemMetrics(sysmetrics[i].Index));
TextOut(hdc, x + 22 * cxCaps+40*cxChar, y, szBuffer,lstrlen(szBuffer));
SetTextAlign(hdc, TA_LEFT | TA_TOP);

}
EndPaint(hwnd, &ps);
return 0;


case WM_DESTROY:
PostQuitMessage(0);
return 0;

}
return DefWindowProc(hwnd, message, wParam, lParam);
}

使用老式函数时拖动滑块只有当鼠标松开,才会重绘客户区

现在只要是鼠标拖着竖直滑块有动作,客户区实时更新

image-20220808144125813

绷不住了