简介:这是一份面向C#初学者与Windows窗体开发入门者的计算器源代码资源,帮助读者通过一个完整可运行的小型项目理解GUI编程的基本流程。资源包共56个文件,以14个cs源文件为核心,配合6个resx资源文件、6个ico图标、12张jpg界面素材及3个可执行exe等,压缩包约1.21MB,涵盖窗体设计、事件处理与逻辑运算等模块。代码围绕Windows Forms控件、按钮点击事件、运算符状态保存与结果显示展开,并包含除以零等错误处理思路,同时附带关于计算器、日历、手机等辅助窗体与说明图片,便于对照学习界面布局与代码组织。目前已有546人学习下载,适合希望从零掌握C#窗体应用开发、需要可运行示例参考的读者。
1. 从双击到出结果:一个 Windows 窗体计算器到底能跑多快
很多人第一次接触 C# 桌面开发,都是从拖控件开始的。工具箱里拽几个 Button,摆一个 TextBox,双击按钮写两行Click事件,一个能算加减乘除的窗口就出来了。但真把这份 C# 计算器源代码下载下来跑一遍,你会发现它和“拖控件作业”完全不是一回事——它把表达式解析、状态机、异常处理、键盘事件都串起来了,是一个能直接当模板用的 Windows 窗体程序。这份源码适合两类人:一类是刚学完 C# 语法、想找一个完整窗体项目练手的开发者;另一类是手里有类似小工具需求、想直接拿一份结构清晰的源程序改吧改吧就用的人。它不依赖数据库、不依赖网络,双击就能跑,改起来也不用担心牵一发动全身。
2. 项目结构拆解:窗体、逻辑、表达式三层怎么分
2.1 目录与文件构成
拿到源码包,第一件事不是急着按 F5,而是先看目录。一个规整的 C# 窗体计算器,通常长这样:
Calculator/ ├── Calculator.sln # 解决方案文件,双击用 VS 打开 ├── Calculator/ │ ├── Program.cs # 程序入口,Main 方法在这里 │ ├── Form1.cs # 主窗体逻辑(事件处理) │ ├── Form1.Designer.cs # 设计器自动生成的控件布局代码 │ ├── Form1.resx # 窗体资源文件 │ ├── Calculator.cs # 计算核心类(表达式解析/运算) │ └── Properties/ │ ├── AssemblyInfo.cs │ └── Settings.settings这里有个新手最容易犯的错:直接去改Form1.Designer.cs。这个文件是设计器自动生成的,你在代码里手动加一行,下次在窗体设计器里拖一下控件,它就被覆盖了。所有控件布局的调整,要么在可视化设计器里拖,要么在Form1.cs的构造函数里用代码写。
Program.cs是入口,内容很固定:
// Program.cs —— 应用程序入口 using System; using System.Windows.Forms; namespace Calculator { static class Program { [STAThread] // 单线程单元模式,WinForms 必须 static void Main() { Application.EnableVisualStyles(); // 启用视觉样式 Application.SetCompatibleTextRenderingDefault(false); Application.Run(new Form1()); // 启动主窗体 } } }[STAThread]这个特性不能删,删了之后文件对话框、剪贴板操作会出玄学问题。Application.Run(new Form1())是消息循环的起点,窗体关闭后 Main 才返回。
2.2 控件布局与命名规范
窗体上的控件命名,直接决定了后面写事件处理时顺不顺手。常见做法是给每个按钮起有意义的名字,而不是用默认的button1、button2。比如:
| 控件类型 | 命名示例 | 用途 |
|---|---|---|
| TextBox | txtDisplay | 显示输入和结果 |
| Button | btnDigit0~btnDigit9 | 数字键 |
| Button | btnAdd/btnSub/btnMul/btnDiv | 四则运算 |
| Button | btnEquals | 等号 |
| Button | btnClear | 清除 |
| Button | btnBackspace | 退格 |
命名规范这件事,一个人写无所谓,但只要项目要给别人看或者自己过两周再回来改,btnDigit7和button7的差别就是“一眼看懂”和“翻半天”的差别。
2.3 计算核心类的职责划分
把计算逻辑从窗体代码里抽出来,是这个项目值得学的地方。窗体只负责“收集用户按了什么键”,计算类负责“根据当前状态决定显示什么”。核心类通常长这样:
// Calculator.cs —— 计算核心,与界面解耦 namespace Calculator { public class CalcEngine { private double _operand1 = 0; // 第一个操作数 private double _operand2 = 0; // 第二个操作数 private string _pendingOp = ""; // 待执行的运算符 private bool _isNewEntry = true; // 是否开始输入新数字 // 输入数字时调用 public string InputDigit(string current, string digit) { if (_isNewEntry) { current = digit; // 新数字,直接替换 _isNewEntry = false; } else { if (current == "0") current = digit; else current += digit; // 追加到末尾 } return current; } // 按下运算符时调用 public string SetOperator(string current, string op) { if (_pendingOp != "" && !_isNewEntry) { // 已有待执行运算,先算出来 current = ExecutePending(current); } _operand1 = double.Parse(current); _pendingOp = op; _isNewEntry = true; return current; } // 按下等号时调用 public string Equals(string current) { if (_pendingOp == "") return current; return ExecutePending(current); } private string ExecutePending(string current) { _operand2 = double.Parse(current); double result = 0; switch (_pendingOp) { case "+": result = _operand1 + _operand2; break; case "-": result = _operand1 - _operand2; break; case "*": result = _operand1 * _operand2; break; case "/": if (_operand2 == 0) return "错误"; // 除零保护 result = _operand1 / _operand2; break; } _pendingOp = ""; _isNewEntry = true; return result.ToString(); } } }这段代码的关键在于_isNewEntry这个状态位。它解决的是“按下 5 再按 + 再按 3,显示应该是 3 而不是 53”的问题。没有这个标志位,输入逻辑就会乱套。ExecutePending里对除零做了保护,返回“错误”而不是抛异常,这是桌面计算器的常见做法——用户看到“错误”比看到程序崩溃要好得多。
3. 从零跑起来:环境、编译、第一次运行
3.1 开发环境准备
这份源码是标准的 .NET Framework WinForms 项目,跑起来需要:
- Visual Studio 2019 或更高版本(社区版免费,够用)
- 安装时勾选“.NET 桌面开发”工作负载
- .NET Framework 4.7.2 或更高版本(Win10/Win11 自带)
如果你不想装完整的 VS,也可以用dotnetCLI 跑,但 WinForms 的窗体设计器只有 VS 才有,纯 CLI 改布局会很痛苦。我一般建议至少装个 VS 社区版,拖控件、看属性、调试断点都方便。
3.2 编译与运行步骤
打开解决方案后,确认启动项目是Calculator,然后:
# 方式一:VS 里直接按 F5(调试运行)或 Ctrl+F5(不调试运行) # 方式二:命令行编译运行 dotnet build Calculator.sln dotnet run --project Calculator/Calculator.csproj第一次编译如果报“找不到 Windows.Forms 引用”,说明项目文件里的 TargetFramework 写错了。WinForms 项目必须是:
<TargetFramework>net472</TargetFramework> <UseWindowsForms>true</UseWindowsForms>如果是 .NET Core 风格的 WinForms(net6.0-windows 及以上),UseWindowsForms必须显式设为 true,否则编译能过但运行时报“无法加载 Windows.Forms 程序集”。
3.3 事件绑定:按钮怎么知道该干什么
窗体上的每个按钮,都需要绑定 Click 事件。在Form1.cs里,常见做法是在构造函数里统一绑定:
// Form1.cs 构造函数片段 public Form1() { InitializeComponent(); BindEvents(); } private void BindEvents() { // 数字键统一绑定到同一个处理方法 btnDigit0.Click += DigitButton_Click; btnDigit1.Click += DigitButton_Click; // ... 其余数字键同理 btnAdd.Click += OperatorButton_Click; btnSub.Click += OperatorButton_Click; btnMul.Click += OperatorButton_Click; btnDiv.Click += OperatorButton_Click; btnEquals.Click += (s, e) => { txtDisplay.Text = _engine.Equals(txtDisplay.Text); }; btnClear.Click += (s, e) => { txtDisplay.Text = "0"; _engine = new CalcEngine(); // 重置状态 }; } private void DigitButton_Click(object sender, EventArgs e) { Button btn = sender as Button; string digit = btn.Text; // 按钮上的文字就是数字 txtDisplay.Text = _engine.InputDigit(txtDisplay.Text, digit); } private void OperatorButton_Click(object sender, EventArgs e) { Button btn = sender as Button; string op = btn.Text; // "+" "-" "*" "/" txtDisplay.Text = _engine.SetOperator(txtDisplay.Text, op); }这里用sender as Button拿到触发事件的按钮,再读它的Text属性来决定输入什么。好处是十个数字键只需要一个处理方法,不用写十遍。btnEquals和btnClear用 lambda 表达式直接内联,代码更紧凑。
注意:
sender as Button如果转换失败会返回 null,后面访问btn.Text就会抛 NullReferenceException。稳妥写法是加一句if (btn == null) return;。
3.4 键盘支持:让计算器不只能用鼠标点
一个合格的计算器,应该支持键盘输入。数字键、运算符、回车、退格都要能用。做法是在窗体上挂KeyPress或KeyDown事件:
// 在 Form1 构造函数里加上 this.KeyPreview = true; // 让窗体先于控件接收键盘事件 this.KeyPress += Form1_KeyPress; private void Form1_KeyPress(object sender, KeyPressEventArgs e) { char c = e.KeyChar; if (char.IsDigit(c)) { txtDisplay.Text = _engine.InputDigit(txtDisplay.Text, c.ToString()); e.Handled = true; // 阻止系统发出“嘀”声 } else if ("+-*/".Contains(c)) { txtDisplay.Text = _engine.SetOperator(txtDisplay.Text, c.ToString()); e.Handled = true; } else if (c == '=' || c == '\r') // 等号或回车 { txtDisplay.Text = _engine.Equals(txtDisplay.Text); e.Handled = true; } else if (c == '\b') // 退格 { string t = txtDisplay.Text; txtDisplay.Text = t.Length > 1 ? t.Substring(0, t.Length - 1) : "0"; e.Handled = true; } }KeyPreview = true是关键,不设的话键盘事件会被 TextBox 先吃掉。e.Handled = true用来阻止系统发出默认的提示音,不然每按一个键就“嘀”一声,体验很差。
4. 避坑与排查:编译能过但运行不对的几种情况
4.1 除零后继续运算,结果全是“错误”
现象:输入 5 ÷ 0 =,显示“错误”,然后按 C 清除,再算 1 + 1,结果还是“错误”。
原因:ExecutePending里除零返回了“错误”字符串,但_pendingOp没有清空,_operand1也没有重置。下次运算时double.Parse("错误")会抛异常,被上层吞掉后继续返回“错误”。
解决:除零分支里要把状态重置干净:
case "/": if (_operand2 == 0) { _pendingOp = ""; _isNewEntry = true; _operand1 = 0; return "错误"; } result = _operand1 / _operand2; break;4.2 连续按运算符,操作数被覆盖
现象:输入 5 + 3 +,期望显示 8,实际显示 3。
原因:第二次按+时,SetOperator里判断_isNewEntry为 false 才执行待处理运算。但第一次按+后_isNewEntry被设为 true,第二次按+时_isNewEntry还是 true,跳过了ExecutePending,直接把_operand1覆盖成了 3。
解决:SetOperator里判断条件改成if (_pendingOp != ""),不管_isNewEntry是什么,只要有待处理运算符就先算:
public string SetOperator(string current, string op) { if (_pendingOp != "") { current = ExecutePending(current); } _operand1 = double.Parse(current); _pendingOp = op; _isNewEntry = true; return current; }4.3 小数点可以按出多个
现象:输入 3.14.15,显示 3.14.15,解析时抛异常。
原因:InputDigit只处理了数字,没有对小数点做去重判断。
解决:在InputDigit里加一个分支,或者单独写一个InputDecimal方法:
public string InputDecimal(string current) { if (_isNewEntry) { current = "0."; _isNewEntry = false; } else if (!current.Contains(".")) { current += "."; } return current; }4.4 窗体缩放后控件错位
现象:把窗体拉大,按钮还挤在左上角,右边一大片空白。
原因:控件的Anchor属性默认是 Top+Left,不会随窗体缩放。
解决:在设计器里选中按钮,把Anchor设为 Top+Left+Right(或者用 TableLayoutPanel 做布局容器)。更省事的做法是直接用TableLayoutPanel把按钮按行列排好,每个按钮Dock = Fill,缩放时自动等比例分配空间。
4.5 发布后在其他电脑上打不开
现象:自己电脑上跑得好好的,拷给别人双击没反应,或者提示“缺少 .NET Framework 4.7.2”。
原因:目标机器没装对应版本的 .NET Framework,或者项目用了 64 位特定配置。
解决:发布时在项目属性里把目标框架设低一点(比如 4.6.1),兼容性更好。或者用 ClickOnce 发布,勾选“从与应用程序相同的位置下载系统必备组件”,安装时会自动引导装框架。
5. 二次开发:从计算器到可复用小工具的改造思路
5.1 把计算核心抽成独立类库
现在CalcEngine和窗体在同一个项目里,如果想在别的项目里复用这套计算逻辑,可以把它拆成独立的类库:
dotnet new classlib -n CalcCore # 把 Calculator.cs 移过去,命名空间改成 CalcCore # 主项目添加引用 dotnet add Calculator/Calculator.csproj reference CalcCore/CalcCore.csproj拆出去之后,CalcCore不依赖任何 UI 框架,可以在控制台、WPF、甚至 Web 后端里用。这是从“作业”走向“工程”的第一步。
5.2 用栈实现表达式解析,支持括号和优先级
现在的实现是“即时运算”模式,不支持2 + 3 * 4这种带优先级的表达式。要支持的话,得引入表达式解析。常见做法是用两个栈:一个存操作数,一个存运算符,遇到优先级更高的运算符先算:
// 简化版双栈解析思路 public double Evaluate(string expr) { var values = new Stack<double>(); var ops = new Stack<char>(); for (int i = 0; i < expr.Length; i++) { char c = expr[i]; if (char.IsDigit(c)) { // 解析完整数字(含小数点) string num = ""; while (i < expr.Length && (char.IsDigit(expr[i]) || expr[i] == '.')) num += expr[i++]; i--; values.Push(double.Parse(num)); } else if (c == '(') ops.Push(c); else if (c == ')') { while (ops.Peek() != '(') values.Push(ApplyOp(ops.Pop(), values.Pop(), values.Pop())); ops.Pop(); // 弹出 '(' } else if ("+-*/".Contains(c)) { while (ops.Count > 0 && Priority(ops.Peek()) >= Priority(c)) values.Push(ApplyOp(ops.Pop(), values.Pop(), values.Pop())); ops.Push(c); } } while (ops.Count > 0) values.Push(ApplyOp(ops.Pop(), values.Pop(), values.Pop())); return values.Pop(); }这段代码是很多计算器项目的分水岭。加上它之后,计算器就从“按一步算一步”变成了“输入完整表达式再求值”,用户体验和代码复杂度都上了一个台阶。Priority方法里*/返回 2,+-返回 1,(返回 0。
5.3 加历史记录和内存功能
一个实用的小工具,通常还需要 M+、M-、MR、MC 这几个内存键,以及一个历史记录列表。内存功能实现很简单,在CalcEngine里加一个_memory字段:
private double _memory = 0; public void MemoryAdd(double value) => _memory += value; public void MemorySub(double value) => _memory -= value; public double MemoryRecall() => _memory; public void MemoryClear() => _memory = 0;历史记录可以用一个List<string>存每次运算的表达式和结果,界面上加一个ListBox显示。双击历史记录项,把结果回填到显示框,这个交互很实用。
5.4 验证改造是否正确的几个用例
改完核心逻辑后,别急着点界面,先写几个单元测试把边界情况跑一遍:
| 测试输入 | 期望输出 | 验证点 |
|---|---|---|
| 5 ÷ 0 = | 错误 | 除零保护 |
| 0.1 + 0.2 = | 0.3 | 浮点精度(注意实际可能是 0.30000000000000004) |
| 5 + 3 + = | 8 | 连续运算符 |
| 3.14.15 | 3.1415 或拒绝输入 | 小数点去重 |
| 999999999 × 999999999 | 9.99999998E+17 | 大数溢出显示 |
浮点精度那条要特别注意,0.1 + 0.2在 double 下不等于 0.3。如果要求精确,得换成decimal类型。我一般会在CalcEngine里把double全换成decimal,代价是运算速度慢一点,但计算器场景完全够用,而且不会出现0.30000000000000004这种让人一脸问号的显示。
从那以后我每次拿到一份窗体源码,都强制先跑一遍除零、连续运算、小数点这三个用例,确认核心逻辑没问题再动界面。希望这份拆解能帮你把这份 C# 计算器源代码真正跑起来、改起来。
本文还有配套的精品资源,点击获取