C#计算器源码与项目开发详解
简介:本资源围绕”C#计算器源码”展开,详细介绍如何使用C#语言开发一个具备基础数学运算功能的计算器程序。C#是微软推出的一种面向对象的编程语言,广泛应用于Windows应用、Web系统及游戏开发。资源内容涵盖控制台输入输出、运算符重载、表达式解析、异常处理、图形界面设计、多线程优化、设计模式应用以及单元测试等关键知识点。通过完整实现一个计算器项目,学习者可以掌握C#编程核心技能,提升软件开发与工程实践能力,适用于编程初学者和进阶开发者。
1. C#编程语言基础与计算器项目概述
C#(发音为“C Sharp”)是由微软公司开发的一种现代、面向对象的编程语言,广泛应用于Windows应用程序、Web开发、游戏开发(如Unity引擎)以及企业级系统构建。它融合了C++的强大性能与Java的高级抽象能力,同时集成在.NET框架中,提供了丰富的类库支持和跨平台能力(通过.NET Core/.NET 5+)。
本项目将以一个功能完整的 计算器应用 为实践主线,帮助读者掌握C#编程语言的核心语法和开发技巧。从基础的控制台输入输出,到图形界面设计,再到项目架构优化,逐步构建一个可扩展、健壮的计算器系统。通过该项目,读者将掌握C#开发的核心技能,并具备独立开发小型应用的能力。
2. 控制台输入输出与数值类型转换
2.1 控制台输入输出的基本操作
2.1.1 使用Console.WriteLine与Console.ReadLine进行数据交互
在C#中,控制台应用程序的输入输出是通过 System.Console 类提供的方法实现的。其中, Console.WriteLine 用于向控制台输出信息,而 Console.ReadLine 则用于接收用户的输入。
以下是一个简单的示例代码,演示如何使用这两个方法进行基本的交互操作:
using System;
class Program
{
static void Main()
{
Console.WriteLine("请输入您的姓名:"); // 输出提示信息
string name = Console.ReadLine(); // 读取用户输入
Console.WriteLine("您好," + name + "!"); // 输出欢迎信息
}
}
逐行代码解读:
- 第1行:
using System;引入命名空间System,其中包含了Console类。 - 第3行:定义
Program类。 - 第5行:定义
Main方法,作为程序的入口点。 - 第7行:使用
Console.WriteLine向控制台输出“请输入您的姓名:”。 - 第8行:调用
Console.ReadLine方法读取用户输入,并将其存储在字符串变量name中。 - 第9行:输出问候语,拼接用户输入的姓名。
该代码实现了最基础的交互流程,用户可以在控制台输入姓名,程序会根据输入做出响应。这种模式适用于命令行工具、调试信息输出、以及简单的控制台计算器项目输入处理等场景。
2.1.2 输入输出重定向与多语言支持
在某些场景下,我们可能希望将控制台程序的输入输出重定向到文件或其他流,以实现日志记录、批量测试等功能。C# 提供了 Console.SetIn 和 Console.SetOut 方法来实现输入输出流的重定向。
以下是一个示例,展示如何将标准输出重定向到文件:
using System;
using System.IO;
class Program
{
static void Main()
{
// 创建一个StreamWriter对象,用于将输出写入文件
StreamWriter writer = new StreamWriter("output.txt");
Console.SetOut(writer); // 重定向标准输出到文件
Console.WriteLine("这条消息将被写入文件output.txt");
writer.Close(); // 关闭写入流
}
}
逐行代码解读:
- 第1~2行:引入
System和System.IO命名空间,后者用于文件操作。 - 第6行:创建一个
StreamWriter对象,目标文件为output.txt。 - 第7行:调用
Console.SetOut方法,将标准输出流重定向到writer。 - 第9行:执行输出操作,该行文本将被写入
output.txt文件。 - 第11行:关闭流以释放资源。
此外,C#控制台程序也支持多语言输入输出,主要通过 Console.InputEncoding 和 Console.OutputEncoding 设置字符编码。例如,要支持中文输出,可以设置如下:
Console.OutputEncoding = System.Text.Encoding.UTF8;
这在处理非英文字符时非常关键,尤其是在跨平台或国际化应用中尤为重要。
2.2 数值类型与类型转换机制
2.2.1 整型、浮点型与十进制类型的适用场景
C# 提供了丰富的数值类型,主要包括整型、浮点型和十进制类型,适用于不同的计算需求。
| 类型 | 占用字节 | 范围/精度 | 适用场景 |
|---|---|---|---|
int |
4 | -2,147,483,648 ~ 2,147,483,647 | 一般整数运算 |
long |
8 | -9,223,372,036,854,775,808 ~ 9.2e18 | 大整数处理 |
float |
4 | ±1.5e-45 ~ ±3.4e38(7位有效数字) | 科学计算、图形处理 |
double |
8 | ±5.0e-324 ~ ±1.7e308(15位有效数字) | 更高精度的浮点计算 |
decimal |
16 | ±1.0e-28 ~ ±7.9e28(28-29位有效数字) | 货币、金融计算 |
在开发计算器项目时,根据运算需求选择合适的类型非常重要:
- 对于整数运算,如加减乘除,使用
int或long; - 对于科学计算或图形渲染,使用
float或double; - 对于涉及金额、财务的计算,必须使用
decimal,以避免浮点误差。
以下是一个数值类型使用的示例:
using System;
class Program
{
static void Main()
{
int a = 100;
double b = 3.14159265358979;
decimal c = 123.45678901234567890123456789m;
Console.WriteLine("int: " + a);
Console.WriteLine("double: " + b);
Console.WriteLine("decimal: " + c);
}
}
逐行代码解读:
- 第6行:声明
int类型变量a,存储整数。 - 第7行:声明
double类型变量b,存储高精度浮点数。 - 第8行:声明
decimal类型变量c,存储更高精度的数值,注意需要添加m后缀。 - 第10~12行:输出各变量的值。
2.2.2 显式转换与隐式转换的使用原则
在C#中,数值类型之间的转换可以是隐式的(自动完成)或显式的(强制转换)。
- 隐式转换 :发生在不丢失数据或精度的情况下,例如从
int到double。 - 显式转换 :需要使用强制类型转换操作符,例如从
double到int,可能会丢失精度。
using System;
class Program
{
static void Main()
{
int i = 100;
double d = i; // 隐式转换
double x = 3.14159265358979;
int j = (int)x; // 显式转换,结果为3
Console.WriteLine("隐式转换结果:" + d);
Console.WriteLine("显式转换结果:" + j);
}
}
逐行代码解读:
- 第7行:
int类型变量i被自动转换为double类型,属于隐式转换。 - 第10行:使用
(int)进行显式转换,将浮点数x截断为整数,精度丢失。
类型转换原则总结:
- 优先使用隐式转换 :在类型兼容且不会丢失数据的情况下,使用隐式转换更安全。
- 使用显式转换时需谨慎 :尤其是将浮点型转换为整型时,可能会导致精度丢失或数值错误。
- 使用
Convert类或Parse方法进行字符串到数值的转换 :
-int.Parse("123")
-Convert.ToDouble("3.14")
2.3 字符串解析与数值提取
2.3.1 使用Split、Substring等方法提取运算符与操作数
在实现计算器功能时,常常需要将用户输入的表达式字符串(如“123+456”)拆解为操作数和运算符。我们可以使用 Split 、 Substring 等字符串操作方法来实现。
以下是一个使用 Split 方法提取操作数的示例:
using System;
class Program
{
static void Main()
{
string input = "123+456";
string[] parts = input.Split('+');
int operand1 = int.Parse(parts[0]);
int operand2 = int.Parse(parts[1]);
Console.WriteLine("操作数1:" + operand1);
Console.WriteLine("操作数2:" + operand2);
}
}
逐行代码解读:
- 第7行:使用
Split('+')方法将字符串按“+”分割为数组。 - 第8~9行:将分割后的字符串片段转换为整型数值。
- 第11~12行:输出两个操作数。
此外,也可以使用 Substring 方法结合 IndexOf 获取运算符位置,进行更灵活的解析。
graph TD
A[输入表达式] --> B{是否存在运算符?}
B -->|是| C[使用Split或Substring拆分表达式]
B -->|否| D[抛出异常]
C --> E[提取操作数]
E --> F[转换为数值]
F --> G[执行运算]
2.3.2 使用正则表达式匹配表达式结构
对于更复杂的表达式(如“123+456*789”),使用正则表达式可以更高效地提取操作数和运算符。
以下是一个使用正则表达式解析表达式的示例:
using System;
using System.Text.RegularExpressions;
class Program
{
static void Main()
{
string input = "123+456*789";
Regex regex = new Regex(@"(\d+)([+\-*/])(\d+)([+\-*/])(\d+)");
Match match = regex.Match(input);
if (match.Success)
{
Console.WriteLine("第一个操作数:" + match.Groups[1].Value);
Console.WriteLine("第一个运算符:" + match.Groups[2].Value);
Console.WriteLine("第二个操作数:" + match.Groups[3].Value);
Console.WriteLine("第二个运算符:" + match.Groups[4].Value);
Console.WriteLine("第三个操作数:" + match.Groups[5].Value);
}
else
{
Console.WriteLine("表达式格式不正确!");
}
}
}
逐行代码解读:
- 第7行:定义正则表达式模式,其中
\d+表示一个或多个数字,[+\-*/]表示运算符。 - 第8行:调用
Regex.Match方法尝试匹配输入字符串。 - 第10~16行:如果匹配成功,则提取各操作数和运算符并输出。
- 第18~19行:否则提示表达式格式错误。
正则表达式可以有效应对多操作符、带空格、甚至带括号的复杂表达式结构,是构建表达式解析器的重要工具。
总结性说明:
本章从控制台输入输出的基本操作出发,逐步深入到数值类型的选用与转换机制,并进一步探讨了字符串解析技术,特别是在计算器项目中如何提取表达式中的操作数与运算符。通过结合代码示例、流程图、表格等多形式内容,展示了C#中数据交互与处理的多种实现方式,为后续章节中构建完整计算器逻辑奠定了坚实基础。
3. 基础运算逻辑与异常处理机制
在现代编程实践中,基础运算逻辑不仅是程序运行的核心组成部分,同时也是构建复杂系统的重要基石。本章将深入探讨如何在C#中实现基础运算逻辑,尤其是如何利用运算符重载机制为自定义类型赋予直观的数学操作能力。此外,我们还将研究表达式解析的基本方法,为构建完整的计算器功能提供技术支撑。同时,为了确保程序的健壮性和用户输入的容错性,我们将详细讲解异常处理机制,帮助开发者编写更稳定、安全的代码。
3.1 运算符重载与自定义类型计算
3.1.1 理解运算符重载的实现原理
运算符重载是C#语言提供的强大功能之一,它允许开发者为自定义类型定义常见的运算符行为(如加法、减法、比较等),从而提升代码的可读性和逻辑表达能力。
在C#中,运算符重载通过静态方法实现。例如, + 运算符的重载通常定义如下:
public static ClassName operator +(ClassName a, ClassName b)
{
// 实现加法逻辑
return new ClassName();
}
运算符重载的实现机制:
- 静态方法 :运算符重载必须是类的静态方法。
- 参数限制 :至少一个参数必须是当前类的类型。
- 返回类型 :通常返回当前类的实例,也可以根据需求返回其他类型。
- 运算符列表 :支持的运算符包括
+,-,*,/,==,!=,<,>,++,--等。
优点:
- 提升代码可读性。
- 使自定义类型像基本类型一样自然使用。
- 增强代码的抽象能力。
限制:
- 不能改变运算符的优先级和结合性。
- 不能创建新的运算符。
3.1.2 在自定义数值类型中实现加减乘除运算
我们以一个简单的自定义数值类型 Rational (有理数)为例,演示如何实现加减乘除运算符重载。
示例代码:实现有理数类的加法
public class Rational
{
public int Numerator { get; private set; }
public int Denominator { get; private set; }
public Rational(int numerator, int denominator)
{
if (denominator == 0)
throw new ArgumentException("Denominator cannot be zero.");
// 简单约分
int gcd = GCD(numerator, denominator);
Numerator = numerator / gcd;
Denominator = denominator / gcd;
}
// 计算最大公约数
private int GCD(int a, int b)
{
return b == 0 ? a : GCD(b, a % b);
}
// 重载加法运算符
public static Rational operator +(Rational a, Rational b)
{
int numerator = a.Numerator * b.Denominator + b.Numerator * a.Denominator;
int denominator = a.Denominator * b.Denominator;
return new Rational(numerator, denominator);
}
public override string ToString()
{
return $"{Numerator}/{Denominator}";
}
}
代码逻辑分析
- 构造函数 :构造函数接收分子和分母,检查分母是否为零,并调用
GCD方法进行约分。 - GCD 方法 :采用递归方式实现欧几里得算法,用于约分。
- 运算符重载
+: - 根据分数加法公式计算新分子和分母。
- 构造新的
Rational实例并返回。 - ToString 方法 :用于输出格式化字符串。
示例使用代码
Rational r1 = new Rational(1, 2);
Rational r2 = new Rational(1, 3);
Rational result = r1 + r2;
Console.WriteLine(result); // 输出:5/6
重载其他运算符示例
// 重载减法
public static Rational operator -(Rational a, Rational b)
{
int numerator = a.Numerator * b.Denominator - b.Numerator * a.Denominator;
int denominator = a.Denominator * b.Denominator;
return new Rational(numerator, denominator);
}
// 重载乘法
public static Rational operator *(Rational a, Rational b)
{
return new Rational(a.Numerator * b.Numerator, a.Denominator * b.Denominator);
}
// 重载除法
public static Rational operator /(Rational a, Rational b)
{
if (b.Numerator == 0)
throw new DivideByZeroException("Cannot divide by zero rational number.");
return new Rational(a.Numerator * b.Denominator, a.Denominator * b.Numerator);
}
使用场景与注意事项
- 运算符重载应保持直观性 :例如,
+应表示加法而非其他操作。 - 避免滥用 :仅在确实能提升代码可读性时使用。
- 注意异常处理 :如除法中的除零操作,应在构造函数或运算符中显式处理。
3.2 表达式解析与简单计算器实现
3.2.1 使用字符串分割实现简单表达式计算
在计算器项目中,用户输入通常是一个字符串形式的数学表达式,如 "12+34-56" 。我们可以利用字符串处理技术,将其解析为多个操作数和运算符,并依次进行计算。
示例代码:基于字符串分割的简单解析
public static double SimpleCalculate(string expression)
{
// 按照加减号分割操作数
string[] tokens = Regex.Split(expression, @"([+-])");
double result = double.Parse(tokens[0]);
int i = 1;
while (i < tokens.Length)
{
string op = tokens[i++];
double number = double.Parse(tokens[i++]);
switch (op)
{
case "+":
result += number;
break;
case "-":
result -= number;
break;
}
}
return result;
}
参数说明
expression:用户输入的表达式字符串,例如"12+34-56"。- 使用正则表达式
@"([+-])"分割字符串,保留运算符。
逻辑分析
- 使用
Regex.Split按照加减号分割字符串,保留操作符。 - 首个元素为初始值。
- 遍历数组,依次取出运算符和操作数,执行对应运算。
优缺点分析
| 优点 | 缺点 |
|---|---|
| 实现简单,适合初学者 | 无法处理括号和复杂运算顺序 |
| 不依赖第三方库 | 不支持乘除等运算 |
| 适合教学演示 | 性能较差,不适合大规模使用 |
3.2.2 使用表达式树构建动态计算逻辑
为了实现更复杂的表达式解析和计算,C# 提供了 System.Linq.Expressions 命名空间,可以动态构建表达式树,并编译为可执行的委托对象。
示例代码:使用表达式树实现动态计算
using System;
using System.Linq.Expressions;
public class ExpressionCalculator
{
public static double Evaluate(string expression)
{
// 构建参数表达式
ParameterExpression param = Expression.Parameter(typeof(double), "x");
// 将表达式字符串解析为表达式树
var lambda = new System.Linq.Dynamic.DynamicExpressionParser()
.ParseLambda(new ParameterExpression[] { param }, typeof(double), expression);
// 编译表达式
var compiled = (Func<double, double>)lambda.Compile();
// 执行计算
return compiled(0); // x 为 0,不影响表达式计算
}
}
注意:上面代码中使用了
System.Linq.Dynamic,这是一个非官方库,可以通过 NuGet 安装System.Linq.Dynamic包。
逻辑流程图(Mermaid)
graph TD
A[开始] --> B[接收表达式字符串]
B --> C[构建参数表达式]
C --> D[解析为表达式树]
D --> E[编译为可执行委托]
E --> F[执行计算并返回结果]
F --> G[结束]
示例使用代码
string expr = "12 + 34 * 56 - 78 / 3";
double result = ExpressionCalculator.Evaluate(expr);
Console.WriteLine(result); // 输出:1870
优点与适用场景
| 优点 | 适用场景 |
|---|---|
| 支持复杂的运算顺序和括号 | 需要动态解析用户输入的数学表达式 |
| 可扩展性强,支持变量、函数 | 用于构建通用计算器、公式解析器 |
| 可编译为高效代码 | 适用于性能要求较高的表达式计算 |
3.3 异常处理与程序健壮性设计
3.3.1 捕获除零异常与无效输入异常
在计算器程序中,用户输入的不可预测性是导致程序崩溃的主要原因之一。因此,合理的异常处理机制是保障程序健壮性的关键。
示例代码:处理除零异常和无效输入
try
{
Console.Write("请输入表达式:");
string input = Console.ReadLine();
// 解析并计算表达式
double result = SimpleCalculate(input);
Console.WriteLine($"结果:{result}");
}
catch (FormatException)
{
Console.WriteLine("错误:输入格式不正确,请输入有效的数学表达式。");
}
catch (DivideByZeroException)
{
Console.WriteLine("错误:除法操作中除数为零。");
}
catch (Exception ex)
{
Console.WriteLine($"发生未知错误:{ex.Message}");
}
异常类型说明
- FormatException :用户输入无法被解析为数字时抛出。
- DivideByZeroException :在除法运算中除数为零时抛出。
- Exception :通用异常,用于捕获未预料的错误。
使用场景
- 用户输入验证失败。
- 数学运算中的边界条件处理。
- 提升用户体验,避免程序崩溃。
3.3.2 使用 try-catch-finally 结构进行错误恢复与日志记录
除了捕获异常,我们还需要在程序中进行错误恢复和日志记录,以帮助开发者排查问题和改进系统。
示例代码:带 finally 的异常处理与日志记录
try
{
// 模拟一个计算操作
int a = 10;
int b = 0;
int result = a / b; // 会抛出除零异常
}
catch (DivideByZeroException ex)
{
Console.WriteLine("捕获到除零异常");
// 写入日志文件
File.WriteAllText("error.log", $"错误:{ex.Message},时间:{DateTime.Now}");
}
finally
{
Console.WriteLine("清理资源,无论是否发生异常都会执行。");
}
流程图(Mermaid)
graph TD
A[开始执行代码] --> B[尝试执行可能出错的代码]
B --> C{是否发生异常?}
C -->|是| D[进入 catch 块处理异常]
D --> E[记录日志]
C -->|否| F[继续执行正常逻辑]
D --> G[执行 finally 块]
F --> G
G --> H[结束]
参数说明与逻辑分析
- try 块 :包含可能抛出异常的代码。
- catch 块 :捕获并处理特定类型的异常。
- finally 块 :无论是否发生异常都会执行,常用于资源释放、日志写入等操作。
日志记录建议
- 使用
System.IO或第三方日志框架(如 NLog、Serilog)记录异常信息。 - 记录时间戳、异常类型、堆栈跟踪等信息,便于后续分析。
异常处理最佳实践
| 实践 | 说明 |
|---|---|
| 避免空 catch 块 | 应至少记录异常信息 |
| 细粒度捕获异常 | 按类型捕获,避免泛用 Exception |
| 使用 finally 清理资源 | 如文件流、数据库连接等 |
| 提供用户友好提示 | 避免暴露堆栈信息给普通用户 |
| 记录日志供后续分析 | 方便排查和修复问题 |
4. 图形界面开发与事件驱动模型
图形界面是现代应用程序不可或缺的一部分,尤其在用户交互频繁的场景中,图形界面不仅提升了用户体验,还增强了程序的可操作性和可维护性。在本章中,我们将深入探讨使用 Windows Forms 和 WPF(Windows Presentation Foundation) 构建图形界面的基础知识,并介绍事件驱动模型的核心机制,以及如何通过异步编程提升界面响应性。
本章内容将逐步展开,从界面设计、控件布局、事件绑定到异步操作,帮助开发者构建一个结构清晰、响应灵敏的图形界面计算器应用。
4.1 Windows Forms与WPF界面开发基础
Windows Forms 和 WPF 是两种主流的 Windows 桌面应用开发框架。Windows Forms 是基于 GDI+ 的传统界面开发方式,适合快速开发;而 WPF 基于 DirectX,支持更丰富的 UI 特性和数据绑定机制。
4.1.1 创建窗体与控件布局设计
我们以 Windows Forms 为例,演示如何创建一个简单的计算器界面。
示例:创建基本窗体和控件
using System;
using System.Windows.Forms;
namespace CalculatorApp
{
public class CalculatorForm : Form
{
private TextBox txtDisplay;
private Button btnAdd, btnSubtract, btnMultiply, btnDivide, btnCalculate;
private Button[] numberButtons = new Button[10];
public CalculatorForm()
{
InitializeComponents();
}
private void InitializeComponents()
{
this.Text = "简易计算器";
this.Size = new System.Drawing.Size(300, 400);
txtDisplay = new TextBox();
txtDisplay.Location = new System.Drawing.Point(20, 20);
txtDisplay.Width = 240;
txtDisplay.ReadOnly = true;
this.Controls.Add(txtDisplay);
// 创建数字按钮
for (int i = 0; i < 10; i++)
{
numberButtons[i] = new Button();
numberButtons[i].Text = i.ToString();
numberButtons[i].Width = 60;
numberButtons[i].Height = 50;
numberButtons[i].Location = new System.Drawing.Point(20 + (i % 3) * 70, 80 + (i / 3) * 60);
numberButtons[i].Click += NumberButton_Click;
this.Controls.Add(numberButtons[i]);
}
// 创建运算符按钮
btnAdd = CreateOperatorButton("+", 20, 260);
btnSubtract = CreateOperatorButton("-", 90, 260);
btnMultiply = CreateOperatorButton("*", 160, 260);
btnDivide = CreateOperatorButton("/", 230, 260);
btnCalculate = new Button();
btnCalculate.Text = "=";
btnCalculate.Location = new System.Drawing.Point(230, 320);
btnCalculate.Width = 60;
btnCalculate.Height = 50;
btnCalculate.Click += CalculateButton_Click;
this.Controls.Add(btnCalculate);
}
private Button CreateOperatorButton(string text, int x, int y)
{
Button btn = new Button();
btn.Text = text;
btn.Width = 60;
btn.Height = 50;
btn.Location = new System.Drawing.Point(x, y);
btn.Click += OperatorButton_Click;
this.Controls.Add(btn);
return btn;
}
private void NumberButton_Click(object sender, EventArgs e)
{
txtDisplay.Text += (sender as Button).Text;
}
private void OperatorButton_Click(object sender, EventArgs e)
{
txtDisplay.Text += " " + (sender as Button).Text + " ";
}
private void CalculateButton_Click(object sender, EventArgs e)
{
try
{
// 这里可以调用表达式解析方法
string expression = txtDisplay.Text;
// 示例:解析表达式并计算结果
txtDisplay.Text = "结果:" + EvaluateExpression(expression);
}
catch (Exception ex)
{
MessageBox.Show("计算错误:" + ex.Message);
}
}
private double EvaluateExpression(string expression)
{
// 简单实现,实际应用中应使用更安全的方式
var dataTable = new System.Data.DataTable();
return Convert.ToDouble(dataTable.Compute(expression, ""));
}
[STAThread]
static void Main()
{
Application.EnableVisualStyles();
Application.Run(new CalculatorForm());
}
}
}
代码逻辑分析:
- InitializeComponents 方法用于初始化窗体中的控件。
- NumberButton_Click 事件处理方法将数字添加到文本框中。
- OperatorButton_Click 事件处理方法将运算符插入到表达式中。
- CalculateButton_Click 方法尝试解析并计算表达式,使用
DataTable.Compute()进行简单计算。 - Main 方法启动窗体应用。
参数说明:
| 参数 | 描述 |
|---|---|
TextBox |
用于显示输入和结果 |
Button |
控制器,用于数字和运算符输入 |
Click 事件 |
用户点击按钮时触发的逻辑处理 |
4.1.2 数据绑定与界面更新机制
在 WPF 中,数据绑定是构建动态界面的核心机制。WPF 使用 MVVM(Model-View-ViewModel)模式来实现数据与界面的分离。
示例:WPF 数据绑定实现
<!-- MainWindow.xaml -->
<Window x:Class="WpfCalculator.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="WPF计算器" Height="350" Width="400">
<Grid>
<TextBox Text="{Binding DisplayText}" Margin="10" Height="40" FontSize="20" IsReadOnly="True"/>
<StackPanel Orientation="Horizontal" HorizontalAlignment="Center" VerticalAlignment="Bottom">
<Button Content="7" Width="50" Height="50" Command="{Binding NumberCommand}" CommandParameter="7"/>
<Button Content="8" Width="50" Height="50" Command="{Binding NumberCommand}" CommandParameter="8"/>
<Button Content="9" Width="50" Height="50" Command="{Binding NumberCommand}" CommandParameter="9"/>
<Button Content="+" Width="50" Height="50" Command="{Binding OperationCommand}" CommandParameter="+"/>
</StackPanel>
<!-- 更多按钮... -->
</Grid>
</Window>
// ViewModel.cs
public class CalculatorViewModel : INotifyPropertyChanged
{
private string displayText = string.Empty;
public string DisplayText
{
get { return displayText; }
set
{
displayText = value;
OnPropertyChanged();
}
}
public ICommand NumberCommand { get; }
public ICommand OperationCommand { get; }
public CalculatorViewModel()
{
NumberCommand = new RelayCommand(OnNumberPressed);
OperationCommand = new RelayCommand(OnOperationPressed);
}
private void OnNumberPressed(object param)
{
DisplayText += param as string;
}
private void OnOperationPressed(object param)
{
DisplayText += " " + (param as string) + " ";
}
public event PropertyChangedEventHandler PropertyChanged;
protected void OnPropertyChanged([CallerMemberName] string name = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name));
}
}
// RelayCommand 简化实现
public class RelayCommand : ICommand
{
private readonly Action<object> _execute;
public RelayCommand(Action<object> execute) => _execute = execute;
public bool CanExecute(object parameter) => true;
public void Execute(object parameter) => _execute(parameter);
public event EventHandler CanExecuteChanged;
}
表格:WPF 数据绑定机制解析
| 元素 | 作用 |
|---|---|
Text="{Binding DisplayText}" |
将 TextBox 的 Text 属性绑定到 ViewModel 的 DisplayText 属性 |
Command="{Binding NumberCommand}" |
将按钮点击绑定到 ViewModel 的命令 |
INotifyPropertyChanged |
通知界面属性变更,实现自动刷新 |
4.2 事件驱动编程模型的应用
事件驱动模型是图形界面编程的核心机制,用户操作(如点击按钮、输入文本)触发事件,程序通过事件处理函数做出响应。
4.2.1 按钮点击事件与文本框输入事件的绑定
在 Windows Forms 中,事件绑定通常通过代码实现,如前面章节所示。
示例:文本框内容变更事件
txtDisplay.TextChanged += TxtDisplay_TextChanged;
private void TxtDisplay_TextChanged(object sender, EventArgs e)
{
Console.WriteLine("当前表达式:" + txtDisplay.Text);
}
参数说明:
| 事件 | 触发条件 |
|---|---|
TextChanged |
文本框内容发生变化时触发 |
Click |
按钮被点击时触发 |
4.2.2 使用委托与事件实现模块化交互
委托(Delegate)是 C# 中用于封装方法引用的类型,事件(Event)是基于委托的封装,用于实现观察者模式。
示例:使用委托实现模块化事件通信
// 定义委托
public delegate void CalculationEventHandler(object sender, CalculationEventArgs e);
// 定义事件参数
public class CalculationEventArgs : EventArgs
{
public string Expression { get; set; }
public double Result { get; set; }
}
// 发布者类
public class CalculatorEngine
{
public event CalculationEventHandler CalculationPerformed;
public void Calculate(string expression)
{
try
{
double result = EvaluateExpression(expression);
OnCalculationPerformed(new CalculationEventArgs { Expression = expression, Result = result });
}
catch (Exception ex)
{
OnCalculationPerformed(new CalculationEventArgs { Expression = expression, Result = double.NaN });
}
}
protected virtual void OnCalculationPerformed(CalculationEventArgs e)
{
CalculationPerformed?.Invoke(this, e);
}
private double EvaluateExpression(string expression)
{
var table = new System.Data.DataTable();
return Convert.ToDouble(table.Compute(expression, ""));
}
}
// 订阅者类
public class Logger
{
public void OnCalculation(object sender, CalculationEventArgs e)
{
Console.WriteLine($"表达式: {e.Expression}, 结果: {e.Result}");
}
}
使用示例:
var engine = new CalculatorEngine();
var logger = new Logger();
engine.CalculationPerformed += logger.OnCalculation;
engine.Calculate("10 + 20");
流程图:事件驱动交互流程
graph TD
A[用户点击按钮] --> B[触发Click事件]
B --> C[调用Calculate方法]
C --> D[触发CalculationPerformed事件]
D --> E[Logger.OnCalculation被调用]
E --> F[日志输出计算结果]
4.3 提升界面响应性与多线程应用
在图形界面开发中,耗时操作(如网络请求、复杂计算)若在主线程执行,会导致界面卡顿。为此,C# 提供了多线程和异步编程模型。
4.3.1 使用 BackgroundWorker 处理耗时计算
BackgroundWorker 是 Windows Forms 中用于执行后台操作并更新 UI 的组件。
示例:使用 BackgroundWorker 计算
private BackgroundWorker backgroundWorker;
private void InitializeBackgroundWorker()
{
backgroundWorker = new BackgroundWorker();
backgroundWorker.WorkerReportsProgress = true;
backgroundWorker.DoWork += BackgroundWorker_DoWork;
backgroundWorker.RunWorkerCompleted += BackgroundWorker_RunWorkerCompleted;
}
private void btnStart_Click(object sender, EventArgs e)
{
string expression = txtDisplay.Text;
backgroundWorker.RunWorkerAsync(expression);
}
private void BackgroundWorker_DoWork(object sender, DoWorkEventArgs e)
{
string expression = e.Argument as string;
double result = EvaluateExpression(expression);
e.Result = result;
}
private void BackgroundWorker_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e)
{
if (e.Error != null)
{
MessageBox.Show("计算错误:" + e.Error.Message);
}
else
{
txtDisplay.Text = "结果:" + e.Result.ToString();
}
}
表格:BackgroundWorker 生命周期方法
| 方法 | 说明 |
|---|---|
DoWork |
在后台线程中执行耗时操作 |
ProgressChanged |
报告进度更新(需启用) |
RunWorkerCompleted |
后台任务完成后在 UI 线程中执行回调 |
4.3.2 使用 async/await 实现异步操作与 UI 更新
C# 提供了 async/await 关键字,使异步编程更简洁。
示例:异步计算表达式
private async void btnCalculateAsync_Click(object sender, EventArgs e)
{
string expression = txtDisplay.Text;
try
{
double result = await Task.Run(() => EvaluateExpression(expression));
txtDisplay.Text = "结果:" + result;
}
catch (Exception ex)
{
MessageBox.Show("计算错误:" + ex.Message);
}
}
private double EvaluateExpression(string expression)
{
// 模拟长时间计算
Thread.Sleep(1000);
var table = new System.Data.DataTable();
return Convert.ToDouble(table.Compute(expression, ""));
}
表格:async/await 与线程关系
| 术语 | 说明 |
|---|---|
async |
标记方法为异步方法 |
await |
等待异步任务完成,不阻塞主线程 |
Task.Run |
将任务放入线程池中异步执行 |
小结
在本章中,我们系统性地讲解了图形界面开发的基础知识,包括:
- 使用 Windows Forms 和 WPF 构建用户界面;
- 使用数据绑定机制实现界面与逻辑分离;
- 使用事件驱动模型处理用户交互;
- 利用多线程和异步编程提升界面响应性。
通过本章内容,开发者可以掌握构建图形界面应用的基本技能,并为后续实现完整计算器项目打下坚实基础。
5. 计算器项目架构设计与优化实践
在本章中,我们将从项目架构设计入手,探讨如何通过应用经典设计模式来提升计算器项目的可扩展性与可维护性。随后,我们会深入讲解如何进行项目测试与性能优化,确保程序的高效稳定运行。接着,介绍如何使用Git进行版本控制与团队协作开发,最后完成整个项目的打包与发布流程,为最终部署做好准备。
5.1 设计模式在计算器项目中的应用
设计模式是解决软件设计中常见问题的可复用解决方案。在计算器项目中,我们可以使用工厂模式与策略模式来提升代码的灵活性与扩展性。
5.1.1 使用工厂模式实现操作对象的创建
工厂模式允许我们通过统一接口创建不同类型的对象,避免在客户端代码中直接实例化具体类。以下是一个简单的工厂类实现:
public interface IOperation
{
double Execute(double a, double b);
}
public class AddOperation : IOperation
{
public double Execute(double a, double b)
{
return a + b;
}
}
public class OperationFactory
{
public IOperation CreateOperation(string operationType)
{
switch (operationType)
{
case "add":
return new AddOperation();
case "subtract":
return new SubtractOperation();
// 可扩展其他操作
default:
throw new ArgumentException("未知的操作类型");
}
}
}
代码说明:
IOperation是所有操作的统一接口。AddOperation实现了加法逻辑。OperationFactory是工厂类,根据输入字符串返回对应的操作实例。
5.1.2 使用策略模式分离不同的运算逻辑
策略模式允许运行时选择不同的算法,从而实现行为的动态替换。我们可以将每个运算逻辑封装为独立的策略类。
public class CalculatorContext
{
private IOperation _operation;
public void SetStrategy(IOperation operation)
{
_operation = operation;
}
public double ExecuteStrategy(double a, double b)
{
return _operation.Execute(a, b);
}
}
使用方式:
var context = new CalculatorContext();
context.SetStrategy(new AddOperation());
double result = context.ExecuteStrategy(5, 3); // 返回 8
通过策略模式,我们可以在不修改上下文类的前提下,灵活切换不同的运算逻辑,极大地增强了系统的可扩展性。
5.2 项目测试与性能优化
在完成架构设计后,必须对项目进行系统性测试与性能调优,以确保其在各种使用场景下的稳定性与效率。
5.2.1 编写单元测试验证计算逻辑的正确性
使用 MSTest 或 xUnit 等测试框架,可以为每个操作类编写单元测试。以下是一个 MSTest 示例:
[TestClass]
public class OperationTests
{
[TestMethod]
public void AddOperation_ShouldReturnCorrectResult()
{
var add = new AddOperation();
double result = add.Execute(3, 4);
Assert.AreEqual(7, result);
}
}
测试说明:
- 使用
[TestMethod]标记测试方法。 - 使用
Assert.AreEqual验证预期结果与实际结果是否一致。
5.2.2 使用性能分析工具优化关键路径
Visual Studio 自带的诊断工具(如性能探查器)可以帮助我们识别程序中的性能瓶颈。例如,我们可以:
- 分析 CPU 使用情况,找出耗时最长的函数。
- 查看内存分配情况,优化频繁的 GC 压力。
以下是一个使用 Stopwatch 手动测试性能的代码示例:
var stopwatch = new Stopwatch();
stopwatch.Start();
// 执行耗时操作
for (int i = 0; i < 100000; i++)
{
var result = context.ExecuteStrategy(10, 5);
}
stopwatch.Stop();
Console.WriteLine($"执行时间:{stopwatch.ElapsedMilliseconds} ms");
输出示例:
执行时间:12 ms
5.3 项目版本控制与协作开发
良好的版本控制是团队协作开发的基础。Git 是目前最流行的分布式版本控制系统。
5.3.1 使用Git进行代码版本管理
在项目根目录下初始化 Git 仓库:
git init
git add .
git commit -m "初始提交:基础计算器项目结构"
常用命令:
| 命令 | 说明 |
|---|---|
git status |
查看当前仓库状态 |
git log |
查看提交历史 |
git diff |
查看文件修改差异 |
5.3.2 使用分支策略与Pull Request提升协作效率
推荐使用 Git Flow 分支管理策略:
main:稳定版本分支develop:开发主分支feature/xxx:功能分支hotfix/xxx:紧急修复分支
在 GitHub 或 GitLab 上,通过 Pull Request(PR)机制可以进行代码审查和讨论:
git checkout -b feature/add-multiplication
# 开发完成后提交
git push origin feature/add-multiplication
之后在网页端创建 PR,等待 Review 通过后合并至 develop 分支。
5.4 完整计算器项目的构建与发布
完成开发与测试后,需要将项目打包并发布,以便部署到目标环境中。
5.4.1 项目打包与部署方式选择
在 Visual Studio 中,右键项目 -> “发布” -> 选择发布目标,例如:
- 文件系统部署 :适用于本地测试
- ClickOnce :适合企业内部部署
- MSI 安装包 :通过 WiX Toolset 创建安装程序
使用命令行发布:
dotnet publish -c Release -r win-x64 --self-contained
参数说明:
| 参数 | 含义 |
|---|---|
-c Release |
指定构建配置为 Release |
-r win-x64 |
指定目标平台为 Windows x64 |
--self-contained |
打包运行时环境,无需目标机安装 .NET |
5.4.2 发布安装包与跨平台兼容性处理
为提升兼容性,建议:
- 使用
.NET 6或更高版本,支持跨平台构建 - 对 UI 使用 WPF 或 MAUI 以提升体验一致性
- 在安装包中附带依赖检查与自动安装脚本
以下是一个跨平台构建的流程图示例:
graph TD
A[源码] --> B[构建配置]
B --> C{目标平台}
C -->|Windows| D[生成EXE安装包]
C -->|Linux| E[生成DEB/RPM包]
C -->|macOS| F[生成DMG安装包]
D --> G[发布到用户]
E --> G
F --> G
通过上述流程,可以确保计算器项目在不同操作系统上都能顺利运行。
简介:本资源围绕”C#计算器源码”展开,详细介绍如何使用C#语言开发一个具备基础数学运算功能的计算器程序。C#是微软推出的一种面向对象的编程语言,广泛应用于Windows应用、Web系统及游戏开发。资源内容涵盖控制台输入输出、运算符重载、表达式解析、异常处理、图形界面设计、多线程优化、设计模式应用以及单元测试等关键知识点。通过完整实现一个计算器项目,学习者可以掌握C#编程核心技能,提升软件开发与工程实践能力,适用于编程初学者和进阶开发者。
更多推荐


所有评论(0)