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 | ShowWindow(hwnd, iCmdShow); |
即使这里不写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 | PAINTSTRUCT ps; |
1 | HDC BeginPaint( |
两个函数的第一个参数均为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定义了无效矩形边界,以像素为单位,相对于客户区左上角的距离
该矩形区域就是需要重绘的区域,也就是调用BeginPaint之后重绘的区域
BeginPaint
如果调用成功则返回HDC句
EndPaint
释放一个HDC句柄,调用成功则返回TRUE
绘图GDI函数的调用要夹在两个函数之间
如果只写这两个函数
1 | PAINTSTRUCT ps; |
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 | HDC GetDC( |
两个函数的使用一定是在处理同一条消息时配对完成的
GetDC返回的设备句柄可以在整个客户区绘制,但是BeginPaint只允许在失效矩形绘制
GetDC不会将无效矩形有效化.如有需要,显式调用ValidateRect(hwnd,rect)
,当rect为NULL则表示整个客户区
GetWindowDC/ReleaseWindowDC
GetDC绘制的只有客户区,对于窗口标题栏这种非客户区无能为力
GetWindowDC则可以绘制包括客户区,非客户区在内该窗口的所有区域
相应的消息是WM_NCPAINT
GDI绘制函数
TEXTOUT
1 | BOOL TextOutA( |
关于x,y坐标
GetTextMatrics获取字体尺寸
系统字体取决于分辨率和字号大小,不能假设字体大小,而是调用函数获取信息,编写设备无关代码
1 | typedef struct tagTEXTMETRICA { |
GetTextMetricsW
第一个参数是一个hdc设备环境句柄,该函数就是返回该hdc设备环境中的字体属性
第二个参数是一个TEXTMETRICA
类型的引用,用来承载返回值
该结构体类型有20个成员,成员意义:
文本格式化
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 | LRESULT CALLBACK WndProc(HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam) |
wsprintf格式化字符串
不管是TextOut
还是DrawText
,决定往屏幕输出的参数都是字符串类型,不能是结构体或者整数或者浮点数
想要类似使用cout<<5
这种直接打印整数到窗口是不可能的
那么怎么打印整数呢?用wsprintf先把要打印的所有东西,格式化到一个字符中,然后输出这个字符串
比如统计鼠标左键在客户区按下的次数
1 | case WM_LBUTTONDOWN: |
每次按下鼠标左键都会更新cLBUTTONDOWN这个统计数字
显式多行
windows32编程上给出的例子是打印SystemMetrics所有的系统参数
效果如图
已经有一个窗口程序的雏形了
使用GetSystemMetrics获取系统参数
1 | int GetSystemMetrics( |
该函数使用一个下标作为参数,意思是查询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 | struct |
GetSystemMetrics(Index)
将会和systemtrics[Index]
配套使用
然后主程序包含该头文件,相当于直接在主程序中定义了这么一个结构体数组
这个数组还有GetSystemMetrics
函数在啥时候发挥的作用呢?
在WndProc消息处理函数中,下面炎鸠以下这个回调函数干了啥
1 | LRESULT CALLBACK WndProc(HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam) |
打印不开问题
由于心胸狭隘
如果电脑屏幕比较小,那么可以显式的行数就少,可能一个屏幕没法全部打印出来.甚至屏幕窄了一行都显式不全,比如:
这是因为本程序没有考虑客户区的大小,它只管打印它的,不管人能不能看见
怎么获取客户区的大小呢?
GetClientRect
1 | BOOL GetClientRect( |
第一个参数是设备环境句柄
第二个参数是返回值,用一个RECT结构体引用承载当前客户区信息
这个结构体啥样呢?
1 | typedef struct tagRECT { |
GetClientRect返回的矩形中,左和上坐标都是0,即以客户区的左上角为基准
右和下坐标是实际大小(像素)
处理WM_SIZE消息
更好的方法是处理WM_SIZE消息
每当窗口大小发生变化(位置变化不算),Windows就会向窗口过程发送一条WM_SIZE消息,此时传递给WndProc处理函数的lParam参数就有实际意义了,高16位是新的窗口高度,低16位是新的窗口宽度,可以使用两个静态变量来承载保存这个两个值,静态的好处是只会定义一次,那么在本次处理WM_SIZE消息时修改这两个值,就可以在处理下一条消息比如WM_PAINT时使用刚才保存的值
1 | WndProc(...): |
其中LOWORD和HIWORD是定义在WINDEF.H中的两个宏
1
2
3
4LOWORD(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 | MAXLINES = cyClient / cyChar; |
首先MAXLINES计算得到当前客户区最大能够容纳的完整行数,然后下面打印的时候取现有行数和能够打印的最大完整行数最小值.
这样就不会在客户区最底下打印出短斤少两的半行了
但是这样没有解决不能显式完全的问题,甚至说只能解决边界上的显式好不好看问题,真是吹毛求疵
要用有限的屏幕空间浏览长于一个屏幕的信息,最好的方法就是添加一个滚动条,拖到下面看下面,拖到上面看上面,
比如任务管理器的滚动条
滚动条
滚动条的效果就类似拿着放大镜看一个巨大的报纸,但是放大镜固定不动,动的是报纸
下滑滚动条就是报纸往彼方移动,相当于放大镜往己方移动
与其说是放大镜,不如说是一张扣了个方框的不透明纸压在报纸上,透过这个方框看报纸
添加滚动条
滚动条属于窗口实例的风格,只需要在实例化窗口对象的时候往窗口对象风格标识符上按位与上滚动条特性WS_VSCROLL|WS_HSCROLL
垂直滚动条|水平滚动条
比如:
1 | hwnd = CreateWindow(szAppName, TEXT("Get System Metrics No. 1"), |
之后的效果如图:
确实窗口右侧和下方都出现了滚动条
但是这时候拖动滚动条是没有效果的,它只是个摆设
Windows操作系统负责处理滚动条上的鼠标动作,但是不负责键盘接口.如果想要通过键盘控制滚动条,需要显式给出对应关系,但这是后话,目前的任务是给滚动条加上管理范围,让它不是摆设
Windows操作系统和应用程序
位置和范围
范围是一对整数,表明滚动条的最小值和最大值
位置是滑块实时在这个范围中的值,位置永远属于是范围这个整数集合
默认滚动条的范围是[0,100)
范围
可以调用setScrollRange修改其范围
1 | BOOL SetScrollRange( |
参数 | 意义 |
---|---|
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 | case WM_SIZE: |
位置
滑块的位置是一个整数,比如当滚动条范围是[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 | typedef _W64 unsigned int UINT_PTR, *PUINT_PTR; |
其低位字代表了鼠标在滚动条的动作,又称为”通知码”,低位字的枚举值在winuser.h中有定义
1 | #define SB_LINEUP 0 |
各个枚举值的对应效果
wParam的高位字表示滑块位置
使用滚动条
加入滚动条处理的WndProc函数
1 | LRESULT CALLBACK WndProc(HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam) { |
关于绘图时的处理
y = cyChar * (i - iVscrollPos);
实际上干了一个将第i条作为客户区的第i-iVscrollPos行打印
立刻重绘
处理WM_VSCROLL时并没有立刻重绘客户区,而是调用InvalidateRect将这个皮球踢到下一次消息处理,WM_PAINT的处理.
WM_PAINT这个消息的优先级最低,当窗口过程的消息队列中有多种消息(比如多种对窗口有改动的消息)时,会首先处理其他消息,最后才会处理WM_PAINT消息.
这就好比市领导要求某中学视察,苦逼老师们提前一周就得造假材料,补完教案.等这一些都忙活完了,到领导视察当天,把所有材料一汇总,呈递给领导审阅
造材料就相当于处理其他消息
处理WM_PAINT就是呈递的临门一脚
如果要让WM_PAINT立刻被处理,需要在InvalidateRect之后立刻UpdateWindow
1 | if (iVscrollPos != GetScrollPos(hwnd, SB_VERT)) { |
Get/SetScrollInfo
先前的窗口中,滑块的大小是固定的,那么一小点都点不到.
而人家的滑块似乎是和总行数挂钩的,总行数越少滑块越大
确实如此,理论上可以得到一个公式
$$
\frac{滑块大小}{滚动条长度}=\frac{页面大小}{范围}=\frac{文档显示数量}{文档总大小}
$$
我们先进的Set/GetScrollInfo函数已经超过了老式的Set/GetScrollRange,Set/GetScrollPos函数Set/GetScrollInfo就可以考虑这一点了
使用Set/GetScrollInfo完全可以做到先前的老式函数.
这俩函数可以设置/获得滚动条的所有信息
1 | typedef struct tagSCROLLINFO {//SCROLLINFO结构体 |
奇怪的是,结构体的第一个成员是自己的大小,这不随便用sizeof
一算就有了吗?
windows程序设计给出的解释是,方便以后扩充结构使用
使用该结构体之前需要ixan将cbSize字段填充
1
2 SCROLLINFO si;
si.cbSize=sizeof(SCROLLINFO);真™抽象
fMask有效值:
1 |
不管是使用Set还是GetScrollInfo方法,引用传递的lpsi参数都要指定fMask这个成员
对于GetScrollInfo方法,指定了fMask=SIF_POS ,那么函数执行后lpsi引用的nPos就是有效值,其他成员无效
对于SetScrollInfo方法,指定了fMask=SIF_POS,那么函数根据lpsi的nPos值修改滚动条参数.其他成员不予理睬
修改滑块位置
使用SetScrollInfo改进的WM_CREATE处理函数
1 | static SCROLLINFO si; |
设置滚动条范围
1 | case WM_SIZE: |
虽然我们传递的si.nMax=NUMLINES-1
但是Windows操作系统会自动将滚动条范围最大值设置为si.nMax-si.nPage+1
,这就是SetScrollInfo函数相对于SetScrollRange的好处
更完善的滑动效果
1 | LRESULT CALLBACK WndProc(HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam) { |
使用老式函数时拖动滑块只有当鼠标松开,才会重绘客户区
现在只要是鼠标拖着竖直滑块有动作,客户区实时更新
绷不住了