MFC——基础框架内容

简介: MFC——基础框架内容

文章目录

MFC入门

Windows消息机制

基本概念

Windows编程模型

创建一个win32程序,实现创建一个窗口并响应部分事件消息。

窗口类具体说明

消息循环

完整代码示例


MFC入门

微软基础类库(英语:Microsoft Foundation Classes,简称MFC)是微软公司提供的一个类库(class libraries),以C++类的形式封装了Windows API,并且包含一个应用程序框架,以减少应用程序开发人员的工作量。其中包含大量Windows句柄封装类和很多Windows的内建控件和组件的封装类。

MFC(微软基础类库)是对于Windows平台下做GUI开发的一个很好的选择。


Windows消息机制

要想熟练掌握 Windows 应用程序的开发, 首先需要理解 Windows 平台下程序运行的内部机制。如果想要更好的学习掌握 MFC,必须要先了解Windows 程序的内部运行机制。


基本概念

在Windows平台下,也有类似标准库下的函数可供调用:不同的是,这些函数是由Windows操作系统本身提供的(windows API)。


SDK 和 API


  • SDK:软件开发工具包(Software Development Kit),一般都是一些被软件工程师用于为特定的软件包、软件框架、硬件平台、操作系统等建立应用软件的开发工具的集合。
  • API函数: Windows操作系统提供给应用程序编程的接口(Application Programming Interface)。Windows应用程序API函数是通过C语言实现的,所有主要的 Windows 函数都在 Windows.h 头文件中进行了声明。Windows 操作系统提供了 1000 多种 API函数。

窗口和句柄


  • 窗口:窗口是 Windows 应用程序中一个非常重要的元素,一个 Windows 应用程序至少要有一个窗口,称为主窗口。

窗口是屏幕上的一块矩形区域,是 Windows 应用程序与用户进行交互的接口。利用窗口可以接收用户的输入、以及显示输出。

一个应用程序窗口通常都包含标题栏、菜单栏、系统菜单、最小化框、最大化框、 可调边框,有的还有滚动条。

窗口可以分为客户区和非客户区, 如下图。 客户区是窗口的一部分, 应用程序通常在客户区中显示文字或者绘制图形。

标题栏、 菜单栏、 系统菜单、 最小化框和最大化框、 可调边框统称为窗口的非客户区, 它们由 Windows 系统来管理, 而应用程序则主要管理客户区的外观及操作。

窗口可以有一个父窗口, 有父窗口的窗口称为子窗口。除了上图所示类型的窗口外, 对话框和消息框也是一种窗口。 在对话框上通常还包含许多子窗口, 这些子窗口的形式有按钮、 单选按钮、 复选框、 组框、 文本编辑框等。

在 Windows 应用程序中, 窗口是通过窗口句柄( HWND) 来标识的。 我们要对某个窗口进行操作, 首先就要得到这个窗口的句柄。

20200405113327279.png

  • 句柄(HANDLE):是 Windows 程序中一个重要的概念, 使用也非常频繁。 在 Windows 程序中, 有各种各样的资源( 窗口、 图标、光标,画刷等), 系统在创建这些资源时会为它们分配内存, 并返回标识这些资源的标识号, 即句柄。 在后面的内容中我们还会看到图标句柄( HICON)、 光标句柄( HCURSOR) 和画刷句柄( HBRUSH)。

消息与消息队列

Windows 程序设计是一种完全不同于传统的 DOS 方式的程序设计方法。它是一种事件驱动方式的程序设计模式,主要是基于消息的。

每一个 Windows 应用程序开始执行后, 系统都会为该程序创建一个消息队列, 这个消息队列用来存放该程序创建的窗口的消息。

例如,当用户在窗口中画图的时候,按下鼠标左键,此时,操作系统会感知到这一事件,于是将这个事件包装成一个消息,投递到应用程序的消息队列中,等待应用程序的处理。然后应用程序通过一个消息循环不断地从消息队列中取出消息,并进行响应。

