ARTICLE DETAIL

资讯详情

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

MFC俄罗斯方块实战:Win32 GUI开发能力压力测试

MFC俄罗斯方块实战:Win32 GUI开发能力压力测试 简介本资源是一份基于MFC框架实现经典俄罗斯方块游戏的完整C源码工程面向Windows桌面应用开发初学者及C/MFC进阶学习者旨在帮助读者深入理解图形界面编程、游戏逻辑设计与面向对象建模。项目采用标准MFC对话框程序结构包含方块类CBlock、游戏板类CGameBoard等核心模块覆盖方块生成、移动、旋转、碰撞检测与消行判定等关键机制并提供清晰的类设计与状态管理思路。压缩包共33个文件含8个头文件h、7个源文件cpp构成主体逻辑3个图标ico与2个位图bmp支撑界面资源另有sln/suo/dsw等工程配置文件及ReadMe说明文档整体大小1.26MB目录组织规范便于逐模块研读与调试。目前已有60人学习下载适合通过实战项目掌握MFC消息响应、GDI绘图、定时器控制及游戏循环架构的开发者。1. MFC 俄罗斯方块不是怀旧彩蛋而是 Windows 桌面 GUI 工程能力的「压力测试仪」你手头有个刚配好的 Visual Studio 2022想验证自己是否真能驾驭 Windows 原生 UI 开发——不是靠拖控件、不是靠 WPF XAML 绑定而是从消息循环、设备上下文DC、定时器、键盘钩子到双缓冲绘图一整套 Win32 API 的封装逻辑。这时候“MFC 俄罗斯方块”就不是个玩具项目它是工程师对 MFC 框架底层控制力的一次实名制考核。它不依赖任何第三方图形库不调用 DirectX 或 OpenGL纯靠 CDC::Rectangle、CDC::FillSolidRect、CClientDC 和 WM_TIMER 搭建出实时响应、无撕裂、可暂停/加速/重置的完整游戏闭环。新手能靠它吃透 CWnd 消息映射机制和资源管理老手则用它校验自己对 GDI 双缓冲时机、WM_PAINT 重绘边界、以及 CDialog 数据同步的理解是否还经得起实战。尤其当你在现有 VS MFC 工程上要“增加按钮弹出对话框并显示实时数据图表”时俄罗斯方块里那个状态栏实时刷新分数、行数、等级的逻辑就是最贴近生产环境的训练靶心。2. 从零搭建 MFC 框架用向导生成最小可运行骨架再亲手注入游戏内核MFC 俄罗斯方块不是“写个类扔进去就能跑”的脚本工程它必须扎根于 MFC 的文档/视图Document/View或基于 CDialog 的单窗口架构。我们选后者——更轻量、更可控、更适合理解消息流。这一步的目标不是快速出效果而是建立一个可调试、可断点、可逐帧观察 GDI 绘制行为的干净起点。2.1 创建 Dialog-Based 工程并禁用默认控件干扰打开 Visual Studio → 新建项目 → 选择 “MFC 应用程序” → 项目名设为TetrisMFC→ 在向导中关键设置如下应用程序类型基于对话框Dialog based高级功能取消勾选“使用 Unicode 库”避免字符集引发的资源加载异常尤其在中文系统下用户界面功能仅保留“使用标准 Windows 控件样式”其余全取消如“启用视觉样式”会干扰自绘逻辑生成的类名保持默认CTetrisMFCApp和CTetrisMFCDlg提示不要勾选“使用共同控件”或“支持 Windows XP 样式”这些会在OnInitDialog()中自动插入CMFCToolBar::EnableDocking()等无关代码污染主窗口消息处理链。创建完成后立即删除IDD_ABOUTBOX对话框资源右键资源视图 → 删除并在CTetrisMFCDlg::OnInitDialog()中删掉AboutDlg.DoModal()调用。此时编译运行应看到一个空白对话框——这是你即将注入游戏逻辑的纯净画布。2.2 定义核心游戏数据结构用 C 类封装方块逻辑与 MFC UI 解耦游戏规则旋转、碰撞检测、消行必须独立于 UI 层。我们新建TetrisGame.h/.cpp定义如下结构// TetrisGame.h #pragma once #include vector #include array struct Point { int x, y; }; struct Block { std::arrayPoint, 4 cells; int rotation 0; }; class CTetrisGame { public: static constexpr int BOARD_WIDTH 10; static constexpr int BOARD_HEIGHT 20; static constexpr int CELL_SIZE 20; // 像素单位 CTetrisGame(); void Reset(); bool MoveLeft(); bool MoveRight(); bool MoveDown(); bool Rotate(); bool IsGameOver() const { return m_isGameOver; } int GetScore() const { return m_score; } int GetLinesCleared() const { return m_linesCleared; } // 获取当前方块在板上的像素坐标用于绘制 std::vectorCRect GetCurrentBlockRects() const; private: std::arraystd::arrayint, BOARD_WIDTH, BOARD_HEIGHT m_board; Block m_currentBlock; Block m_nextBlock; bool m_isGameOver false; int m_score 0; int m_linesCleared 0; void SpawnNewBlock(); bool IsValidPosition(const Block b, int offsetX 0, int offsetY 0) const; void LockBlock(); void ClearFullRows(); };这个类不继承任何 MFC 类纯 C 实现。所有游戏状态变更移动、旋转、消行都通过成员函数触发返回布尔值表示操作是否合法——这是后续 UI 响应的唯一依据。GetCurrentBlockRects()返回CRect向量直接对接CDC::FillSolidRect避免在 UI 层做坐标转换。2.3 在对话框中集成游戏实例并接管绘图消息在TetrisMFCDlg.h头文件中包含#include TetrisGame.h并在类声明中添加成员// TetrisMFCDlg.h class CTetrisMFCDlg : public CDialogEx { // ... 其他声明 private: CTetrisGame m_game; CBrush m_brushEmpty; // 空格背景刷 CBrush m_brushBlock[7]; // 7 种方块颜色刷I/O/T/S/Z/J/L CFont m_fontStatus; // 状态栏字体 };在OnInitDialog()中初始化// TetrisMFCDlg.cpp BOOL CTetrisMFCDlg::OnInitDialog() { CDialogEx::OnInitDialog(); // 初始化游戏 m_game.Reset(); // 创建颜色刷RGB 值按经典 Tetris 配色 m_brushEmpty.CreateSolidBrush(RGB(30, 30, 30)); m_brushBlock[0].CreateSolidBrush(RGB(0, 255, 255)); // I - Cyan m_brushBlock[1].CreateSolidBrush(RGB(255, 255, 0)); // O - Yellow m_brushBlock[2].CreateSolidBrush(RGB(180, 0, 255)); // T - Purple m_brushBlock[3].CreateSolidBrush(RGB(0, 255, 0)); // S - Green m_brushBlock[4].CreateSolidBrush(RGB(255, 0, 0)); // Z - Red m_brushBlock[5].CreateSolidBrush(RGB(0, 0, 255)); // J - Blue m_brushBlock[6].CreateSolidBrush(RGB(255, 165, 0)); // L - Orange // 创建状态栏字体 m_fontStatus.CreatePointFont(120, _T(Consolas), dc); // 启动游戏主定时器400ms 初始下落间隔 SetTimer(1, 400, nullptr); return TRUE; }关键点在于定时器 ID 设为 1且不使用ON_WM_TIMER()宏映射而是在OnTimer()中手动处理。这是因为我们需要在OnTimer()中同时处理“自动下落”和“用户按键加速”两种节奏统一调度更可控。3. 实现无撕裂双缓冲绘图绕过 WM_PAINT 直接操作内存 DC把 GDI 性能拉满MFC 默认的CDialog::OnPaint()是单缓冲直接往窗口 DC 上画会导致严重闪烁——尤其当方块高速下落时每一帧都在重绘整个游戏区域。我们必须用双缓冲先在内存 DC 中画好整帧再一次性 BitBlt 到屏幕。这不是可选项是 MFC 俄罗斯方块能流畅运行的生死线。3.1 重载 OnPaint 并构建内存 DC 绘制链在TetrisMFCDlg.h中声明protected: virtual void OnPaint() override; void DoDoubleBufferedPaint(CDC* pDC); CBitmap m_bmpBuffer; CDC m_dcBuffer;在OnInitDialog()末尾添加缓冲区初始化// 获取客户区大小 CRect rectClient; GetClientRect(rectClient); // 创建与客户区等大的兼容位图 m_bmpBuffer.CreateCompatibleBitmap(pDC, rectClient.Width(), rectClient.Height()); m_dcBuffer.CreateCompatibleDC(pDC); m_dcBuffer.SelectObject(m_bmpBuffer);OnPaint()实现如下void CTetrisMFCDlg::OnPaint() { CPaintDC dc(this); // 构造时自动调用 BeginPaint DoDoubleBufferedPaint(dc); } void CTetrisMFCDlg::DoDoubleBufferedPaint(CDC* pDC) { // 1. 清空内存 DC填黑底 CRect rectClient; GetClientRect(rectClient); m_dcBuffer.FillSolidRect(rectClient, RGB(0, 0, 0)); // 2. 绘制游戏板10x20 网格 const int cellSize CTetrisGame::CELL_SIZE; const int boardX (rectClient.Width() - CTetrisGame::BOARD_WIDTH * cellSize) / 2; const int boardY 50; // 留出顶部状态栏空间 // 绘制已锁定的方块从 m_game.m_board 读取 for (int y 0; y CTetrisGame::BOARD_HEIGHT; y) { for (int x 0; x CTetrisGame::BOARD_WIDTH; x) { if (m_game.m_board[y][x] ! 0) { int colorIdx m_game.m_board[y][x] - 1; // 1~7 映射到刷子索引 0~6 CRect rect( boardX x * cellSize, boardY y * cellSize, boardX (x 1) * cellSize, boardY (y 1) * cellSize ); m_dcBuffer.FillSolidRect(rect, m_brushBlock[colorIdx]); // 绘制边框增强辨识度 m_dcBuffer.Draw3dRect(rect, RGB(80, 80, 80), RGB(40, 40, 40)); } } } // 3. 绘制当前活动方块 auto rects m_game.GetCurrentBlockRects(); for (const auto r : rects) { CRect screenRect( boardX r.left * cellSize, boardY r.top * cellSize, boardX r.right * cellSize, boardY r.bottom * cellSize ); int colorIdx m_game.GetActiveBlockColorIndex(); // 需在 CTetrisGame 中补充此函数 m_dcBuffer.FillSolidRect(screenRect, m_brushBlock[colorIdx]); m_dcBuffer.Draw3dRect(screenRect, RGB(200, 200, 200), RGB(100, 100, 100)); } // 4. 绘制状态栏文字分数、行数、等级 CString strStatus; strStatus.Format(_T(分数%d 行数%d 等级%d), m_game.GetScore(), m_game.GetLinesCleared(), m_game.GetLevel()); m_dcBuffer.SelectObject(m_fontStatus); m_dcBuffer.SetBkMode(TRANSPARENT); m_dcBuffer.SetTextColor(RGB(200, 200, 200)); m_dcBuffer.TextOut(20, 10, strStatus); // 5. 一次性 BitBlt 到屏幕 pDC-BitBlt(0, 0, rectClient.Width(), rectClient.Height(), m_dcBuffer, 0, 0, SRCCOPY); }注意GetCurrentBlockRects()返回的是逻辑坐标0~9, 0~19需乘以cellSize并加上boardX/Y偏移才能转为屏幕像素坐标。这个转换必须在DoDoubleBufferedPaint()内完成不能放在CTetrisGame类里——因为游戏逻辑不该知道 UI 布局细节。3.2 动态调整定时器间隔实现随等级提升的加速曲线俄罗斯方块的核心体验在于“越玩越快”。我们不在OnTimer()中硬编码SetTimer(1, 200, nullptr)而是让CTetrisGame管理当前下落速度并在每次消行后动态更新// TetrisGame.cpp 补充 int CTetrisGame::GetDropIntervalMs() const { // 经典 Tetris 加速公式level 0→400ms, level 1→300ms, level 2→200ms... 最低 50ms int base 400 - m_level * 100; return std::max(50, base); } // TetrisMFCDlg.cpp 中 OnTimer 实现 void CTetrisMFCDlg::OnTimer(UINT_PTR nIDEvent) { if (nIDEvent 1) { // 主游戏定时器 if (!m_game.IsGameOver()) { if (!m_game.MoveDown()) { // 下落失败 → 锁定方块并检查消行 m_game.LockBlock(); m_game.ClearFullRows(); m_game.SpawnNewBlock(); if (!m_game.IsValidPosition(m_game.m_currentBlock)) { m_game.m_isGameOver true; } } } // 无论是否移动成功都强制重绘因为状态可能变 InvalidateRect(nullptr, FALSE); } CDialogEx::OnTimer(nIDEvent); }然后在ClearFullRows()后添加void CTetrisGame::ClearFullRows() { int cleared 0; for (int y BOARD_HEIGHT - 1; y 0; --y) { bool full true; for (int x 0; x BOARD_WIDTH; x) { if (m_board[y][x] 0) { full false; break; } } if (full) { // 删除该行上方下移 for (int yy y; yy 0; --yy) { for (int x 0; x BOARD_WIDTH; x) { m_board[yy][x] m_board[yy-1][x]; } } // 清空顶行 for (int x 0; x BOARD_WIDTH; x) { m_board[0][x] 0; } cleared; --y; // 重新检查同一行因下移后可能又满 } } if (cleared 0) { m_linesCleared cleared; m_score cleared * 100 * (m_level 1); // 每行基础分 × 等级 if (m_linesCleared (m_level 1) * 10) { m_level; // 关键通知 UI 更新定时器 // 这里不直接 SetTimer而是发自定义消息给对话框 ::PostMessage(m_hWnd, WM_USER 100, 0, 0); } } }在TetrisMFCDlg.h中声明消息处理afx_msg LRESULT OnUpdateSpeed(WPARAM, LPARAM);在TetrisMFCDlg.cpp中实现LRESULT CTetrisMFCDlg::OnUpdateSpeed(WPARAM, LPARAM) { KillTimer(1); SetTimer(1, m_game.GetDropIntervalMs(), nullptr); return 0; }并在消息映射中添加ON_MESSAGE(WM_USER 100, CTetrisMFCDlg::OnUpdateSpeed)这样游戏逻辑通过PostMessage异步通知 UI 层更新定时器彻底解耦且避免在ClearFullRows()中直接调用SetTimer导致的 GDI 状态冲突。4. 键盘与状态栏深度集成让方向键真正驱动游戏状态栏成为实时数据看板MFC 的键盘消息默认被对话框框架拦截用于 Tab 导航必须显式重载PreTranslateMessage才能让方向键、空格键直达游戏逻辑。同时“MFC 状态栏怎么显示”这个高频问题在俄罗斯方块里就落地为一行TextOut——但要让它稳定、不闪烁、不覆盖其他元素需要精细控制。4.1 拦截并分发键盘消息覆盖默认对话框导航行为在TetrisMFCDlg.h中声明virtual BOOL PreTranslateMessage(MSG* pMsg) override;实现如下BOOL CTetrisMFCDlg::PreTranslateMessage(MSG* pMsg) { if (pMsg-message WM_KEYDOWN !m_game.IsGameOver()) { switch (pMsg-wParam) { case VK_LEFT: m_game.MoveLeft(); break; case VK_RIGHT: m_game.MoveRight(); break; case VK_DOWN: m_game.MoveDown(); // 快速下落 break; case VK_UP: m_game.Rotate(); break; case VK_SPACE: // 空格硬下降直接落到最低 while (m_game.MoveDown()) {} break; case P: case p: // 暂停/继续 if (m_bPaused) { SetTimer(1, m_game.GetDropIntervalMs(), nullptr); m_bPaused false; } else { KillTimer(1); m_bPaused true; } break; default: break; } // 关键消费掉这些键阻止它们触发对话框默认行为如按钮焦点切换 if (pMsg-wParam VK_LEFT || pMsg-wParam VK_RIGHT || pMsg-wParam VK_UP || pMsg-wParam VK_DOWN || pMsg-wParam VK_SPACE || pMsg-wParam P || pMsg-wParam p) { return TRUE; // 表示已处理不再传递 } } return CDialogEx::PreTranslateMessage(pMsg); }提示VK_*常量必须包含windows.h且PreTranslateMessage是 MFC 消息泵最早期的钩子比OnKeyDown更早捕获是唯一能可靠拦截方向键的入口。若只用ON_WM_KEYDOWN()会发现方向键根本收不到——因为对话框已把它当导航键吃了。4.2 状态栏信息动态刷新用 TextOut 替代 CStatusBar 控件规避布局陷阱很多教程教用CStatusBar类但在 Dialog-Based 工程中CStatusBar需要额外创建、停靠、管理尺寸极易与CDialog的DockControlBar冲突。而“我要将一些信息显示在状态栏”这个需求本质只是在固定位置持续输出文本。我们用最原始也最可靠的方式在DoDoubleBufferedPaint()的最后阶段用TextOut直接画在内存 DC 的指定坐标。回顾DoDoubleBufferedPaint()中的状态栏绘制段// 4. 绘制状态栏文字分数、行数、等级 CString strStatus; strStatus.Format(_T(分数%d 行数%d 等级%d), m_game.GetScore(), m_game.GetLinesCleared(), m_game.GetLevel()); m_dcBuffer.SelectObject(m_fontStatus); m_dcBuffer.SetBkMode(TRANSPARENT); m_dcBuffer.SetTextColor(RGB(200, 200, 200)); m_dcBuffer.TextOut(20, 10, strStatus);这里20,10是绝对像素坐标距左上角 20px, 10px完全可控。SetBkMode(TRANSPARENT)确保文字背景不遮盖下方黑色底图SetTextColor指定灰白色与深色背景形成高对比。不需要任何CStatusBar对象不占额外资源不引入额外消息循环——这就是 MFC 原生开发的“直球”优势。若需扩展如显示“PAUSED”提示只需加一行if (m_bPaused) { m_dcBuffer.SetTextColor(RGB(255, 100, 100)); m_dcBuffer.TextOut(rectClient.Width() - 120, 10, _T(PAUSED)); }4.3 实现“在现有 VS MFC 工程上增加按钮弹出对话框并显示实时数据图表”的复用路径这个热搜词背后的真实诉求是如何把俄罗斯方块里的实时数据分数、行数、等级导出供其他模块消费答案藏在CTetrisGame类的设计里——它本身就是个纯数据容器所有 getter 都是const成员函数。假设你已有另一个CRealTimeChartDlg对话框含CChartCtrl图表控件只需在CTetrisMFCDlg中添加一个按钮点击时创建并传入引用// 在 TetrisMFCDlg.h 中添加 public: CTetrisGame* GetGamePtr() { return m_game; } // 在按钮事件中 void CTetrisMFCDlg::OnBnClickedBtnChart() { CRealTimeChartDlg dlg; dlg.SetGameSource(GetGamePtr()); // 假设 CRealTimeChartDlg 有此函数 dlg.DoModal(); }CRealTimeChartDlg内部用定时器如SetTimer(2, 500, nullptr)每 500ms 调用m_pGame-GetScore()等函数获取最新值并更新图表。这种“数据源分离 定时轮询”模式正是“在现有工程上增加图表”的标准解法——俄罗斯方块项目天然提供了这个可复用的数据出口。5. 避坑指南MFC 俄罗斯方块开发中 4 个血泪经验换来的致命陷阱MFC 开发最大的坑不是语法而是消息生命周期与 GDI 对象管理的隐式耦合。以下全是我在 VS 2019/2022 上反复翻车后记下的真实案例每一条都对应一个Debug Assertion Failed或GDI handle leak。5.1 现象程序运行几分钟后卡死任务管理器显示 GDI objects 持续上涨至 10000原因在DoDoubleBufferedPaint()中反复CreateCompatibleBitmap却未DeleteObject。常见错误是把m_bmpBuffer声明为局部变量或在OnPaint()中每次都新建位图。解决m_bmpBuffer和m_dcBuffer必须是对话框类的成员变量在OnInitDialog()中创建一次在OnDestroy()中销毁void CTetrisMFCDlg::OnDestroy() { CDialogEx::OnDestroy(); m_bmpBuffer.DeleteObject(); m_dcBuffer.DeleteDC(); }注意DeleteDC()必须在DeleteObject()之后调用否则位图句柄泄漏。5.2 现象按下方向键后对话框按钮获得焦点并触发点击游戏却没响应原因PreTranslateMessage中未正确返回TRUE消费掉按键消息。常见错误是只对VK_LEFT返回TRUE却忘了VK_RIGHT等其他键。解决确保switch分支末尾有return TRUE;且default:分支不执行return TRUE否则所有键都被吞。最佳实践是用布尔标志BOOL CTetrisMFCDlg::PreTranslateMessage(MSG* pMsg) { bool bConsumed false; if (pMsg-message WM_KEYDOWN !m_game.IsGameOver()) { switch (pMsg-wParam) { case VK_LEFT: m_game.MoveLeft(); bConsumed true; break; case VK_RIGHT: m_game.MoveRight(); bConsumed true; break; // ... 其他键 } } if (bConsumed) return TRUE; return CDialogEx::PreTranslateMessage(pMsg); }5.3 现象游戏运行一段时间后方块绘制错位出现“幽灵方块”或颜色混乱原因m_brushBlock数组中的画刷未在OnDestroy()中释放导致 GDI 句柄耗尽后FillSolidRect使用无效刷子。解决在OnDestroy()中显式删除所有画刷void CTetrisMFCDlg::OnDestroy() { CDialogEx::OnDestroy(); m_bmpBuffer.DeleteObject(); m_dcBuffer.DeleteDC(); m_brushEmpty.DeleteObject(); for (int i 0; i 7; i) { m_brushBlock[i].DeleteObject(); } m_fontStatus.DeleteObject(); }提示CBrush、CFont、CPen等 GDI 对象必须手动DeleteObject()MFC 不会自动析构它们。5.4 现象InvalidateRect(nullptr, FALSE)后画面不刷新或只刷新部分区域原因FALSE参数表示不擦除背景但双缓冲绘制时内存 DC 已被FillSolidRect清空无需系统擦除。若误用TRUE系统会先用对话框背景色通常是灰色填充客户区再BitBlt造成闪烁。解决始终坚持InvalidateRect(nullptr, FALSE)并在DoDoubleBufferedPaint()开头用m_dcBuffer.FillSolidRect清空内存 DC。这是双缓冲的铁律——擦除动作由你控制不由系统代劳。6. 进阶技巧用 C20 Concepts 约束游戏逻辑让 MFC 项目具备现代 C 可维护性做到上面五章你已经能写出一个稳定、流畅、可调试的 MFC 俄罗斯方块。但真正的工程价值不在“能跑”而在“好改、好测、好扩”。比如当需求变成“支持多人联机”或“接入 Xbox 手柄”你不想重写整个CTetrisGame。这时把游戏核心抽象成接口用现代 C 特性加固契约就是拉开差距的关键。6.1 用 Concept 限定游戏逻辑的输入输出契约我们不修改现有CTetrisGame而是新增一个概念约束层。在TetrisGame.h顶部添加// TetrisGame.h #include concepts templatetypename T concept TetrisGameLike requires(T t) { { t.Reset() } - std::same_asvoid; { t.MoveLeft() } - std::same_asbool; { t.MoveRight() } - std::same_asbool; { t.MoveDown() } - std::same_asbool; { t.Rotate() } - std::same_asbool; { t.GetScore() } - std::same_asint; { t.GetLinesCleared() } - std::same_asint; { t.IsGameOver() } - std::same_asbool; };然后重构CTetrisMFCDlg中的游戏成员为模板化// TetrisMFCDlg.h templateTetrisGameLike GameType class CTetrisMFCDlgT : public CDialogEx { public: GameType m_game; // ... 其余成员不变 };这样CTetrisMFCDlg就变成了CTetrisMFCDlgTCTetrisGame的特化实例。未来若要替换为网络版游戏逻辑如CNetworkTetrisGame只需确保它满足TetrisGameLike约束UI 层代码一行不用改。6.2 用 std::function 注入渲染回调解耦绘制与逻辑当前GetCurrentBlockRects()返回CRect向量强绑定 GDI。若未来要支持 Direct2D 或打印导出就得改游戏类。更好的方式是让游戏逻辑只提供“数据”绘制由 UI 决定// TetrisGame.h 中添加 using RenderCallback std::functionvoid(const std::vectorPoint, int colorIdx); void RenderCurrentBlock(RenderCallback cb) const;RenderCurrentBlock()内部遍历m_currentBlock.cells对每个Point调用cb(point, colorIdx)。UI 层传入 lambda// TetrisMFCDlg.cpp 中 m_game.RenderCurrentBlock([this, m_dcBuffer, boardX, boardY, cellSize](const std::vectorPoint cells, int colorIdx) { for (const auto p : cells) { CRect rect( boardX p.x * cellSize, boardY p.y * cellSize, boardX (p.x 1) * cellSize, boardY (p.y 1) * cellSize ); m_dcBuffer.FillSolidRect(rect, m_brushBlock[colorIdx]); } });这样CTetrisGame彻底不知道 GDI甚至不知道CRect是什么——它只负责“告诉 UI 哪些格子该画什么颜色”。6.3 用 RAII 封装定时器告别SetTimer/KillTimer手动配对MFC 定时器是典型的资源泄漏高发区。我们写一个ScopedTimer// ScopedTimer.h class ScopedTimer { HWND m_hWnd; UINT_PTR m_nID; public: ScopedTimer(HWND hWnd, UINT_PTR nID, UINT uElapse, TIMERPROC lpTimerFunc nullptr) : m_hWnd(hWnd), m_nID(nID) { ::SetTimer(m_hWnd, m_nID, uElapse, lpTimerFunc); } ~ScopedTimer() { if (m_hWnd m_nID) { ::KillTimer(m_hWnd, m_nID); } } ScopedTimer(const ScopedTimer) delete; ScopedTimer operator(const ScopedTimer) delete; };在CTetrisMFCDlg中// TetrisMFCDlg.h private: ScopedTimer m_mainTimer; // OnInitDialog() 中 m_mainTimer ScopedTimer(m_hWnd, 1, m_game.GetDropIntervalMs(), nullptr);m_mainTimer析构时自动KillTimer再也不用担心OnDestroy()忘写。我带新人时总让他们先写一个 MFC 俄罗斯方块不是为了怀旧而是逼他们亲手捅穿 MFC 的皮——看清楚CWnd消息怎么流转、CDC句柄怎么泄漏、WM_PAINT何时触发、PreTranslateMessage在哪拦截。这些不是过时的知识而是 Windows 桌面开发的底层肌肉记忆。当你能把一个俄罗斯方块从零搭到上线再顺手给现有工程加个实时图表按钮你就真的拿到了那把打开 Windows 原生开发大门的钥匙。希望帮到你。本文还有配套的精品资源点击获取
返回列表