1 MFC需要了解的相关概念
- 1) SDK和API
SDK: 软件开发工具包(Software Development Kit),一般都是一些被软件工程师用于为特定的软件包、软件框架、硬件平台、操作系统等建立应用软件的开发工具的集合,也称第三方工具包。
API函数: Windows操作系统提供给应用程序编程的接口(Application Programming Interface)。Windows应用程序API函数是通过C语言实现的,所有主要的 Windows 函数都在 Windows.h 头文件中进行了声明。Windows 操作系统提供了 1000 多种 API函数。Windows前的W不区分大小写。
- 2) 窗口和句柄
窗口是 Windows 应用程序中一个非常重要的元素,一个 Windows 应用程序至少要有一个窗口,称为主窗口。窗口是屏幕上的一块矩形区域,是 Windows 应用程序与用户进行交互的接口。利用窗口可以接收用户的输入、以及显示输出。一个应用程序窗口通常都包含标题栏、菜单栏、系统菜单、最小化框、最大化框、 可调边框,有的还有滚动条。如下图:
窗口可以分为客户区和非客户区, 如上图。 客户区是窗口的一部分, 应用程序通常在客户区中显示文字或者绘制图形。标题栏、 菜单栏、 系统菜单、 最小化框和最大化框、 可调边框统称为窗口的非客户区, 它们由 Windows 系统来管理, 而应用程序则主要管理客户区的外观及操作。
窗口可以有一个父窗口, 有父窗口的窗口称为子窗口。除了上图所示类型的窗口外, 对话框和消息框也是一种窗口。 在对话框上通常还包含许多子窗口, 这些子窗口的形式有按钮、 单选按钮、 复选框、 组框、 文本编辑框等。
在 Windows 应用程序中, 窗口是通过窗口句柄( HWND) 来标识的。 我们要对某个窗口进行操作, 首先就要得到这个窗口的句柄。
句柄( HANDLE) 是 Windows 程序中一个重要的概念, 使用也非常频繁。 在 Windows 程序中, 有各种各样的资源( 窗口、 图标、光标,画刷等), 系统在创建这些资源时会为它们分配内存, 并返回标识这些资源的标识号, 即句柄。 在后面的内容中我们还会看到图标句柄( HICON)、 光标句柄( HCURSOR) 和画刷句柄( HBRUSH)。或者学过linux的都知道,句柄等同于文件描述符的作用。
- 3)消息与消息队列
Windows 程序设计是一种完全不同于传统的 DOS 方式的程序设计方法。它是一种事件驱动方式的程序设计模式,主要是基于消息的。每一个 Windows 应用程序开始执行后, 系统都会为该程序创建一个消息队列, 这个消息队列用来存放该程序创建的窗口的消息。
例如,当用户在窗口中画图的时候,按下鼠标左键,此时,操作系统会感知到这一事件(注意,并非是应用程序先捕获,而是操作系统先捕获,然后添加到消息队列再给回应用程序),于是将这个事件包装成一个消息,投递到应用程序的消息队列中,等待应用程序的处理。
然后应用程序通过一个消息循环不断地从消息队列中取出消息,并进行响应。
在这个处理过程中,操作系统也会给应用程序“ 发送消息”。所谓“ 发送消息”,实际上是操作系统调用程序中一个专门负责处理消息的函数,这个函数称为窗口过程,即窗口过程就是回调函数。
再次强调,按下应用程序的按钮后,消息首先被操作系统捕获,而不是应用程序本身先捕获。
- 4)WinMain函数
当Windows操作系统启动一个程序时,它调用的就是该程序的WinMain函数( 实际是由插入到可执行文件中的启动代码调用的)。WinMain是Windows程序的入口点函数,与DOS程序的入口点函数main的作用相同,当WinMain 函数结束或返回时,Windows应用程序结束,即与DOS的main函数返回一样代表进程结束。
2 Windows 编程模型
一个完整的Win32程序(#include <windows.h>),该程序实现的功能是创建一个窗口,并在该窗口中响应键盘及鼠标消息,程序的实现步骤为:
//1、设计窗口
//2、注册窗口
//3、创建窗口
//4、显示和更新
//5、通过循环取消息
//6、处理消息 (窗口过程)
本人软件为VS2017。
- 1) 项目的创建。VS2015是没有桌面桌面应用程序,需要选择Win32项目。
- 2)然后将.cpp改成.c后缀,否则会出现类型转换失败,因为C++类型检查严谨,不想浪费时间的就直接改后缀。
- 3)然后将自动创建的文件里面的代码全部删除,将下面代码复制即可。
3 案例
代码注释已经非常详细,并且对照上面的消息循环图理解即可。本篇案例只是为了让大家了解MFC的底层消息处理,平时已经很少这样写代码,一般都是直接使用Qt或者MFC开发了。
#include <windows.h> //底层实现窗口 的头文件
//6处理窗口过程
//CALLBACK 代表__stdcall 参数的传递顺序:从右到左 以此入栈,并且在函数返回前 清空堆栈
LRESULT CALLBACK WindowProc(
HWND hwnd, //消息所属的窗口句柄
UINT uMsg, //具体消息名称 WM_XXXX 消息名
WPARAM wParam, //键盘附加消息
LPARAM lParam //鼠标附加消息
)
{
switch (uMsg)
{
case WM_CLOSE:
//所有xxxWindow为结尾的方法,都不会进入到消息队列中,而是直接执行。即底层流程图直接走窗口过程。
DestroyWindow(hwnd); //DestroyWindow 发送另一个消息 WM_DESTROY
break;
case WM_DESTROY:
PostQuitMessage(0);/* 使下面的while循环变成FALSE退出应用程序 */
break;
case WM_LBUTTONDOWN: //鼠标左键按下
{
int xPos = LOWORD(lParam);
int yPos = HIWORD(lParam);
char buf[1024];
wsprintf(buf, TEXT("x = %d,y = %d"), xPos, yPos);
MessageBox(hwnd, buf, TEXT("鼠标左键按下"), MB_OK);
break;
}
case WM_KEYDOWN: //键盘
MessageBox(hwnd, TEXT("键盘按下"), TEXT("键盘按下"), MB_OK);
break;
case WM_PAINT: //绘图
{
PAINTSTRUCT ps; //绘图结构体
HDC hdc = BeginPaint(hwnd, &ps);
TextOut(hdc, 100, 100, TEXT("HELLO"), strlen("HELLO"));
EndPaint(hwnd, &ps);
}
break;
}
//返回值用默认处理方式
return DefWindowProc(hwnd, uMsg, wParam, lParam);
}
//程序入口函数
//WINAPI 代表__stdcall 参数的传递顺序:从右到左 以此入栈,并且在函数返回前 清空堆栈
int WINAPI WinMain(
HINSTANCE hInstance, //应用程序实例句柄
HINSTANCE hPrevInstance, //上一个应用程序句柄,在win32环境下,参数一般为NULL,不起作用了
LPSTR lpCmdLine, //是一个以空终止的字符串, 指定传递给应用程序的命令行参数,相当于C或C++中的main函数中的参数char *argv[]
int nShowCmd) //表示一个窗口的显示,表示它是要最大化显示、最小化显示、正常大小显示还是隐藏显示。
{
//1、设计窗口
//2、注册窗口
//3、创建窗口
//4、显示和更新
//5、通过循环取消息
//6、处理消息 (窗口过程)
//1、设计窗口
WNDCLASS wc;
wc.cbClsExtra = 0; //类的额外的内存
wc.cbWndExtra = 0; //窗口额外的内存
wc.hbrBackground = (HBRUSH)GetStockObject(WHITE_BRUSH); //设置背景
wc.hCursor = LoadCursor(NULL, IDC_HAND); //设置光标,如果第一个参数为NULL,代表使用系统提供的光标
wc.hIcon = LoadIcon(NULL, IDI_ERROR); //图标,如果第一个参数为NULL,代表使用系统提供的光标
wc.hInstance = hInstance; //应用程序实例句柄 传入WinMain中的形参即可
wc.lpfnWndProc = WindowProc; //窗口过程即回调函数
wc.lpszClassName = TEXT("WIN"); //指定窗口类名称
wc.lpszMenuName = NULL; //菜单名称
wc.style = 0; //显示风格 0代表默认风格
//2、注册窗口类
RegisterClass(&wc);
//3、创建窗口
/*
lpClassName, 类名
lpWindowName, 标题名
dwStyle, 风格(宏),这里使用常用的WS_OVERLAPPEDWINDOW
x, 显示坐标,CW_USEDEFAULT,默认值
y,
nWidth, 宽高
nHeight,
hWndParent, 父窗口,传NULL即可
hMenu, 菜单,NULL
hInstance, 实例句柄,hInstance
lpParam) 附加值,NULL
*/
HWND hwnd = CreateWindow(
wc.lpszClassName,
TEXT("WINDOWS"),
WS_OVERLAPPEDWINDOW,
CW_USEDEFAULT,
CW_USEDEFAULT,
CW_USEDEFAULT,
CW_USEDEFAULT,
NULL,
NULL,
hInstance,
NULL);
//4、 显示和更新
ShowWindow(hwnd, SW_SHOWNORMAL);
UpdateWindow(hwnd);
//5、 通过循环取消息
/*
HWND hwnd; 主窗口句柄
UINT message; 具体消息名称,int值类型
WPARAM wParam; 附加消息,键盘消息
LPARAM lParam; 附加消息,鼠标消息的左中右键
DWORD time; 消息产生时间
POINT pt; 附加消息,鼠标消息按下的点x,y
*/
MSG msg;
/******************下面过程是等待用户触发消息事件,操作系统捕获后进入while然后消息循环分发,可对比底层流程图理解*************************/
while (1)
{
/*
_Out_ LPMSG lpMsg, 消息
_In_opt_ HWND hWnd, 捕获窗口 填NULL代表捕获所有的窗口
_In_ UINT wMsgFilterMin, //最小和最大的过滤的消息 一般填入0
_In_ UINT wMsgFilterMax) //填0代表捕获所有消息
*/
if (GetMessage(&msg, NULL, 0, 0) == FALSE)
{
break;
}
//翻译消息,是为了对用户的组合键等复杂操作处理后再分发消息
TranslateMessage(&msg);
//不为false则分发消息
DispatchMessage(&msg);
}
return 0;
}
结果:
按下左键:
注意:有个别人可能按照下面平时创建项目的方法创建了控制台应用程序的。不想重新创建项目的按照下面解决:
这里因为我们是想要理解MFC的底层实现,由于MFC的底层实现是C语言,所以创建.c文件。
控制台程序转Windows程序解决方法: