ARTICLE DETAIL

资讯详情

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

MFC桌面抽奖游戏开发:从对话框到随机数生成实战

MFC桌面抽奖游戏开发:从对话框到随机数生成实战 1. 项目概述从零到一用MFC打造一个桌面抽奖小游戏最近在整理旧项目时翻出了一个多年前用MFC写的抽奖小游戏。这个项目虽然不大但麻雀虽小五脏俱全它完整地串联起了MFC对话框应用程序开发的核心流程、C面向对象编程思想以及随机数生成这个看似简单实则暗藏玄机的技术点。对于刚接触Windows桌面开发或者想从控制台程序转向图形界面GUI开发的朋友来说亲手实现这样一个项目远比看十篇理论文章来得有效。这个抽奖小游戏的功能很直观界面上有一个显示区域用于滚动显示候选的“奖品”或“号码”一个开始按钮点击后开始快速滚动一个停止按钮点击后滚动停止并最终定格在一个结果上。其核心逻辑就是利用随机数来模拟抽奖的不可预测性。通过这个项目你不仅能学会如何用Visual Studio搭建一个MFC对话框工程、拖拽控件并为其绑定事件更能深入理解Windows消息循环、控件交互以及如何正确地在C中生成和使用随机数。你会发现很多在控制台里用cin和cout就能搞定的事情在GUI世界里需要换一种“事件驱动”的思维方式。2. 核心思路与方案设计为什么选择MFC对话框在动手写代码之前我们先来聊聊为什么选MFC和对话框。现在C的GUI框架选择不少Qt、wxWidgets都很流行。但对于Windows平台的原生开发尤其是想深入理解Win32 API和消息机制MFCMicrosoft Foundation Classes依然是一个绕不开的经典。它封装了Win32 API用C类的形式提供了一套框架让你能用面向对象的方式开发Windows程序。而“对话框”则是MFC里最快上手的一种应用程序类型它本质上就是一个窗口上面可以摆放按钮、文本框、列表框等各种控件特别适合用来做这种工具类、小游戏类的界面。我们的设计方案很清晰程序类型创建一个基于对话框的MFC应用程序。界面设计在资源编辑器中设计主对话框界面。主要包含一个静态文本Static Text控件或列表框作为抽奖结果的显示区域。一个“开始”按钮Button。一个“停止”按钮Button。可以再加一个静态文本用于显示“恭喜中奖”之类的提示信息。核心逻辑“开始”按钮被点击时启动一个定时器Timer。这个定时器会以极短的时间间隔比如50毫秒不断触发在触发事件中我们生成一个随机数并根据这个随机数更新显示区域的内容制造出“滚动”的视觉效果。“停止”按钮被点击时关闭定时器。此时显示区域的内容停止在最后一次随机数生成的结果上这个结果就是本次抽奖的最终结果。随机数方案这是项目的技术核心之一。C中生成随机数有多种方法我们需要选择一种适合本场景的。注意很多初学者会直接使用C标准库的rand()和srand()。这在简单的演示中没问题但正如微软官方文档所指出的rand()生成的是伪随机数且其随机性质量和范围0到RAND_MAX通常为32767有限不适合对随机性要求高的场景如加密。对于我们的抽奖小游戏虽然要求不高但我们将采用现代C推荐的更健壮的方式作为最佳实践来讲解。3. 开发环境搭建与项目创建工欲善其事必先利其器。我们首先需要准备好开发环境。3.1 安装Visual Studio与MFC支持我使用的是Visual Studio 2019/2022它们对MFC的支持依然很完善。在安装VS时务必在“工作负载”中选择“使用C的桌面开发”并在右侧的“安装详细信息”中勾选“用于x86和x64的Visual C MFC”。这样安装程序就会把MFC所需的库、头文件和资源编辑器等工具一并装好。3.2 创建MFC对话框项目打开Visual Studio点击“创建新项目”。在搜索框里输入“MFC”选择“MFC应用程序”模板点击下一步。给项目起个名字比如“LotteryGame”选择好项目位置。 在接下来的“MFC应用程序向导”中有几个关键步骤在“应用程序类型”页面选择“基于对话框”。这是我们程序的主框架。在“用户界面功能”页面可以保持默认或者取消“关于框”等不需要的选项让项目更简洁。在“高级功能”页面确保“公共控件清单”是选中的默认就是这能确保程序使用新版本的控件样式。其他页面保持默认点击“完成”。向导会自动生成一个最基本的对话框应用程序包含一个主对话框资源和一些核心的C类文件如LotteryGameDlg.h和LotteryGameDlg.cpp它们对应着我们程序的主窗口。3.3 认识自动生成的代码结构项目创建好后花几分钟浏览一下生成的文件LotteryGame.h/cpp包含CWinApp派生类CLotteryGameApp代表应用程序本身。InitInstance函数是程序入口里面创建并显示了我们的主对话框。LotteryGameDlg.h/cpp包含CDialogEx派生类CLotteryGameDlg这就是主对话框对应的类。我们绝大部分的代码都会写在这里。Resource.h和LotteryGame.rc资源文件。LotteryGame.rc里定义了我们的对话框模板、图标、字符串等资源。双击.rc文件可以在VS的资源编辑器中打开进行可视化设计。4. 界面设计与控件布局现在我们来设计抽奖小游戏的界面。在“解决方案资源管理器”中双击打开LotteryGame.rc文件它会自动在资源编辑器中打开。找到名为IDD_LOTTERYGAME_DIALOG的对话框资源这就是我们的主界面。4.1 添加与布置控件从“工具箱”窗口中拖拽以下控件到对话框上静态文本Static Text拖拽一个将其ID改为IDC_STATIC_RESULTCaption标题清空或设为“等待抽奖...”。这个控件将用来动态显示滚动的和中奖的结果。为了醒目你可以通过属性窗口将其字体调大。按钮Button拖拽两个。第一个ID改为IDC_BUTTON_STARTCaption设为“开始抽奖”。第二个ID改为IDC_BUTTON_STOPCaption设为“停止”并初始将其“Disabled”属性设为True因为一开始不能直接停止。另一个静态文本可以再拖拽一个ID为IDC_STATIC_INFOCaption清空用于显示额外的提示信息如“恭喜抽中XXX”。调整控件的位置和大小使其布局美观。一个简单的布局可以是大号的结果显示文本在中间靠上两个按钮并排在下方面。4.2 为控件关联变量为了在代码中操作这些控件比如改变显示的文字、启用/禁用按钮我们需要为它们关联成员变量。在对话框资源编辑器中右键点击IDC_STATIC_RESULT静态文本控件选择“添加变量”。会弹出“添加成员变量向导”。变量类型对于静态文本我们通常关联一个CString类型的Value变量或者一个CONTROL类型的控件变量。为了直接更新文本这里选择Value类别变量类型为CString变量名设为m_strResult。同样地为IDC_STATIC_INFO添加一个CString类型的值变量命名为m_strInfo。对于按钮我们通常不需要值变量但为了在代码中方便地引用它们以修改状态如禁用“开始”按钮可以为IDC_BUTTON_START和IDC_BUTTON_STOP添加Control类别的变量类型为CButton变量名可以设为m_btnStart和m_btnStop。添加完成后向导会在CLotteryGameDlg类的头文件中声明这些变量并在DoDataExchange函数中完成对话框数据交换DDX/DDV的绑定。4.3 为按钮添加事件处理程序界面上的按钮需要响应用户的点击。右键点击“开始抽奖”按钮选择“添加事件处理程序”。在弹出的对话框中消息类型选择BN_CLICKED按钮点击类列表选择CLotteryGameDlg函数处理程序名称会自动生成比如OnBnClickedButtonStart。点击“添加编辑”VS会自动在LotteryGameDlg.cpp中生成该函数的框架并跳转到那里。用同样的方法为“停止”按钮添加BN_CLICKED事件处理程序OnBnClickedButtonStop。5. 核心逻辑实现定时器与随机数生成这是整个项目的代码核心我们将把逻辑填充到刚刚生成的事件处理函数和新增的定时器处理函数中。5.1 定义奖品池与成员变量首先在LotteryGameDlg.h文件的CLotteryGameDlg类定义中添加一些必要的成员变量。// LotteryGameDlg.h class CLotteryGameDlg : public CDialogEx { // ... 其他生成的代码 ... private: CString m_strResult; // 与IDC_STATIC_RESULT关联 CString m_strInfo; // 与IDC_STATIC_INFO关联 CButton m_btnStart; // 与IDC_BUTTON_START关联 CButton m_btnStop; // 与IDC_BUTTON_STOP关联 // 新增成员变量 bool m_bIsRolling; // 标记抽奖是否正在进行滚动中 UINT_PTR m_nTimerID; // 定时器ID std::vectorCString m_vecPrizes; // 奖品池 int m_nFinalIndex; // 最终中奖的索引 // ... 其他生成的代码 ... };然后在LotteryGameDlg.cpp的对话框初始化函数OnInitDialog()中初始化这些变量并填充奖品池。// LotteryGameDlg.cpp BOOL CLotteryGameDlg::OnInitDialog() { CDialogEx::OnInitDialog(); // ... 其他自动生成的初始化代码 ... // 初始化成员变量 m_bIsRolling false; m_nTimerID 0; m_nFinalIndex -1; // 初始化奖品池这里用一些字符串示例 m_vecPrizes.push_back(_T(一等奖笔记本电脑)); m_vecPrizes.push_back(_T(二等奖智能手机)); m_vecPrizes.push_back(_T(三等奖蓝牙耳机)); m_vecPrizes.push_back(_T(幸运奖定制水杯)); m_vecPrizes.push_back(_T(谢谢参与)); m_vecPrizes.push_back(_T(再来一次)); m_vecPrizes.push_back(_T(优惠券一张)); m_vecPrizes.push_back(_T(纪念徽章)); // 初始化界面显示 m_strResult _T(点击开始进行抽奖); m_strInfo.Empty(); UpdateData(FALSE); // 将变量值更新到控件 // 设置按钮初始状态 m_btnStart.EnableWindow(TRUE); m_btnStop.EnableWindow(FALSE); return TRUE; }5.2 实现“开始抽奖”逻辑“开始”按钮的任务是启动视觉上的滚动效果。我们通过设置一个Windows定时器来实现。定时器会每隔一段时间发送WM_TIMER消息我们在这个消息的处理函数中更新显示。首先在CLotteryGameDlg类中为WM_TIMER消息添加处理函数。在类视图Class View中右键点击CLotteryGameDlg选择“属性”点击属性窗口顶部的“消息”图标在列表中找到WM_TIMER添加OnTimer函数。然后实现“开始”按钮的点击事件处理函数。// LotteryGameDlg.cpp void CLotteryGameDlg::OnBnClickedButtonStart() { // 如果已经在滚动则不做任何事 if (m_bIsRolling) return; // 设置状态为滚动中 m_bIsRolling true; // 禁用开始按钮启用停止按钮 m_btnStart.EnableWindow(FALSE); m_btnStop.EnableWindow(TRUE); // 清空或设置提示信息 m_strInfo _T(抽奖进行中...); UpdateData(FALSE); // 设置一个定时器间隔50毫秒定时器ID设为1 // SetTimer的第一个参数是定时器ID第二个是间隔毫秒第三个为NULL表示使用WM_TIMER消息 m_nTimerID SetTimer(1, 50, NULL); if (m_nTimerID 0) { AfxMessageBox(_T(创建定时器失败)); m_bIsRolling false; m_btnStart.EnableWindow(TRUE); m_btnStop.EnableWindow(FALSE); } }5.3 实现定时器逻辑与随机数生成这是最核心的部分。在OnTimer函数中我们需要做两件事生成随机数来选择一个奖品并更新界面显示。这里我们采用现代C11的random库来生成随机数它比传统的rand()更灵活、更健壮。首先在stdafx.h或LotteryGameDlg.cpp文件顶部包含必要的头文件。#include random // 用于现代C随机数 #include chrono // 用于获取时间种子然后实现OnTimer函数。// LotteryGameDlg.cpp void CLotteryGameDlg::OnTimer(UINT_PTR nIDEvent) { // 判断是否是我们的抽奖定时器 if (nIDEvent 1 m_bIsRolling) { // --- 核心使用现代C随机数库 --- // 定义一个静态的随机数引擎使用时间作为种子确保每次运行序列不同 static std::default_random_engine generator(static_castunsigned int(std::chrono::system_clock::now().time_since_epoch().count())); // 定义一个分布器范围是[0, 奖品池大小-1] std::uniform_int_distributionint distribution(0, static_castint(m_vecPrizes.size()) - 1); // 生成一个随机索引 int randomIndex distribution(generator); // 更新显示文本 m_strResult m_vecPrizes[randomIndex]; UpdateData(FALSE); // 更新到控件 // 也可以在这里记录最后一次的索引作为停止时的最终结果 // 但更常见的做法是在“停止”时再生成最终结果。这里为了演示滚动效果每次定时都更新。 } CDialogEx::OnTimer(nIDEvent); }为什么用random而不用rand()质量与范围rand()通常生成0到32767之间的整数且随机数质量可能不高。random库提供了多种高质量的随机数引擎如mt19937梅森旋转算法和灵活的分布器均匀分布、正态分布等可以轻松生成任意范围的整数或实数。可控性你可以明确选择随机数引擎和分布行为更可预测和可控。线程安全C11标准库的随机数组件在正确使用下是线程安全的。实操心得在OnTimer中我们将随机数引擎generator定义为static。这是因为如果每次调用OnTimer都重新创建引擎和用当前时间播种在极短的时间间隔如50ms内system_clock::now()可能返回相同或非常接近的值导致播种相同从而生成相同的随机数序列失去了“随机”效果。定义为静态后引擎只在第一次进入函数时初始化一次后续调用都使用同一个引擎产生序列保证了随机性。5.4 实现“停止抽奖”逻辑“停止”按钮需要做三件事关闭定时器、生成最终的中奖结果、更新界面状态。// LotteryGameDlg.cpp void CLotteryGameDlg::OnBnClickedButtonStop() { // 如果不在滚动中则不做任何事 if (!m_bIsRolling) return; // 1. 关闭定时器 if (m_nTimerID ! 0) { KillTimer(m_nTimerID); m_nTimerID 0; } // 2. 生成最终的中奖结果 // 注意这里需要重新生成一次随机数作为最终结果。 // 因为OnTimer中的随机数是用于滚动的停止瞬间的显示可能不是我们想要“定格”的结果。 // 使用和OnTimer中相同配置的随机数生成器但注意引擎不能是static的否则会延续OnTimer的序列 std::random_device rd; // 非确定性随机数生成器用于播种 std::mt19937 gen(rd()); // 以random_device播种的梅森旋转引擎 std::uniform_int_distribution distrib(0, static_castint(m_vecPrizes.size()) - 1); m_nFinalIndex distrib(gen); CString strFinalPrize m_vecPrizes[m_nFinalIndex]; // 3. 更新界面显示 m_strResult strFinalPrize; m_strInfo.Format(_T(恭喜抽中%s), strFinalPrize); UpdateData(FALSE); // 4. 重置状态 m_bIsRolling false; m_btnStart.EnableWindow(TRUE); m_btnStop.EnableWindow(FALSE); }为什么停止时要重新生成随机数这是一个重要的细节。定时器回调函数OnTimer是异步执行的。当你点击“停止”按钮时我们立即杀死了定时器但最后一次OnTimer调用可能已经发生也可能还没发生。即使我们记录最后一次OnTimer的索引也存在竞态条件。最可靠的做法是在停止的瞬间同步地、独立地生成一个最终的随机数作为结果。这样能确保结果是在用户点击“停止”那一时刻决定的逻辑上更清晰。5.5 完善与收尾别忘了在对话框析构时清理定时器资源。在CLotteryGameDlg类的析构函数中或者在OnDestroy消息处理函数中添加清理代码。// LotteryGameDlg.cpp CLotteryGameDlg::~CLotteryGameDlg() { if (m_nTimerID ! 0) { KillTimer(m_nTimerID); } }至此一个具备基本功能的MFC抽奖小游戏就完成了。编译并运行F5点击“开始抽奖”你会看到奖品名称在快速滚动点击“停止”它会定格在一个结果上并显示祝贺信息。6. 功能扩展与优化思路基础版本完成后我们可以考虑添加更多功能让它更像一个完整的“游戏”。6.1 添加抽奖音效音效能极大提升体验。你可以使用MFC的PlaySound函数或更底层的Win32多媒体API。准备两个WAV格式的音效文件一个用于“开始滚动”如start.wav一个用于“停止中奖”如win.wav。将文件添加到项目的资源文件中。在资源视图中右键点击项目资源选择“添加资源”然后导入WAV文件。在代码中#include mmsystem.h并链接winmm.lib库在项目属性-链接器-输入-附加依赖项中添加。在OnBnClickedButtonStart中调用PlaySound(MAKEINTRESOURCE(IDR_WAVE_START), AfxGetResourceHandle(), SND_RESOURCE | SND_ASYNC);播放开始音效。在OnBnClickedButtonStop中播放中奖音效。6.2 实现更华丽的滚动动画目前的滚动只是文本切换。可以升级为图形化使用List Control或List Box将奖品列表放在一个列表控件中通过定时器快速切换选中项高亮显示。使用GDI绘图在对话框的OnPaint函数或一个静态控件的自绘中使用GDI绘制更炫酷的转盘、跑马灯效果。这需要更多的图形编程知识但效果也最好。6.3 增加奖品权重与概率控制真实的抽奖往往不是完全均匀的。我们可以为每个奖品设置一个权重概率。修改奖品池的数据结构用一个struct包含奖品名和权重。struct PrizeItem { CString name; int weight; // 权重如 1, 5, 10 }; std::vectorPrizeItem m_vecPrizes;在生成最终随机数时不再使用均匀分布而是根据权重计算总权重和生成一个[0, 总权重)的随机数然后遍历奖品池累加权重直到超过随机数从而确定中奖项。这实现了加权随机。6.4 添加抽奖记录与历史在对话框中添加一个列表框List Box控件IDC_LIST_HISTORY。为其关联一个CListBox类型的控件变量m_listHistory。在OnBnClickedButtonStop函数中生成最终结果后将结果字符串和时间戳添加到列表框中。CTime currentTime CTime::GetCurrentTime(); CString strRecord; strRecord.Format(_T([%s] %s), currentTime.Format(_T(%H:%M:%S)), strFinalPrize); m_listHistory.AddString(strRecord);7. 常见问题排查与调试技巧在开发过程中你可能会遇到一些典型问题。7.1 界面更新不显示问题在OnTimer或按钮事件中修改了m_strResult等变量但调用UpdateData(FALSE)后界面上的文本没有变化。排查确认控件ID和关联的变量名是否正确绑定。检查DoDataExchange函数中的DDX_Text等语句。确保在修改成员变量后调用了UpdateData(FALSE)。UpdateData(TRUE)是从控件读取数据到变量UpdateData(FALSE)才是将变量数据写回控件。如果是在多线程中更新界面本例不是必须通过消息传递到UI线程不能直接在其他线程中操作控件或调用UpdateData。7.2 定时器不工作或过于频繁问题点击开始后界面没有滚动效果或者滚动速度异常。排查定时器未启动检查SetTimer的返回值。如果为0表示创建失败例如系统定时器资源耗尽。确保SetTimer在正确的时机被调用如按钮点击事件内。定时器ID混淆在OnTimer函数中第一行代码应判断nIDEvent参数是否等于你设置的定时器ID本例中是1。如果程序中有多个定时器必须通过ID区分。间隔时间太短如果间隔设得太短如1毫秒系统可能无法精确响应并且会过度消耗CPU。对于视觉滚动50-200毫秒的间隔通常足够。如果需要更平滑的动画可以考虑使用多媒体定时器(timeSetEvent)或游戏循环但复杂度会增加。7.3 随机数结果总是相同问题每次运行程序抽奖结果序列或最终结果似乎都是一样的。排查播种问题使用rand()时常见如果使用srand(time(NULL))确保它只在程序开始时调用一次而不是每次生成随机数前都调用。如果在OnTimer中频繁调用srand(time(NULL))而time(NULL)在短时间内值不变就会导致播种相同序列重复。使用了static的rand()相关变量如果错误地使用了静态局部变量来保存rand()的状态也可能导致问题。在我们使用random库的方案中将引擎定义为static是正确的它保证了引擎状态的持续性从而产生看似随机的序列。但引擎的初始化播种应使用高精度时间或std::random_device。检查随机数范围确保分布器的范围设置正确覆盖了整个奖品池索引。7.4 程序崩溃或断言失败问题点击按钮或程序退出时弹出调试断言窗口。排查访问越界在OnTimer中用随机索引访问m_vecPrizes时确保索引在[0, m_vecPrizes.size()-1]范围内。我们的分布器distribution(0, m_vecPrizes.size() - 1)已经保证了这一点。控件变量未初始化确保在DoDataExchange中所有DDX/DDV控件交换调用之前已经调用了CDialogEx::DoDataExchange(pDX)。资源泄漏确保每个SetTimer都有对应的KillTimer。最好在对话框的析构函数或OnDestroy消息处理中检查并清理。使用UpdateData的时机避免在对话框还未完全创建如OnInitDialog中某些操作之前或已销毁时调用UpdateData。7.5 调试技巧使用TRACE宏输出日志在关键位置如OnTimer开始、生成随机数后使用TRACE(_T(“Timer fired, index%d\n”), randomIndex);。这些信息会输出到Visual Studio的输出窗口调试时帮助你跟踪程序流程。设置断点在OnBnClickedButtonStart、OnTimer、OnBnClickedButtonStop函数开始处设置断点逐步执行观察变量值的变化。检查资源ID确保按钮、静态文本等控件的ID在代码如ON_BN_CLICKED宏和资源编辑器中的定义一致。不一致会导致消息无法映射事件处理函数永远不会被调用。8. 项目总结与进阶思考通过这个MFC抽奖小项目我们走完了一个完整的Windows桌面小程序开发流程从环境搭建、项目创建、界面设计、控件关联、事件处理到核心逻辑定时器、随机数的实现。它虽然小但触及了MFC开发中几个非常关键的概念消息映射、对话框数据交换、资源管理、以及Windows编程中常见的定时器使用。在随机数的选择上我们刻意对比了传统的rand()和现代C的random库。对于学习而言了解rand()和srand()的原理是必要的但在实际的新项目中我更推荐使用random库。它不仅仅是“更好”它提供了一种更标准、更安全、功能更强大的随机数生成范式。特别是当你需要非均匀分布如正态分布、泊松分布时random库的优势是无可替代的。这个项目可以作为一个起点进行无限扩展。除了前面提到的音效、动画、权重和历史记录你还可以网络化尝试将抽奖逻辑放在服务器端客户端通过Socket连接实现一个网络抽奖系统。数据库集成使用ODBC或ADO将中奖记录保存到Access或SQLite数据库中。多语言支持利用MFC的资源DLL机制实现中英文界面切换。更复杂的UI使用第三方皮肤库如BCGControlBar来美化界面。最后关于MFC的学习我想说它或许不是当今最时髦的UI框架但它所蕴含的Windows编程思想消息循环、资源、GDI是宝贵的。理解这些再学习Qt、WinForms甚至WPF都会有一种“知其所以然”的通透感。动手把这个小游戏做出来调试通过再试着去添加一两个新功能你收获的将不仅仅是一个程序更是一套解决问题的思维方法。
返回列表