拓十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

MFC贪吃蛇课程设计:消息映射、定时器与GDI双缓冲实战指南

MFC贪吃蛇课程设计:消息映射、定时器与GDI双缓冲实战指南 简介这是一份基于MFC的贪吃蛇课程设计资源包面向VC程序设计初学者及需要完成期末大作业的在校学生。程序基于对话框式MFC框架实现在传统贪吃蛇玩法上作了创新从内置词库中选择一个单词玩家操控蛇按顺序吃到正确字母同时保留撞墙、碰到自身或吃到错误字母立即结束的核心规则若顺利吃完整单词还可进入下一关并自动保存个人最好成绩。资源共90个文件压缩包大小约14.72MB主要包含C工程源文件.cpp、.h、.dsw等、设计报告文档.doc、可直接运行的exe程序以及bmp格式界面素材、mp3与wav音效、ttf字体等多媒体文件开发环境为VC6.0。目前已有52人学习浏览。整份资源完整度高既有可运行的Release版本也有便于研读的源程序与设计报告代码结构清晰界面友好美观玩法寓教于乐既能支撑课程答辩也能帮助读者理解MFC对话框的消息映射、控件使用及游戏循环逻辑同时通过字母拼词的形式辅助记忆单词或编程关键词是非常适合自学的课程设计范例。1. 课程设计选MFC贪吃蛇图的是什么两周交出一个能跑的完整闭环很多同学看到《VC高级程序设计》的课程设计要求第一反应是找现成的贪吃蛇源码改一改就交。但等你真把源码下下来打开你会发现最难的不是那几百行游戏逻辑而是MFC那套消息映射和GDI绘图机制——为什么定时器不触发、为什么方向键没反应、为什么窗口一刷新画面闪成幻灯片。这门课设计考察的不是你会不会写蛇移动而是你能不能把 Windows 消息驱动、画布重绘、资源释放这三件事串起来。基于MFC的贪吃蛇项目恰好是这三个知识点的最小完整载体核心算法简单到新手三天能写完但让它在对话框上流畅跑起来、能用键盘控制、关闭后不报内存泄漏就需要把 MFC 的消息循环和绘图机制真正吃透。我见过不少同学拿着整段整段的源码却交不出能运行的exe程序原因不在算法而在工程配置。这篇笔记就按我自己做这个项目的路径来写从类设计到GDI双缓冲从键盘消息到定时器加速最后落到交付验证和设计报告怎么写每一步都给出可以直接抄的代码和参数。2. 拆解MFC贪吃蛇的代码骨架对话框程序里谁在管什么2.1 从CSnakeGame类开始把游戏逻辑和界面分离基于MFC的对话框程序工程代码里最常见的错误是把所有东西塞进CAboutDlg::OnPaint或者按钮响应函数里。蛇身数组、方向枚举、食物坐标、碰撞检测全写在界面层最后代码乱到没法调试。我的做法是先写一个完全不知道“窗口”是什么的CSnakeGame类它只负责游戏逻辑界面层只负责“把逻辑状态画出来”。// SnakeGame.h #pragma once #include vector #include afxtempl.h enum GameState { STATE_READY, // 初始就绪 STATE_RUNNING, // 游戏中 STATE_PAUSED, // 暂停 STATE_OVER // 撞墙或撞到自己游戏结束 }; enum Direction { DIR_UP, DIR_DOWN, DIR_LEFT, DIR_RIGHT }; class CSnakeGame { public: CSnakeGame(); virtual ~CSnakeGame(); void InitGame(int nCols, int nRows); // 初始化地图与蛇 void SetDirection(Direction dir); // 设置下一步方向 bool MoveSnake(); // 蛇移动一格返回是否存活 bool IsFoodEaten() const { return m_bFoodEaten; } void ResetFoodEaten() { m_bFoodEaten false; } const std::vectorCPoint GetSnake() const { return m_vSnake; } CPoint GetFood() const { return m_ptFood; } int GetScore() const { return m_nScore; } GameState GetState() const { m_State; } private: void GenerateFood(); // 在空位上随机生成食物 bool IsPointOnSnake(CPoint pt) const; std::vectorCPoint m_vSnake; // 蛇身索引0是蛇头 CPoint m_ptFood; // 食物坐标 Direction m_dir; // 当前移动方向 GameState m_State; // 游戏状态 int m_nCols, m_nRows;// 地图行列数 int m_nScore; // 得分 bool m_bFoodEaten; // 本帧是否吃到食物 };设计逻辑说明一下蛇身用std::vectorCPoint存CPoint是 MFC 自带的结构体包含 x 和 y 两个成员表示网格坐标不需要自己定义点结构。InitGame里的nCols和nRows决定地图大小这个参数后面要跟界面画布尺寸严格对应不然蛇会画出地图边界。MoveSnake返回布尔值表示这步是否合法撞墙/撞死返回 false界面层拿到 false 就切到STATE_OVER不用关心具体怎么撞的。这个类的好处是方便单元验证在OnInitDialog里创建一个CSnakeGame实例然后写一个CWinApp级别的调试函数手动调MoveSnake不用开窗口就能验证边界条件。设计报告里画的类图核心也就是这个类和对话框类之间的单向依赖关系。2.2 对话框模板与消息映射MFC四大类的协作方式VC 里 MFC 的对话框程序工作的是CWinApp、CDialog、CWnd和CDC这老四样。CWinApp管程序启动和消息泵CGameDlg继承自CDialog承载界面控件和消息响应CWnd是窗口基类提供SetTimer、KillTimer、OnPaint这些基础设施CDC是所有绘图的入口。很多教程把四大类当概念讲但在贪吃蛇这个项目里你会实际用到每一个theApp是CWinApp的对象CGameDlg是你的主窗口OnPaint里拿到的CPaintDC dc(this)就是CDC的实例。// GameDlg.h 的核心声明 class CGameDlg : public CDialog { public: CGameDlg(CWnd* pParent nullptr); enum { IDD IDD_GAME_DIALOG }; protected: virtual void DoDataExchange(CDataExchange* pDX); virtual BOOL OnInitDialog(); afx_msg void OnPaint(); afx_msg void OnTimer(UINT_PTR nIDEvent); afx_msg void OnDestroy(); virtual BOOL PreTranslateMessage(MSG* pMsg); DECLARE_MESSAGE_MAP() private: CSnakeGame m_game; // 游戏逻辑对象 bool m_bPaused; // 暂停标志 int m_nTimerInterval; // 当前定时器间隔毫秒 };// GameDlg.cpp 的消息映射入口 BEGIN_MESSAGE_MAP(CGameDlg, CDialog) ON_WM_PAINT() ON_WM_TIMER() ON_WM_DESTROY() END_MESSAGE_MAP()这里的ON_WM_TIMER()宏对应OnTimer函数ON_WM_PAINT()对应OnPaint这些是 MFC 预定义的消息响应宏。新人最容易在这里踩的第一个坑是自己写了OnTimer但忘了在消息映射表里加ON_WM_TIMER于是定时器到点后系统找不到处理函数你看到的表象就是“蛇不动了”。记住一个原则——凡是用afx_msg声明的函数必须有对应的ON_WM_XXX()宏这个宏就是消息和处理函数的绑定关系MFC 把这个表叫消息映射表。设计报告里画消息流图时这张表就是核心内容。2.3 设计报告里要画哪几张图课程设计报告不是代码注释的堆积评审老师看的是你有没有把“问题拆解清楚”。这个项目我建议至少画三张图。第一张是类图画出CSnakeGame与CGameDlg的关联关系标注蛇身的存储结构和状态枚举。第二张是状态图READY → RUNNING → PAUSED → RUNNING → OVER这五条转化路径是什么事件触发的。第三张是消息时序图画键盘按下到画面更新的完整链路PreTranslateMessage捕获方向键 → 修改方向 →SetTimer触发OnTimer→MoveSnake更新逻辑 →Invalidate触发重绘 →OnPaint双缓冲输出画面。这三张图画完报告的核心篇幅就够了老师一眼能看出你对消息驱动机制是有理解的。3. 把贪吃蛇画出来地图坐标系、移动算法与GDI双缓冲3.1 地图逻辑坐标与像素坐标的换算贪吃蛇解析里最容易让新手绕晕的就是坐标系。游戏逻辑里我们只关心“第几行第几列”这是网格坐标而 MFC 绘图函数要的是像素坐标单位是设备无关的像素点。我的做法是每格固定 20 像素地图区域左上角留 20 像素边距这样逻辑坐标到像素坐标的换算就是一行代码。#define GRID_SIZE 20 // 每格像素 #define MAP_OFFSET 20 // 地图左边距和上边距 // 逻辑坐标转像素坐标 CRect GridToRect(int col, int row) { return CRect( MAP_OFFSET col * GRID_SIZE, MAP_OFFSET row * GRID_SIZE, MAP_OFFSET (col 1) * GRID_SIZE, MAP_OFFSET (row 1) * GRID_SIZE ); }参数说明GRID_SIZE取 20 是综合手感与性能的常见选择。小于 16 蛇身和食物在屏幕上挤成一团大于 32 地图信息量不够难度显得低。地图建议做 24 列 × 18 行也就是画面宽约 520 像素、高约 400 像素这个尺寸在标准对话框模板里刚好放下不需要调窗口大小。如果你在OnInitDialog里用SetWindowPos改过窗口尺寸记得同步改MAP_OFFSET否则蛇会在错误的位置绘制典型的“逻辑对得上画面错位”。GenerateFood的随机生成也要放在这个坐标系里做范围限定在地图内部列号 0 到m_nCols - 1行号 0 到m_nRows - 1。如果你在初始化时用墙围了一圈那么要把边界那圈排除掉蛇的活动范围是 1 到nCols-2否则食物会生成在墙上。3.2 移动算法尾部删除与头部插入蛇的移动不需要真的“移动”每一个身体节常见的做法是把新蛇头坐标插入到 vector 头部再根据是否吃到食物决定删不删尾节点。没吃到食物时删尾蛇身长度不变吃到食物时保留尾节点蛇身长一截。这样移动算法的时间复杂度是 O(n)n 是蛇身长度最坏几百节也能在一帧内算完。bool CSnakeGame::MoveSnake() { if (m_State ! STATE_RUNNING) return true; // 非运行状态不处理移动 CPoint newHead m_vSnake[0]; // 取当前蛇头 switch (m_dir) { case DIR_UP: newHead.y--; break; case DIR_DOWN: newHead.y; break; case DIR_LEFT: newHead.x--; break; case DIR_RIGHT: newHead.x; break; } // 撞墙检测 if (newHead.x 0 || newHead.x m_nCols || newHead.y 0 || newHead.y m_nRows) { m_State STATE_OVER; return false; } // 撞自己检测跳过尾部因为尾部这一步会移走 for (size_t i 0; i m_vSnake.size() - 1; i) { if (m_vSnake[i] newHead) { m_State STATE_OVER; return false; } } m_vSnake.insert(m_vSnake.begin(), newHead); // 新头插入 // 吃食物判断 if (newHead m_ptFood) { m_nScore 10; m_bFoodEaten true; // 不删除尾部蛇身自动增长 } else { m_vSnake.pop_back(); // 没吃到删尾保持长度 } return true; }这里有一个新手容易写错的细节撞自己检测要跳过 vector 的最后一个元素。因为蛇每走一步尾部会移动原来的尾节点在新一轮移动里让出了位置新蛇头走到旧尾节点位置是合法的。如果你用“全元素碰撞”判断蛇在绕圈时会误判自己撞自己。另外insert到 vector 头部是 O(n) 操作对贪吃蛇这个数量级没问题不需要优化成 deque。当MoveSnake返回false后游戏进入STATE_OVER状态界面层这时要KillTimer停止移动并弹出结果框。一个常被忽略参数是得分每吃一个食物加多少分没有标准答案我用 10 分报告里说明“每次食物得分设定为10便于计算总分与蛇长对应关系”即可老师不会在这上面卡你。3.3 双缓冲绘图为什么 Invalidate(TRUE) 会闪成幻灯片MFC 的OnPaint默认用CPaintDC直接向屏幕绘制。如果你的地图是逐步画几十个矩形和圆每条蛇每帧清屏再重画画面会因为前景和背景交替闪烁。原因在于Invalidate(TRUE)会先擦除整个窗口背景再触发WM_PAINT重绘前景擦除和绘制两个动作之间有肉眼可见的时间差。解法是双缓冲先在内存里建一张画布把所有图形画到内存画布上再一次BitBlt整块拷贝到屏幕。屏幕只有一次写入自然不闪。这是 GDI 编程里工程量最小、收益最稳定的技巧我建议所有 MFC 绘图项目都无脑用双缓冲。void CGameDlg::OnPaint() { CPaintDC dc(this); // 设备上下文 // 双缓冲创建内存DC和位图 CDC memDC; memDC.CreateCompatibleDC(dc); CRect rcClient; GetClientRect(rcClient); CBitmap bmp; bmp.CreateCompatibleBitmap(dc, rcClient.Width(), rcClient.Height()); CBitmap* pOldBmp memDC.SelectObject(bmp); // 1. 用背景色填充整个客户区 memDC.FillSolidRect(rcClient, RGB(240, 240, 240)); // 2. 画地图边界 CPen pen(PS_SOLID, 2, RGB(80, 80, 80)); CPen* pOldPen memDC.SelectObject(pen); memDC.Rectangle(MAP_OFFSET - 1, MAP_OFFSET - 1, MAP_OFFSET m_nCols * GRID_SIZE 1, MAP_OFFSET m_nRows * GRID_SIZE 1); memDC.SelectObject(pOldPen); // 3. 画食物用红色实心圆 CBrush foodBrush(RGB(255, 60, 60)); CRect rcFood GridToRect(m_game.GetFood().x, m_game.GetFood().y); memDC.SelectObject(foodBrush); memDC.Ellipse(rcFood.left 2, rcFood.top 2, rcFood.right - 2, rcFood.bottom - 2); // 4. 画蛇身蛇头深绿身体浅绿 const std::vectorCPoint snake m_game.GetSnake(); for (size_t i 0; i snake.size(); i) { CRect rc GridToRect(snake[i].x, snake[i].y); CBrush bodyBrush(i 0 ? RGB(0, 120, 0) : RGB(0, 180, 0)); memDC.SelectObject(bodyBrush); memDC.Rectangle(rc.left 1, rc.top 1, rc.right - 1, rc.bottom - 1); } // 5. 一次拷贝到屏幕 dc.BitBlt(0, 0, rcClient.Width(), rcClient.Height(), memDC, 0, 0, SRCCOPY); // 恢复并清理资源 memDC.SelectObject(pOldBmp); }代码里关注的参数CreateCompatibleBitmap创建的位图大小必须和客户区一致如果只创建了地图区域大小BitBlt超出部分会出现花屏。FillSolidRect填充的背景色取浅灰RGB(240,240,240)地图边框用 2 像素宽的深灰画笔蛇头深绿、身体浅绿形成区分。每个画刷和画笔在SelectObject之后要把旧的选回去这是 GDI 资源管理的规矩——内存 DC 删除前必须把自己创建的对象换出来否则DeleteObject时会出问题。如果你想让界面更好看可以把背景色换成黑色、蛇身用发光绿那就是纯美术层面的调参不影响逻辑。注意如果用了双缓冲重绘时调用Invalidate(FALSE)FALSE 表示不擦除背景。因为双缓冲本身会在内存画布上FillSolidRect覆盖全图擦除背景这步纯属多余反而会导致闪烁。4. 键盘、定时器与加速把游戏的“节奏感”调出来4.1 对话框里的键盘消息处理为什么方向键经常没反应在 Win32 窗口里处理键盘用WM_KEYDOWN就完事了但 MFC 对话框有一个额外的机制对话框会把某些键先当作控件导航键处理比如回车键会触发默认按钮方向键会在控件之间跳转焦点。所以如果你只重写OnKeyDown方向键根本到不了你手里。正确处理是在对话框类里重写PreTranslateMessage这是 MFC 在消息分发到控件之前给出的一个拦截点。只要返回 TRUE消息就不会继续往下传。BOOL CGameDlg::PreTranslateMessage(MSG* pMsg) { if (pMsg-message WM_KEYDOWN) { UINT nKey (UINT)pMsg-wParam; switch (nKey) { case VK_UP: if (m_game.GetDirection() ! DIR_DOWN) // 禁止直接掉头 m_game.SetDirection(DIR_UP); return TRUE; case VK_DOWN: if (m_game.GetDirection() ! DIR_UP) m_game.SetDirection(DIR_DOWN); return TRUE; case VK_LEFT: if (m_game.GetDirection() ! DIR_RIGHT) m_game.SetDirection(DIR_LEFT); return TRUE; case VK_RIGHT: if (m_game.GetDirection() ! DIR_LEFT) m_game.SetDirection(DIR_RIGHT); return TRUE; case VK_SPACE: // 空格暂停/继续 TogglePause(); return TRUE; } } return CDialog::PreTranslateMessage(pMsg); }代码里最关键的是“禁止直接掉头”这三个判断当蛇头向右移动时你按左键不响应。如果不加这个限制蛇头会直接穿进自己的身体判死或者抽疯。这是做贪吃蛇最容易犯的规则性错误。TogglePause是自己的暂停函数空格控制在RUNNING和PAUSED之间切换同时要处理定时器的启停。如果你用SetFocus把焦点设到某个静态文本控件上方向键也不会进入PreTranslateMessage所以依托这个函数是最省心的做法。4.2 定时器与难度参数从 200ms 开始往下压MFC 的定时器不像游戏引擎里的帧循环它只是一个在固定时间间隔到达时向窗口发WM_TIMER消息的机制。贪吃蛇天然适合用定时器驱动定时器每触发一次蛇移动一格。// OnInitDialog 中启动定时器 m_nTimerInterval 200; // 初始速度每秒5格 SetTimer(1, m_nTimerInterval, NULL);void CGameDlg::OnTimer(UINT_PTR nIDEvent) { if (nIDEvent 1 !m_bPaused) { bool bAlive m_game.MoveSnake(); if (!bAlive) { KillTimer(1); m_game.SetState(STATE_OVER); MessageBox(_T(游戏结束得分) ... /* 拼接分数 */, _T(提示), MB_OK); return; } if (m_game.IsFoodEaten()) { m_game.ResetFoodEaten(); // 加速每次缩短20ms下限60ms if (m_nTimerInterval 60) { m_nTimerInterval max(60, m_nTimerInterval - 20); SetTimer(1, m_nTimerInterval, NULL); } } Invalidate(FALSE); // 双缓冲下用FALSE避免多余擦除 } CDialog::OnTimer(nIDEvent); }启动间隔取 200ms每秒 5 格这个速度对第一次玩的人刚好不会因为手忙脚乱而立刻撞墙。每吃一个食物减少 20ms到达 60ms 后不再加速。60ms 意味着每秒 16.6 格已经接近人眼反应极限。如果你想让难度曲线更平滑可以把减速梯度改成 10ms同时把下限调到 80ms这取决于你想要的最终难度。计时器的间隔参数可以直接写进设计报告的“游戏参数设计”一节配合一张“食物数/间隔/速度”的表格非常有说服力。SetTimer的第二个参数注意是 UINT 毫秒。我在调试时发现有些教程用SetTimer(1, 200, NULL)的第三个参数传回调函数那在 MFC 窗口程序里很容易踩内存错误。约定俗成传 NULL让它向窗口发消息就行。另外重复调用SetTimer同一个 ID 会重置间隔不会创建多个定时器所以加速时直接再调一次是安全的。4.3 暂停/继续的状态机不能只靠标志位做TogglePause时我遇到过一个现象按空格暂停后再按空格继续画面第一帧突然跳了一格。原因是只是m_bPausedfalse时上一次OnTimer已经在队列里排了一条消息暂停期间它一直被拦截没执行恢复后立即执行造成“瞬移”。实在的解决方法是暂停时KillTimer(1)继续时重新SetTimer(1, m_nTimerInterval, NULL)彻底把未决消息清掉。void CGameDlg::TogglePause() { if (m_game.GetState() STATE_RUNNING) { KillTimer(1); m_game.SetState(STATE_PAUSED); m_bPaused TRUE; } else if (m_game.GetState() STATE_PAUSED) { SetTimer(1, m_nTimerInterval, NULL); m_game.SetState(STATE_RUNNING); m_bPaused FALSE; } }对应的游戏开始和结束处理要区分清楚第一次进入STATE_RUNNING是在OnInitDialog里调用m_game.InitGame和SetTimer重建游戏要全部重置——蛇身回到初始位置、分数清零、间隔回到 200ms。如果你急着交作业可以在游戏结束时写一个“按 R 重新开始”的快捷键加两行判断就行。状态机设计这部分写进设计报告是加分项尤其要把“暂停后不能再次触发 MoveSnake”这条边界描述清楚。5. MFC贪吃蛇五个经典翻车点现象、原因与解决5.1 画面狂闪Invalidate(TRUE) 引发的全量擦除现象蛇每移动一格整个窗口先变白一下再画出新画面连续移动时屏幕像翻书一样闪。出问题的地方在OnPaint里用了CPaintDC直接画并且界面层调用了Invalidate(TRUE)。原因TRUE 会擦除窗口背景而 GDI 的擦除和重画都在界面线程串行执行擦除后的空白窗口被显示到屏幕上人眼就捕捉到了那一帧。解决把绘图改成内存 DC 双缓冲重绘调用Invalidate(FALSE)。如果你已经用了双缓冲还闪检查BitBlt的目标 DC 是否直接拿CPaintDC用的目标 DC 必须是this的窗口DC不能是内存 DC 拷贝给自己。5.2 方向键完全没反应焦点被按钮控件吃掉了现象程序跑起来用鼠标点一下窗口空白处方向键正常如果点过某个按钮再按方向键蛇不理你。原因对话框内的按钮控件拿到焦点后方向键被 MFC 的控件导航机制消费掉WM_KEYDOWN不会传递给对话框。解决用PreTranslateMessage在消息分发到控件前拦截代码见 4.1 节。我在实际项目里还发现一个变体如果你在窗口上放了一个“开始”按钮用户点开始后焦点留在按钮上键盘立刻失灵必须在OnBnClickedStart里加一句SetFocus()把焦点还给窗口或者一个不可选中的静态文本。5.3 食物生成在蛇身上随机函数没有校验冲突现象游戏玩到蛇身比较长时新食物偶尔出现在蛇身体里等蛇头走到那里时被误判成“撞自己”游戏莫名结束。原因GenerateFood只做了rand() % nCols和rand() % nRows没检查这个坐标是否被蛇身占用。解决生成后循环遍历蛇身冲突则重新生成连续尝试最多 100 次。蛇身极长时为了避免死循环可以遍历空格子后随机取一个但课程设计阶段用while重试足够。5.4 关闭窗口后内存泄漏报错定时器没释放现象退出程序时Debug 输出窗口提示Detected memory leaks!或者对话框关闭后进程还在后台挂了一段时间。原因OnTimer里的MoveSnake还在跑但窗口句柄已经销毁MFC 的调试版本会报告异常。解决在OnDestroy里KillTimer(1)并在OnPaint里对所有 GDI 对象做完恢复再删。泄漏点往往是CBitmap和CBrush这类 GDI 对象必须保证SelectObject把旧对象选回去后再让局部对象析构这是 MFC 里最容易被忽视的资源管理细节。5.5 Debug下正常Release下错乱字符集与运行时库不一致现象Debug 配置运行正常切到 Release 后中文变成乱码或者直接启动失败。原因项目使用的字符集设置不匹配——常见的是代码里用了_T(得分)但工程设置成多字节字符集或者 Release 用的运行时库是动态 MFC目标机器没装 VC 运行库。解决在项目属性里把字符集统一设成“使用 Unicode 字符集”同时若是交付给别人运行用“静态链接 MFC”重新编译一次把可独立运行的 exe 交给老师。6. 从 Debug 到 Release把 exe 交付物打磨到“能直接交”6.1 切到 Release 并静态链接 MFC课程设计要交 exe 程序交 Debug 版是大忌——Debug 版依赖调试运行时库体积大、启动慢换台机器经常报缺 dll。我在项目属性里做两步配置管理器切 Release然后把“MFC 的使用”从“使用共享 DLL”改成“在静态库中使用 MFC”。这一步会让 exe 体积增加 12MB但换到没装 VS 的机器也能直接跑。6.2 花十分钟做跨机器验证把生成的 exe 复制到另一台没有装 Visual Studio 的电脑上双击运行测试三件事启动是否报缺库、方向键和空格是否能正常控制、关闭窗口后任务管理器里进程是否消失。如果是 64 位系统确认编译平台是 x64 而不是 Win32如果目标机器是老系统平台选 Win32 兼容性更好。这十几分钟能避免交作业当天在教室投影仪上弹出“缺少 VCRUNTIME140.dll”的尴尬。给设计报告用的三张图在这一步也顺手完善类图、状态图、消息时序图。我的习惯是把实际代码里的类名和方法标注到类图上老师看着和你代码对得上时序图把PreTranslateMessage、OnTimer、OnPaint三个函数之间的箭头画清楚就能完整呈现你对消息驱动机制的理解。分数和速度参数做一张小表放进报告显得整个设计是有数据支撑的。我自己的教训是第一版把Invalidate(TRUE)用在双缓冲里结果内存 DC 被擦除后一片黑花了两个小时查原因。从那以后我养成一个习惯——所有 MFC 绘图代码开头先写双缓冲模板重绘一律Invalidate(FALSE)再也没有为闪烁和黑屏加过班。把这条路走通一遍你收获的不只是能交差的贪吃蛇源码而是对 Windows 消息循环和 GDI 资源管理的一套肌肉记忆这些在后面的课程和实习里会反复用到。希望这篇笔记帮到你。本文还有配套的精品资源点击获取
返回列表