dustland

dustball in dustland

win32程序设计-chapter3 窗口与消息

windows SDK chapter 3 窗口与消息

从例程开始

圣经windows程序设计给出的例程:

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
#include <windows.h>
LRESULT CALLBACK WndProc(HWND, UINT, WPARAM, LPARAM);
int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance,
PSTR szCmdLine, int iCmdShow)
{
static TCHAR szAppName[] = TEXT("HelloWin");
HWND hwnd;
MSG msg;
WNDCLASS wndclass;
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;
if (!RegisterClass(&wndclass))
{
MessageBox(NULL, TEXT("This program requires Windows NT!"),
szAppName, MB_ICONERROR);
return 0;
}
hwnd = CreateWindow(szAppName, // window class name
TEXT("The Hello Program"), // window caption
WS_OVERLAPPEDWINDOW, // window style
CW_USEDEFAULT, // initial x position
CW_USEDEFAULT, // initial y position
CW_USEDEFAULT, // initial x size
CW_USEDEFAULT, // initial y size
NULL, // parent window handle
NULL, // window menu handle
hInstance, // program instance handle
NULL); // creation parameters

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)
{
HDC hdc;
PAINTSTRUCT ps;
RECT rect;

switch (message)
{
case WM_CREATE:
PlaySound(TEXT("SenbonZakura.wav"), NULL, SND_FILENAME | SND_ASYNC);
return 0;
case WM_PAINT:
hdc = BeginPaint(hwnd, &ps);

GetClientRect(hwnd, &rect);

DrawText(hdc, TEXT("Hello, Windows 98!"), -1, &rect,
DT_SINGLELINE | DT_CENTER | DT_VCENTER);
EndPaint(hwnd, &ps);
return 0;

case WM_DESTROY:
PostQuitMessage(0);
return 0;
}
return DefWindowProc(hwnd, message, wParam, lParam);
}

要求:和exe文件同目录下有一个SenbonZakura.wav才能听歌.没有也不至于出错

至于windows系统版本要求,win xp是可以的,再老一点的win2000应该也可以,但是win98不是狠支持unicode编码,不是很行了

主函数

接口定义

WinMain函数,其固定格式:

1
2
3
4
5
INT WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance,
PSTR lpCmdLine, INT nCmdShow)
{
return 0;
}

返回值为INT,在minwindef.h中有typedef int INT;

调用约定为WINAPIminwindef.h中有:

1
2
3
4
5
6
#define CALLBACK    __stdcall
#define WINAPI __stdcall
#define WINAPIV __cdecl
#define APIENTRY WINAPI
#define APIPRIVATE __stdcall
#define PASCAL __stdcall

关于stdcall调用约定:

Element Implementation
参数传递顺序 从右向左压栈,不使用寄存器
参数传递规则(值传递/引用传递) 除非参数是指针或者引用类型,否则采用值传递
栈维护 被调用者自己清理自己用到的栈
命名修饰规则 下划线开头,然后@,然后是十进制表示的参数表字节大小. 因此int func(int a,double b)将会被修饰为_func@12(int四个字节+double八个字节)
大小写转换规定
返回值位置 放在eax,rax寄存器中

四个参数:

HINSTANCE hInstance:句柄类型,实例句柄或者模块句柄.实际上是一个数,但是可以唯一地标识某些东西.例程中的hInstance就标识本程序自己.

句柄类似于文件描述符,用一个数字对应一个打开的文件

HINSTANCE hPrevInstance:本程序前一个打开的实例的句柄.如果a.exe已经有一个实例在运行了,那么此时再打开a.exe,则hPrevInstance就是刚才实例的句柄.32位windows中该参数已经弃用

PSTR lpCmdLine:用来运行程序的命令行

PSTR是个什么类型呢?

1
2
typedef char CHAR;
typedef _Null_terminated_ CHAR *NPSTR, *LPSTR, *PSTR;

原来是以NULL结尾的char*字符串

INT nCmdShow:指明程序最初如何显示,包括最大化,最小化,正常显示

调用链

关于入口点的问题还需要炎鸠,暂且这样认为

1
2
3
4
WinMainCRTStartup或mainCRTStartup
->__tmainCRTStartup
->main
->WinMai

用ida观察最开始貌似有两个入口,但是实际上是不同类型的exe的进入点不一样

mainCRTStartup() ANSI + 控制台程序 wmainCRTStartup() UNICODE + 控制台程序 WinMainCRTStartup() ANSI + GUI程序 wWinMainCRTStartup() UNICODE + GUI程序

然后__tmainCRTStartup这个函数,

使用GetStartupInfo获取进程启动信息,

然后使用_inititem初始化全局变量和对象,

最后调用main、wmain、WinMain、wWinMain进入程序

注册窗口类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
HWND hwnd;
WNDCLASS wndclass;
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;
if (!RegisterClass(&wndclass))
{
MessageBox(NULL, TEXT("This program requires Windows NT!"),
szAppName, MB_ICONERROR);
return 0;
}

WNDCLASS wndclass

窗口类(实际上是一个C结构体配合面向对象风格的函数使用罢了),创建应用程序窗口之前必须注册窗口类

这个类的成员都有啥呢?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
typedef struct tagWNDCLASSW {
UINT style;
WNDPROC lpfnWndProc;
int cbClsExtra;
int cbWndExtra;
HINSTANCE hInstance;
HICON hIcon;
HCURSOR hCursor;
HBRUSH hbrBackground;
LPCWSTR lpszMenuName;
LPCWSTR lpszClassName;
} WNDCLASSW, *PWNDCLASSW, NEAR *NPWNDCLASSW, FAR *LPWNDCLASSW;
#ifdef UNICODE
typedef WNDCLASSW WNDCLASS;

