0xAA55 发表于 2023-4-21 17:06:42

【MFC】最精简 MFC 窗口程序

# 最简 MFC 窗口程序

通过 MSVC 的 MFC 模板向导生成的 MFC 工程让人看得眼花缭乱,给你生成了一堆“可能用得到”(很可能用不到)的垃圾代码,让人觉得不太好上手。

但其实 MFC 不需要那么复杂,就像纯 C 写 Win32 App 一样,一个单文件即可搞定。

需要注意的是 MFC 的代码风格是古早时期的“VC++”的风格,可以说是罪孽深重:
* 不使用命名空间
* 不使用智能指针(但是却半吊子使用 RAII 就很让人抓耳挠腮)
* 使用 114514 个宏,其中包括 `TRUE`、 `FALSE`、`NO_ERROR`、`WAIT_FAILED` (去和 khronos.org 打一架吧)
* MFC 相关的 C++ 头文件的后缀竟然是 `.h`。
* 混合使用驼峰式、匈牙利式、咆哮蛇式(全大写 + 下划线分词)命名风格
* 混合使用异常处理模型:
   - 调用函数,判断返回值是 `TRUE` 还是 `FALSE` 的模型
   例子代码:`if (!xxx()) goto FailExit;`
   - 调用函数,判断返回值是 `SUCCESS`(值为零) 还是 `FAIL`(值为各种各样的错误代码)的模型
   例子代码:`if ((err = xxx()) != 0) goto FailExit;`
   - 调用函数,这个函数设置一个全局的 last error,然后让你去判断 last error 的值的模型
   例子代码:`xxx(); if ((err = GetLastError()) != 0) goto FailExit;`
   - C++ 的 try throw catch 异常处理模型

非常不清真,五味杂陈,污染严重。各位千万注意自己写 C++ 的时候一定不要这样搞(因此也请珍爱生命、远离 MFC),请合理使用命名空间、智能指针、统一的代码风格,统一的异常处理模型(请合理使用 try throw catch 异常处理模型),避免定义宏常量,而是要使用 enum,并且对头文件需要合理使用文件后缀来区分 C 语言的头文件(`.h`)和 C++ 的头文件(`.hpp`)。

以下是最简 MFC 窗口程序(我开了一个 Win32 Console 工程,所以你会看到我写了 `int main()` 作为我的程序入口点)。

## main.cpp

```
#include <afxwin.h>

class MinMFCDemo : public CWinApp
{
protected:

public:
        BOOL InitInstance() override;
        int ExitInstance() override;
};

class DemoMainWindow : public CFrameWnd
{
public:
        virtual BOOL DestroyWindow() override;
};

BOOL MinMFCDemo::InitInstance()
{
        if (!CWinApp::InitInstance()) return FALSE;

        auto MainWindow = new DemoMainWindow();
        MainWindow->Create(NULL, _T("MFC Demo"));
        MainWindow->ShowWindow(SW_SHOW);

        m_pMainWnd = MainWindow;
        return TRUE;
}

int MinMFCDemo::ExitInstance()
{
        return CWinApp::ExitInstance();
}

BOOL DemoMainWindow::DestroyWindow()
{
        AfxPostQuitMessage(0);
        return CFrameWnd::DestroyWindow();
}

MinMFCDemo g_MinMFCDemoApp;

extern int AFXAPI AfxWinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPTSTR lpCmdLine, int nCmdShow);
int main(int argc, char** argv)
{
        return AfxWinMain(GetModuleHandle(NULL), NULL, GetCommandLine(), SW_SHOW);
}
```

## 开发思路(对比纯 C 写 Win32 窗口)

其实使用 MFC 的开发思路依然是 Win32 API 的思路,创建窗口,处理窗口消息,走消息循环,退出程序。但是因为 MFC 对 Win32 的 API 进行了一层面向对象的方式封装,我们按照 MFC 的这层封装来 **重新理解** Win32 API:
* 所有 Win32 API 的“句柄”(比如 `HWND`、`HDC` 等)都被包一层 class 变为类(比如 `CWnd`,`CDC`),使用这些类可以使原本看起来混乱的 Win32 API 函数在 MFC 的组织下被归类到每个类的成员方法里,配合自动提示,可以使 Win32 API 更容易使用。
   - 可以取回原始句柄,比如 `CWnd` 有个成员变量 `m_hWnd` 以及方法 `GetSafeHwnd()` (这个方法内部就是判断一下 `this` 是不是 `NULL`,如果是则返回 `NULL`,否则返回 `m_hWnd`)。
   - 类析构的时候会自动销毁句柄。
