欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

Windows应用程序(C/C++)(1)消息机制

程序员文章站 2022-03-05 10:21:17
...

Windows消息机制

1.1 基本概念

API: Application Programming Interface, 应用程序编程接口。编写Windows应用程序本质上就是调用Windows提供的大量API函数,这些API函数大多数都在头文件Windows.h中声明。

SDK: Software Development Kit, 软件开发包。指的是由厂商提供的软件开发包,内含API函数库,帮助文档,示例程序,开发工具等等。

以上两个概念都是很宽泛的,没有特指某个特定的设备,语言或者是厂商。只要是在已经准备好的接口之上进行软软件开发,我们都可以把这个编程接口成为API, 把实现该借口的库,使用文档,开发工具统称为SDK。

窗口:Windows平台下应用程序和用户进行交互的图形接口,他表现为屏幕上的矩形区域。一个典型的窗口包含客户区非客户区两大块,客户区域通常用来显示文字和图形,是窗口的主题;非客户区域则包含菜单栏,标题栏,系统菜单,最大最小化框等等。除了典型的窗口外,应用程序弹出的对话框和消息框也属于窗口,Window桌面其实也是由系统创建的窗口。

句柄: 资源的标识称为句柄,常见的资源包括窗口,图标,光标等等,他们在创建的时候都会被分配内存,而句柄就是这段内存的标识,可以理解成是指针的一种抽象。需要记住的几个句柄类型:HWND(窗口句柄),HICON(图标句柄),HBRUSH(画刷句柄),HCURSOR(光标句柄)。

1.2 Windows API中常见宏定义

宏名 实际类型
CHAR: char
WCHAR: wchar_t
LPARAM: long
WPARAM: unsigned int
LRESULT long
LPSTR: char *
LPWSTR: wchar_t *
LPCSTR: const char *
LPCWSTR: cong wchar_t *
WORD: unsigned short
DWORD: unsigned long
TCHAR: 如果定义UNICODE则为WCHAR,否则是CHAR
LPTSTR: 如果定义UNICODE则为LPWSTR,否则是LPSTR
LPCTSTR: 如果定义UNICODE则为LPCWSTR,否则是LPCSTR
__TEXT(): 根据UNICODE决定是否转换为宽字符
TEXT(): __TEXT()

一些跟字符有关的函数也通过宏的方法封装了宽字符和ASCII两种版本:

宏名 实际类型
wsprintf sprintfA或者sprintfW
MessageBox MessageBoxA或者MessageBoxW
_tmain main或者wmain
_tWinMain WinMain或者wWinMain

其中**_tXXX系列宏tchar.h**中定义,很多函数都由这种版本

1.3 消息和消息队列

不同于控制台应用程序,Windows桌面应用程序采用事件驱动机制,换句话说程序的执行不再是简单的顺序执行(配合循环和分支结构),事件驱动机制下程序可以响应事件(event)来完成特定功能,而事件驱动的底层原理就是Windows的消息机制。用户的操作(鼠标点击,键盘按下等等)都会被操作系统感知并封装为消息(message)再投放到应用程序的消息队列中,而应用程序内部通过消息循环不断从消息队列中取出并**解析(translate)消息,之后应用程序会将消息再次分派(dispatch)**给操作系统并由操作系统调用窗口过程函数来对特定消息做出响应。

消息:Windows通过MSG结构体来封装消息

typedef struct tagMSG{
    HWND hwnd;//消息所属窗口的句柄
    UINT message;//消息常量(WM_XXX),实际上是一个整数标识
    WPARAM wParam;//附加信息
    LPARAM lParam;//附加信息
    DWORD time;//消息产生的事件
    POINT pt;//消息产生时光标位置
}MSG;

消息队列:一个由操作系统给程序维护的数据结构,该程序所创建的窗口的全部队列消息都会被操作系统塞进程序消息队列中

**队列消息和非队列消息:**队列消息是要进入消息队列的,非队列消息则直接通过窗口过程响应。

1.4 WinMain函数

windows console application的入口函数是main(),windows application的入口函数是WinMain(),二者类似都是被可执行文件中的启动代码调用。

