
简介面向VC6.0与MFC初学者的完整计算器程序源码包基于MFC对话框类实现基础四则运算适合正在学习Windows窗口程序设计、进行课后实践或希望掌握MFC类库应用的开发者参考。压缩包内共有31个文件涵盖C源文件与头文件、资源脚本、图标/光标/位图素材以及说明文档整体仅106KB结构紧凑便于直接载入学习与查阅。已有383人浏览学习源码包含数字与运算符按钮的消息映射、事件处理函数以及基于CString和double的数值转换逻辑并提供MFC说明文档可帮助理解对话框程序从创建、调试到优化的完整流程。工程中还附带了Flash控件调用的扩展演示代码逻辑清晰既能用于课程设计也可作为后续MFC项目开发的基础骨架。通过研读工程文件与对话框类实现可以快速上手MFC界面开发和消息响应机制。1. 用 VC6.0 做 MFC 计算器这个老古董组合为什么还在课程设计里刷屏打开毕业设计或期末课程设计的选题表“基于 VC6.0 的 MFC 计算器”几乎是常青树。你可能会觉得 2025 年了还在用 1998 年的 IDE 写一个 Windows 对话框程序是不是太复古了。但真去搜一圈就会发现vc6.0、MFC、计算器这三个词凑在一起搜索量依然不小——因为很多学校还在教 MFC很多学生的电脑上装的还是 VC6.0 绿色版而计算器恰好是能把消息映射、对话框、控件操作、字符串处理全部串起来的最小完整项目。这篇笔记按我实际调试通过的路线来讲怎么建工程、怎么把按钮和计算逻辑粘起来、怎么处理那些一编译就红的错误。适合两类人一是第一次碰 MFC 的初学者跟着步骤能把一个能算加减乘除的窗口跑起来二是被课程设计逼着交代码的老哥看完能快速把功能补全、把坑绕开。文章里没有玄学只有我踩过的坑和验证过的参数。2. 从零搭 MFC 计算器界面Dialog 资源、控件布局与类绑定2.1 创建基于对话框的 MFC 工程AppWizard 里最关键的三处选择打开 VC6.0新建工程选“MFC AppWizard(exe)”填入工程名后别急着点确定。AppWizard 第一步问“应用程序类型”这里选“Dialog based”——我们做的是计算器一个对话框窗口加一堆按钮就够了不需要文档视图结构。第二处关键选择在第 2 步是否支持“上下文相关帮助”和“ActiveX 控件”这两个都取消掉省得生成一堆用不到的文件。第三处是第 4 步AppWizard 会给你生成两个类一个是CAboutDlg关于对话框一个是以工程名命名的对话框主类比如CCalculatorDlg。直接点完成等它生成代码。生成后打开资源视图你会看到IDD_CALCULATOR_DIALOG这个对话框模板默认带一个“确定”按钮和一个“取消”按钮。先把这两个按钮删掉因为它们必会导致误点退出。右键对话框选“属性”把对话框标题改成“计算器”ID 也顺手改成IDD_CALCULATOR_DLG——虽然名字无所谓但别和系统资源冲突。2.2 布置按钮和编辑框资源编辑器里的实操细节设计界面时我从工具栏拖一个 Edit Box编辑框放在顶部用来显示数字。属性里勾上Read-only只读因为计算器不允许直接键盘输入只能点按钮输入防止用户往里面乱敲字符。再勾上Number注意Number属性只允许输入数字但我们会输小数点所以不要勾Number而是把Auto HScroll勾上这样数字长了不会在控件里折行。按钮的摆放就按真实计算器的布局0-9、小数点、加号、减号、乘号、除号、等号、清空 C、退格 Backspace。每个按钮的 ID 都要有规律。我习惯这样命名数字键 0-9 对应IDC_BTN_0、IDC_BTN_1小数为IDC_BTN_DOT加号IDC_BTN_ADD、减号IDC_BTN_SUB、乘号IDC_BTN_MUL、除号IDC_BTN_DIV、等号IDC_BTN_EQU、清空IDC_BTN_CLEAR、退格IDC_BTN_BACK。这里的关键是数字键的 ID 要有规律可循比如数字 5 的 ID 就是IDC_BTN_5这样后面写代码时可以直接用控件 ID 的数值来区分是哪个数字做统一处理时少写 10 个消息处理函数。控件放好后先编译运行一次。这一步的目的是确认资源脚本没有写错、对话框能正常弹出来。我见过新手拖控件时不小心把按钮拖到对话框边界外面运行时按钮消失还以为是代码问题。所以布局完先用预览功能看一眼。3. 按钮消息映射把“按一下”变成“算一步”3.1 用 ClassWizard 添加 BN_CLICKED 消息处理比手写消息映射省事MFC 里按钮被点击后系统会发出BN_CLICKED通知对话框类需要通过消息映射把通知交给对应的成员函数。手工写在BEGIN_MESSAGE_MAP里的宏容易拼错我推荐用 ClassWizardCtrlW自动生成。对每个按钮在 ClassWizard 里选上工程的主对话框类在 Object IDs 列表里选按钮 IDMessages 列表选BN_CLICKED点 Add Function函数名会自动生成比如OnBnClickedBtn5。如果你不想一个个点也可以手写。消息映射的基本形式是BEGIN_MESSAGE_MAP(CCalculatorDlg, CDialogEx) ON_BN_CLICKED(IDC_BTN_0, CCalculatorDlg::OnBnClickedBtn0) ON_BN_CLICKED(IDC_BTN_1, CCalculatorDlg::OnBnClickedBtn1) // ... END_MESSAGE_MAP()这里ON_BN_CLICKED宏的第一个参数是控件 ID第二个是响应函数指针。特别要注意的是VC6.0 里写消息映射函数必须声明在类的afx_msg区域如果你在代码里手动加函数但忘了在头文件里声明编译会报“函数不是CCalculatorDlg的成员”之类的错误。我一般建议所有按钮都用 ClassWizard 生成至少不会因为头文件没改而踩坑。3.2 处理数字键与运算符键的统一入口控件的 ID 与 GetDlgItem如果给 10 个数字键每个都写一个独立函数代码会很长。更常见的做法是把数字按钮统一映射到一个函数上。原理很简单按钮的消息处理函数不关心按钮是哪一个它从消息路由里拿不到控件 ID但我们可以用同一个函数处理多个不同 ID 的按钮。在 ClassWizard 里加一个函数然后手动在消息映射里补充宏ON_BN_CLICKED(IDC_BTN_0, CCalculatorDlg::OnDigit) ON_BN_CLICKED(IDC_BTN_1, CCalculatorDlg::OnDigit) // ... 一直到 IDC_BTN_9 ON_BN_CLICKED(IDC_BTN_DOT, CCalculatorDlg::OnDigit)在OnDigit函数里怎么知道按的是哪个按钮用GetFocus()拿当前焦点控件 ID 是不可靠的因为按钮在点击后焦点就跑到该按钮上了。正确做法是在消息处理函数里调用GetCurrentMessage()。在 MFC 中CWnd的成员函数OnCommand会带上控件 ID但普通消息处理函数拿不到。更简单粗暴的方法给每个数字按钮设置不同的 ID然后用GetDlgItem(IDC_EDIT display)-GetWindowText拿当前显示文本再想办法知道用户按了哪个数字。其实 MFC 的消息处理函数可以拿到触发控件的句柄但拿不到 ID。我常用的方案是给每个数字键写独立函数函数体只有一行——调用AppendChar(5)这样的公共函数。这样代码虽然多几行但逻辑非常清楚void CCalculatorDlg::OnBnClickedBtn5() { AppendChar(5); }数字键 0-9 和小数点都这样处理。AppendChar内部维护一个CString m_strInput负责把字符追加到当前输入缓冲再刷新到编辑框。这个方法避免了所有控件 ID 的运行时判断问题而且后续排查逻辑时非常好调试。运算符键同理每个运算符键调用SetOperator(PLUS)之类的方法把四则运算和等号分成两个入口。4. 计算核心表达式解析与状态机别急着写 eval4.1 用操作数栈和运算符栈实现中缀表达式求值很多初学者拿到计算器第一反应是“把字符串转换成表达式然后求值”但在 MFC 里没有现成的eval自己写一个通用的算术表达式解析器工作量不小。课程设计类计算器不需要处理括号只需要连续加减乘除和优先级这时用两个栈实现中缀表达式求值最稳。思路参考常见的“调度场算法”维护一个数字栈和一个运算符栈扫描用户输入的每个字符数字入数字栈运算符则根据优先级决定是否先计算栈顶。一个简化版处理函数如下// 压入一个运算符如果当前运算符优先级比栈顶低或相等先计算栈顶 void CCalculatorDlg::PushOperator(char op) { while (!m_opStack.empty() GetPriority(op) GetPriority(m_opStack.top())) { ComputeTop(); // 弹出两个操作数和一个操作符计算结果压回数字栈 } m_opStack.push(op); } void CCalculatorDlg::ComputeTop() { char op m_opStack.top(); m_opStack.pop(); double b m_numStack.top(); m_numStack.pop(); double a m_numStack.top(); m_numStack.pop(); switch (op) { case : m_numStack.push(a b); break; case -: m_numStack.push(a - b); break; case *: m_numStack.push(a * b); break; case /: if (b 0.0) { m_resultText 错误; } else m_numStack.push(a / b); break; } }这个函数里有几个关键点。一是b和a的出栈顺序栈里先弹出的是后压入的所以减法a - b不能写成b - a这是最常见的翻车原因。二是除法除零要单独判断否则a / 0产生无穷大或非数显示到界面上是乱七八糟的字符。三是GetPriority只需要给乘除返回 2加减返回 1等号当作一个优先级最低的特殊操作符处理。4.2 在 MFC 里维护输入状态连续运算、等号、清空怎么处理计算器的状态比算法更麻烦。用户可能连续按“12 3 4 ”也可能按“5 * ”有些计算器会重复上次运算而课程设计通常只要求基本的连续运算。我维护一套状态机来避免乱套变量m_strInput保存当前正在输入的数字字符串bool m_bNewNumber表示下一次按数字键时是否要清空显示double m_dAccumulator保存已经计算好的左侧值char m_lastOperator保存最近一次按下的运算符。流程是这样的初次按数字键时m_bNewNumber为 true于是把m_strInput清空再追加按运算符时先把m_strInput转成浮点数压入数字栈再压入运算符按等号时把最后一个数字压栈然后不停计算直到运算符栈为空。清空按钮重置所有栈和状态。退格按钮则只处理m_strInput删除最后一个字符。这里有个容易做的事显示更新的时机。每次AppendChar、每次运算符按下、每次等号计算完都要调用UpdateDisplay()把结果写到编辑框。一个经典的错误是只在按钮处理里更新但计算函数中间产生了中间结果界面来不及刷新。我建议UpdateDisplay里调用SetDlgItemText而不是UpdateData(FALSE)因为SetDlgItemText只操作指定控件不会触发DoDataExchange省掉绑定变量的麻烦。5. MFC 计算器避坑指南Edit 控件刷新、字符集、资源 ID 冲突5.1 控件内容不刷新UpdateData(FALSE) 与 SetDlgItemText 的区别现象点数字按钮编辑框没有任何变化但调试时看到变量已经更新。原因很多人把数字绑定到控件变量然后在AppendChar里修改m_strDisplay最后调用UpdateData(FALSE)刷新。但UpdateData(FALSE)会在对话框的DoDataExchange里重新从变量传值到控件如果控件变量绑定的是 Value 类型而你又同时用SetWindowText改了控件两者会互相覆盖。解决统一用SetDlgItemText(IDC_EDIT_DISPLAY, m_strDisplay)不要通过UpdateData也不要给编辑框绑定 CString 变量。只绑定CEdit类型的控件变量可以但没必要。用SetDlgItemText的另一个好处是它自动把数字转成字符串不需要CString::Format里手工指定%lf之类。5.2 编译报错C2664 和 ASCII / Unicode 字符串转换现象在 VC6.0 里编译SetDlgItemText(IDC_EDIT, 123)报错error C2664: SetWindowTextA : cannot convert parameter 2 from const char [4] to const unsigned short *。这个错出现时工程被设置成了 Unicode 字符集但字符串字面量是窄字符。原因VC6 默认是 ANSI但有些模板或笔者自己改了工程属性。解决要么把工程字符集改回“Use Multi-Byte Character Set”要么在字符串前面加_T()宏如SetDlgItemText(IDC_EDIT, _T(123))。我建议所有字符串字面量统一用_T()包裹这样将来把代码搬到 VS2008 以上版本也不会翻车。另一个坑是CString::Format(%lf, value)在 Unicode 下也可能报错。原因是%lf会被误认为宽字符格式化。解决就是用_T(%lf)。如果你的代码里还有其他char*和CString混用注意CString隐含转换到LPCTSTR但反过来不行所以在函数参数传递时保持类型一致。5.3 按钮按了没反应消息映射没加或 ON_BN_CLICKED 宏写错现象编译通过、对话框正常显示但点击任何按钮都没有反应。原因最常见的是按钮 ID 和消息映射的 ID 不一致。比如资源编辑器里把按钮 ID 改成了IDC_BTN_5但 ClassWizard 生成的函数是OnBnClickedBtn5消息映射却是ON_BN_CLICKED(IDC_BTN_5, OnBnClickedBtn5)乍看没错。问题可能出在按钮控件没有Notify属性在对话框上的按钮默认会发送BN_CLICKED但如果按钮是从工具栏拖出来的“Push Button”肯定没问题除非你用的是Button控件且BS_NOTIFY没设置。不过常规 Broadcast 下应该没事。另一个隐蔽原因是你在OnInitDialog里把按钮的EnableWindow(FALSE)禁用了。解决检查消息映射宏确认 ID 完全相同再检查OnInitDialog里有没有禁止操作。还有一个容易忽略的如果对话框模板不是主对话框或按钮在子对话框中父窗口没有把消息映射到对应类上也会没反应。计算器这种单对话框程序不存在这个问题但如果你做到后面加了“关于”对话框要记得给那个对话框类也加消息映射。5.4 除零和负数显示格式化输出与错误状态标志现象除零后界面显示-1.#IND或1.#INF程序不崩溃但没法继续输入。原因浮点数除零在 IEEE 754 里是合法的结果是无穷大或非数显示在控件里成了乱码。解决在除法计算里加b 0.0判断然后设置一个m_bError标志更新显示时如果错误标志为真直接把编辑框文本设为“除数不能为零”并且让后续所有数字键和运算符键都不生效直到按下清空键。这里要注意浮点判等用是可以的因为我们比较的是用户输入字符串转出来的双精度零不是某个计算出来的近似零。如果你的运算涉及sin、cos之类那需要改成正则或者误差范围但这篇计算器用不到。负数显示也有坑。当用户输入“5 - 3 ”时结果 2 没问题但如果用户想算“-5 3”很多计算器允许先按负号再按 5。我的方案是提供正负号按钮IDC_BTN_NEG按下时如果当前输入字符串不为空则判断首位是否是负号是则删掉否则在最前面插入-。这样不会与减号混淆。更新显示时用CString s; s.Format(_T(%g), result);用%g而不是%f可以自动去掉小数末尾多余的 0让结果显示更干净。5.5 窗口大小固定禁止拖动边界现象运行后窗口可以随意拉伸按钮和编辑框的相对位置变了界面显得很丑。原因对话框默认有WS_THICKFRAME样式允许用户改变窗口大小。解决在OnInitDialog里去掉厚边框或者直接改资源脚本。最稳妥的方式是在对话框模板的属性里把Border设为Dialog Frame并且把System Menu、Minimize Box、Maximize Box按需求保留。我一般还会在OnInitDialog里调用SetWindowPos固定窗口大小SetWindowPos(NULL, 0, 0, 240, 320, SWP_NOMOVE | SWP_NOZORDER);这行代码把窗口尺寸强制设为 240 x 320 像素。SWP_NOMOVE表示保持当前位置SWP_NOZORDER表示不改变 Z 序。注意如果你在资源编辑器里设置了Dialog Frame样式用户仍然可以通过系统菜单最大化需要在OnGetMinMaxInfo里限制或者把最大化按钮去掉。关于“禁止拖动窗口大小”最偷懒的做法是重写OnSysCommand拦截SC_SIZE和SC_MAXIMIZE但课程设计一般不追究那么细固定尺寸够用。6. 把计算器做成能交作业的样子状态栏、图标与自测清单MFC 对话框程序默认没有状态栏但很多设计说明里会要求“状态栏显示提示信息”。给对话框加状态栏其实不难定义一个CStatusBar m_statusBar成员在OnInitDialog里创建if (!m_statusBar.Create(this)) { TRACE0(未能创建状态栏\n); return FALSE; } m_statusBar.SetIndicators(indicators, sizeof(indicators)/sizeof(UINT)); m_statusBar.SetPaneText(0, _T(就绪));indicators是一个静态数组通常只包含一个ID_SEPARATOR。然后你可以在鼠标悬停到某个按钮时在OnSetCursor或按钮的WM_CTLCOLOR里更新状态栏文字。更简单的做法是在每个按钮消息处理函数里切换状态栏提示比如按运算符后状态栏显示“等待输入第二个操作数”。对于交作业来说这样就能体现出“状态栏显示一些信息”的加分项。图标方面VC6 生成的工程自带一个res\\工程名.ico你可以在资源里导入自己的图标替换IDR_MAINFRAME。如果懒得画用系统图标也行在OnInitDialog里写SetIcon(m_hIcon, TRUE)已经由模板生成。要注意的是编辑框和按钮的字体最好统一改成“MS Sans Serif”或者“宋体”否则在不同 Windows 版本上显示会错位。我的经验是在对话框属性里把字体设为“宋体 9 号”所有控件都会跟着变。交付前我习惯跑一遍自测清单。这个清单很机械但很管用连续输入123 456 得 579输入2 * 3 4 得 10验证优先级输入10 - 5 - 2 得 3验证左结合输入7 / 0 显示错误提示输入0.1 0.2 得 0.3用%g格式化没问题按退格删除到空再按退格不崩溃按清空后所有状态归零。如果这些都过了基本可以提交。最后说一个习惯我在写 MFC 计算器时会把所有运算逻辑放在CCalculatorDlg的私有方法里按钮消息处理函数只做两件事——更新输入缓冲、调用运算逻辑。这样以后想换成 QT 或者控制台程序只用重写界面层计算核心可以直接搬走。自己动手做一遍你会发现MFC 的难点不在 C 语法而在消息路由和控件交互的细节上但这些坑踩过一次就记住了。希望这篇笔记能帮你少走两步冤枉路。本文还有配套的精品资源点击获取