ARTICLE DETAIL

资讯详情

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

VS2019 MFC计算器开发实战:从环境搭建到状态机设计

VS2019 MFC计算器开发实战:从环境搭建到状态机设计 1. 项目概述与核心价值最近在整理一些老项目的代码翻到了一个用C MFC在VS2019里写的计算器。这玩意儿虽然看起来简单就是个标准四则运算器但麻雀虽小五脏俱全。对于刚接触Windows桌面开发特别是想从控制台程序转向有图形界面的朋友来说用MFC做一个计算器是一个绝佳的练手项目。它几乎涵盖了MFC对话框程序的所有基础要素界面布局、控件事件响应、数据验证、业务逻辑与界面分离甚至还能浅浅地涉及到一些状态管理。网上很多教程要么太老用的还是VC6.0或者VS2008环境配置就劝退一片要么就是只给个最终代码中间的坑一个不提。今天我就把这个项目的完整实现过程包括那些容易踩坑的细节从头到尾捋一遍附带可以直接在VS2019里跑起来的代码。这个计算器实现的是基础功能加、减、乘、除、清空、退格以及连续运算。我们会采用经典的MFC对话框应用程序架构。通过这个项目你不仅能学会如何在VS2019中创建和配置MFC项目更能理解消息映射机制如何将按钮点击转化为具体的运算指令以及如何设计一个健壮、不易出错的运算逻辑。代码我会放在最后但更重要的是中间的设计思路和调试过程这些才是你以后做其他项目时真正用得上的东西。2. 开发环境搭建与项目创建2.1 VS2019中MFC支持的安装很多新手第一步就卡住了打开VS2019新建项目发现找不到MFC的选项。这是因为在默认安装模式下VS2019不会包含用于桌面开发的C MFC组件。你需要通过Visual Studio安装程序手动添加。操作步骤如下关闭所有VS实例在Windows开始菜单找到“Visual Studio Installer”并打开。找到你已安装的VS2019版本点击“修改”。在弹出的工作负载选项卡中你需要确保“使用C的桌面开发”这一项是被勾选的。但这还不够关键在“单个组件”选项卡里。在搜索框输入“MFC”你会看到两个关键组件“用于最新v142生成工具的C MFCx86 x64”和可选的“MFC用于ATL支持”。勾选前者这是核心。如果你计划进行一些更底层的COM操作可以一并勾选后者。然后点击右下角的“修改”按钮等待安装程序下载并安装这些组件。这个过程可能需要一些时间取决于你的网速。注意务必确认安装的是对应你VS2019版本v142工具集的MFC组件。如果你之前安装过其他版本的VS如2017系统里可能存在多个版本的MFC库在项目属性设置时需要留意避免链接错误。2.2 创建MFC对话框项目组件安装成功后启动VS2019点击“创建新项目”。在项目模板搜索框输入“MFC”选择“MFC应用程序”注意图标别选成了ATL项目。点击“下一步”。在“配置项目”页面给你的项目起个名字比如“SimpleCalculator”。选择项目存放的位置。注意“解决方案”和“解决方案名称”默认会和项目名一致放在一个文件夹里这对于单个小项目来说很清晰点击“创建”。接下来是MFC应用程序向导这里有几处关键配置应用程序类型选择“基于对话框”。我们不需要文档/视图架构那种复杂的结构一个简单的对话框足矣。“资源语言”根据你需求选择这里选“中文简体中国”。用户界面功能保持默认即可。“使用经典菜单”可以勾选但我们计算器通常不需要顶部菜单栏所以不勾选。“关于框”可以保留它是一个标准的对话框用于显示版本信息。高级功能通常不需要改动。确保“公共控件清单”是启用的这关系到界面控件的外观比如是否启用Visual Style。生成的类最后一步向导会展示即将生成的类。最主要的是CSimpleCalculatorDlg类它继承自CDialogEx是我们程序的主窗口类。你可以在这里修改类名或头文件名但一般保持默认就好。点击“完成”VS2019会自动为你生成一个MFC对话框应用程序的骨架。编译并运行F5你应该能看到一个空白的对话框窗口标题是你的项目名。这说明你的MFC开发环境已经搭建成功。3. 计算器界面设计与控件布局3.1 界面元素规划与控件选择计算器的界面我们模仿经典Windows计算器的布局。主要分为两大区域显示区和按键区。显示区需要一个控件来显示输入的数字、运算符和最终结果。这里我们选用Edit Control编辑框控件。为什么不直接用Static Text因为编辑框自带光标、文本选择、以及通过程序控制文本内容SetWindowText/GetWindowText非常方便虽然我们禁用了用户直接键盘输入但编程操作很顺手。按键区包括数字按钮0-9、运算符按钮 - * / 、功能按钮清除C 退格← 小数点.。这些全部使用Button Control按钮控件。我们需要考虑一个细节计算器的显示框应该只能显示不允许用户用键盘胡乱输入字符干扰计算逻辑。这可以通过设置编辑框的属性为“只读”来实现。3.2 使用对话框编辑器进行布局在“解决方案资源管理器”中展开“资源文件”双击打开SimpleCalculator.rc文件再展开其中的Dialog文件夹双击IDD_SIMPLECALCULATOR_DIALOG。这会打开对话框资源编辑器。首先调整对话框大小拖拽右下角使其变成一个纵向的矩形适合计算器的形状。删除向导自动生成的“TODO在此放置对话框控件。”静态文本和“确定”、“取消”按钮。然后从“工具箱”面板如果没看到可以从“视图”菜单打开拖拽控件拖拽一个Edit Control到对话框顶部拉长至几乎与对话框同宽。这是我们的显示屏。关键设置选中这个编辑框在“属性”面板通常在右侧找到“Styles”下的Read-Only将其设置为True。再找到“外观”下的Align text选择Right右对齐这样数字就像真的计算器一样靠右显示。为了更美观还可以修改Font属性选择一个等宽字体如Consolas并调大字号。拖拽Button Control来创建按键。建议先制作一个数字“1”的按钮调整好大小和字体。然后使用复制粘贴CtrlC, CtrlV来快速创建其他数字和运算符按钮这样可以保证所有按钮大小一致。粘贴后只需修改新按钮的Caption属性显示的文字和ID属性控件的唯一标识符。控件ID命名规范建议为了代码清晰不要使用默认的IDC_BUTTON1这样的ID。建议按功能命名例如数字按钮IDC_BUTTON_0,IDC_BUTTON_1, ...IDC_BUTTON_9运算符按钮IDC_BUTTON_ADD,IDC_BUTTON_SUB,IDC_BUTTON_MUL,IDC_BUTTON_DIV等号IDC_BUTTON_EQU清除IDC_BUTTON_CLR退格IDC_BUTTON_BCK小数点IDC_BUTTON_DOT按照计算器常见的布局3x4数字键右侧一列运算符排列好所有按钮。你可以使用对话框编辑器提供的“使宽度相同”、“使高度相同”、“左对齐”、“水平居中”等布局工具来快速对齐控件让界面看起来整齐专业。4. 核心计算逻辑与状态设计在动手写代码关联按钮之前我们必须先想清楚计算器的大脑——也就是运算逻辑和状态管理。这是本项目最核心的部分设计的好坏直接决定了代码是清晰优雅还是一团乱麻。4.1 运算模型与状态变量一个支持连续运算如输入3 4 * 5 的计算器需要维护几个关键状态。我们在主对话框类CSimpleCalculatorDlg中声明一些成员变量来保存这些状态。打开SimpleCalculatorDlg.h文件在CSimpleCalculatorDlg类定义的protected区域添加以下变量// SimpleCalculatorDlg.h class CSimpleCalculatorDlg : public CDialogEx { // ... 其他代码 ... protected: CString m_strDisplay; // 显示在编辑框中的字符串 double m_dOperand1; // 第一个操作数 double m_dOperand2; // 第二个操作数 int m_nOperator; // 当前选择的运算符 (用整数常量代表) BOOL m_bWaitForNewNumber; // 标志是否等待输入新数字按了运算符或等号后置为TRUE BOOL m_bHasDecimal; // 标志当前输入的数字是否已包含小数点 };变量作用解析m_strDisplay与编辑框控件关联始终反映当前应该显示的内容。使用CString操作字符串非常方便。m_dOperand1和m_dOperand2双精度浮点数用于存储参与运算的两个数。m_dOperand1也经常作为累计结果的暂存地。m_nOperator用一个整数来标识当前选择的运算。我们可以定义一些常量例如OP_NONE0OP_ADD1OP_SUB2OP_MUL3OP_DIV4。m_bWaitForNewNumber这是一个非常重要的状态标志。当用户点击了运算符-*/或等号之后下一次按数字键应该开始输入一个新的数字而不是追加到当前显示的数字后面。这个标志为TRUE时就表示处于这种“等待新数”的状态。m_bHasDecimal用于防止用户在一个数字里输入多个小数点。当用户点击了小数点按钮后此标志置为TRUE直到开始输入新数字时再重置。4.2 运算流程与状态机计算器的行为可以看作一个简单的状态机。我们梳理一下典型操作序列如计算3 4 初始状态显示0m_bWaitForNewNumber TRUE因为一开始就可以输入新数m_nOperator OP_NONE。用户按3因为m_bWaitForNewNumber为真所以清空显示或从0开始显示3然后将m_bWaitForNewNumber设为FALSE表示正在输入这个数的后续位。用户按此时需要将当前显示的数字3保存到m_dOperand1中同时记录运算符m_nOperator OP_ADD并设置m_bWaitForNewNumber TRUE等待输入第二个数。用户按4因为m_bWaitForNewNumber为真清空显示开始显示4并置m_bWaitForNewNumber FALSE。用户按将当前显示的数字4保存到m_dOperand2中。根据m_nOperatorOP_ADD对m_dOperand1和m_dOperand2执行加法运算将结果7存回m_dOperand1以便支持连续运算同时显示结果7。最后设置m_bWaitForNewNumber TRUE因为结果显示后再按数字应开始新输入。对于连续运算如3 4 5 关键在于第5步按完第一个后输入4再按第二个时此时m_bWaitForNewNumber为假正在输入4所以需要立即计算34得到7显示7并将7存入m_dOperand1同时记录新的运算符第二个再等待输入5。这个逻辑确保了运算符键除了“记录操作”外有时也触发“立即计算”。5. 控件事件绑定与消息处理界面和逻辑设计好后就要让按钮“活”起来。MFC通过“消息映射”机制将控件事件如按钮点击关联到类的成员函数。5.1 为按钮添加事件处理程序在对话框编辑器中右键点击数字按钮“1”IDC_BUTTON_1选择“添加事件处理程序”。在弹出的对话框中“消息类型”选择BN_CLICKED按钮点击类列表选择CSimpleCalculatorDlg。点击“添加编辑”VS会自动在SimpleCalculatorDlg.cpp中创建消息映射条目和对应的函数框架并跳转到该函数体。按照此方法为所有数字按钮0-9、小数点按钮、运算符按钮 - * /、等号按钮、清除按钮、退格按钮都添加BN_CLICKED事件处理程序。函数名可以保持默认如OnBnClickedButton1但为了可读性我建议在添加时修改一下比如数字按钮的函数名可以叫OnDigitClicked然后在函数内部根据按钮ID来区分具体是哪个数字。不过为了教学清晰我们先为每个按钮创建独立的处理函数。5.2 初始化对话框与变量在CSimpleCalculatorDlg类的OnInitDialog函数中我们需要初始化所有成员变量和显示控件。找到BOOL CSimpleCalculatorDlg::OnInitDialog()函数在调用父类的OnInitDialog()之后添加初始化代码BOOL CSimpleCalculatorDlg::OnInitDialog() { CDialogEx::OnInitDialog(); // ... 其他自动生成的代码如设置图标等... // 初始化计算器状态 m_strDisplay _T(0); m_dOperand1 0.0; m_dOperand2 0.0; m_nOperator OP_NONE; // 需要在头文件定义这些常量例如 enum {OP_NONE, OP_ADD, OP_SUB, OP_MUL, OP_DIV}; m_bWaitForNewNumber TRUE; m_bHasDecimal FALSE; // 将初始化后的显示字符串更新到编辑框控件 UpdateDisplay(); return TRUE; }UpdateDisplay()是我们需要自己写的一个辅助函数它的作用就是将成员变量m_strDisplay的内容设置到ID为IDC_EDIT_DISPLAY这是你之前给显示编辑框设置的ID的控件上。void CSimpleCalculatorDlg::UpdateDisplay() { SetDlgItemText(IDC_EDIT_DISPLAY, m_strDisplay); }5.3 实现数字按钮的处理函数所有数字按钮0-9的处理逻辑是相似的将对应的数字字符追加到显示字符串的末尾。但需要根据m_bWaitForNewNumber标志决定是追加还是重新开始。以IDC_BUTTON_1的处理函数OnBnClickedButton1为例void CSimpleCalculatorDlg::OnBnClickedButton1() { AppendDigitToDisplay(_T(1)); }我们将核心逻辑抽离成一个公共函数AppendDigitToDisplay(TCHAR chDigit)这样所有数字按钮的处理函数只需调用它并传入不同的字符即可。void CSimpleCalculatorDlg::AppendDigitToDisplay(TCHAR chDigit) { // 如果处于等待新数字状态或者当前显示的就是0 if (m_bWaitForNewNumber || m_strDisplay _T(0)) { m_strDisplay.Empty(); // 清空显示 m_bWaitForNewNumber FALSE; // 结束等待状态 m_bHasDecimal FALSE; // 新数字开始重置小数点标志 } // 追加数字字符 m_strDisplay chDigit; // 更新显示 UpdateDisplay(); }这样数字按钮2-9以及0的处理函数都类似只是传入的字符不同。注意对于0按钮逻辑稍微特殊一点因为要处理“显示0”和“追加0”的情况但上面的AppendDigitToDisplay函数已经通过判断m_strDisplay _T(0)处理了初始状态所以可以直接复用。5.4 实现运算符按钮的处理函数运算符按钮 - * /的逻辑是核心中的核心。它需要处理两种场景1. 首次设置运算符2. 连续运算中更换运算符。我们以加法按钮IDC_BUTTON_ADD的处理函数为例void CSimpleCalculatorDlg::OnBnClickedButtonAdd() { PerformPendingOperation(); // 如果已有待处理的操作先执行它 m_nOperator OP_ADD; // 设置当前运算符为加法 StoreDisplayToOperand1(); // 将当前显示的值存储为第一个操作数 m_bWaitForNewNumber TRUE; // 等待用户输入第二个数 }这里引入了两个新的辅助函数PerformPendingOperation()检查m_nOperator是否是一个有效的运算符非OP_NONE并且m_bWaitForNewNumber是否为FALSE表示用户已经输入了第二个数的一部分。如果条件满足则执行m_dOperand1和当前显示值作为m_dOperand2的运算并将结果显示出来结果同时存回m_dOperand1。这个函数实现了“连续运算”中当你输入3 4 时按下第二个会先计算出7的功能。StoreDisplayToOperand1()将当前显示字符串m_strDisplay转换为双精度浮点数并存储到m_dOperand1中。可以使用_ttof函数进行转换。减法、乘法、除法的处理函数与此完全类似只需改变m_nOperator为对应的常量即可。5.5 实现等号、清除、退格和小数点功能等号按钮等号的处理类似于运算符按钮但它执行运算后会将运算符重置为OP_NONE表示一次完整的计算结束。void CSimpleCalculatorDlg::OnBnClickedButtonEqu() { if (m_nOperator ! OP_NONE !m_bWaitForNewNumber) { // 将当前显示值作为第二个操作数 m_dOperand2 _ttof(m_strDisplay); // 根据运算符进行计算 CalculateResult(); // 计算完成后重置运算符并设置等待新数字状态 m_nOperator OP_NONE; m_bWaitForNewNumber TRUE; } }CalculateResult()函数根据m_nOperator执行具体的四则运算并将结果格式化后存入m_strDisplay同时更新m_dOperand1为结果值。清除按钮最简单直接调用一个ResetCalculator()函数将所有状态变量和显示重置为初始值。void CSimpleCalculatorDlg::OnBnClickedButtonClr() { ResetCalculator(); } void CSimpleCalculatorDlg::ResetCalculator() { m_strDisplay _T(0); m_dOperand1 0.0; m_dOperand2 0.0; m_nOperator OP_NONE; m_bWaitForNewNumber TRUE; m_bHasDecimal FALSE; UpdateDisplay(); }退格按钮从显示字符串末尾删除一个字符。如果删除后字符串为空或者只剩下一个负号则显示0并设置m_bWaitForNewNumber TRUE。还需要注意如果删除了小数点要将m_bHasDecimal标志重置为FALSE。void CSimpleCalculatorDlg::OnBnClickedButtonBck() { int nLen m_strDisplay.GetLength(); if (nLen 1) { // 检查删除的是否是小数点 if (m_strDisplay.GetAt(nLen - 1) _T(.)) { m_bHasDecimal FALSE; } m_strDisplay m_strDisplay.Left(nLen - 1); // 取左边nLen-1个字符 } else { m_strDisplay _T(0); m_bWaitForNewNumber TRUE; } UpdateDisplay(); }小数点按钮首先检查m_bHasDecimal标志如果当前数字还没有小数点则追加一个小数点.并将m_bHasDecimal设为TRUE。同样需要处理m_bWaitForNewNumber状态。void CSimpleCalculatorDlg::OnBnClickedButtonDot() { if (m_bWaitForNewNumber) { m_strDisplay _T(0); m_bWaitForNewNumber FALSE; } if (!m_bHasDecimal) { m_strDisplay _T(.); m_bHasDecimal TRUE; UpdateDisplay(); } }6. 关键代码实现与难点解析6.1 执行待定运算函数详解PerformPendingOperation()是保证连续运算正确的关键。它的实现需要仔细处理状态。void CSimpleCalculatorDlg::PerformPendingOperation() { // 只有在已有运算符且不处于“等待新数”状态时才需要执行前一个运算 // “不处于等待新数”意味着用户已经在输入第二个操作数了 if (m_nOperator ! OP_NONE !m_bWaitForNewNumber) { // 将当前显示的内容作为第二个操作数 m_dOperand2 _ttof(m_strDisplay); // 执行计算 CalculateResult(); // 计算后结果显示在m_strDisplay中同时m_dOperand1也更新为结果 // 此时m_dOperand1已经保存了最新的结果作为下一次运算的第一个操作数 // 注意这里不重置m_nOperator也不设置m_bWaitForNewNumber // 因为调用此函数后外部函数如运算符处理函数可能会设置新的运算符 } }6.2 核心计算函数与除零处理CalculateResult()函数负责具体的算术运算和错误处理。void CSimpleCalculatorDlg::CalculateResult() { double dResult 0.0; BOOL bError FALSE; switch (m_nOperator) { case OP_ADD: dResult m_dOperand1 m_dOperand2; break; case OP_SUB: dResult m_dOperand1 - m_dOperand2; break; case OP_MUL: dResult m_dOperand1 * m_dOperand2; break; case OP_DIV: if (fabs(m_dOperand2) 1e-12) // 判断除数是否为零考虑浮点误差 { bError TRUE; m_strDisplay _T(错误除零); } else { dResult m_dOperand1 / m_dOperand2; } break; default: // 不应该执行到这里 dResult m_dOperand2; // 如果没有运算符直接显示第二个操作数 break; } if (!bError) { // 将结果转换为字符串。这里需要处理格式化例如避免显示过多小数位。 FormatResult(dResult); // 将结果存储回m_dOperand1以便后续连续运算 m_dOperand1 dResult; } else { // 发生错误如除零更新显示即可不更新m_dOperand1 UpdateDisplay(); } }FormatResult(double dValue)是一个辅助函数用于将双精度浮点数格式化为一个美观的字符串。一个简单的实现是使用CString的Format方法并控制小数位数例如void CSimpleCalculatorDlg::FormatResult(double dValue) { // 如果结果是整数则显示为整数格式否则显示为带两位小数的格式 if (fabs(dValue - (int)dValue) 1e-12) { m_strDisplay.Format(_T(%.0f), dValue); } else { // 限制小数位数避免因浮点精度产生一长串数字 m_strDisplay.Format(_T(%.10g), dValue); // %.10g 格式会根据数值自动选择定点或科学计数法并保留最多10位有效数字 } UpdateDisplay(); }7. 项目调试、常见问题与优化建议7.1 编译与运行时常见问题无法打开源文件“stdafx.h”这是MFC项目使用的预编译头文件。确保你没有误删stdafx.h和stdafx.cpp文件并且在每个.cpp文件的开头都#include stdafx.h。在VS2019中创建MFC项目时默认配置是使用预编译头的。控件ID未定义错误如果你在代码中使用了IDC_BUTTON_ADD这样的ID但编译时提示“未声明的标识符”请检查“资源视图”中控件的ID是否与你代码中写的一致。最可靠的方法是在对话框编辑器中双击按钮让VS自动生成消息处理函数它会使用正确的ID。链接错误 LNK2001/LNK2019通常是成员函数声明了但未定义或者定义了但实现签名参数、常量性与声明不匹配。仔细检查.h文件中的函数声明和.cpp文件中的函数定义是否完全一致。特别是消息映射函数它们不需要在.h中声明但必须在.cpp的消息映射宏BEGIN_MESSAGE_MAP和END_MESSAGE_MAP之间正确关联。程序运行后点击按钮无反应首先检查是否为按钮正确添加了BN_CLICKED消息处理程序在“类视图”中展开CSimpleCalculatorDlg查看是否有对应的函数如OnBnClickedButtonAdd。检查消息映射打开SimpleCalculatorDlg.cpp找到BEGIN_MESSAGE_MAP(CSimpleCalculatorDlg, CDialogEx)和END_MESSAGE_MAP()之间的部分确认有类似ON_BN_CLICKED(IDC_BUTTON_ADD, CSimpleCalculatorDlg::OnBnClickedButtonAdd)的条目。如果没有需要手动添加或者通过属性窗口的“事件”选项卡重新添加。7.2 逻辑错误排查技巧连续运算结果不对这是最常见的逻辑bug。使用调试器在PerformPendingOperation、运算符处理函数和等号处理函数中设置断点。逐步跟踪m_dOperand1m_dOperand2m_nOperator和m_bWaitForNewNumber这几个关键变量的值变化画出状态转移图与你的设计逻辑对比。小数点可以输入多个检查m_bHasDecimal标志的逻辑。确保在AppendDigitToDisplay函数中开始输入新数字时即m_bWaitForNewNumber为真时将其重置为FALSE。确保在点击小数点按钮时正确设置为TRUE并且在退格删除小数点时重置为FALSE。显示格式问题比如计算结果出现“3.0000000001”或“2.9999999999”。这是浮点数精度问题的典型表现。在FormatResult函数中不要直接使用%f格式化而是像上面示例一样先判断是否为“整数”在极小的误差范围内或者使用%g格式符来控制有效数字位数。7.3 功能扩展与优化建议一个基础计算器完成后你可以尝试以下扩展这能极大提升你对MFC和编程逻辑的理解支持键盘输入重写对话框的PreTranslateMessage函数拦截键盘消息WM_KEYDOWN将数字键、回车键等号、退格键、运算符键映射到相应的按钮点击事件。添加历史记录功能在对话框上增加一个列表框List Box控件每次完成一次计算按等号或连续运算中按运算符将运算式如“3 4 7”添加到列表框中。实现括号和优先级这需要将中缀表达式如3 4 * 5转换为后缀表达式逆波兰式再进行求值或者使用两个栈操作数栈和运算符栈来模拟求值过程。这是一个经典的算法练习。改进UI使用位图按钮CBitmapButton来制作更漂亮的皮肤。或者尝试使用MFC的CMFCButton类如果项目配置了BCGControlBar或类似库它支持更多的视觉样式。代码重构将计算核心逻辑状态管理、运算执行抽离到一个独立的CCalculatorEngine类中。对话框类只负责界面交互和调用引擎类的方法。这样实现了模型与视图的分离代码更清晰也便于单元测试。通过这个完整的MFC计算器项目你不仅学会了拖控件和写事件处理函数更重要的是理解了如何为一个交互式应用设计清晰的状态机如何组织代码使其易于维护和扩展。这些经验在你未来使用Qt、WPF乃至各种前端框架开发GUI应用时都是相通的。
返回列表