在这个处理过程中,操作系统也会给应用程序“ 发送消息”。所谓“ 发送消息”,实际上是操作系统调用程序中一个专门负责处理消息的函数,这个函数称为窗口过程。

2020040511363663.png

WinMain函数

当Windows操作系统启动一个程序时,它调用的就是该程序的WinMain函数( 实际是由插入到可执行文件中的启动代码调用的)。 WinMain是Windows程序的入口点函数,与DOS程序的入口点函数main的作用相同,当WinMain 函数结束或返回时,Windows应用程序结束。


Windows编程模型

一个完整的Win32程序(#include <windows.h>),该程序实现的功能是创建一个窗口,并在该窗口中响应键盘及鼠标消息,程序的实现步骤为:


  1. WinMain函数的定义
  2. 创建一个窗口
  3. 进行消息循环
  4. 编写窗口过程函数

创建一个win32程序,实现创建一个窗口并响应部分事件消息。

创建项目

在visual studio下创建一个空白的win32应用程序项目。


  • VS2010版本如下:

20200405114140380.png

20200405114146582.png


20200405114150798.png


20200405114155675.png

20200405114159928.png

  • VS2019版本如下:

20200405114347409.png20200405114434364.png

WinMain函数的定义

int WINAPI WinMain(
  HINSTANCE hInstance,  //应用程序实例
  HINSTANCE hPrevInstance,  //上一个应用程序实例
  LPSTR lpCmdLine,  //命令行参数
  int nShowCmd);  //窗口显示的样式


  • WINAPI:是一个宏,它代表的是__stdcall(注意是两个下划线),表示的是参数传递的顺序:从右往左入栈,同时在函数返回前自动清空堆栈。
  • hInstance:表示该程序当前运行的实例的句柄,这是一个数值。当程序在Windows下运行时,它唯一标识运行中的实例(注意,只有运行中的程序实例, 才有实例句柄)。一个应用程序可以运行多个实例,每运行一个实例,系统都会给该实例分配一个句柄值,并通过hInstance参数传递给 WinMain 函数。
  • hPrevInstance:表示当前实例的前一个实例的句柄。在Win32环境下,这个参数总是NULL,即在Win32环境下,这个参数不再起作用。
  • lpCmdLine:是一个以空终止的字符串, 指定传递给应用程序的命令行参数,相当于C或C++中的main函数中的参数char *argv[]。
  • nShowCmd:表示一个窗口的显示,表示它是要最大化显示、最小化显示、正常大小显示还是隐藏显示。

创建一个窗口

创建一个完整的窗口,需要经过下面几个步骤:


  • 设计一个窗口类

声明一个窗口对象WNDCLASS类实例化,并初始化其中的参数。

typedef struct _WNDCLASS{
  UINT        style;//窗口样式
  WNDPROC     lpfnWndProc;//窗口回调函数
  int         cbClsExtra;
  int         cbWndExtra;
  HINSTANCE   hInstance;
  HICON       hIcon;
  HCURSOR     hCursor;
  HBRUSH      hbrBackground;
  LPCWSTR     lpszMenuName;
  LPCWSTR     lpszClassName;
} WNDCLASS;


  • 注册窗口类

设计完窗口类(WNDCLASS)后, 需要调用RegisterClass函数对其进行注册,注册成功后,才可以创建该类型的窗口。

注册函数的原型声明如下:

ATOM RegisterClass(CONST WNDCLASS *lpWndClass);
//使用示例:RegisterClass(&wc);


  • 创建窗口

设计好窗口类并且将其成功注册之后, 即可用CreateWindow函数产生这种类型的窗口了。

//返回值说明:如果窗口创建成功,CreateWindow函数将返回系统为该窗口分配的句柄,否则,返回NULL。
HWND CreateWindow(
  LPCTSTR lpClassName,//指定窗口类的名称,此名字必须和WNDCLASS的lpszClassName成员指定的名称一样。
  LPCTSTR lpWindowName,//指定窗口的名字,即窗口的标题。
  DWORD dwStyle,//指定创建的窗口的样式,常指定为指WS_OVERLAPPEDWINDOW类型,这是一种多种窗口类型的组合类型。
  int x,int y,//指定窗口左上角的x,y坐标。如果参数x被设为CW_USEDEFAULT,那么系统为窗口选择默认的左上角坐标并忽略y参数。
  int nWidth,int nHeight,//指定窗口窗口的宽度,高度。如果参数nWidth被设为 CW_USEDEFAULT,那么系统为窗口选择默认的宽度和高度,参数nHeight被忽略。
  HWND hWndParent,//指定被创建窗口的父窗口句柄,没有父窗口,则设置NULL。
  HMENU hMenu,//指定窗口菜单的句柄,没有,则设置为NULL。
  HINSTANCE hInstance,//窗口所属的应用程序实例的句柄,用WinMain中的形参hInstance为其赋值。
  LPVOID lpParam);//作为WM_CREATE消息的附加参数lParam传入的数据指针。通常设置为NULL。


  • 显示和更新窗口

显示窗口函数原型:

BOOL ShowWindow(HWND hWnd, int nCmdShow);


更新窗口函数原型:


BOOL UpdateWindow(HWND hWnd);
//示例
ShowWindow(hWnd, SW_SHOWNORMAL); //SW_SHOWNORMAL为普通模式
UpdateWindow(hWnd);


窗口类具体说明

typedef struct _WNDCLASS{
  UINT        style;//窗口样式
  WNDPROC     lpfnWndProc;//窗口回调函数
  int         cbClsExtra;
  int         cbWndExtra;
  HINSTANCE   hInstance;
  HICON       hIcon;
  HCURSOR     hCursor;
  HBRUSH      hbrBackground;
  LPCWSTR     lpszMenuName;
  LPCWSTR     lpszClassName;
} WNDCLASS;


  • style:指定窗口的样式(风格),常用的样式如下

类型 含义

CS_HREDRAW 当窗口水平方向上的宽度发生变化时, 将重新绘制整个窗口。 当窗口发生重绘时, 窗口中的文字和图形将被擦除。如果没有指定这一样式,那么在水平方向上调整窗口宽度时,将不会重绘窗口。

CS_VREDRAW 当窗口垂直方向上的高度发生变化时,将重新绘制整个窗口。如果没有指定这一样式,那么在垂直方向上调整窗口高度时,将不会重绘窗口。

CS_NOCLOSE 禁用系统菜单的 Close 命令,这将导致窗口没有关闭按钮。

CS_DBLCLKS 当用户在窗口中双击鼠标时,向窗口过程发送鼠标双击消息。

  • lpfnWndProc:指定一个窗口回调函数,是一个函数的指针。

当应用程序收到给某一窗口的消息时,就应该调用某一函数来处理这条消息。这一调用过程不用应用程序自己来实施,而由操作系统来完成,但是,回调函数本身的代码必须由应用程序自己完成。对于一条消息,操作系统调用的是接受消息的窗口所属的类型中的lpfnWndProc成员指定的函数。每一种不同类型的窗口都有自己专用的回调函数,该函数就是通过lpfnWndProc成员指定的。

LRESULT CALLBACK WindowProc(
  HWND hWnd,  //信息所属的窗口句柄
  UINT uMsg,  //消息类型
  WPARAM wParam,  //附加信息(如键盘哪个键按下)
  LPARAM lParam //附加信息(如鼠标点击坐标)
  );


  • cbClsExtra:类的附加内存,通常数情况下为0。
  • cbWndExtra:窗口附加内存,通常情况下为0。
  • hInstance:当前实例句柄,用WinMain中的形参hInstance为其赋值。
  • hIcon:指定窗口类的图标句柄,设置为NULL,则使用默认图标,也可用如下函数进行赋值:
HICON LoadIcon(HINSTANCE hInstance, LPCTSTR lpIconName);
如:LoadIcon(NULL, IDI_WARNING); //第一个参数为NULL,加载系统默认图标


  • hCursor:指定窗口类的光标句柄,设置为NULL,则使用默认图标,也可用如下函数进行赋值:
HCURSOR LoadCursor(HINSTANCE hInstance, LPCTSTR lpCursorName);
如:LoadCursor(NULL, IDC_HELP); //第一个参数为NULL,加载系统默认光标


  • hbrBackground:指示窗口的背景颜色,可用如下函数进行赋值:
HGDIOBJ GetStockObject(int fnObject);
如:GetStockObject(WHITE_BRUSH);


  • lpszMenuName:指定菜单资源的名字。如果设置为NULL,那么基于这个窗口类创建的窗口将没有默认菜单。
  • lpszClassName:指定窗口类的名字。

示例代码


WNDCLASS wc;  //窗口类变量
  wc.cbClsExtra = 0;  //类附加内存
  wc.cbWndExtra = 0;  //窗口附加内存
  wc.hbrBackground = (HBRUSH)GetStockObject(WHITE_BRUSH); //背景色为白色
  wc.hCursor = (HCURSOR)LoadCursor(NULL, IDC_HELP); //帮助光标
  wc.hIcon = (HICON)LoadIcon(NULL, IDI_WARNING);  //警告图标
  wc.hInstance = hInstance; //应用程序实例,为WinMain第1个形参
  wc.lpfnWndProc = WinProc; //窗口过程函数名字
  wc.lpszClassName = TEXT("MyWin"); //类的名字
  wc.lpszMenuName = NULL; //没有菜单
  wc.style = 0; //类的风格,填0,使用默认风格
  //注册窗口类
  RegisterClass(&wc);
  //创建窗口
  HWND  hWnd = CreateWindow(TEXT("MyWin"), TEXT("测试"), WS_OVERLAPPEDWINDOW, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, NULL, NULL, hInstance, NULL);
  //显示及更新窗口
  ShowWindow(hWnd, SW_SHOWNORMAL);
  UpdateWindow(hWnd);


消息循环

在创建窗口、显示窗口、更新窗口后,我们需要编写一个消息循环,不断地从消息队列中取出消息,并进行响应。


消息结构体

在Windows程序中,消息是由MSG结构体来表示的。MSG结构体的定义如下:


typedef struct tagMSG {
  HWND hWnd;   
  UINT message;   
  WPARAM wParam;
  LPARAM lParam;   
  DWORD time;   
  POINT pt;
} MSG;


  • hWnd:消息所属的窗口。我们通常开发的程序都是窗口应用程序,一个消息一般都是与某个窗口相关联的。例如,在某个活动窗口中按下鼠标左键,产生的按键消息就是发给该窗口的。
  • message:消息的标识符,是由一个数值来表示的,不同的消息对应不同的数值。Windows将消息对应的数值定义为WM_XXX宏(WM是Windows Message的缩写)的形式, XXX对应某种消息的英文拼写的大写形式。例如,鼠标左键按下消息是WM_LBUTTONDOWN,键盘按下消息是WM_KEYDOWN,字符消息是 WM_CHAR……。
  • wParam: 指定消息的附加信息,如键盘按下会触发WM_KEYDOWN消息,但是,具体按下哪个按键需要wParam区分。
  • lParam:指定消息的附加信息,如鼠标左击会触发WM_LBUTTONDOWN消息,但是,具体点击的坐标需要lParam区分。
  • time:标识一个消息产生时的时间。
  • pt:表示产生这个消息时光标或鼠标的坐标。

取消息

要从消息队列中取出消息,我们需要调用GetMessage()函数,该函数的原型声明如下:


BOOL GetMessage(
  LPMSG lpMsg,
  HWND hWnd,
  UINT wMsgFilterMin,
  UINT wMsgFilterMax);


参数说明:


  • lpMsg:指向一个消息结构体(MSG),GetMessage从线程的消息队列中取出的消息信息将保存在该结构体变量中。
  • hWnd:指定接收属于哪一个窗口的消息。通常我们将其设置为NULL,用于接收属于调用线程的所有窗口的窗口消息。
  • wMsgFilterMin:指定消息的最小值。
  • wMsgFilterMax:指定消息的最大值。如果wMsgFilterMin和wMsgFilterMax都设置为0, 则接收所有消息。
  • 返回值说明:GetMessage函数接收到除 WM_QUIT 外的消息均返回非零值。对于WM_QUIT消息,该函数返回零。如果出现了错误,该函数返回-1,例如,当参数hWnd是无效的窗口句柄或lpMsg是无效的指针时。

建立消息循环


MSG msg;
  while (GetMessage(&msg, NULL, 0, 0))
  {
  TranslateMessage(&msg);
  DispatchMessage(&msg);
  }


  • TranslateMessage:用于翻译、处理和转换消息并把新消息投放到消息队列中,并且此过程不会影响原来的消息队列。
  • DispatechMessage:用于把收到的消息传到窗口回调函数进行分析和处理。即将消息传递给操作系统,让操作系统调用窗口回调函数,来对信息进行处理。

消息处理机制

20200405120846486.png


  • 操作系统接收到应用程序的窗口消息,将消息投递到该应用程序的消息队列中。
  • 应用程序在消息循环中调用GetMessage函数从消息队列中取出一条一条的消息。取出消息后,应用程序可以对消息进行一些预处理,例如,放弃对某些消息的响应,或者调用TranslateMessage产生新的消息。
  • 应用程序调用DispatchMessage,将消息回传给操作系统。消息是由 MSG结构体对象来表示的,其中就包含了接收消息的窗口的句柄。因此, DispatchMessage函数总能进行正确的传递。
  • 系统利用WNDCLASS结构体的lpfnWndProc成员保存的窗口过程函数的指针调用窗口过程,对消息进行处理。

窗口过程函数(消息处理函数)

在完成上述步骤后,剩下的工作就是编写一个窗口过程函数,用于处理发送给窗口的消息。

窗口过程函数的名字可以随便取, 如WinProc, 但函数定义的形式必须和下面声明的形式相同:


LRESULT CALLBACK WinProc( //CALLBACK 和WINAPI 作用一样
  HWND hWnd,  //信息所属的窗口句柄
  UINT uMsg,  //消息类型
  WPARAM wParam,  //附加信息(如键盘哪个键按下)
  LPARAM lParam //附加信息(如鼠标点击坐标)
  );


  • DefWindowProc函数:DefWindowProc函数调用默认的窗口过程,对应用程序没有处理的其他消息提供默认处理。
  • WM_CLOSE:对WM_CLOSE消息的响应并不是必须的,如果应用程序没有对该消息进行响应,系统将把这条消息传给DefWindowProc函数而 DefWindowProc函数则调用DestroyWindow函数来响应这条WM_CLOSE消息。
  • WM_DESTROY:DestroyWindow函数在销毁窗口后,会给窗口过程发送 WM_DESTROY消息,我们在该消息的响应代码中调用PostQuitMessage函数。
  • PostQuitMessage函数向应用程序的消息队列中投递一条 WM_QUIT消息并返回。WinMain函数中,GetMessage 函数只有在收到WM_QUIT消息时才返回0,此时消息循环才结束,程序退出。传递给 PostQuitMessage函数的参数值将作为WM_QUIT消息的wParam参数,这个值通常用做WinMain函数的返回值。

完整代码示例

#include <Windows.h>
//处理消息
LRESULT CALLBACK Windowproc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam)
{
    switch (uMsg)
    {
    case WM_CLOSE:
        DestroyWindow(hwnd);
        break;
    case WM_DESTROY:
        PostQuitMessage(0);
        break;
    case WM_LBUTTONDOWN://左键按下
    {
        int xPos, yPos;
        xPos = LOWORD(lParam);
        yPos = HIWORD(lParam);
        //打印操作
        TCHAR 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 world"), strlen("hello world"));
        EndPaint(hwnd, &ps);
    }
        break;
    default:
        break;
    }
    return DefWindowProc(hwnd, uMsg, wParam, lParam);
}
int APIENTRY wWinMain(_In_ HINSTANCE hInstance,
    _In_opt_ HINSTANCE hPrevInstance,
    _In_ LPWSTR    lpCmdLine,
    _In_ int       nCmdShow)
{
    //实现底层窗口步骤
    //1、设计窗口类
    //2、注册窗口类
    //3、创建窗口类
    //4、通过循环获取消息
    //5、处理消息(窗口过程)
    //设计窗口
    WNDCLASS wc;
    wc.cbClsExtra = 0;//额外内存为0
    wc.cbWndExtra = 0;//窗口额外内存
    wc.hbrBackground = (HBRUSH)GetStockObject(WHITE_BRUSH);//设置背景
    wc.hCursor = LoadCursor(NULL, IDC_HAND);//设置光标
    wc.hIcon = LoadIcon(NULL, IDI_WARNING);//设置鼠标
    wc.hInstance = hInstance;//当前实例句柄.
    wc.lpfnWndProc = Windowproc;//窗口过程回调函数.自定义
    wc.lpszClassName = TEXT("WINDOW");//指定窗口类名
    wc.lpszMenuName = NULL;//菜单名
    wc.style = 0;//默认风格
    //注册窗口类
    RegisterClass(&wc);
    //创建窗口类
    /*
    lpClassName, 类名
    lpWindowName, 窗口名
    dwStyle, 显示风格
    x, y, 窗口起始坐标
    nWidth,窗口宽度
    nHeight,高度
    hWndParent,父窗口, NULL
    hMenu, 菜单,NULL
    hInstance,实例句柄
    lpParam,其他参数
    */
    HWND hwnd = CreateWindow(wc.lpszClassName, TEXT("TEXT WINDOW"), WS_OVERLAPPEDWINDOW, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, NULL, NULL, hInstance, NULL);
    //显示和更新
    ShowWindow(hwnd, SW_SHOWNORMAL);
    UpdateWindow(hwnd);
    //通过循环取消息
    MSG msg;
    while (1)
    {
        if (GetMessage(&msg, NULL, 0, 0) == FALSE)
        {
            break;
        }
        //翻译消息
        TranslateMessage(&msg);
        //分发消息
        DispatchMessage(&msg);
    }
    return 0;
}


相关文章
|
前端开发 JavaScript 开发者
封装库/工具库中重要概念之UI框架
UI(User Interface)框架是前端开发中十分重要的一部分,它提供了各种组件和样式,用于构建页面和用户界面。在前端开发中,封装库/工具库可以帮助我们更加高效地使用 UI 框架。
147 0
|
程序员
MFC应用程序开发教程2——基于对话框编程
MFC应用程序开发教程2——基于对话框编程
122 1
MFC应用程序开发教程2——基于对话框编程
|
测试技术 C# 图形学
Unity C#代码封装dll文件完美教程(Chinar-开发者必经之路)
Unity C#代码封装dll文件完美教程(Chinar-开发者必经之路) 将脚本封装为DLL库,本文提供全流程,中文翻译。 助力快速完成 Unity C#脚本文件封装为 DLL 库文件 DLL (Dynamic Link Library) —— 中文:动态链接库
5200 0
|
容器
COM组件开发实践(一)
   Preface       因为项目需要,开始从事ActiveX方面的工作,看了一些资料,可惜都是些COM原理方面的,没有切合实际动手的东西,在CodeProject上读完David Marcionek的文章【1】后,收获良多,但也遇到一些恼人的小问题,因此在其基础上就一些易错点做些小注解。
1149 0