int WINAPI WinMain(//WINAPI是 对__stdcall的宏定义,Win32 API均要求显示声明该调用约定,而标准C函数则为__cdecl(vc默认)
	HINSTANCE hInstance,//该程序当前实例的句柄
    HINSTANCE hPrevInstance,//该程序上一个实例的句柄,对于Win32 app而言该参数总为NULL,无意义
    LPSTR lpCmdLine,//命令行参数,调用程序时传入的参数会被系统存入这个参数中
    int nCmdShow//程序显示状态(窗口最大化、最小化、隐藏之类的),和应用程序无关,由调用方指定
) ;

创建窗口:大概分为四个步骤

  1. 创建窗口类
  2. 注册窗口类
  3. 创建窗口
  4. 显示更新窗口

创建窗口类:创建一个WNDCLASS结构体的实例

typedef struct _WNDCLASS{
    uint style;//窗口类样式,CS_XXX宏
    WNDCLASS lpfnWndProc;//窗口过程,typedef LRESULT (CALLBACK* WNDCLASS)(HWND, UINT, WPARAM, LPARAM);,因此WNDCLASS用来定义函数指针类型,lpfnWndProc是函数指针变量
    int cbClsExtra;//类附加信息,通常设置0
    int cbWndExtra;//窗口附加信息,通常设置0
    HANDLE hInstance;//实例句柄
    HICON hIcon;//图标句柄,使用默认图标则为NULL
    HCURSOR hCursor;//光标句柄
    HBRUSH hBrush;//背景画刷句柄
    LPCTSTR lpszMenuName;//菜单资源名
    LPCTSTR lpszClassName;//窗口类名
}WNDCLASS;

HICON LoadIcon(HINSTANCE hInstance, LPCTSTR lpIconName);//加载图标,加载系统标准图标时第一个参数为NULL
HCURSOR LoadCursor(HINSTANCE hInstance, LPCTSTR lpCursorName);//加载光标,加载系统标准光标时第一个参数为NULL
HGDIOBJ GetStockObject(int fnObject);//获取GDI资源的句柄(画刷,画笔,字体,调色板等),其返回值要强制转换为HBRUSH类型才能给背景画刷句柄字段赋值


窗口过程是一个回调函数(CALLBACK实际上也是对__stdcall的宏定义),该函数不由应用程序调用,而是在响应特定消息的时候由操作系统调用,窗口类要保存窗口过程指针的目的也是为了让操作系统能够找到回调函数。

LoadIcon函数用于加载图标,第一个参数的意义已经介绍过了,重点是第二个参数,他是字符常量的指针,而VC中资源标识符是一个整数(IDX_XXX宏),需要用MAKEINTRESOURCE宏把ID转换到CONST CHAR*类型。

LoadCursor函数同上类似,只不过加载的是指针资源。

GetStockObject函数会返回要求的画笔,画刷,字体,或者调色板的句柄,其参数形式很多,常用的如BLACK_BRUSH

注册窗口类

ATOM RegisterClass(CONST WNDCLASS* lpWndClass);

该函数接受之前创建的窗口结构体为参数,向操作系统注册该结构体描述的窗口类,操作系统依据这个窗口类创建窗口,自然操作系统也就知道该类窗口对应的窗口过程是什么,基于同一个窗口类创建的全部窗口公用一个窗口过程函数。

创建窗口

HWND CreateWindow(
	LPCTSTR lpClassName,//窗口类名
    LPCTSTR lpWindowName,//窗口名
    DWORD dwStyle,//窗口样式,WS_XXX宏,最常用的是WS_OVERLAPPEDWINDOW,这个样式由很多基本样式组合(或运算)得来,有标题栏,最大最小化按钮,可调边框。
    int x,//左上角,设置为CW_USEDEFAULT则会使用默认左上角坐标,并且忽略y
    int y,//左上角
    int nWidth,//宽度,设置为CW_USEDEFAULT会使用默认宽高,并且忽略nHeight
    int nHeight,//高度
    HWND hWndParent,//父窗口句柄
    HMENU hMenu,//菜单句柄,菜单不是窗口
    HANDLE hInstance,//实例句柄
    LPVOID lpParam//数据指针,指向WM_CREATE消息的lParam字段,一般设置NULL
);