其中最重要的是lpfnWndProc回调函数,lpszClassName窗口类名

style

UINT,unsigned int,无符号32位整型

窗口类样式.枚举类型

取值 说明
CS_BYTEALIGNCLIENT
0x1000
窗口的客户区域以“字符边界”对齐,当系统调整窗口的水平位置时,客户区域的左边坐标是8的整数倍。
CS_BYTEALIGNWINDOW
0x2000
窗口以“字符边界”对齐,当系统调整窗口的水平位置时,客户区域的左边坐标是8的整数倍。
CS_CLASSDC
0x0040
分配一个设备环境并被类中的所有窗体共享。它是可以适用于一个应用程序的若干线程创建的一个相同类的窗体。当多个线程试图同时使用相同的设备环境时,系统只允许一个线程成功地进行绘图操作。
CS_DBLCLKS
0x0008
当用户双击窗口时,将向窗口函数发送鼠标双击消息。
CS_GLOBALCLASS
0x4000
指定此窗体类是一个应用程序全局类。应用程序全局类是由一个在进程中对所有模块有效的exe或dll注册的窗体类。
CS_HREDRAW
0x0002
如果窗口的位置或宽度发生改变,将重绘窗口。
CS_NOCLOSE
0x0200
窗口中的“关闭”按钮不可见。
CS_OWNDC
0x0020
为同一个窗口类中的每个窗口创建一个唯一的设备上下文。
CS_PARENTDC
0x0080
设置子窗口中剪下的矩形区域到父窗口中,以使子窗口可以在父窗口上绘图。指定该风格可以提高应用程序的性能
CS_SAVEBITS
0x0800
把被窗口遮掩的屏幕图像作为位图保存起来。当该窗口被移动时,Windows操作系统使用被保存的位图来重建屏幕图像。
CS_VREDRAW
0x0001
如果窗口的位置或高度改变,将重绘窗口。

多个样式时使用按位或

lpfnWndProc

窗口信息的回调处理函数,窗口的灵魂

其类型是一个函数指针

1
2
3
4
5
6
7
8
9
10
11
12
typedef __int64 LONG_PTR, *PLONG_PTR;
typedef LONG_PTR LRESULT;

#define CALLBACK __stdcall

typedef unsigned __int64 UINT_PTR, *PUINT_PTR;
typedef UINT_PTR WPARAM;

typedef __int64 LONG_PTR, *PLONG_PTR;
typedef LONG_PTR LPARAM;

typedef LRESULT (CALLBACK* WNDPROC)(HWND, UINT, WPARAM, LPARAM);

一个返回值为LRESULT(64位整数),调用约定位CALLBACK(__stdcall),

四个参数分别为句柄,无符号32位整型,无符号64位整型,带符号64位整型

的函数指针WNDPROC

在例程中,它被注册为wndclass.lpfnWndProc = WndProc;

该函数就是过程函数,决定了收到特定消息时窗口的行为

cbClsExtra

根据匈牙利命名法,cb,count bytes,表示字节数前缀.

windows程序为每一个窗口设计类管理一个WNDCLASS结构。在应用程序注册一个窗口类的时候,可以让windows分配一定字节空间的内存,这部分内存成为类的附件内存,有属于这个窗口类的所有窗口共享,类附件内存信息用于存储窗口类的附加信息。windows系统将这部分内存初始化为0,因此我们经常设置此参数为0.

例程中该值设置为0,看来不是必要的

cbWndExtra

windows程序为每一个窗口管理一个内部数据结构,在注册窗口类的时候,系统可以为每一个窗口分配一定的字节数的附加内存空间,称为窗口附件内存。应用程序可使用这部分内存存储窗口特有的数据,windows系统把这部分内存初始化为0.

例程中该值设置为0,看来不是必要的

hInstance

窗体所在句柄

在例程中是这样写的wndclass.hInstance = hInstance;

右值的hInstance来自WinMain的第一个参数hInstacne,也就是当前程序的句柄

hIcon

窗体图标的句柄

例程中wndclass.hIcon = LoadIcon(NULL, IDI_APPLICATION);

调用了一个函数LoadIcon

1
2
3
WINUSERAPI HICON WINAPI LoadIconW
(_In_opt_ HINSTANCE hInstance,_In_ LPCWSTR lpIconName);
#define LoadIcon LoadIconW

hInstance:模块实例句柄,该模块包含了将被加载的图标

lpIconName:被家长的图标资源的名称

如果使用系统图标则hInstance为NULL,lpIconName为宏定义值

Value 含义
IDI_APPLICATION MAKEINTRESOURCE(32512) 默认程序图标
IDI_ASTERISK MAKEINTRESOURCE(32516) Asterisk图标, 与IDI_INFORMATION相同
IDI_ERROR MAKEINTRESOURCE(32513) Hand-shaped图标
IDI_EXCLAMATION MAKEINTRESOURCE(32515) 感叹号图标, 与IDI_WARNING相同
IDI_HAND MAKEINTRESOURCE(32513) Hand-shaped图标, 与IDI_ERROR相同
IDI_INFORMATION MAKEINTRESOURCE(32516) Asterisk图标
IDI_QUESTION MAKEINTRESOURCE(32514) 疑问号图标
IDI_SHIELD MAKEINTRESOURCE(32518) 安全盾图标
IDI_WARNING MAKEINTRESOURCE(32515) 感叹号图标
IDI_WINLOGO MAKEINTRESOURCE(32517) 默认程序图标, Win2000:Windows logo图标

例程中就使用了系统图标IDI_APPLICATION即默认图标(最丑的白板)

给他改成安全盾

image-20220713161727581

函数返回值为HICON即新加载的图标的句柄,如果加载失败则返回NULL

