ARTICLE DETAIL

资讯详情

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

MFC+GDI实现俄罗斯方块:Windows原生C++内存安全实践

MFC+GDI实现俄罗斯方块:Windows原生C++内存安全实践 简介这是一份面向VC初学者与MFC开发进阶者的经典游戏实战源码完整实现Windows平台下的俄罗斯方块核心逻辑与交互功能。资源基于Visual Studio 2008开发深度运用MFC框架、GDI图形绘制、多线程控制及Windows消息机制涵盖方块建模、四格组合表示、左右/下移与旋转逻辑、边界碰撞检测、满行消除、暂停控制等六大关键模块适合用于理解桌面应用事件驱动架构与游戏循环设计。压缩包共20个文件含6个头文件.h定义类结构与接口、3个实现文件.cpp承载核心算法、2个图标资源.ico、1个资源脚本.rc及VS工程配置文件.sln/.vcproj/.suo等整体9.81MB目录组织规范便于逐模块研读与调试。已有311人学习下载读者可直接编译运行深入掌握MFC消息映射机制、CDC绘图流程与线程安全控制实践。1. 这不是怀旧彩蛋而是 MFC 工程师的「内存安全压力测试场」用 Windows 原生 C 实现俄罗斯方块直面 GDI 双缓冲撕裂、消息循环阻塞、资源泄漏黑匣子你手头有个 Visual Studio 2008VC 9.0的老项目要维护或者正被客户要求在 Win7/Win10 环境下交付一个无依赖、免安装、启动即用的桌面小游戏——不是 Electron 打包的 HTMLJS 版那种“保存为 tetris.html 就能跑”的方案在企业内网常因策略被禁也不是 Python PyGame 版客户明确说“不要解释器只要 .exe”。这时候MFC GDI 的俄罗斯方块源码就不是玩具代码而是一份可复用的「Windows 原生 UI 开发最小可行范式」它强制你处理 WM_PAINT 消息的重绘边界、管理 CDC 和 CBitmap 的生命周期、在 OnTimer 中做帧同步而非忙等、用 CRect 精确控制方块坐标而非 float 运算漂移。我去年帮某工业 HMI 组态软件团队重构状态栏实时刷新模块就是从这个俄罗斯方块工程里抄了 CStatusBar 的子类化逻辑和 OnUpdateStatusBarText 的线程安全写法。它不炫技但每行代码都在教你怎么让 VC 在真实 Windows 环境里不崩、不卡、不漏——尤其当你面对的是那些连 VC 2008 Runtime 都装不上的老旧工控机时。2. 从空 MFC 对话框工程开始四步搭出俄罗斯方块骨架绕过 VS2008 的 MFC 向导陷阱Visual Studio 2008VC 9.0是当前最稳妥的 MFC 开发环境——它自带完整 MFC 库不依赖后续版本的 redistributable且与 Windows XP/7 兼容性极佳。但它的向导默认生成的是基于 CFormView 的文档/视图架构对俄罗斯方块这种纯窗口逻辑的小游戏反而冗余。我们必须手动构建精简骨架。2.1 创建最小对话框工程禁用“使用 Unicode 字符集”和“使用 MFC 作为共享 DLL”打开 VS2008 → 新建项目 → 选择“MFC 应用程序”项目名设为TetrisMFC。在向导第二步“应用程序类型”中必须选择“基于对话框”第三步“用户界面功能”中取消勾选“使用 Unicode 字符集”否则老设备上 GetWindowTextA 会失效第四步“高级功能”中选择“使用 MFC 作为静态库”避免部署时缺失 msvcr90.dll。这一步看似反直觉——现在都推 Unicode但俄罗斯方块不需要中文输入且静态链接能彻底规避 runtime 安装问题你肯定见过客户机器报错 “ise在win11 win12下报错 vc 2008 runtime libraries are not installed”。// 在 TetrisMFC.h 头文件顶部添加预编译指令防止 vs2008 默认 _UNICODE 宏干扰 #undef _UNICODE #undef UNICODE提示VS2008 默认启用_UNICODE若不显式取消CString 构造函数在 ANSI 环境下会调用错误的宽字符重载导致CString str(Hello);在调试时显示乱码或崩溃。这是血泪经验——我在某次现场调试中花了 3 小时才定位到这行宏定义。2.2 替换默认对话框资源删除所有控件只保留客户区设置窗口风格为 WS_CLIPCHILDREN | WS_CLIPSIBLINGS右键资源视图 → Dialog → IDD_TETRISMFC_DIALOG → 删除所有默认按钮OK/Cancel。进入属性面板将Style改为ChildBorder设为NoneSystem Menu设为False。关键一步在OnInitDialog()函数末尾添加// TetrisMFCDlg.cpp BOOL CTetrisMFCDlg::OnInitDialog() { CDialog::OnInitDialog(); // 移除默认标题栏和边框让游戏全屏渲染 SetWindowLong(m_hWnd, GWL_STYLE, GetWindowLong(m_hWnd, GWL_STYLE) ~(WS_CAPTION | WS_THICKFRAME)); // 强制双缓冲防止 GDI 绘图撕裂Windows 7 有效 SetWindowLong(m_hWnd, GWL_EXSTYLE, GetWindowLong(m_hWnd, GWL_EXSTYLE) | WS_EX_COMPOSITED); // 调整窗口大小为 480x640标准俄罗斯方块比例 MoveWindow(0, 0, 480, 640, TRUE); return TRUE; }这段代码干了三件事去掉标题栏获得纯净客户区启用WS_EX_COMPOSITED让 Windows 合成器接管绘制比手动双缓冲更稳定固定窗口尺寸避免 DPI 缩放错乱。注意WS_EX_COMPOSITED在 Windows XP 上无效但你的目标环境大概率是 Win7所以不必回退到传统双缓冲——那是玄学优化实际增加代码复杂度却收效甚微。2.3 注册自定义窗口类用 RegisterClassEx 替代 MFC 默认注册掌控 WNDPROCMFC 默认注册的窗口类无法拦截底层消息如 WM_KEYDOWN 的方向键必须手动注册。在CTetrisMFCDlg::OnInitDialog()中添加// TetrisMFCDlg.cpp BOOL CTetrisMFCDlg::OnInitDialog() { // ... 前面的 MoveWindow 等代码 // 手动注册窗口类替换 MFC 默认行为 WNDCLASSEX wc {0}; wc.cbSize sizeof(WNDCLASSEX); wc.style CS_HREDRAW | CS_VREDRAW; wc.lpfnWndProc ::TetrisWndProc; // 自定义窗口过程 wc.hInstance AfxGetInstanceHandle(); wc.hCursor LoadCursor(NULL, IDC_ARROW); wc.hbrBackground (HBRUSH)(COLOR_WINDOW1); wc.lpszClassName _T(TetrisGameWindow); if (!RegisterClassEx(wc)) { AfxMessageBox(_T(窗口类注册失败)); return FALSE; } // 创建新窗口并替换原对话框句柄 HWND hWndNew CreateWindowEx(0, _T(TetrisGameWindow), _T(俄罗斯方块), WS_POPUP | WS_VISIBLE, 0, 0, 480, 640, NULL, NULL, AfxGetInstanceHandle(), NULL); if (!hWndNew) { AfxMessageBox(_T(窗口创建失败)); return FALSE; } // 销毁原对话框窗口保留新窗口 DestroyWindow(m_hWnd); m_hWnd hWndNew; return TRUE; } // 全局窗口过程函数必须声明在 .cpp 文件顶部不能在类内 LRESULT CALLBACK TetrisWndProc(HWND hWnd, UINT msg, WPARAM wParam, LPARAM lParam) { static CTetrisMFCDlg* pThis nullptr; if (msg WM_CREATE) { pThis (CTetrisMFCDlg*)((CREATESTRUCT*)lParam)-lpCreateParams; SetWindowLongPtr(hWnd, GWLP_USERDATA, (LONG_PTR)pThis); } else { pThis (CTetrisMFCDlg*)GetWindowLongPtr(hWnd, GWLP_USERDATA); } if (pThis pThis-m_hWnd hWnd) { return pThis-WindowProc(msg, wParam, lParam); // 转发给类成员函数 } return DefWindowProc(hWnd, msg, wParam, lParam); }这段代码是核心转折点它把 MFC 对话框“降级”为普通窗口容器让CTetrisMFCDlg直接接管WM_KEYDOWN、WM_TIMER、WM_PAINT等关键消息。为什么不用 MFC 的PreTranslateMessage因为PreTranslateMessage在模态对话框中可能被截断而俄罗斯方块需要 100% 响应方向键——这是翻车高发区。2.4 初始化游戏状态用位图数组模拟网格避免 new/delete 频繁分配俄罗斯方块的棋盘本质是 10×20 的布尔数组但用bool grid[20][10]在栈上分配易溢出400 字节虽小但嵌套函数调用时风险叠加。我们改用BYTE m_Grid[200]200 字节并用位运算压缩存储// TetrisMFCDlg.h class CTetrisMFCDlg : public CDialog { public: BYTE m_Grid[200]; // 20行×10列 200字节m_Grid[y*10x] 0/1 int m_Score; bool m_IsPaused; void InitGrid() { memset(m_Grid, 0, sizeof(m_Grid)); // 全清空 m_Score 0; m_IsPaused false; } bool IsCellEmpty(int x, int y) { if (x 0 || x 10 || y 0 || y 20) return false; return m_Grid[y * 10 x] 0; } void SetCell(int x, int y, bool occupied) { if (x 0 x 10 y 0 y 20) { m_Grid[y * 10 x] occupied ? 1 : 0; } } };参数说明m_Grid[y * 10 x]是经典一维模拟二维技巧避免指针偏移计算错误BYTE类型确保内存对齐比bool更省且无 paddingmemset比循环赋值快 3 倍以上实测 VS2008 优化后。别用std::vectorbool——MFC 工程禁用 STL 容器这是硬性规范。3. GDI 绘图引擎用内存 DC BitBlt 实现零撕裂渲染把 640×480 分辨率榨出 60FPS俄罗斯方块对帧率不敏感30FPS 足够但对画面稳定性极度敏感——任何一帧撕裂都会让用户觉得“卡”。GDI 本身不支持垂直同步必须靠双缓冲 BitBlt 组合拳解决。3.1 创建兼容内存 DC用 CreateCompatibleDC CreateCompatibleBitmap尺寸严格匹配客户区在CTetrisMFCDlg类中添加成员变量// TetrisMFCDlg.h class CTetrisMFCDlg : public CDialog { private: CDC* m_pMemDC; // 内存设备上下文 CBitmap* m_pMemBmp; // 内存位图 CRect m_ClientRect; // 客户区矩形缓存避免每次 GetClientRect };在OnInitDialog()末尾初始化// TetrisMFCDlg.cpp BOOL CTetrisMFCDlg::OnInitDialog() { // ... 前面所有初始化代码 // 获取客户区尺寸注意此时窗口已调整MoveWindow 生效 GetClientRect(m_ClientRect); // 创建内存 DC 和位图 m_pMemDC new CDC(); m_pMemDC-CreateCompatibleDC(NULL); m_pMemBmp new CBitmap(); m_pMemBmp-CreateCompatibleBitmap(CDC::FromHandle(GetDC()), m_ClientRect.Width(), m_ClientRect.Height()); // 将位图选入内存 DC m_pMemDC-SelectObject(m_pMemBmp); // 预先填充背景色深灰 m_pMemDC-FillSolidRect(m_ClientRect, RGB(30, 30, 30)); return TRUE; }关键点CreateCompatibleBitmap的参数必须是CDC::FromHandle(GetDC())而不是this-GetDC()——后者在 MFC 对话框中返回的是临时 DC句柄可能失效。这是踩坑高频点。3.2 绘制方块用 DrawRect FillSolidRect 模拟像素块禁用抗锯齿俄罗斯方块的方块是硬边矩形绝不能用RoundRect或GradientFill。每个方块单元格设为 24×24 像素480÷1048但留出 2 像素边距// TetrisMFCDlg.cpp void CTetrisMFCDlg::DrawBlock(CDC* pDC, int x, int y, COLORREF color) { CRect rect; rect.left x * 24 48; // 左边距 48px预留状态栏 rect.top y * 24 16; // 上边距 16px预留标题提示 rect.right rect.left 24; rect.bottom rect.top 24; // 填充内部 pDC-FillSolidRect(rect, color); // 绘制 1px 黑色边框 pDC-Draw3dRect(rect, RGB(0,0,0), RGB(100,100,100)); }注意Draw3dRect第三个参数是亮边颜色第四个是暗边颜色这里用RGB(100,100,100)模拟金属质感比纯黑更符合经典 Tetris 视觉。不要用FrameRect——它不支持 3D 效果看起来像简笔画。3.3 主绘图循环OnPaint 中只 BitBlt所有逻辑绘制在内存 DC 完成重写OnPaint()// TetrisMFCDlg.cpp void CTetrisMFCDlg::OnPaint() { CPaintDC dc(this); // device context for painting // 直接从内存 DC 拷贝到位图 dc.BitBlt(0, 0, m_ClientRect.Width(), m_ClientRect.Height(), m_pMemDC, 0, 0, SRCCOPY); }所有游戏逻辑绘制方块、网格线、分数必须在OnTimer中完成写入m_pMemDC然后OnPaint只负责拷贝。为什么因为OnPaint可能被系统频繁触发如窗口被遮挡后恢复而OnTimer是我们可控的帧节奏。如果在OnPaint里做逻辑会导致 CPU 占用飙升——这是新手最常犯的错误。3.4 绘制网格线用 MoveTo/LineTo 画 11 条竖线 21 条横线禁用 GDI 抗锯齿// TetrisMFCDlg.cpp void CTetrisMFCDlg::DrawGrid(CDC* pDC) { // 设置画笔为 1px 黑色 CPen pen(PS_SOLID, 1, RGB(40,40,40)); CPen* pOldPen pDC-SelectObject(pen); // 竖线x48, 72, ..., 288共11条 for (int i 0; i 10; i) { int x 48 i * 24; pDC-MoveTo(x, 16); pDC-LineTo(x, 16 20 * 24); } // 横线y16, 40, ..., 520共21条 for (int i 0; i 20; i) { int y 16 i * 24; pDC-MoveTo(48, y); pDC-LineTo(48 10 * 24, y); } pDC-SelectObject(pOldPen); }提示CPen必须在栈上创建不能new——MFC 的 GDI 对象有句柄计数new后忘记delete会导致 GDI 句柄泄漏Windows 每进程上限 10000 个泄漏 100 次就卡死。这也是为什么我们不用GraphicsPath或Gdiplus——它们在 VS2008 中需额外配置且性能不如原生 GDI。4. 游戏逻辑核心七种方块形态用位图编码旋转算法用查表法规避浮点误差俄罗斯方块的旋转不是几何变换而是离散状态切换。用浮点数计算旋转矩阵会导致坐标漂移最终方块卡在墙里——这是所有 DIY 版本翻车的根源。4.1 方块数据结构用 4×4 位图数组定义 7 种形态每个形态含 4 个相对坐标// TetrisMFCDlg.h struct Tetromino { short x[4]; // 相对于中心点的 x 偏移 short y[4]; // 相对于中心点的 y 偏移 COLORREF color; }; // 全局常量I, O, T, S, Z, J, L 七种形态按标准 Tetris 顺序 const Tetromino g_Tetrominos[7] { // I 形水平一排 {{-1,0,1,2}, {0,0,0,0}, RGB(0,255,255)}, // O 形2×2 方块 {{0,1,0,1}, {-1, -1, 0, 0}, RGB(255,255,0)}, // T 形 {{-1,0,1,0}, {0,0,0,-1}, RGB(180,0,255)}, // S 形 {{0,1,1,2}, {0,0,-1,-1}, RGB(0,255,0)}, // Z 形 {{-1,0,0,1}, {0,0,-1,-1}, RGB(255,0,0)}, // J 形 {{-1,0,0,0}, {0,0,-1,-2}, RGB(0,0,255)}, // L 形 {{0,0,0,1}, {0,0,-1,-2}, RGB(255,165,0)} };关键设计所有坐标以(0,0)为中心点y轴向下为正Windows 坐标系避免数学坐标系转换。short类型足够-2~2 范围比int节省内存。4.2 旋转算法预计算 4 个朝向的坐标表用 mod 4 查表切换每个方块有 4 个朝向我们为每种形态预存 4 组坐标// TetrisMFCDlg.h struct TetrominoState { short x[4]; short y[4]; }; // 预计算 I 形的 4 个朝向其他形态同理此处仅示意 I const TetrominoState g_IStates[4] { // 朝向 0水平 {{-1,0,1,2}, {0,0,0,0}}, // 朝向 1垂直 {{0,0,0,0}, {-2,-1,0,1}}, // 朝向 2水平反向 {{-2,-1,0,1}, {0,0,0,0}}, // 朝向 3垂直反向 {{0,0,0,0}, {-1,0,1,2}} };在CTetrisMFCDlg中添加当前方块状态// TetrisMFCDlg.h class CTetrisMFCDlg : public CDialog { private: int m_CurrentType; // 0~6对应 g_Tetrominos int m_CurrentRot; // 0~3当前旋转朝向 int m_CurX, m_CurY; // 当前方块左上角坐标以网格为单位 public: void RotateCurrent() { m_CurrentRot (m_CurrentRot 1) % 4; // 旋转后检查是否与墙壁/已落下方块冲突 if (!IsValidPosition()) { m_CurrentRot (m_CurrentRot 3) % 4; // 回退 } } bool IsValidPosition() { const TetrominoState state GetRotatedState(); for (int i 0; i 4; i) { int x m_CurX state.x[i]; int y m_CurY state.y[i]; if (x 0 || x 10 || y 0 || y 20) return false; if (y 0 !IsCellEmpty(x, y)) return false; } return true; } const TetrominoState GetRotatedState() { switch (m_CurrentType) { case 0: return g_IStates[m_CurrentRot]; case 1: return g_OStates[m_CurrentRot]; // 需自行补全 // ... 其他 case } return g_IStates[0]; } };血泪经验IsValidPosition()必须在RotateCurrent()中立即调用且失败时必须m_CurrentRot (m_CurrentRot 3) % 4回退不是-1因为负数 mod 在 C 中结果不确定。我曾因用m_CurrentRot--导致旋转卡死调试三天才发现是负数取模 bug。4.3 下落与碰撞检测OnTimer 中实现重力用 IsCellEmpty 判断落地OnTimer是游戏主循环// TetrisMFCDlg.cpp void CTetrisMFCDlg::OnTimer(UINT_PTR nIDEvent) { if (nIDEvent 1 !m_IsPaused) { // ID 1 为游戏主定时器 // 每 500ms 下落一格可调 if (m_DropTimer 0) { m_DropTimer--; } else { m_DropTimer 10; // 重置计数器 // 尝试下落 int newY m_CurY 1; if (CanMoveTo(m_CurX, newY)) { m_CurY newY; } else { // 碰撞固定当前方块到网格 LockCurrentPiece(); // 检查消行 CheckLines(); // 生成新方块 SpawnNewPiece(); } } } CDialog::OnTimer(nIDEvent); } bool CTetrisMFCDlg::CanMoveTo(int x, int y) { const TetrominoState state GetRotatedState(); for (int i 0; i 4; i) { int gx x state.x[i]; int gy y state.y[i]; if (gx 0 || gx 10 || gy 0 || gy 20) return false; if (gy 0 !IsCellEmpty(gx, gy)) return false; } return true; } void CTetrisMFCDlg::LockCurrentPiece() { const TetrominoState state GetRotatedState(); for (int i 0; i 4; i) { int gx m_CurX state.x[i]; int gy m_CurY state.y[i]; if (gy 0) SetCell(gx, gy, true); } }参数说明m_DropTimer是帧计数器比SetTimer(1, 500, NULL)更精准避免系统定时器抖动CanMoveTo封装了位置合法性检查复用IsValidPosition逻辑LockCurrentPiece直接写入m_Grid不涉及 UI——UI 更新在OnPaint中统一处理。5. 避坑指南MFC 俄罗斯方块开发中 5 个必踩的“静默崩溃点”这些坑不会报错但会让你调试到怀疑人生。它们藏在 MFC 生命周期、GDI 资源管理和 Windows 消息机制的缝隙里。5.1 现象程序运行几分钟后窗口变白CPU 占用 100%任务管理器显示 GDI 对象数超 9900原因OnPaint()中未释放CPaintDC或DrawGrid()中CPen未正确恢复。MFC 的CPaintDC构造函数会调用BeginPaint析构时自动EndPaint但若你在OnPaint中new了CDC却没deleteGDI 句柄持续累积。解决所有 GDI 对象CPen,CBrush,CFont必须在栈上创建或用SelectObject后立即SelectObject(pOldObj)恢复。检查DrawBlock和DrawGrid是否遗漏SelectObject(pOldPen)。5.2 现象方向键偶尔失灵特别是快速连按左/右时方块卡在边界不动原因WM_KEYDOWN消息被PreTranslateMessage截获或OnKeyDown中未调用GetKeyState(VK_LEFT)检查重复按键。Windows 默认对长按按键发送WM_KEYDOWN间隔为 250ms但俄罗斯方块需要 50ms 响应。解决在OnKeyDown中用GetAsyncKeyState轮询非阻塞void CTetrisMFCDlg::OnKeyDown(UINT nChar, UINT nRepCnt, UINT nFlags) { if (nChar VK_LEFT CanMoveTo(m_CurX - 1, m_CurY)) { m_CurX--; } else if (nChar VK_RIGHT CanMoveTo(m_CurX 1, m_CurY)) { m_CurX; } else if (nChar VK_DOWN) { // 快速下落直接触发一次下落不走定时器 if (CanMoveTo(m_CurX, m_CurY 1)) m_CurY; } else if (nChar VK_UP) { RotateCurrent(); } CDialog::OnKeyDown(nChar, nRepCnt, nFlags); }5.3 现象在 Win10 高 DPI 显示器上方块严重变形网格线错位原因VS2008 默认不启用 DPI 感知Windows 强制缩放导致GetClientRect返回逻辑像素而非物理像素。解决在TetrisMFCDlg.cpp的InitInstance()中添加// 在 CWinApp::InitInstance() 中CreateDialog 之前 SetProcessDPIAware(); // Windows Vista API并在OnInitDialog()中用GetDeviceCaps(LOGPIXELSX)校准int dpiX, dpiY; HDC hdc GetDC(NULL); dpiX GetDeviceCaps(hdc, LOGPIXELSX); dpiY GetDeviceCaps(hdc, LOGPIXELSY); ReleaseDC(NULL, hdc); // 用 dpiX/dpiY 缩放坐标此处略实际需重算 24px 单元格尺寸5.4 现象关闭窗口时程序卡死调试器显示CBitmap::DeleteObject在析构中死锁原因CBitmap和CDC的析构顺序错误。m_pMemDC持有m_pMemBmp若先delete m_pMemBmp再delete m_pMemDCm_pMemDC析构时会尝试释放已销毁的位图句柄。解决在OnDestroy()中严格按依赖顺序清理void CTetrisMFCDlg::OnDestroy() { CDialog::OnDestroy(); // 先从 DC 中移除位图 if (m_pMemDC m_pMemBmp) { m_pMemDC-SelectObject(CBitmap::FromHandle((HBITMAP)GetStockObject(NULL_BITMAP))); } // 再 delete 位图 if (m_pMemBmp) { delete m_pMemBmp; m_pMemBmp nullptr; } // 最后 delete DC if (m_pMemDC) { delete m_pMemDC; m_pMemDC nullptr; } }5.5 现象生成新方块时偶尔出现“幽灵方块”——方块部分像素残留或颜色错乱原因m_pMemDC的背景未在每次OnTimer绘制前清除旧帧残留。GDI 不自动清屏BitBlt只拷贝不擦除。解决在OnTimer末尾、InvalidateRect之前强制重绘背景void CTetrisMFCDlg::OnTimer(UINT_PTR nIDEvent) { // ... 游戏逻辑 // 关键每次逻辑更新后清空内存 DC 背景 m_pMemDC-FillSolidRect(m_ClientRect, RGB(30,30,30)); // 绘制网格、当前方块、已落地方块 DrawGrid(m_pMemDC); DrawAllBlocks(m_pMemDC); // 触发重绘 InvalidateRect(m_ClientRect, FALSE); CDialog::OnTimer(nIDEvent); }6. 状态栏与实时反馈用 CStatusBar 子类化显示分数/等级/下一块预览避开 MFC 状态栏“文字不显示”玄学客户说“我要将一些信息显示在状态栏”但直接用CStatusBar::SetPaneText常失败——这不是 bug而是 MFC 状态栏需手动设置 pane 宽度且不响应WM_SIZE。我们必须子类化并接管绘制。6.1 创建 CCustomStatusBar重写 DrawItem 处理自定义文本与方块预览// CustomStatusBar.h class CCustomStatusBar : public CStatusBar { DECLARE_DYNAMIC(CCustomStatusBar) public: CCustomStatusBar(); virtual ~CCustomStatusBar(); protected: afx_msg void OnDrawItem(int nIDCtl, LPDRAWITEMSTRUCT lpDrawItemStruct); DECLARE_MESSAGE_MAP() public: int m_Score; int m_Level; int m_NextType; // 下一块类型索引 void UpdateStatus(int score, int level, int nextType) { m_Score score; m_Level level; m_NextType nextType; Invalidate(); } };// CustomStatusBar.cpp IMPLEMENT_DYNAMIC(CCustomStatusBar, CStatusBar) CCustomStatusBar::CCustomStatusBar() : m_Score(0), m_Level(1), m_NextType(0) {} void CCustomStatusBar::OnDrawItem(int nIDCtl, LPDRAWITEMSTRUCT lpDrawItemStruct) { CDC* pDC CDC::FromHandle(lpDrawItemStruct-hDC); CRect rect lpDrawItemStruct-rcItem; // 清空背景 pDC-FillSolidRect(rect, RGB(40,40,40)); // 绘制分数左端 CString scoreStr; scoreStr.Format(_T(分数%d), m_Score); pDC-SetTextColor(RGB(0,255,0)); pDC-DrawText(scoreStr, rect, DT_LEFT | DT_VCENTER | DT_SINGLELINE); // 绘制等级中端 CString levelStr; levelStr.Format(_T(等级%d), m_Level); CRect midRect rect; midRect.left rect.Width() / 3; midRect.right rect.Width() * 2 / 3; pDC-SetTextColor(RGB(255,255,0)); pDC-DrawText(levelStr, midRect, DT_CENTER | DT_VCENTER | DT_SINGLELINE); // 绘制下一块预览右端24×24 区域 CRect previewRect rect; previewRect.left rect.Width() * 2 / 3 10; previewRect.right previewRect.left 24; previewRect.top rect.top 4; previewRect.bottom previewRect.top 24; // 用 g_Tetrominos[m_NextType].color 填充预览区 pDC-FillSolidRect(previewRect, g_Tetrominos[m_NextType].color); pDC-Draw3dRect(previewRect, RGB(0,0,0), RGB(100,100,100)); }6.2 在主对话框中集成用 CreateStatusBar 创建并设置 pane 数量与宽度在CTetrisMFCDlg::OnInitDialog()中// TetrisMFCDlg.cpp BOOL CTetrisMFCDlg::OnInitDialog() { // ... 前面所有初始化 // 创建自定义 p a hrefhttps://download.csdn.net/download/JczmDeveloper/85428918 stylecolor:#ec7500;font-size:14px; 本文还有配套的精品资源点击获取 /a img altmenu-r.4af5f7ec.gif srchttps://csdnimg.cn/release/wenkucmsfe/public/img/menu-r.4af5f7ec.gif stylewidth:16px;margin-left:4px;vertical-align:text-bottom;cursor:text; /p
返回列表