句柄

句柄是整个windows编程的基础
一个句柄是指使用的一个唯一的整数值,即一个4字节(64位程序中为8字节)长的数值,
标识应用程序中的不同对象和同类对象中的不同的实例
诸如,一个窗口,按钮,图标,滚动条,输出设备,控件或者文件等。
应用程序能够通过句柄访问相应的对象的信息。
Windows使用了大量的句柄来标识很多对象。
(来自百度百科)


WinMain

注意先协商头文件

1
#include <Windows.h>

然后是WinMain

1
2
3
4
5
6
int WINAPI WinMain(
HINSTANCE hInstance, //应用程序当前实例的句柄
HINSTANCE hPrevinstance,//应用程序的先前实例的句柄
LPSTR lpCmdline, //指向应用程序命令行的字符串的指针
int nCmdshow //窗口如何显示
)

hInstance 应用程序当前实例的句柄
hPrevinstance 应用程序的先前实例的句柄
如果同一个程序打开两次,出现两个窗口,那么第一次打开的窗口就是先前实例的窗口。对于一个32位程序,该参数总为NULL(来自百度百科)
所以如果要防止程序打开两次,可以写:

1
2
3
4
if (hPrevinstance) {
MessageBox(NULL,"应用程序已在运行","注意",MB_OK);
return 0;
}

lpCmdline 类型名是是LPSTR,即char*。是指向应用程序命令行的字符串的指针。
例如在命令行中输入:notepad D:\1.txt;
或是双击这个文件启动记事本程序notepad.exe时,
系统会将”D:\1.txt”字符串作为命令行参数(lpCmdline)传递给记事本程序的WinMain函数。
然后在窗口中显示该文件内容。
nCmdshow 指明窗口如何显示。如最小化SW_SHOWMINIMIZED,不变SW_SHOW,最大化SW_SHOWMAXIMIZED等。


注册窗口类

如果我们写一个函数:MyRegisterClass(hInstance),若返回值为0,那么注册失败。
那么可以写如下代码:

1
2
3
4
if (!MyRegisterClass(hInstance)) {
MessageBox(NULL, "注册窗口失败", "错误", MB_OK);
return 0;
}

问题在于怎么注册窗口类。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
//WNDCLASSEX定义
typedef struct tagWNDCLASSEXA {
UINT cbSize; //该结构字节数
/* Win 3.x */
UINT style; //窗口类风格
WNDPROC lpfnWndProc; //长指针,指向回调函数WndProc。DispatchMessage将把消息发给这个函数
int cbClsExtra; //不清楚
int cbWndExtra; //不清楚
HINSTANCE hInstance; //当前模块的实例句柄
HICON hIcon; //窗口图标的句柄
HCURSOR hCursor; //鼠标指针的句柄
HBRUSH hbrBackground; //绘制窗口背景的画刷的句柄
LPCSTR lpszMenuName; //窗口菜单的资源ID字符串
LPCSTR lpszClassName; //窗口类的名称(之后与CreateWindowEx联系上)
/* Win 4.0 */
HICON hIconSm; //窗口小图标的句柄
} WNDCLASSEXA;
typedef WNDCLASSEXA WNDCLASSEX;

注册窗口类的过程:MyRegisterClass代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
const char CLASSNAME[] = "helloworld";		//窗口类的名称
ATOM MyRegisterClass(HINSTANCE hInstance) { //ATOM和WORD等价,hInstance是当前实例句柄
WNDCLASSEX wcex; //窗口类
wcex.cbSize = sizeof(WNDCLASSEX);
wcex.style = CS_HREDRAW | CS_VREDRAW; //当窗口水平方向、垂直方向宽度改变时,重绘窗口
wcex.lpfnWndProc = WndProc; //指向回调函数
wcex.cbClsExtra = 0;
wcex.cbWndExtra = 0;
wcex.hInstance = hInstance; //当前实例的句柄
wcex.hIcon = LoadIcon(NULL, IDI_APPLICATION); //加载系统图标
wcex.hCursor = LoadCursor(NULL, IDC_ARROW); //加载系统鼠标指针
wcex.hbrBackground = (HBRUSH)GetStockObject(WHITE_BRUSH);//绘制窗口背景时用白色画刷
wcex.lpszMenuName = NULL; //没有菜单
wcex.lpszClassName = CLASSNAME; //窗口类的名称(之后与CreateWindowEx联系上)
wcex.hIconSm = LoadIcon(NULL, IDI_APPLICATION);
return RegisterClassEx(&wcex); //注册窗口类。失败时返回0。
}

这一段代码不太好记。
写的时候一般依靠VS2017的速览定义,查看WNDCLASSEX的每一个变量。
通常我复制粘贴这一段代码O(∩_∩)O


创建、显示、刷新窗口

创建窗口之后,你得到了窗口句柄。
类型:HWND

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
//来自CSDN某博客
HWND CreateWindowEx(
DWORD dwExStyle, //窗口的扩展风格
LPCTSTR lpClassName, //已经注册的窗口类名称
LPCTSTR lpWindowName, //窗口标题栏的名字
DWORD dwStyle, //窗口的基本风格
int x, //窗口左上角水平坐标位置
int y, //窗口左上角垂直坐标位置
int nWidth, //窗口的宽度
int nHeight, //窗口的高度
HWND hWndParent, //窗口的父窗口句柄
HMENU hMenu, //窗口菜单句柄
HINSTANCE hInstance, //应用程序实例句柄
LPVOID lpParam //窗口创建时附加参数
); //创建成功返回窗口句柄