hCursor
1
2
3
4
5
6
7
HCURSOR
WINAPI
LoadCursorW(
_In_opt_ HINSTANCE hInstance,
_In_ LPCWSTR lpCursorName);
#ifdef UNICODE
#define LoadCursor LoadCursorW

光标类句柄

使用方法类似于hIcon,当hInstance=NULL时通过lpCursorName指定一个枚举值,使用系统光标

宏名 宏值 意义
IDC_APPSTARTING MAKEINTRESOURCE(32650) 标准箭头和沙漏
IDC_ARROW MAKEINTRESOURCE(32512) 标准箭头
IDC_CROSS MAKEINTRESOURCE(32515) 十字线
IDC_HAND MAKEINTRESOURCE(32649) 手掌
IDC_HELP MAKEINTRESOURCE(32651) 箭头和问号
IDC_IBEAM MAKEINTRESOURCE(32513) I型
IDC_ICON MAKEINTRESOURCE(32641) 已过时
IDC_NO MAKEINTRESOURCE(32648 禁止圈
IDC_SIZE MAKEINTRESOURCE(32640) 已过时,应该用IDC_SIZEALL
IDC_SIZEALL MAKEINTRESOURCE(32646) 指向东、西、南、北的四方向箭头
IDC_SIZENESW MAKEINTRESOURCE(32643) 指向东南、西北的两方向箭头
IDC_SIZENS MAKEINTRESOURCE(32645) 指向南、北的两方向箭头
IDC_SIZENWSE MAKEINTRESOURCE(32642) 指向西北、东南的两方向箭头
IDC_SIZEWE MAKEINTRESOURCE(32644) 指向东西的两方向箭头
IDC_UPARROW MAKEINTRESOURCE(32516) 竖直箭头
IDC_WAIT MAKEINTRESOURCE(32514) 沙漏

返回值HCURSOR类型的句柄.如果成功,返回最近一次加载的光标句柄。如果失败,返回NULL。

例程中使用的是IDC_ARROW标准箭头

image-20220713162947891
hbrBackground

主窗口背景色,背景刷类的句柄

该值可以是一个物理刷,也可以是纯颜色值

  • COLOR_ACTIVEBORDER
  • COLOR_ACTIVECAPTION
  • COLOR_APPWORKSPACE
  • COLOR_BACKGROUND
  • COLOR_BTNFACE
  • COLOR_BTNSHADOW
  • COLOR_BTNTEXT
  • COLOR_CAPTIONTEXT
  • COLOR_GRAYTEXT
  • COLOR_HIGHLIGHT
  • COLOR_HIGHLIGHTTEXT
  • COLOR_INACTIVEBORDER
  • COLOR_INACTIVECAPTION
  • COLOR_MENU
  • COLOR_MENUTEXT
  • COLOR_SCROLLBAR
  • COLOR_WINDOW
  • COLOR_WINDOWFRAME
  • COLOR_WINDOWTEXT
image-20220713163735682

例程中使用的是物理刷(HBRUSH)GetStockObject(WHITE_BRUSH)

其中GetStockObject函数原型:

1
2
3
HGDIOBJ GetStockObject(
[in] int i
);

参数i的取值有:

含义
BLACK_BRUSH 黑色画刷
DKGRAY_BRUSH 暗灰色画刷
DC_BRUSH 1. 纯色画刷,默认颜色是白色的 2. 调用 SetDCBrushColor 函数可以修改该值的颜色
GRAY_BRUSH 灰色画刷
HOLLOW_BRUSH 空画刷(相当于 NULL_BRUSH)
LTGRAY_BRUSH 浅灰色画刷
NULL_BRUSH 空画刷(相当于 HOLLOW_BRUSH)
WHITE_BRUSH 白色画刷
BLACK_PEN 黑色画笔
DC_PEN 1. 纯色画笔,默认颜色是白色的 2. 调用 SetDCPenColor 函数可以修改该值的颜色
NULL_PEN 空画笔(空画笔不绘制任何东西)
WHITE_PEN 白色画笔
ANSI_FIXED_FONT Windows 中的固定间距(等宽)系统字体
ANSI_VAR_FONT Windows 中的可变间距(比例间距)系统字体
DEVICE_DEFAULT_FONT 设备相关字体
DEFAULT_GUI_FONT 1. 用户界面对象(如菜单、对话框)的默认字体 2. 不推荐使用 DEFAULT_GUI_FONT 或 SYSTEM_FONT 获得对话框或系统的字体 3. 该字体默认是 Tahoma
OEM_FIXED_FONT 原始设备制造商(OEM)相关固定间距(等宽)字体
SYSTEM_FONT 1. 系统字体 2. 默认情况下,Windows 使用系统字体绘制菜单,对话框和文本 3. 不推荐使用 DEFAULT_GUI_FONT 或 SYSTEM_FONT 获得对话框或系统的字体 4. 该字体默认是 Tahoma
SYSTEM_FIXED_FONT 1. 固定间距(等宽)系统字体 2. 该对象仅为兼容 16 位 Windows 版本提供
DEFAULT_PALETTE 默认调色板(该调色板由系统调色板中的静态色彩组成)

函数调用成功则返回所申请的逻辑对象的句柄

失败则返回NULL

lpszMenuName

LPCWSTR类型长指针宽字节型字符串,菜单栏名

lpszClassName

LPCWSTR类型长指针宽字节型字符串,描述窗口类名

例程使用的是

1
2
static TCHAR szAppName[] = TEXT("HelloWin");
wndclass.lpszClassName = szAppName;

这个窗口类名将是一个窗口和该窗口类发生联系的唯一接口

RegisterClass

1
2
3
WINUSERAPI ATOM WINAPI RegisterClassW(_In_ CONST WNDCLASSW *lpWndClass);
#ifdef UNICODE
#define RegisterClass RegisterClassW

参数是WNDCLASS类型,如果注册成功则返回ATOM,否则返回NULL

1
2
typedef unsigned short      WORD;
typedef WORD ATOM;

ATOM也是一个句柄

例程中

1
2
3
4
5
if (!RegisterClass(&wndclass))
{
MessageBox(NULL, TEXT("This program requires Windows NT!"),szAppName, MB_ICONERROR);
return 0;
}

没有保存RegisterClass的值

为啥这里要判断一下是否注册成功呢?

因为RegisterClass有两个实现,RegisterClassA和RegisterClassW,分别使用ASCII或者Unicode决定传递给窗口的信息的类型

但是WIN98系统上RegisterClassW并没有实现,只是一个桩函数,返回NULL,因此该程序在Win98上运行会寄

然而MessageBoxW也需要宽字符支持啊?谁知Win98就挑了几个需要unicode的函数实现了,包括MessageBoxW

创建窗口类实例

为啥要打印一个窗口到屏幕这么费劲啊,又是注册又是创建?

刚才注册的是一个模子,这个模子可以有很多实例,模子作为实例的相同部分,每个实例还可以有自己的特点

在CreateWindow函数中我们将会了解到CreateWindow和RegisterClass分别干了啥

HWND hwnd

句柄类型,hwnd将来要承载CreateWindow的返回值,用来标识唯一的窗口

1
2
3
4
winnt.h
#define DECLARE_HANDLE(name) struct name##__{int unused;}; typedef struct name##__ *name
windef.h
DECLARE_HANDLE(HWND);

会发现这个DECLARE_HANDLE(name)的宏定义很奇怪,

struct name##,这两个井号是干啥的

一个井号的时候,其作用为自动添加双引号

1
2
#define ToString(x) #x
char* str = ToString(123132); // 就成了str="123132";

两个井号的时候起拼接作用

1
2
3
4
#define Conn(x,y) x##y 
int n = Conn(123,456); // 结果就是n=123456;
char* str = Conn("asdf", "adf") // 结果就是 str = "asdfadf";

那么#define DECLARE_HANDLE(name) struct name##__{int unused;}; typedef struct name##__ *name宏定义展开就是

1
2
3
4
struct HWND__{
int unused;
};
typedef struct HWND__ *HWND

这里##的作用就是拼接HWND和下划线__,下划线的作用是避免命名冲突

CreateWindow

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
WINUSERAPI HWND WINAPI CreateWindowExW(
_In_ DWORD dwExStyle,
_In_opt_ LPCWSTR lpClassName,
_In_opt_ LPCWSTR lpWindowName,
_In_ DWORD dwStyle,
_In_ int X,
_In_ int Y,
_In_ int nWidth,
_In_ int nHeight,
_In_opt_ HWND hWndParent,
_In_opt_ HMENU hMenu,
_In_opt_ HINSTANCE hInstance,
_In_opt_ LPVOID lpParam);

#define CreateWindowW(lpClassName, lpWindowName, dwStyle, x, y,\
nWidth, nHeight, hWndParent, hMenu, hInstance, lpParam)\

