恒美微站
首页
关于我们
建站服务
主题模板
案例展示
资讯中心
联系我们
孙鑫VC++学习笔记复盘:从SDK到MFC的CWnd与CDC实战梳理(TaoToken)
首页
资讯中心
/
孙鑫VC++学习笔记复盘:从SDK到MFC的CWnd与CDC实战梳理(TaoToken)
孙鑫VC++学习笔记复盘:从SDK到MFC的CWnd与CDC实战梳理(TaoToken)
发布时间:2026/10/10 20:41:23
1. 从 SDK 到 MFC孙鑫 VC 前五讲到底在讲什么如果你刚开始接触 Windows 下的 C 开发大概率会经历一个很拧巴的阶段能看懂WinMain里的消息循环但一打开 MFC 生成的工程就不知道从哪下手知道CWnd是窗口类却说不清它和 SDK 里那个HWND到底什么关系写了个OnLButtonDown结果画线画不出来或者画完程序一关就报内存泄漏。孙鑫的 VC 教程前五讲恰好就是冲着这些拧巴点去的——它没有一上来就丢给你一堆类库文档而是先用 SDK 把窗口的“手工活”走一遍再告诉你 MFC 把这些手工活封装成了什么。这篇复盘面向的是刚接触 Windows 开发的 C 程序员目标很明确把 SDK 创建窗口的四步、MFC 中CWnd与CDC的核心用法、消息映射的三处修改、以及绘图上下文的获取与释放串成一条能动手复现的路径。我会给出可复制的 MFC 最小工程配置、逐讲验证清单以及我在对照笔记复现时踩过的坑。你不需要先看完五讲视频跟着下面的步骤走每个示例都能自己跑起来。核心检索词先摆在这里VC、MFC、SDK、CWnd、CDC。这五个词基本覆盖了前五讲的全部内容。SDK 是“手动挡”MFC 是“自动挡”CWnd是窗口对象的 C 封装CDC是绘图上下文的封装。理解这四者的关系前五讲就通了。我试过把 SDK 的CreateWindow和 MFC 的CWnd::Create放在一起对照发现 MFC 并没有变魔术它只是把WNDCLASS注册、窗口过程回调、消息分发这些步骤藏进了AfxWinMain和消息映射表里。你看到的BEGIN_MESSAGE_MAP/END_MESSAGE_MAP本质就是一张“消息 ID → 成员函数”的查找表。下面从 SDK 的窗口四步开始一步步过渡到 MFC。2. SDK 窗口创建四步与 MFC 最小工程配置2.1 SDK 创建窗口的四个步骤孙鑫第一讲里反复强调一个完整的 SDK 窗口要经过四步设计窗口类、注册窗口类、创建窗口、显示窗口。这四步在代码里对应WNDCLASS结构体、RegisterClass、CreateWindow、ShowWindowUpdateWindow。很多人卡在第一步因为WNDCLASS有十个字段漏一个就注册失败。#include windows.h #include stdio.h LRESULT CALLBACK MyProc(HWND, UINT, WPARAM, LPARAM); int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrev, LPSTR lpCmd, int nShow) { WNDCLASS wndcls; wndcls.style CS_HREDRAW | CS_VREDRAW; wndcls.lpfnWndProc MyProc; // 窗口过程函数名即函数指针 wndcls.cbClsExtra 0; wndcls.cbWndExtra 0; wndcls.hInstance hInstance; wndcls.hIcon LoadIcon(NULL, IDI_APPLICATION); wndcls.hCursor LoadCursor(NULL, IDC_ARROW); wndcls.hbrBackground (HBRUSH)GetStockObject(WHITE_BRUSH); wndcls.lpszMenuName NULL; wndcls.lpszClassName MyWndClass; if (!RegisterClass(wndcls)) return 0; // 第二步注册 HWND hwnd CreateWindow(MyWndClass, SDK Window, WS_OVERLAPPEDWINDOW, CW_USEDEFAULT, CW_USEDEFAULT, 600, 400, NULL, NULL, hInstance, NULL); ShowWindow(hwnd, nShow); // 第四步显示 UpdateWindow(hwnd); MSG msg; while (GetMessage(msg, NULL, 0, 0)) { TranslateMessage(msg); DispatchMessage(msg); } return msg.wParam; } LRESULT CALLBACK MyProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam) { switch (uMsg) { case WM_CHAR: break; case WM_DESTROY: PostQuitMessage(0); break; default: return DefWindowProc(hWnd, uMsg, wParam, lParam); } return 0; }这里有个细节值得单独拎出来wndcls.lpfnWndProc MyProc;这一行函数名代表函数代码的首地址可以直接赋给函数指针。孙鑫在笔记里专门提了这一点因为后面 MFC 的消息映射就是靠类似的机制把消息和成员函数关联起来的。2.2 MFC 最小工程配置从 SDK 切到 MFC第一步是建一个能跑的最小工程。我用的是 Visual Studio新建“MFC 应用程序”选择“单文档”其余默认。生成后你会看到stdafx.h新版叫pch.h里包含了afxwin.h而afxwin.h里又隐式包含了windows.h。这就是为什么 MFC 工程里不用再手动#include windows.h。如果你不想用向导想手动配一个最小 MFC 工程核心配置如下// pch.h #pragma once #include afxwin.h // MFC 核心含 CWnd、CDC、CString 等 #include afxext.h// MyApp.h #pragma once #include pch.h class CMyApp : public CWinApp { public: virtual BOOL InitInstance(); }; class CMyFrame : public CFrameWnd { public: CMyFrame(); };// MyApp.cpp #include pch.h #include MyApp.h CMyApp theApp; // 全局对象WinMain 之前就被构造 BOOL CMyApp::InitInstance() { CMyFrame* pFrame new CMyFrame(); m_pMainWnd pFrame; pFrame-ShowWindow(SW_SHOW); pFrame-UpdateWindow(); return TRUE; } CMyFrame::CMyFrame() { Create(NULL, MFC 最小窗口, WS_OVERLAPPEDWINDOW, CRect(0, 0, 600, 400)); }这段代码里最关键的是CMyApp theApp;这个全局对象。孙鑫第三讲提到在WinMain之前theApp已经被构造并初始化由于子类构造函数执行前父类构造函数先执行所以CWinApp的构造函数先跑theApp对象产生后AfxWinMain里的pThread和pApp才有内容。这就是 MFC 程序启动的隐藏入口。注意CMyFrame的Create第二个参数是窗口标题第四个参数是CRect。如果你漏了WS_VISIBLE窗口不会自动显示必须手动调ShowWindow。2.3 CWnd 与 HWND 的关系所有从CWnd派生的类都有m_hWnd句柄。CWnd是对HWND的 C 封装m_hWnd就是它内部持有的窗口句柄。你可以用FromHandle从句柄反查对象指针CWnd* pWnd CWnd::FromHandle(hwnd);这个函数在第四讲里被专门提到因为后面画线时经常需要从HWND拿到CWnd*。FromHandle是静态成员函数可以由类名直接调用也可以由对象调用。静态成员函数不能访问非静态成员变量静态成员变量必须在类的外部初始化——这些是第二讲里关于静态成员的要点在 MFC 里到处都能碰到。3. 消息映射三处修改与 CDC 绘图上下文配置3.1 消息映射的三处修改孙鑫第四讲的核心是消息映射。在 MFC 里加一个鼠标左键按下的响应要在三个地方改代码。以单文档的 View 类为例第一处头文件里声明消息处理函数// MyView.h protected: //{{AFX_MSG(CMyView) afx_msg void OnLButtonDown(UINT nFlags, CPoint point); afx_msg void OnLButtonUp(UINT nFlags, CPoint point); afx_msg void OnMouseMove(UINT nFlags, CPoint point); //}}AFX_MSG DECLARE_MESSAGE_MAP()第二处cpp 文件的BEGIN_MESSAGE_MAP和END_MESSAGE_MAP之间加宏// MyView.cpp BEGIN_MESSAGE_MAP(CMyView, CView) //{{AFX_MSG_MAP(CMyView) ON_WM_LBUTTONDOWN() ON_WM_LBUTTONUP() ON_WM_MOUSEMOVE() //}}AFX_MSG_MAP END_MESSAGE_MAP()第三处实现函数void CMyView::OnLButtonDown(UINT nFlags, CPoint point) { m_ptOrigin m_ptOld point; m_bDraw TRUE; CView::OnLButtonDown(nFlags, point); }这三处缺一不可。我踩过的坑是只加了头文件声明和函数实现忘了在BEGIN_MESSAGE_MAP里加ON_WM_LBUTTONDOWN()结果鼠标点下去毫无反应编译也不报错。消息映射表是运行时查找的漏了宏就等于没注册。3.2 CDC 绘图上下文的四种获取方式第四讲另一个重点是画线。孙鑫列了四种方法从 SDK 的HDC到 MFC 的CDC、CClientDC、CWindowDC。这四种方式的区别和释放规则是新手最容易搞混的地方。第一种直接用 SDK 的HDCHDC hdc ::GetDC(m_hWnd); MoveToEx(hdc, m_ptOrigin.x, m_ptOrigin.y, NULL); LineTo(hdc, point.x, point.y); ::ReleaseDC(m_hWnd, hdc); // 必须成对第二种用CDC*CDC* pDC GetDC(); pDC-MoveTo(m_ptOrigin); pDC-LineTo(point); ReleaseDC(pDC); // 必须成对第三种用CClientDC构造时自动获取 DC析构时自动释放CClientDC dc(this); dc.MoveTo(m_ptOrigin); dc.LineTo(point); // 不需要 ReleaseDC第四种用CWindowDC可以覆盖整个窗口区域甚至用GetDesktopWindow()在整个屏幕上画CWindowDC dc(GetDesktopWindow()); dc.MoveTo(m_ptOrigin); dc.LineTo(point);关键差异在于GetDC/ReleaseDC和BeginPaint/EndPaint必须成对使用否则内存泄漏而CClientDC和CWindowDC在析构函数里自动调用ReleaseDC不需要手动释放。孙鑫在笔记里专门用“必须成对使用”标注了前两种就是因为这是最常见的泄漏点。3.3 可复制的 settings 片段如果你用 VS Code 配合 Cline 或 Claude Code 来辅助写 MFC 代码可以在项目根目录放一个.vscode/settings.json把 MFC 的头文件路径和库路径配进去避免 IntelliSense 报红{ C_Cpp.default.includePath: [ ${workspaceFolder}/**, C:/Program Files/Microsoft Visual Studio/2022/Community/VC/Tools/MSVC/14.38.33130/atlmfc/include, C:/Program Files (x86)/Windows Kits/10/Include/10.0.22621.0/um, C:/Program Files (x86)/Windows Kits/10/Include/10.0.22621.0/shared ], C_Cpp.default.defines: [ _AFXDLL, WIN32, _WINDOWS ], C_Cpp.default.cppStandard: c17 }路径里的 MSVC 版本号和 Windows Kits 版本号要换成你本机实际安装的。_AFXDLL这个宏必须定义否则afxwin.h里的很多声明会走静态链接分支和动态库版本对不上。如果你用 Cline 的 MCP 配置来管理模型接入可以在cline_mcp_settings.json里写{ mcpServers: { taotoken: { url: https://taotoken.net/api, env: { API_KEY: 你的Key, BASE_URL: https://taotoken.net/api, MODEL_ID: claude-sonnet-4-20250514 } } } }Base URL、Key、Model ID 这三件套要写全缺一个就连不上。Key 在控制台的 API Keys 页面生成模型 ID 可以在模型对话页面确认当前可用的版本。4. 验证请求与逐讲复现清单4.1 验证 MFC 工程能跑起来配好工程后先验证最小窗口能显示。编译运行如果窗口没出来按这个顺序查先看InitInstance里有没有ShowWindow(SW_SHOW)和UpdateWindow()。再看CMyFrame的Create有没有传WS_OVERLAPPEDWINDOW。最后看theApp是不是全局对象——如果写成了局部变量WinMain之前不会被构造整个 MFC 启动链就断了。窗口出来后在 View 里加鼠标响应验证消息映射三处都改对了。点一下鼠标如果断点能进OnLButtonDown说明映射表生效。4.2 逐讲验证清单第一讲验证点SDK 窗口四步是否完整LoadCursor、MessageBox的声明是否来自windows.hsprintf是否包含stdio.hGetDC/ReleaseDC是否成对。第二讲验证点结构体和类定义末尾的分号是否漏#include xxx.h和#include xxx.h的查找路径差异引用传参的交换函数是否写对头文件卫士#ifndef/#define/#endif是否加了。第三讲验证点theApp全局对象是否在WinMain之前构造CSingleDocTemplate是否把 Doc、Frame、View 三个类关联起来在CMainFrame::OnCreate里创建的CButton对象是否是成员变量而非局部变量。第四讲验证点消息映射三处是否齐全四种画线方式是否都能画出线CPen选中后是否恢复旧画笔CBrush::FromHandle的静态调用是否写对。第五讲验证点CreateSolidCaret和ShowCaret是否配对GetTextMetrics拿到的字体高度是否用于换行路径层BeginPath/EndPath/SelectClipPath是否保护了先前的文本SetTimer回调函数里获取 View 指针的链路是否通。4.3 用模型对话验证代码理解如果你对某段代码的语义不确定可以把代码贴到模型对话页面让模型解释CWnd::Create的参数含义或者对比CClientDC和CWindowDC的裁剪区域差异。验证模型是否理解正确可以问它“GetDC和CClientDC在释放时机上有什么不同”看它能不能说出析构自动释放这个点。对于长期要写 MFC 或 Windows 桌面开发的人可以考虑用 Coding Plan 来管理日常的代码补全和重构任务把模型接入配好之后写消息映射和 CDC 绘图这类重复性代码会快很多。5. 本篇常见报错排查5.1 401 与 local proxy failed如果你在配置模型接入时遇到 401先检查 Key 是否复制完整有没有多余空格。Base URL 要写https://taotoken.net/api不要多加斜杠或路径。如果报local proxy failed通常是本地代理配置和实际网络环境不匹配检查cline_mcp_settings.json里的url字段是否指向了正确的 API 地址。5.2 reading choices 报错这个报错一般出现在模型返回格式和客户端解析格式对不上时。检查MODEL_ID是否写成了客户端不认识的名称。模型对话页面里能正常对话的模型 ID才是当前可用的。如果换了模型 ID 后报reading choices换回默认的再试。5.3 OAuth 相关报错Claude Code 接入时如果走 OAuth 流程报错先确认是不是混用了 API Key 和 OAuth 两种认证方式。用 API Key 接入时不需要再走 OAuth 授权用 OAuth 时Key 字段留空。两种方式选一种不要同时配。5.4 MFC 编译报错 afxwin.h 找不到这是头文件路径没配。检查 VS 安装时有没有勾选“MFC 组件”。如果装了但 IntelliSense 报红在.vscode/settings.json的includePath里补上atlmfc/include路径。编译能过但编辑器报红是 IntelliSense 的路径问题不影响实际编译。5.5 画线画不出来按这个顺序查OnLButtonDown有没有进m_bDraw有没有置 TRUEOnMouseMove里有没有判断m_bDrawCClientDC有没有构造成功MoveTo和LineTo的坐标是不是同一个点。如果用的是CDC*方式检查ReleaseDC有没有漏漏了会导致后续 DC 获取失败。5.6 内存泄漏GetDC/ReleaseDC、BeginPaint/EndPaint、SelectObject选入后有没有选回旧对象这三处是泄漏高发区。CPen、CBrush、CFont选入 DC 后一定要用SelectObject(pOldPen)恢复。CClientDC和CWindowDC不需要手动释放但CDC*方式必须成对。6. 把 SDK 的手动挡换成 MFC 的自动挡前五讲走下来最值得记住的一句话是MFC 没有替代 SDK它只是把 SDK 的手工步骤封装成了类。CWnd封装了HWND和窗口过程CDC封装了HDC和绘图函数消息映射表封装了switch-case消息分发。你理解了 SDK 的四步再看 MFC 的InitInstance和BEGIN_MESSAGE_MAP就不会觉得它是黑盒。如果你要动手复现建议按这个顺序先用 SDK 写一遍窗口四步确保能跑再用 MFC 向导建一个单文档工程在 View 里加鼠标响应和画线最后把四种画线方式都试一遍对比释放规则。每讲验证清单里的点过一遍打个勾。模型接入方面API Key 在控制台的 API Keys 页面生成接入文档在文档页有完整的 Base URL 和参数说明。遇到 401 或local proxy failed先查 Key 和 URL再查模型 ID。需要长期写代码的Coding Plan 比按次调用更省心。