显示窗口

BOOL ShowWindow(
    HWND hwnd, 
    int ncmdShow//显示样式,SW_XXX宏
);

更新窗口

BOOL UpdateWindow(HWND hwnd);//该函数给窗口发送WM_PAINT消息,该消息是非队列消息

该函数用于更新窗口的客户区(client area)。当窗口的客户区非空时,该函数向指定窗口的窗口过程发送WM_PAINT消息,否则不发送任何消息。需要注意的是WM_PATIN消息会跳过消息队列。

消息循环

消息循环的作用前面已经介绍过了,这里看看消息循环的固定实现方法:

//先看看消息循环中用到的几个重要的API
/*
 *该函数用于从调用线程的消息队列中取回消息,该函数会等待消息队列中有可供取回的消息再返回。
 *倘若取回的消息不是WM_QUIT,该函数返回非0值,否则返回0
 *当函数发生错误时返回值是-1,比如lpMsg指针非法或者hwnd句柄无效,调用GetLastError()查看函数内错误信息
 *如果第二个参数是hwnd(当前窗口句柄),运行时一切正常,但退出时会出错(消息循环永远无法退出),因为关闭窗口以后GetMessage会因为句柄无效而永远返回-1(返回0才quit)
 *解决方法就是第二个参数写0,此时GetMessage不仅会接受窗口消息还会接受线程消息,线程消息就是由PostThreadMessage()发送的消息
 *WM_QUIT就是由PostQuitMessage()发送的线程消息,所以即便窗口退出了该消息还是可以接收到
 *如果第二个参数非0那就必然出现在关闭窗口后返回-1的情况,此时应该退出主程序
 */
BOOL GetMessage(
  LPMSG lpMsg,
  HWND  hWnd,//为0时接受全部窗口的消息和线程消息
  UINT  wMsgFilterMin,
  UINT  wMsgFilterMax
);
//与之类似的另一个函数PeekMessage()同样从消息队列取消息,但是该函数不会等待消息被送入队列

/*
 *把虚拟键消息转译为字符消息再重新投入线程消息队列
 */
BOOL TranslateMessage(
  const MSG *lpMsg
);

/*
 *发送消息到窗口过程,通常都是通过GetMessage得到的消息
 */
LRESULT DispatchMessage(
  const MSG *lpMsg
);

while((bRet = GetMessage(&msg, hwnd, 0, 0)) != 0){
    if(bRet == -1){
        //可能是WM_DESTROY导致窗口销毁,程序应该退出
        return -1;
    }
    TranslateMessage(&msg);
    DispatchMessage(&msg);
}

窗口过程函数

窗口过程函数用于处理发送给窗口的消息,该函数声明如下:

LRESULT CALLBACK WindowProc( //自己创建的窗口过程原型与此相同即可,函数名称随意,另窗口类中的函数指针指向这个函数即可
    HWND hwnd,
    UINT uMsg,
    WPARAM wParam,
    LPARAM lParam
)

窗口过程的一般形式:

LRESULT CALLBACK WindowProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam){
    switch(){
        case WM_CHAR:
        case WM_LBUTTONDOWN:
        case WM_PAINT:
        case WM_CLOSE:
        case WM_DESTROY:
        default:
            return DefWindowProc(hwnd, uMsg, wParam, lParam);//如果没有匹配的消息就调用默认窗口过程处理,这一项是必须的
    }
}

1.5 第一个Windows应用程序

#include<Windows.h>
#include<stdio.h>

LRESULT CALLBACK WinMainProc(
	HWND hwnd, 
	UINT uMsg, 
	WPARAM wParam, 
	LPARAM lParam
);