CreateWindowExW(0L, lpClassName, lpWindowName, dwStyle, x, y,\
nWidth, nHeight, hWndParent, hMenu, hInstance, lpParam)

#ifdef UNICODE

#define CreateWindow CreateWindowW

这个皮球踢了三jio才能落实

CreateWindow->CreateWindowW->CreateWindowExW

从CreateWindowW到CreateWindowExW多了一个参数dwExStyle,并且该参数默认是0,没有其他区别了

该函数的返回值为指向所创建窗口的句柄,历程中是这样写的hwnd = CreateWindow...,即该句柄保存在hwnd中.

该函数执行完毕之后不会在屏幕上显示窗口,而是在内存中分配了一块,用来保存传递给CreateWindow函数的各种参数信息,以及一些其他信息.可以通过hwnd句柄调用这些信息

lpClassName

这就是刚才RegisterClass注册的窗口类,最后一个参数wnd.lpszClassName

例程中该值为L"HelloWin",因此在CreateWindow中才可以使用"HelloWin"作为窗口类名.意思是当前创建的窗口使用"HelloWin"类的设定

lpWindowName

窗口标题

例程中该值为TEXT("The Hello Program"), // window caption

窗口运行时左上角的标题就是The Hello Program

image-20220713173027546
dwStyle

指定窗口的风格