* 窗口的 MFC 类(`HWND` 包一层 class 变为 `CWnd`):我继承这个类以便于实现我自己的主窗口,控制主窗口的行为。
   - 比如我想在窗口被销毁的时候(收到窗口消息 `WM_DESTROY` 的时候)调用 `PostQuitMessage()` 来结束消息循环。按照 MFC 的风格,你的窗口收到 `WM_DESTROY` 消息的时候,类的成员函数 `DestroyWindow()` 被 MFC 框架调用),我给我的窗口的类编写 `DestroyWindow()` 方法,它的内部调用 `AfxPostQuitMessage()` 来通知退出消息循环。
   - 上述代码我继承的是 `CFrameWnd` 类,这是一种“典型单文档框架窗口”,它的父类是 `CWnd`。
* 整个应用程序是个类( `CWinApp` ),按照 MFC 的框架,你的应用程序启动的时候会需要加载你的资源、进行一个初始化的动作(此时 MFC 框架调用 `InitInstance()`,你重载这个函数来进行你的初始化),运行的时候要跑消息循环(MFC 框架调用 `Run()`),退出的时候要清理内存(MFC 框架调用 `ExitInstance()`)。
   - 因此我继承这个类,编写我自己的 `InitInstance()` 方法,重载父类的 `InitInstance()` 。我在这个方法实现里初始化我的东西,比如:创建我的主窗口。
   - MFC 的 `CWinApp` 有个成员变量 `CWnd* m_pMainWnd` 是用来存你的主窗口的,你自己先 new 出你的主窗口的类,然后再赋值过去给它以便于 MFC 框架管理你的主窗口。注意这个成员变量会在窗口被关闭后被 MFC 框架自动销毁。这里其实有个很令人不爽的一点是它是个 **原始指针**。如果是智能指针的话,它啥时候会被销毁掉这一点会变得很明确。但是因为 MFC 似乎诞生于智能指针被引入 C++ 之前的年代,当时是没有智能指针可以用的。所以这是个历史遗毒。
   - 你的应用程序类需要被写成全局变量,如上述代码的 `MinMFCDemo g_MinMFCDemoApp;` 这一行。这个地方会产生函数调用:你的应用程序类的构造函数会在你的程序入口点进入前被调用,它会把自己注册到 MFC 的框架里,这样 MFC 的框架就知道哪个是你的应用程序类了。其实全局变量是坏文明,它其实很妨碍编译器优化,但 MFC 这个性质的东西是为了让你写 Win32 App 的时候可以爽,所以在这块,针对你的应用程序类的方法调用的优化并不是一个重要的考虑内容。
* MFC 框架有它自己的程序入口点:`AfxWinMain()`,调用约定和参数都和普通的 `_tWinMain()` 一样。这个函数会初始化 MFC 的框架,找到你的应用程序类,调用它的`InitApplication()`,然后调用 `InitInstance()` 并判断返回值,如果返回了 `FALSE` 那就拒绝启动,否则继续;调用 `Run()` 进入主消息循环,等到 `Run()` 返回了,则调用 `ExitInstance()` 来让你把你分配的内存释放一下,最后清理内存,退出程序。
   - 你既可以通过调用 MFC 提供的入口点函数来使 MFC 框架运行,也可以你自己去初始化 MFC 的环境然后你自己调用你的 `InitApplication()`、`InitInstance()`、`Run()`、`ExitInstance()` 来跑你的应用程序类。

## 示例工程下载

使用 VS2022 打开 sln 文件。

戈登走過去 发表于 2023-4-26 23:06:46

想知道如果不手动释放内存,不按标准流程走,直接退出程序的话...
Windows 也还是会完美地回收干净这个进程使用的内存吧?

0xAA55 发表于 2023-4-28 15:10:52

戈登走過去 发表于 2023-4-26 23:06
想知道如果不手动释放内存,不按标准流程走,直接退出程序的话...
Windows 也还是会完美地回收干净这个进 ...

如果只说“内存”的话,是这样。

但是按照 RAII 的思想,对象的资源并不仅仅是内存,它还可以是别的资源,比如 TCP 连接、比如临时文件。类可以在构造对象的时候产生临时文件,析构对象的时候销毁临时文件。

正常走流程退出程序的时候,所有的析构都会被自动调用,自动处理所有的资源回收,而如果不正常走流程退出,直接以结束进程的方式退出,不进行类实例析构,虽然内存可以被 Windows 回收,但是其它的资源比如临时文件则不一定会被清理。

页: [1]
查看完整版本: 【MFC】最精简 MFC 窗口程序