int WINAPI WinMain(
	HINSTANCE hInstance,
	HINSTANCE hPrevInstance,
	LPSTR lpCmdLine,
	int nCmdShow
) 
{
	WNDCLASS wndcls;
	wndcls.cbClsExtra = 0;
	wndcls.cbWndExtra = 0;
	wndcls.hbrBackground = (HBRUSH)GetStockObject(BLACK_BRUSH);
	wndcls.hCursor = LoadCursor(NULL,IDC_CROSS);
	wndcls.hIcon = LoadIcon(NULL,IDC_APPSTARTING);
	wndcls.lpfnWndProc = WinMainProc;
	wndcls.hInstance = hInstance;
	wndcls.lpszClassName = L"WinMain";//L前缀表示宽字符
	wndcls.lpszMenuName = NULL;
	wndcls.style = CS_HREDRAW | CS_VREDRAW;

	RegisterClass(&wndcls);
	HWND hwnd;
	hwnd = CreateWindow(L"WinMain", L"Windows Application", 
		WS_OVERLAPPEDWINDOW, CW_USEDEFAULT, CW_USEDEFAULT,
	CW_USEDEFAULT, CW_USEDEFAULT, NULL, NULL, hInstance, NULL);
	ShowWindow(hwnd, SW_SHOWNORMAL);
	UpdateWindow(hwnd);//发送WM_PAINT消息
	MSG msg;
	BOOL bRet;
    
	while ((bRet=GetMessage(&msg,hwnd,0,0)) != 0) {
        //如果窗口句柄不是NULL就要判断返回值-1的情形
		if (bRet == -1) {
			return -1;
		}
		TranslateMessage(&msg);
		DispatchMessage(&msg);
	}
	return msg.wParam;//最后一次取出来的消息必然是WM_QUIT
}

LRESULT CALLBACK WinMainProc(
	HWND hwnd,
	UINT uMsg,
	WPARAM wParam,
	LPARAM lParam
) 
{
	switch (uMsg) {
		case WM_CHAR:
			wchar_t szChar[20];
			wsprintf(szChar, L"char code is %d", wParam);
			MessageBox(hwnd, szChar, L"char", 0);
			break;
		case WM_LBUTTONDOWN:
			MessageBox(hwnd, L"mouse clicked", L"message", 0);
			HDC hdc;
			hdc = GetDC(hwnd);//获取设备上下文
			TextOut(hdc, 0, 50, L"Hello,World!",lstrlen(L"Hello,World!"));
			ReleaseDC(hwnd, hdc);
			break;
        //客户区全部或者部分无效后系统发送WM_PAINT,此外UpdateWindow也会发送该消息
        //不在WM_PAINT中进行的绘图在窗口重绘后就消失了
		case WM_PAINT:
			HDC hDC;
            //该结构体用于存放绘制信息,只有WM_PAINT才会携带绘制信息,所以BeginPaint只能响应WM_PAINT
			PAINTSTRUCT ps;
			hDC = BeginPaint(hwnd, &ps);//BeginPaint只能用于响应WM_PAINT消息
			TextOut(hDC, 0, 0, L"http://cmiao.me", lstrlen(L"http://cmiao.me"));
			EndPaint(hwnd, &ps);
			break;

		case WM_CLOSE:
			if (IDYES == MessageBox(hwnd, L"是否真的结束?",L"message", MB_YESNO)) {
				DestroyWindow(hwnd);//DestroyWindow()在关闭窗口的同时产生WM_DESTROY消息,该消息不进入队列
			}
			break;
		
		case WM_DESTROY:
			PostQuitMessage(0);//该函数产生WM_QUIT(线程消息),如果GetMessage第二个参数为某个窗口将无法接受该消息
			break;
		default:
			return DefWindowProc(hwnd, uMsg, wParam, lParam);
			break;
	}
	return 0;
}

注意,调用BeginPaint时如果客户区的背景还没有擦除,则BeginPaint会给窗口发送WM_ERASEBKGND,系统会使用窗口类的画刷字段来擦除背景,该函数返回设备上下文

1.6 匈牙利命名法

遵循这种标识符命名约定有助于在开发中理解记忆变量类型和含义:

前缀 含义
a 数组
b 布尔
c 字符
p 指针
fn 函数
dw 无符号长整型
h 句柄
i 整型
l 长整型
lp 长指针
s 字符串
sz 以零结尾的字符串
w 无符号整型
x,y 无符号整型(坐标)
cb 字节数
相关标签: Windows API