
简介基于C#的Windows窗体计算器源程序包是一份面向C#初学者的完整工程示例。它围绕加、减、乘、除等基础运算演示了窗体设计、控件布局、事件处理、逻辑判断与错误处理等关键知识点适合正在学习.NET WinForms或准备课程设计的读者参考。压缩包为rar格式共56个文件约1.21MB核心为14个.cs源代码文件含Form1.cs、Program.cs及多个Form2相关窗体配以resx资源、ico图标、Settings和sln项目文件并附3个可直接运行的exe程序、调试文件与说明文本结构清晰便于对照学习。已有544人学习下载。通过这份源码读者可直接查看计算器标准型、关于页、键盘说明、日历与联系我等模块的实现思路快速掌握多窗体应用的组织方式同时借鉴对话框、图片资源与输入校验等细节处理为独立开发Windows桌面程序打下基础。1. 项目定位为什么拿C# WinForms练手一定要写个计算器计算器这个项目我前前后后带过不少新人写过也在不同场合自己重写过好几遍。很多刚接触C#的朋友会问网上现成的计算器源码一堆直接抄一个不就行了吗我的回答是抄完能跑和真正理解每一行代码为什么这么写是两码事。拿计算器当切入点是因为它麻雀虽小五脏俱全——要处理界面布局、事件响应、输入合法性校验、字符串解析、数学运算逻辑、异常处理甚至还牵涉到浮点数精度这种经典问题。这一套流程走下来WinForms开发的主要知识点几乎都能覆盖到而且调试起来直观不容易劝退新手。再往实际需求上说计算器往往也是很多工业软件、上位机项目里一个不起眼但绕不开的模块。比如我做过的几个工控小工具面板上经常要放一个参数换算的小计算区域。虽然逻辑比Windows自带的计算器简单得多但原理一脉相承。所以认认真真把这个项目吃透后面遇到类似的输入-处理-输出场景你脑子里会立刻浮现出一套清晰的实现路径不用重新踩坑。这篇文章我直接以Visual Studio 2017/2019/2022通用的方式把完整的实现思路、核心代码、踩坑记录都摊开讲。适合刚学完C#语法、想找个窗口程序练手的人也适合想快速搭一个带界面小工具的工控人参考。下面所有代码我都基于.NET Framework或.NET Core/.NET 5的WinForms模板差异不大照着敲就行。2. 界面搭建先把窗口和控件安排明白2.1 控件布局与属性设置计算器的界面不需要什么花活一个显示结果的文本框加一排按钮就够。但布局这件事我建议你在一开始就养成“用锚点、用固定间距、用统一字号”的习惯别随手拖控件。具体操作上我在窗体上放了一个TextBox名字叫txtDisplay用来显示当前输入和计算结果。TextBox比Label好用的地方在于它自带光标、支持选中复制而且可以很自然地响应键盘输入后面想加键盘操作时省事。TextAlign设置为Right这样数字靠右显示符合常规计算器的观感。Font我习惯统一设为14或16号太小的字号在高DPI屏幕上会显得很局促。按钮方面我按照常规计算器的布局排成四行四列7 8 9 / 4 5 6 * 1 2 3 - 0 . C 最后一列放运算符下面单独一行放一个等号按钮宽度横跨整行。这样排布用WinForms的TableLayoutPanel来做最合适设置好行列百分比窗口缩放时按钮能自动拉伸对齐不会出现控件重叠的尴尬。当然你图省事直接用Anchor属性也可以但TableLayoutPanel的扩展性更好后期想加一个退格按钮或正负号按钮时改布局更容易。C按钮是清除键这个严格来说是清空当前输入不是退格。我见过不少新手把C做成退格结果算到一半想重新开始只能一个个删体验很差。C就是Clear清空所有状态这个概念在一开始就要区分清楚。2.2 事件订阅与统一处理按钮的事件处理我最开始写的时候是给每个按钮单独绑一个Click事件结果代码里密密麻麻全是button1_Click、button2_Click这种名字改一个逻辑要动好几个方法非常蠢。后来我改为统一订阅。在窗体构造函数里把需要相同处理的按钮分组绑到同一个事件处理器上public Form1() { InitializeComponent(); // 数字按钮统一处理 foreach (Control ctl in tableLayoutPanel1.Controls) { if (ctl is Button btn 0123456789.Contains(btn.Text)) { btn.Click DigitButton_Click; } } // 运算符按钮统一处理 btnAdd.Click OperatorButton_Click; btnSub.Click OperatorButton_Click; btnMul.Click OperatorButton_Click; btnDiv.Click OperatorButton_Click; btnEquals.Click EqualsButton_Click; btnClear.Click ClearButton_Click; btnDot.Click DotButton_Click; }这样做的好处后面你会体会到假如要加一个“负数输入”功能只要把负号按钮归到运算符那一组半天就能搞定如果要改数字输入的规则也只有一个入口要动。代码的可维护性不是靠花哨的设计模式堆出来的这种小项目里把事件处理入口收敛好就是实实在在的整洁。3. 核心逻辑从输入到计算的完整链条3.1 状态管理搞清楚屏幕上显示的是什么写计算器最容易绕晕的地方不是不会写“123”而是写着写着忘了当前屏幕上这个数字处于什么状态。我的经验是用一个明确的模型来管理三样东西当前输入的数值、上一次的运算符、是否刚按过等号。一开始我为了方便直接用一个字符串firstNumber和一个字符串secondNumber来存数字用char operator来存运算符通过判断运算符是否为\0来决定是否是新运算。但后来发现这种方案在连续运算的场景下很别扭。比如用户输入“10 20 30”按我最初的做法第一次按时10进去第二次按时20应该和10先算再把30等在这里这个逻辑容易写成一团乱麻。所以我把状态管理简化为两个关键字段private double currentValue 0; // 当前累计结果 private double pendingOperand 0; // 暂存的操作数 private string currentOperator ; // 当前运算符 private bool isNewEntry true; // 是否开始输入新数字 private bool isResultShown false; // 当前是否显示的是计算结果isNewEntry和isResultShown这两个布尔值新手往往意识不到它们有多重要。isNewEntry解决的是“我按完运算符之后再按数字新数字应该替换掉屏幕上这个旧的数字而不是追加在后面”。isResultShown解决的是“我按完等号出结果之后再按数字应该开始一个全新计算而不是把结果当第一个操作数的一部分继续追加”。3.2 运算符优先级从“能用”到“算得准”如果你只是要求“123”这种一步计算那直接两个操作数一个运算符就够了。但实际使用中用户一定会输入“12*3”这种混合运算。如果按从左到右算结果是9按数学规则算结果是7。哪个对当然是7。我知道很多人图省事会用DataTable.Compute(12*3, null)这种黑科技。这种方案确实能算出7因为它内部调用了表达式解析器支持四则混合运算和括号。但问题在于第一你没有学到解析的原理面试官一问就露馅第二DataTable.Compute的性能和安全性在大工程项目里是有争议的把它塞进一个计算器小工具里等于是拿大炮打蚊子还带了一堆不必要的依赖。我建议的实现方式是用两个栈做表达式求值一个存数字一个存运算符。这是数据结构里最经典的中缀表达式求值算法原理几句话能说清楚遇到数字压入数字栈。遇到运算符先看运算符栈顶元素的优先级。如果栈顶优先级大于等于当前运算符就先把栈顶运算符弹出来从数字栈弹出两个数做计算把结果压回数字栈然后继续比较新的栈顶直到栈顶优先级比当前运算符低再把当前运算符压栈。表达式结束后把运算符栈里剩余的运算符逐个弹出计算。优先级表用整数表示乘除为2加减为1。这个实现方式的好处是以后你想加括号只需要在遇到右括号时把左括号之上的运算符全部计算完即可扩展路径非常清晰。3.3 完整核心代码落地我在这里写一个基于上述状态管理的完整实现。这段代码在WinForms里可以直接跑逻辑上也覆盖了连续运算、等号重复点击、除数不能为零等典型场景。首先是输入数字的处理private void DigitButton_Click(object sender, EventArgs e) { Button btn sender as Button; if (isNewEntry) { txtDisplay.Text btn.Text; isNewEntry false; isResultShown false; } else { // 防止出现 001 这种无意义的前导零但允许 0.5 这种 if (txtDisplay.Text 0 btn.Text ! .) { txtDisplay.Text btn.Text; } else if (txtDisplay.Text.Length 15) // 防止显示溢出 { txtDisplay.Text btn.Text; } } }处理小数点时要注意同一段数字里只能有一个小数点private void DotButton_Click(object sender, EventArgs e) { if (isNewEntry) { txtDisplay.Text 0.; isNewEntry false; isResultShown false; } else if (!txtDisplay.Text.Contains(.)) { txtDisplay.Text .; } }运算符处理是这段代码里最核心的部分private void OperatorButton_Click(object sender, EventArgs e) { Button btn sender as Button; string op btn.Text; // 如果是在一次计算之后按运算符比如 23 之后按 // 应该把结果作为下一次计算的第一个操作数 if (isResultShown) { currentValue double.Parse(txtDisplay.Text); currentOperator op; isNewEntry true; isResultShown false; return; } double operand double.Parse(txtDisplay.Text); if (currentOperator ) { // 第一次按运算符没有历史计算暂存当前值 currentValue operand; } else { // 已经有运算符了说明需要先算一次 currentValue Calculate(currentValue, operand, currentOperator); txtDisplay.Text currentValue.ToString(); } currentOperator op; isNewEntry true; }Calculate方法实现两个数的四则运算同时处理除零private double Calculate(double a, double b, string op) { switch (op) { case : return a b; case -: return a - b; case *: return a * b; case /: if (b 0) throw new DivideByZeroException(除数不能为0); return a / b; default: throw new ArgumentException(未知运算符); } }等号的逻辑稍微特殊一点因为它和连续按等号有关。比如“2 3 ”在实体计算器上会依次得到5、8、11也就是每次把上次的第二个操作数再加一遍。这个功能虽然小众但很能体现一个人有没有把状态管理想清楚private double lastOperand 0; private void EqualsButton_Click(object sender, EventArgs e) { if (currentOperator ) return; double operand double.Parse(txtDisplay.Text); if (!isResultShown) { // 第一次按等号正常计算 lastOperand operand; currentValue Calculate(currentValue, operand, currentOperator); } else { // 重复按等号用上次的操作数继续计算 currentValue Calculate(currentValue, lastOperand, currentOperator); } txtDisplay.Text currentValue.ToString(); currentOperator ; isNewEntry true; isResultShown true; }清除按钮则要重置所有状态别只清屏幕上的文本private void ClearButton_Click(object sender, EventArgs e) { txtDisplay.Text 0; currentValue 0; currentOperator ; isNewEntry true; isResultShown false; lastOperand 0; }这里你会发现一个关键点我上面这套实现其实还没做运算符优先级的栈处理它采用的是“逐步计算”的策略也就是每遇到一个运算符就把前两个数算掉。这种策略对“加减乘除混合”是不准确的因为它会从左到右算。要解决混合运算我建议把表达式完整记录成一个字符串然后用栈解析器来做。这是两种不同路线我在这里把它们分开讲清楚。如果你追求的是“输入12*3按等号得到7”的体验那正确的做法不是输入一个运算符就算一次而是记录完整算式等按等号时统一解析。记录算式的话只需要在数字和运算符按钮的事件里拼接字符串就行注意点在于要记录运算符优先级。我给出一个完整的栈计算方案private string expression ; private void OperatorButton_Click(object sender, EventArgs e) { expression txtDisplay.Text ((Button)sender).Text ; isNewEntry true; } private void EqualsButton_Click(object sender, EventArgs e) { expression txtDisplay.Text; double result EvaluateExpression(expression); txtDisplay.Text result.ToString(); expression ; isResultShown true; } private double EvaluateExpression(string expr) { // 用空格切分得到操作数和运算符的序列 var tokens expr.Split( , StringSplitOptions.RemoveEmptyEntries); Stackdouble values new Stackdouble(); Stackstring ops new Stackstring(); foreach (var token in tokens) { if (double.TryParse(token, out double num)) { values.Push(num); } else if (IsOperator(token)) { while (ops.Count 0 GetPriority(ops.Peek()) GetPriority(token)) { ApplyOperator(values, ops); } ops.Push(token); } } while (ops.Count 0) { ApplyOperator(values, ops); } return values.Pop(); } private void ApplyOperator(Stackdouble values, Stackstring ops) { double b values.Pop(); double a values.Pop(); string op ops.Pop(); values.Push(Calculate(a, b, op)); } private bool IsOperator(string token) { return token || token - || token * || token /; } private int GetPriority(string op) { return op * || op / ? 2 : 1; }这个方法算“12*3”得到的确实是7。配合前面说的isNewEntry和isResultShown状态管理整个计算器就完整了。我个人把这两种方案都实现过实际体验是如果只是给内部工具用且使用者知道“按运算符就立即计算”的规则那么逐步计算方案够用且代码量少很多如果是给普通用户用的通用计算器必须上栈解析方案否则用户一定会觉得你算错了。4. 实操中的完整接入与UI细节4.1 键盘输入的接入WinForms程序如果不支持键盘操作总让人觉得差了点什么。我在这个项目里额外加了KeyPress事件支持让用户能用小键盘直接输入数字和运算符。在窗体构造函数里加上this.KeyPreview true; this.KeyPress Form1_KeyPress;然后处理按键private void Form1_KeyPress(object sender, KeyPressEventArgs e) { if (char.IsDigit(e.KeyChar)) { // 把字符转成对应的数字按钮文本复用现有逻辑 AppendDigit(e.KeyChar.ToString()); } else if (e.KeyChar .) { DotButton_Click(null, EventArgs.Empty); } else if (e.KeyChar ) HandleOperator(); else if (e.KeyChar -) HandleOperator(-); else if (e.KeyChar *) HandleOperator(*); else if (e.KeyChar /) HandleOperator(/); else if (e.KeyChar (char)13) // Enter { EqualsButton_Click(null, EventArgs.Empty); } else if (e.KeyChar (char)27) // Esc { ClearButton_Click(null, EventArgs.Empty); } }注意如果你使用的是栈解析方案HandleOperator和界面按钮的OperatorButton_Click逻辑是一样的按钮点击时也可以调用同一个方法代码不要复制抽成公共方法就好。4.2 计算结果格式化别让浮点数精度毁掉体验计算器显示“0.10.2”结果变成“0.30000000000000004”这在普通用户看来就是程序Bug。C#里的double本身存在二进制浮点表示误差这是所有编程语言都绕不过去的问题。在计算器这种场景下合理做法是对结果做一次格式化。我习惯用以下方式处理显示private string FormatResult(double value) { if (double.IsInfinity(value)) return 错误; if (double.IsNaN(value)) return 错误; // 如果数值在合理范围内直接用G15格式化消除多余尾数 // G15能把绝大多数浮点误差吃掉比如 0.30000000000000004 - 0.3 return value.ToString(G15); }在按等号计算完之后显示用FormatResult包一层这样既保证了精度不丢失内部计算还是用double又避免了屏幕上出现一长串难看的尾巴。小数点后的无效零G15格式也会自动去掉不会出现“5.00”这种情况显示上更干净。如果你希望固定显示两位小数比如做金额类计算器那应该用value.ToString(0.00)。但这个项目是通用计算器固守两位小数反而会误导用户——比如1÷3显示0.33看着就“不够精确”所以G15是更好的选择。4.3 溢出、除零与异常处理的完整策略函数Calculate里直接抛出了异常但在UI层不能让它裸奔否则程序会弹一个难看的错误框然后崩溃。我建议在等号和运算符处理的外层统一包一个try-catchprivate void EqualsButton_Click(object sender, EventArgs e) { try { // 原有的等号计算流程 } catch (DivideByZeroException) { txtDisplay.Text 除数不能为0; ResetAfterError(); } catch (OverflowException) { txtDisplay.Text 数值溢出; ResetAfterError(); } catch (Exception ex) { txtDisplay.Text 输入错误; ResetAfterError(); } } private void ResetAfterError() { currentValue 0; currentOperator ; isNewEntry true; isResultShown true; lastOperand 0; }注意出错误之后不要直接把txtDisplay设成“0”而是设置成提示文字同时进入“结果已显示”的状态。这样用户看清楚错误后直接按数字就能开始下一次计算不用多按一次清除。这个细节是自己用过一段时间后才能体会到的。5. 遇到过的坑与排查方法5.1 “0”和“00”的问题很多新手实现数字输入时直接在文本框后面append结果用户按了“0”之后再按“5”屏幕上变成“05”看着非常业余。我在DigitButton_Click里专门判断了文本框内容是否是“0”是的话就用新数字替换掉这就解决了。但要注意“0.”这种合法的小数开头不能被替换否则用户输入0.5时按完小数点再按5就会被错误的逻辑换成“5”所以我加了“btn.Text ! .”这个判断条件。另一个相关的坑是用户输入完之后什么都没按直接把焦点切到别的地方再点回窗体TextBox还是那个值这没问题。但如果你做了失焦自动清空之类的多余操作就会让用户崩溃。保持简单是王道。5.2 连续按“”的预期行为实体计算器上按“2 3 ”第一次出5第二次出8第三次出11。很多人不实现这个逻辑但用户一旦用了这个功能就会觉得你的计算器“不聪明”。用我之前说过的lastOperand字段就能解决。其实你仔细想想lastOperand的本质是记住上一次的第二个操作数每次等号都把它重新参与运算这正好符合实体计算器的行为。这个功能在“工控工具”类的小程序里基本用不到因为那种场景都是输入参数后一次性算结果。但既然做的是一个通用计算器这个细节我认为值得花几行代码去实现。5.3 显示长度溢出导致界面错乱TextBox宽度有限如果算出来的结果是一个十几位的大数字不处理就会撑破布局或者显示成科学计数法的一长串。我在DigitButton_Click里限定了输入长度不超过15位但计算结果的长度没法靠输入限制来控制。所以FormatResult里返回的字符串我额外加了一行判断if (result.Length 15) { // 用科学计数法显示并保证最多15个字符 result value.ToString(G10); }G10会把特别大或特别小的数自动转成科学计数法而且只保留10位有效数字在显示上不会过度溢出。5.4 TableLayoutPanel 行列比例设置我在排按钮布局时用的是TableLayoutPanel这块有一个常见坑如果你设置每列的SizeType为Percent且百分比加起来不等于100面板的列宽会显示异常。比如第0列设成22%第1列设成22%第2列设成22%最后一列设成34%加起来刚好100才能正常等分。如果哪次你设了22% x3再加30%面板不会报错但最后一列会明显比其他列窄。推荐的做法是在TableLayoutPanel的Columns属性里把每一列的SizeType都设为Percent值设成25%一共四列。Rows同理。行高的比例按“等号按钮特殊处理”的方式去调整时也要时刻注意Percent值的总和。5.5 窗口拉伸时控件尺寸不变新手常遇到一个问题运行后拖动窗口边缘放大结果按钮和文本框大小纹丝不动界面右上角空出一大块非常难看。这是因为你没有设置Anchor属性。WinForms里Anchor决定了控件与容器边缘的相对距离。快速解决TextBox的Anchor设为Top, Left, Right这样宽度会随窗口拉伸按钮区域如果用的是TableLayoutPanel把TableLayoutPanel的Anchor设为Top, Bottom, Left, Right行列会自动等比缩放。我的实际操作经验是TableLayoutPanel的Anchor比逐个按钮设Anchor要省心得多因为它内部的行列比例是自动分配的。6. 这个项目后续还能怎么扩展写到这基础的计算器已经在你的工程里跑起来了。但说实话这只是个开始。你可以顺着下面几条线继续练手一是加上历史记录。用ListBox或ListView记录前20次计算结果再配合一个“清除历史”按钮。这个功能会让你接触到集合控件绑定和滚动条管理对真实项目里常见的日志列表实现很有帮助。二是改成WPF版本。WPF和WinForms的界面逻辑完全不一样WPF用XAML声明式布局MVVM模式下的数据绑定和命令系统都要重新设计。同一个计算器项目用两套技术栈各写一遍会非常直观地体会到两者在设计思路上的差异。很多人问我是先学WinForms还是WPF我的建议是先用WinForms把逻辑搞明白再跳WPF时你会少踩一半坑。三是把核心计算逻辑抽成一个独立的类库然后在一个ASP.NET Core的Web API里调用做一个“远程计算器”接口。这条线比较野但很有意思它会迫使你思考界面层、业务层、服务层的边界到底在哪里。我见过不少上位机项目把业务逻辑全塞在Form1.cs里最后想加一个自动化测试都没法写。如果你提前在计算器这个小项目里养成“逻辑和界面分离”的习惯后面写大项目会轻松得多。我在这个计算器项目上踩过的坑、重构过的代码基本都是上面这些内容。最后给你一个实在的建议写完一个版本后把源码保存好隔两周再打开试着不借助任何资料、不问任何人独立把它重写一遍。第一次重写你会发现很多当时“背下来”的代码其实没理解透第二次重写你就会开始思考“这个设计合理吗”到了第三次写出来的东西才真正内化成你脑子里的东西。C# WinForms计算器虽然是个老掉牙的项目但作为练手和梳理知识体系的载体它真的比你去抄一个复杂管理系统源码有价值得多。本文还有配套的精品资源点击获取