窗口风格 含义
WS_BORDER 创建一个带边框的窗口
WS_CAPTION 创建一个有标题框的窗口(包含了 WS_BODER 风格)
WS_CHILD 创建一个子窗口,这个风格的窗口不能拥有菜单也不能与 WS_POPUP 风格合用
WS_CHILDWINDOW 与 WS_CHILD 相同
WS_CLIPCHILDREN 当在父窗口内绘图时,排除子窗口区域,在创建父窗口时使用这个风格
WS_CLIPSIBLINGS 1. 排除子窗口之间的相对区域,也就是,当一个特定的窗口接收到 WM_PAINT 消息时,WS_CLIPSIBLINGS 风格将所有层叠窗口排除在绘图之外,只重绘指定的子窗口 2. 如果未指定该风格,并且子窗口是层叠的,则在重绘子窗口的客户区时,就会重绘邻近的子窗口
WS_DISABLED 1. 创建一个初始状态为禁止的子窗口,一个禁止状态的窗口不能接受来自用户的输入信息 2. 在窗口创建之后,可以调用 EnableWindow 函数来启用该窗口
WS_DLGFRAME 创建一个带对话框边框风格的窗口,这种风格的窗口不能带标题条
WS_GROUP 1. 指定一组“控制窗口”的第一个“控制窗口” 2. 这个“控制窗口”组由第一个“控制窗口”和随后定义的“控制窗口”组成,自第二个“控制窗口”开始每个“控制窗口”具有 WS_GROUP 风格 3. 每个组的第一个“控制窗口”带有 WS_TABSTOP 风格,从而使用户可以在组间移动 4. 用户随后可以使用光标在组内的控制间改变键盘焦点
WS_HSCROLL 创建一个有水平滚动条的窗口
WS_ICONIC 创建一个初始状态为最小化状态的窗口,与 WS_MINIMIZE 风格相同
WS_MAXIMIZE 创建一个初始状态为最大化状态的窗口
WS_MAXIMIZEBOX 创建一个具有最大化按钮的窗口,该风格不能与 WS_EX_CONTEXTHELP 风格同时出现,同时必须指定 WS_SYSMENU 风格
WS_MINIMIZE 创建一个初始状态为最小化状态的窗口,与 WS_ICONIC 风格相同
WS_MINIMIZEBOX 创建一个具有最小化按钮的窗口,该风格不能与 WS_EX_CONTEXTHELP 风格同时出现,同时必须指定 WS_SYSMENU 风格
WS_OVERLAPPED 产生一个层叠的窗口,一个层叠的窗口有一个标题条和一个边框,与 WS_TILED 风格相同
WS_OVERLAPPEDWINDOW 相当于(WS_OVERLAPPED | WS_CAPTION | WS_SYSMENU | WS_THICKFRAME | WS_MINIMIZEBOX | WS_MAXIMIZEBOX),与 WS_TILEDWINDOW 风格相同
WS_POPUP 创建一个弹出式窗口,该风格不能与 WS_CHILD 风格同时使用。
WS_POPUPWINDOW 相当于(WS_POPUP | WS_BORDER | WS_SYSMENU),但 WS_CAPTION 和 WS_POPUPWINDOW 必须同时设定才能使窗口某单可见
WS_SIZEBOX 创建一个可调边框的窗口,与 WS_THICKFRAME 风格相同
WS_SYSMENU 创建一个在标题条上带有窗口菜单的窗口,必须同时设定 WS_CAPTION 风格
WS_TABSTOP 1. 创建一个“控制窗口”,在用户按下 Tab 键时可以获得键盘焦点。 2. 按下 Tab 键后使键盘焦点转移到下一具有 WS_TABSTOP 风格的“控制窗口”
WS_THICKFRAME 创建一个具有可调边框的窗口,与 WS_SIZEBOX 风格相同
WS_TILED 产生一个层叠的窗口,一个层叠的窗口有一个标题和一个边框,与 WS_OVERLAPPED 风格相同
WS_TILEDWINDOW 相当于(WS_OVERLAPPED | WS_CAPTION | WS_SYSMENU | WS_THICKFRAME | WS_MINIMIZEBOX | WS_MAXIMIZEBOX),与 WS_OVERLAPPEDWINDOW 风格相同
WS_VISIBLE 创建一个初始状态为可见的窗口
WS_VSCROLL 创建一个有垂直滚动条的窗口

例程中该值为WS_OVERLAPPEDWINDOW,

相当于WS_OVERLAPPED | WS_CAPTION | WS_SYSMENU | WS_THICKFRAME | WS_MINIMIZEBOX | WS_MAXIMIZEBOX

层叠 | 有标题 | 标题条上有窗口菜单 | 可调边框 | 可最小化 | 可最大化

x

指定窗口初始水平位置对于层叠或者弹出式窗口,x是相对屏幕左上角的位移

对于子窗口,x是相对于父窗口左上角的偏移

如果该值为CW_USEDEFAULT则系统为窗口选择缺省的左上角左边并忽略y(该值只对层叠窗口有效)

#define CW_USEDEFAULT ((int)0x80000000)

例程中就使用了CW_USEDEFAULT

y

类似于x

nWidth

指定窗口宽度

nHeight

指定窗口高度

hWndParent

父窗口的句柄

例程中该值为NULL,即没有父窗口,显然这是第一个窗口,还没有第二个窗口,谈不上父子关系

注册子窗口时这里要写父窗口的句柄

hMenu

窗口菜单句柄

例程中该值为NULL,看来不是必要的

对于子窗口来说,父窗口过程在建立子窗口的时候需要维护一个子窗口ID,就是在hMenu上指定

hInstance

与窗口相关联的模块实例的句柄

例程中该值为hInstance,也就是winmain的第一个参数,目前阶段只要是hInstance基本都是来自winmain的参数.

lpParam

创建窗口之后发送给该窗口过程的WM_CREATE消息的lParam参数

例程中该值为NULL,目前阶段一般都是NULL,不管他了

显示窗口实例

ShowWindow

RegisterClass注册了窗口类,

CreateWindow创建了窗口实例,

下面就差一步就能将该实例显示出来了

这就是ShowWindow(hwnd,iCmdShow)函数的作用

1
WINUSERAPI BOOL WINAPI ShowWindow(_In_ HWND hWnd,_In_ int nCmdShow);

hwndCreateWindow的返回值,也就是窗口实例的句柄

iCmdShowwinmain函数的第四个参数,即命令行参数,此参数决定窗口的显示方式,最大化最小化或者正常.

如果显示成功则返回TRUE,否则返回FALSE

UpdateWindow

当客户区有改动时使用本函数重绘客户区

实际上通过向窗口过程发送WM_PAINT消息完成

参数是窗口实例句柄,意思是更新该句柄对应窗口实例

消息循环

消息

举一个直观例子,键鼠动作就属于消息.