代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
const char TITLE[] = "Hello World";		//窗口名称
HWND hwnd = CreateWindowEx(
NULL,
CLASSNAME, //此处的CLASSNAME与之前注册窗口类时的CLASSNAME对应。
TITLE, //窗口标题栏名字
WS_OVERLAPPEDWINDOW | WS_VISIBLE, //窗口风格:层叠式可见窗口
CW_USEDEFAULT,
CW_USEDEFAULT, //左上角位置为默认值
640,
480, //窗口大小为640*480
NULL,
NULL,
hInstance, //当前的实例句柄
NULL
); //窗口句柄诞生了!
ShowWindow(hwnd, nCmdshow); //以nCmdshow的形式显示窗口
UpdateWindow(hwnd); //刷新窗口


消息循环

消息循环有3个重要的函数:

BOOL PeekMessage(LPMSG lpMsg, HWND hWnd, UINT wMsgFilterMin, UINT wMsgFilterMax, UINT wRemoveMsg);
PeekMesssge只得到那些与参数hWnd标识的窗口相联系的消息或被lsChild确定为其子窗口相联系的消息,
并且该消息要在由参数wMsgFilterMinwMsgFilherMax确定的范围内。
如果hWnd为NULL,则PeekMessage接收属于当前调用线程的窗口的消息(PeekMessage不接收属于其他线程的窗口的消息)。
如果wMsgFilterMinwMsgFilterMax都为零,PeekMessage返回所有可得的消息(即,无范围过滤)。
当系统无消息时,返回FALSE,继续执行后续代码
如果wRemoveMsg的值是PM_REMOVE,那么取出的消息不会放回消息队列中。

BOOL TranslateMessage(CONST MSG *lpMsg);
该函数将虚拟键消息转换为字符消息。字符消息被寄送到调用线程的消息队列里,
当下一次线程调用函数GetMessage或PeekMessage时字符消息被取出。

LONG DispatchMessage(CONST MSG *lpMsg)
消息传递给操作系统,然后操作系统去调用我们的回调函数
(以上文字来自百度百科)

上一次的笔记给出的伪代码写了消息循环的过程。
以下为详细代码:

1
2
3
4
5
6
7
8
9
while (1) {
MSG msg = { 0 }; //清空
if (PeekMessage(&msg, 0, 0, 0, PM_REMOVE)) { //取出消息
TranslateMessage(&msg);
DispatchMessage(&msg);
}
if (msg.message == WM_QUIT)break; //如果消息是Quit,退出消息循环
Sleep(1); //延迟函数,使CPU占用率降低
}


回调函数WndProc

LRESULT CALLBACK WndProc(HWND hwnd, UINT umsg, WPARAM wparam, LPARAM lparam);
CALLBACK:即回调函数,等价于__stdcall,
WndProc有4个参数。
hwnd:当前窗口句柄
umsg:消息ID
wparam,lparam:消息的参数(附加在消息上的数据)

其中umsg的值可以是
WM_PAINT、WM_SIZE、WM_DESTROY、WM_KEYDOWN等等(所有WM开头的)

当值为WM_DESTROY,意味着窗口被摧毁,但进程还存在。
所以要进行PostQuitMessage(0);发送Quit消息。
当接收到WM_QUIT消息后,消息队列就不再有消息了。

当值为WM_PAINT时,表示窗口需要重新绘制了。
这个消息的触发条件是:程序启动时,用鼠标调整窗口的大小,从最小化还原时,最大化时,使用InvalidateRect函数时,等等。

如果要绘制,需要先BeginPaint(hwnd,&paintstruct)
即为指定窗口hwnd进行绘图工作的准备,并用将和绘图有关的信息填充到一个叫PAINTSTRUCT的结构中
BeginPaint的返回值是:
(HDC)指定窗口的“显示设备描述表”句柄。
这个句柄在绘图时需要用到。
如果我们要输出”Hello World”,一个简单的画字函数:
TextOut(HDC,x,y,text,strlen(text))就能绘制text这个字符串。
HDC是之前BeginPaint返回的句柄,x,y是绘制的字符串的左上角坐标。
EndPaint(hwnd,&paintstruct)是结束绘图。

WndProc最后要return DefWindowProc(hwnd, umsg, wparam, lparam)
表示将消息交给系统处理。
WndProc代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
LRESULT CALLBACK WndProc(HWND hwnd, UINT umsg, WPARAM wparam, LPARAM lparam) {
switch (umsg) {
case WM_PAINT: { //需要进行绘图的消息
PAINTSTRUCT pt;
HDC hdc = BeginPaint(hwnd, &pt); //准备
char text[] = "Hello World"; //字符串
TextOut(hdc, 275, 200, text, strlen(text)); //在显示设备hdc上画字符串
EndPaint(hwnd, &pt); //结束
break;
}
case WM_DESTROY: PostQuitMessage(0); break; //发送Quit消息
}
return DefWindowProc(hwnd, umsg, wparam, lparam); //把消息交给系统处理
}


那么我们的HelloWorld程序就完成了!

运行结果:
截图
编程环境:VS2017
源代码下载:helloworld.zip

WINAPI手册下载:Win32 API.chm