操作系统负责用结构体记录消息,并发送给相应线程的消息队列,每个线程都有一个消息队列

消息结构体:

1
2
3
4
5
6
7
8
9
10
11
typedef struct tagMSG {
HWND hwnd;
UINT message;
WPARAM wParam;
LPARAM lParam;
DWORD time;
POINT pt;
#ifdef _MAC
DWORD lPrivate;
#endif
} MSG, *PMSG, NEAR *NPMSG, FAR *LPMSG;

比如当点选窗口W的关闭按钮X时,操作系统会捕获该鼠标动作,封装成MSG结构体,

然后操作系统将把该结构体塞到对应线程的消息队列中

hwnd

但是塞给哪个线程的消息队列呢?通过该结构体的第一个参数hwnd窗口句柄.

如果该值为NULL则是一个线程消息

一个线程可以有多个窗口,一个进程可以有多个线程

message

发送啥消息呢?通过第二个参数message决定,该值是个枚举类型

1
2
3
4
5
6
7
8
#define WM_NULL                         0x0000
#define WM_CREATE 0x0001//窗口创建伊始操作系统产生的消息
#define WM_DESTROY 0x0002
#define WM_MOVE 0x0003
#define WM_SIZE 0x0005

#define WM_ACTIVATE 0x0006
...
wParam/lParam

额外信息,要和message配合使用,有些信息不需要额外描述,一个message就够了

time

消息进入线程消息队列的时间

pt

指针类型,指向结构体Point

1
2
3
4
5
typedef struct tagPOINT
{
LONG x;
LONG y;
} POINT, *PPOINT, NEAR *NPPOINT, FAR *LPPOINT;

描述消息进入消息队列时,鼠标光标位置

GetMessageW

1
2
3
4
5
6
7
WINUSERAPI BOOL WINAPI GetMessageW(
_Out_ LPMSG lpMsg,
_In_opt_ HWND hWnd,
_In_ UINT wMsgFilterMin,
_In_ UINT wMsgFilterMax);
#ifdef UNICODE
#define GetMessage GetMessageW

本函数的作用是从线程消息队列中取出一条消息,将该消息保存在第一个参数lpMsg中(目的地).

因此例程中在winmain函数栈下开了一个MSG msg;用于存放GetMessageW获取到的信息

GetMessageW传递msg作为参数时使用的是引用传递GetMessage(&msg, NULL, 0, 0)

第二个参数hWnd指定接收属于哪个窗口的消息,如果设置为NULL则表示接收属于调用线程的所有窗口的窗口消息.

这是由于一个线程可以有多个窗口,关闭A窗口的消息要准确地发送给A窗口,不能把B窗口关喽

第三个参数wMsgFilterMin指定要获取的消息的最小值,通常为0

第四个参数wMsgFilterMax指定要获取消息的最大值,如果wMsgFilterMinwMsgFilterMax都是0则接收所有消息

例程中这两个值全是0,也就是接收所有消息

TranslateMessage(&msg)

线程将msg消息还给操作系统,让操作系统进行键盘消息的转换,转换完后操作系统将结果还是放在msg中还给线程

DispatchMessage(&msg)

线程收到操作系统返回的转换信息,又将msg还给操作系统.

操作系统将该消息发送给相应窗口过程WndProc进行处理,即操作系统调用了窗口过程

这里的窗口过程也就是RegisterClass时lpfnWndProc指定的回调函数

窗口过程仅针对当前消息做出相应处理,然后将控制还给操作系统.操作

Dispatch,分发,这里的意思是操作系统根据消息msg的hwnd句柄,决定分发给回调函数进行处理还是返回内核进行处理

细节是咋样的,在此不深究

循环处理

1
2
3
4
5
while (GetMessage(&msg, NULL, 0, 0))
{
TranslateMessage(&msg);
DispatchMessage(&msg);
}

这是一个固定格式,当前线程不断检查其消息队列中是否有消息,如果有则msg是有效负载,否则msg空.

msg不管是不是空,都会经历整个过程,空负载也就是蜻蜓点水地进入转换和分发函数立刻判断失效返回.

窗口过程

在消息循环中,我们直到窗口过程不是线程自己想要调用就调用的,需要操作系统来调用窗口过程

所谓窗口过程,实际上就是注册窗口类时,wndclass.lpfnWndProc这个函数指针指向的函数

前面消息经过一系列踢皮球,不管是os给线程啊,还是线程还给os啊,都只是传来传去,没有对窗口造成任何影响.

最终这个皮球踢给了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
LRESULT CALLBACK WndProc(HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam)
{
HDC hdc;
PAINTSTRUCT ps;
RECT rect;

switch (message)
{
case WM_CREATE:
PlaySound(TEXT("SenbonZakura.wav"), NULL, SND_FILENAME | SND_ASYNC);
return 0;
case WM_PAINT:
hdc = BeginPaint(hwnd, &ps);

GetClientRect(hwnd, &rect);

DrawText(hdc, TEXT("Hello, Windows 98!"), -1, &rect,
DT_SINGLELINE | DT_CENTER | DT_VCENTER);
EndPaint(hwnd, &ps);
return 0;

case WM_DESTROY:
PostQuitMessage(0);
return 0;
}
return DefWindowProc(hwnd, message, wParam, lParam);
}

暂且先不管细节,但就从switch(message)-case可以看出,这是在对message参数传进来的消息进行分拣,例程中给出了三种分拣情况

case WM_CREATE,当窗口创建时,即CreateWindow返回前

case WM_PAINT当窗口绘制完毕时

case WM_DESTROY当窗口关闭时

现在开始管细节

1
LRESULT CALLBACK WndProc(HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam);

这四个参数正好是MSG结构体的前四个成员

1
2
3
4
5
6
7
8
9
10
11
typedef struct tagMSG {
HWND hwnd;
UINT message;
WPARAM wParam;
LPARAM lParam;
DWORD time;
POINT pt;
#ifdef _MAC
DWORD lPrivate;
#endif
} MSG, *PMSG, NEAR *NPMSG, FAR *LPMSG;

hwnd,窗口句柄,作用是告诉WndProc函数,将要根据函数进行的处理,要作用于哪一个窗口

message,告诉WndProc函数,发生甚么事了,指导WndProc做出相应处理

wParam,lParam,配合message,当message一句话说不清发生甚么事的时候,就需要 多说一句甚至两句描述清楚发生了甚么事

这就好比病人去中医院已经看完了大夫,要去药房拿药

病人看大夫,大夫一眼顶针,总结出病人得了什么病,

但是为了挣钱,防止病人自己去其他平价药店买要,因此大夫将处方写的只能药房护士看得懂,病人你休想看懂

病人没办法拿着处方去了药房,这个药房护士就是WndProc函数,他一看,

第一行写了个"玛卡巴卡",哦,皮燕子有毛病

第二行写了个"妈了巴子",哦,要吃人参,树皮,坷拉蛋子

第三行写了个"古西迪西",哦,人参两公斤,树皮两张,坷拉蛋子两公斤,吃不死你

药房护士就开出了药,完成了其使命.

对于例程中的唯一的窗口,hwnd显而易见的来自唯一一次调用的CreateWindow创建的窗口

但是从DisPatchMessage(&msg)到操作系统调用WndProc,中间出现了一个断层,看不到中间的过程心里总是发慌.

并且TranslateMessage(&msg)干了啥也不知道.

调用WndProc有没有创建新进程呢?目前感觉没有,基于两点推测,

一是两个进程的虚拟地址空间独立,不方便WndProc修改窗口参数.

二是刚才已经了解到DisPatchMessage->操作系统相关函数->WndProc->操作系统相关函数->DispatchMessage这个过程只涉及到控制的转移,看来只是函数调用和返回,不涉及到开新进程

怎么解决这些问题呢?动态调试or看源代码,但不是现在

消息分拣与处理

消息处理的过程,就是switch-case分拣和就事论事的过程,其要求是:

如果switch-case可以捕获该消息种类并进行处理,那么处理后返回0

否则,即switch-case处理不了这种消息,则应该把处理消息的皮球踢给叫DefWindowProc的系统函数.然后WndProc返回DefWindowProc的返回值

就好比要吃核桃的时候先用手拨,不行就用牙咬,再不行就用脚踩,要是还不行就只能把核桃交给核桃钳了.

这就好比消息先让WndProc中的switch-case尝试处理,处理不动就交给DefWindowProc进行处理

但是不管用什么方法,最终核桃都要进入人的嘴里

就好比不管谁处理的消息,最终都要经过WndProc返回

例程是严格遵守这个顺序的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
switch (message)
{
case WM_CREATE://第一次尝试
...
return 0;//若能处理则返回0
case WM_PAINT://第二次尝试
...
return 0;

case WM_DESTROY://第三次尝试
...
return 0;
}
//实在能力有限,处理不了,把瓷器活交给金刚钻
return DefWindowProc(hwnd, message, wParam, lParam);//返回DefWindowProc的返回值

这些消息处理感觉就类似于HTML中的onClick,onload等事件处理函数

onload等事件处理函数一般会赋一个javascript脚本的函数,这个js函数负责修改HTML元素,给用户的感觉就是页面会动态改变

类比到win32编程

case WM_CREATE:这句话就相当于onLoad,

case块里的内容就相当于onLoad=等号后面挂钩的js函数

播放音频

消息分拣的第一次尝试

1
2
3
case WM_CREATE:
PlaySound(TEXT("SenbonZakura.wav"), NULL, SND_FILENAME | SND_ASYNC);
return 0;

如果该消息是 窗口创建,则捕获该消息.

怎么处理的呢?听首千本樱吧,当前exe文件同目录下找一个叫SenbonZakura.wav的音频文件.

PlaySound函数的实现在winmm.lib静态库中,因此在编译的时候要加上-lwinmm选项gcc main.c -O0 -o main -m32 -mwindows -lwinmm

单凭这个静态库就知道PlaySound是个很老狠不中用的函数了,甚至都没必要制作成动态库函数.

上网一查API,果然,它只能播放.wav格式的音频文件.怪不得我一开始让他放SenbonZakura.mp3,他不吱声,真的太逊了

1
2
3
4
5
BOOL PlaySound(
 LPCTSTR pszSound,
 HMODULE hmod,
 DWORD   fdwSound
);

第一个参数是资源名,第二个参数目前认为NULL就可以

还有一件事,听歌的时候程序是卡在这句话等着呢,还是立刻返回呢.

如果一直卡着显然不能及时处理下一条消息.并且实际应用比如植物大战僵尸中,音乐都是当作bgm和战斗并行的.

这玩意怎么实现的呢?通过第三个参数

fdwSound枚举值

SND_ALIAS pszSound参数指定了注册表或WIN.INI中的系统事件的别名。

SND_ALIAS_ID pszSound参数指定了预定义的声音标识符。

SND_ASYNC 用异步方式播放声音,PlaySound函数在开始播放后立即返回。

SND_FILENAME pszSound参数指定了WAVE文件名。

SND_LOOP 重复播放声音,必须与SND_ASYNC标志一块使用。

SND_MEMORY 播放载入到内存中的声音,此时pszSound是指向声音数据的指针。

SND_NODEFAULT 不播放缺省声音,若无此标志,则PlaySound在没找到声音时会播放缺省声音。

SND_NOSTOP PlaySound不打断原来的声音播出并立即返回FALSE。

SND_NOWAIT 如果驱动程序正忙则函数就不播放声音并立即返回。

SND_PURGE 停止所有与调用任务有关的声音。若参数pszSound为NULL,就停止所有的声音,否则,停止pszSound指定的声音。

SND_RESOURCE pszSound参数是WAVE资源的标识符,这时要用到hmod参数。

SND_SYNC 同步播放声音,在播放完后PlaySound函数才返回。

SND_SYSTEM 如果是背景窗口,当这个标志被设置,声音是分配到音频会议系统通知的声音。系统音量控制程序(sndvol)显示音量滑块控制系统通知的声音。设置该标志将下控制音量滑块。如果没有设置该标志,声音是分配到默认的音频会议的应用进程。

例程中这个参数为SND_FILENAME | SND_ASYNC

意思是第一个参数指定的是一个wav资源名,第二个参数意思是异步播放,即PlaySound执行后立刻返回,音乐异步播放

重绘客户区

CreateWindow之后,只是在内存中为窗口分配了空间,不能显示窗口

ShowWindow显示CreateWindow在内存中存好的窗口,此后的更新它不知道

如果此后客户区有更新,需要更新客户区,否则还是显示之前的页面

窗口的客户区是会经常发生变动的,比如缩放调整,最小化,最大化等变化.每次变化后都需要及时重绘客户区

还是看看例程中的方法吧

1
2
3
4
5
6
7
8
9
10
11
12
13
HDC hdc;
PAINTSTRUCT ps;
RECT rect;

case WM_PAINT:
hdc = BeginPaint(hwnd, &ps);

GetClientRect(hwnd, &rect);

DrawText(hdc, TEXT("Hello, Windows 98!"), -1, &rect,
DT_SINGLELINE | DT_CENTER | DT_VCENTER);
EndPaint(hwnd, &ps);
return 0;

起于BeginPaint,终于EndPaint,夹在中间的逻辑就是重绘

BeginPaint

BeginPaint会将客户区的背景擦除,使用注册窗口类WNDCLASS时的hbrBackground画刷,当时我们是这样规定这个值的

1
wndclass.hbrBackground = (HBRUSH)GetStockObject(WHITE_BRUSH);

即一个库存的白色的画刷

BeginPaint使能客户区,告诉操作系统,要向显卡输出了,把本次(其余BeginPaint终于EndPaint)的输出放在显示队列里

返回一个设备环境句柄hdc,啥叫设备环境句柄呢?

hdc就对应窗口客户区那块显示屏,对hdc瞎折腾都不会超过客户区的范围,不会说画出去

GetClientRect

第一个参数,窗口句柄,例程中的hwnd来自WndProc函数,该函数由操作系统调用,hwnd就是消息发生所在的窗口

第二个参数,RECT类型,矩形结构指针

1
2
3
4
5
6
7
typedef struct tagRECT
{
LONG left;//左边界
LONG top;//上边界
LONG right;//右边界
LONG bottom;//下边界
} RECT, *PRECT, NEAR *NPRECT, FAR *LPRECT;

其中left和top总是置0,此时right和bottom分别以像素为单位表示客户区高度和宽度

注意到rect传递的是引用,也就是说GetClientRect将会改变这个值.

该函数会获取客户区的大小,写道第二个参数指定的结构体中

为啥要获取客户区大小呢?为下面重绘做准备.经过窗口缩放等改变,客户区显然会变,因此需要重新量身定做

DrawText
1
2
3
4
5
6
7
int DrawText(
[in] HDC hdc,
[in, out] LPCTSTR lpchText,
[in] int cchText,
[in, out] LPRECT lprc,
[in] UINT format
);
1
2
DrawText(hdc, TEXT("Hello, Windows 98!"), -1, &rect,
DT_SINGLELINE | DT_CENTER | DT_VCENTER);

绘制文本,向hdc句柄对应的区域,打印"Hello,Windows 98!",-1表示该字符串以0结尾.

第四个参数是刚从GetClientRect获得客户区信息的矩形结构体,要打印的字符串将放在啊矩形中

至于是居中居左等样式信息,还要取决于第五个参数

Value Meaning
DT_BOTTOM Justifies the text to the bottom of the rectangle. This value is used only with the DT_SINGLELINE value.
DT_CALCRECT Determines the width and height of the rectangle. If there are multiple lines of text, DrawText uses the width of the rectangle pointed to by the lpRect parameter and extends the base of the rectangle to bound the last line of text. If the largest word is wider than the rectangle, the width is expanded. If the text is less than the width of the rectangle, the width is reduced. If there is only one line of text, DrawText modifies the right side of the rectangle so that it bounds the last character in the line. In either case, DrawText returns the height of the formatted text but does not draw the text.
DT_CENTER 文字居中
... ...

例程中使用的是DT_SINGLELINE | DT_CENTER | DT_VCENTER,单行,水平居中,垂直居中

EndPaint

配合BeginPaint使用,这样一对函数的参数必须相同,夹紧一个窗口的变化

关闭窗口

当点击窗口右上角的❌,企图关闭窗口时,该消息发出

1
2
3
case WM_DESTROY:
PostQuitMessage(0);
return 0;

例程中的处理方法是调用函数PostQuitMessage(0)

其功能是将WM_QUIT消息插入线程的消息队列.

而消息循环的判断函数GetMessage,唯独对于WM_QUIT返回0,

当消息循环取出该消息时,GetMessage返回0,判断失败,不进入循环,WinMain结束.线程结束

程序返回

只有当消息循环接收到WM_QUIT时,才会跳出循环,此时WinMain寿终正寝了

1
return msg.wParam;