实验5
在鸿蒙系统实现计算器
一、实验内容
1. 实验目的
本实验根据实验5要求,使用 DevEco Studio 和 ArkTS 开发能够在 HarmonyOS 系统运行的计算器。首先通过 Hello World 工程验证开发环境和模拟器连接,再实现数字输入、四则运算、清空、退格等基础功能。在此基础上加入科学计算、函数绘图、历史记录和主题切换,完成具有个人设计风格的计算器应用。
通过实验,学习 ArkUI 声明式界面、组件状态管理、表达式解析、Canvas 绘图以及设备调试,并理解浮点数计算和数学函数定义域对程序结果的影响。
2. 页面设计思路
应用采用“标准、科学、绘图”三个模式,共用顶部导航与主题切换。各模式的功能分开组织,切换时保留当前输入与结果。界面使用浅绿色与深绿色主题,通过留白、统一圆角和颜色区分数字键、运算键与主要操作。
| 模式或功能 | 主要内容 | 设计目的 |
|---|---|---|
| 标准计算 | 四则运算、小数、正负切换、百分号、清空、退格 | 满足日常计算需求 |
| 科学计算 | 三角函数、反三角函数、对数、开方、幂、阶乘、括号 | 支持完整数学表达式 |
| 结果显示 | DEG/RAD、AUTO/SCI、Ans | 区分角度单位、显示方式,并复用上次结果 |
| 函数绘图 | 双曲线、缩放、取点、参数滑块 | 直观观察函数形状及参数变化 |
| 历史记录 | 标准与科学模式分别保存最近30条记录 | 便于回看计算过程 |
| 主题与布局 | 深浅主题、结果分区、简洁按钮 | 提高可读性,减少多余文字 |
3. 项目结构
miniprogram5/
├── 实验报告.md
├── README.md
└── Calculator/
├── AppScope/ # 应用信息与公共资源
├── build-profile.json5 # 工程构建配置
├── oh-package.json5 # 依赖配置
├── hvigor/ # 构建工具配置
├── entry/src/main/
│ ├── module.json5 # 模块及入口声明
│ ├── resources/ # 字符串、颜色、图标等
│ └── ets/
│ ├── entryability/EntryAbility.ets
│ ├── pages/Index.ets # 标准界面、导航与主题
│ ├── components/
│ │ ├── ScientificPanel.ets # 科学计算界面
│ │ └── GraphPanel.ets # 函数绘图界面
│ └── utils/
│ ├── CalculatorEngine.ts # 标准计算状态机
│ └── ExpressionMath.ts # 解析、求值及曲线采样
├── tests/
│ ├── calculator.test.cjs
│ ├── expression.test.cjs
│ └── device-smoke.ps1
└── screenshots/ # 开发过程中的设备截图
虽然仓库名为 miniprogram5,本实验实际是 HarmonyOS 原生应用,应在 DevEco Studio 中打开 Calculator 目录。
4. 实验过程
4.1 验证开发环境与工程入口
在 Windows 环境下使用 DevEco Studio 打开现有的 Calculator 工程,运行默认 Hello World 页面,确认编译、安装和模拟器运行流程正常。工程通过 EntryAbility 加载 pages/Index,应用包名为 com.example.calculator。
使用 HDC 检查设备连接:
& 'E:\app\DevEco Studio\sdk\default\openharmony\toolchains\hdc.exe' list targets
开发阶段曾检测到设备 127.0.0.1:5555,并通过 HDC 安装和启动应用。当前工程配置的目标 SDK 为 26.0.0,最低兼容版本为 6.1.1(24),设备类型为 Phone。工程未保存签名配置,后续若部署到需要签名的设备,应在本机 DevEco Studio 中配置签名。
4.2 实现标准计算与输入状态管理
标准模式使用 CalculatorEngine 保存当前输入、数字列表、运算符列表以及等待输入、完成计算、发生错误等状态。界面只负责传递按键并同步显示,从而将界面布局与计算规则分离。
private press(key: string): void {
const record = this.engine.press(key);
this.display = this.engine.current;
this.expression = this.engine.expression;
if (record !== '') {
this.history = [record, ...this.history].slice(0, 30);
}
}
求值时先合并乘除项,再计算加减,保证 2 + 3 × 4 得到 14。输入过程中限制重复小数点,连续点击不同运算符时替换上一运算符;退格既能删除数字,也能撤回待输入状态下的运算符。除零时显示错误,重新输入数字或点击 AC 可以恢复。
百分号的规则为“当前数字除以100”,因此 200 + 10% 的结果是 200.1。明确这一规则可以避免与部分商业计算器的百分比语义混淆。
4.3 扩展科学计算和表达式解析
科学计算使用独立的表达式输入区和五列键盘,支持括号、幂、三角函数、对数、平方根、阶乘以及 π、e 等常数。科学模式可以切换 DEG 与 RAD,并通过 Ans 引用上次计算结果。
为使运算规则可控,程序没有将输入字符串交给 eval 执行,而是先进行词法分析,再使用递归下降方法构造表达式树。语法层次为:
加减表达式 → 乘除表达式 → 一元正负号 → 幂与后缀运算 → 数字、常数、括号或函数
其中幂按右结合处理,因此 2^3^2 等价于 2^(3^2);一元负号的优先级低于幂,因此 -2^2 得到 -4。函数计算还检查定义域,例如负数开平方、非正数取对数和非整数阶乘都会给出提示。
科学计算的核心调用如下:
const options = new MathOptions();
options.degrees = this.degrees;
options.ans = this.answer;
const value = evaluateMath(new ExpressionParser(this.formula).parse(), options);
this.answer = value;
this.result = formatMath(value, this.scientific);
AUTO 模式按有效数字显示,SCI 模式使用科学计数法。两者改变的是显示格式,不是另行截断内部计算结果。
4.4 处理特殊角度的浮点残差
开发时发现,在 RAD 模式下计算 sin(π) 会显示约 1.22464679915e-16,而不是数学上的零。这是因为双精度浮点数不能精确表示 π,普通三角函数计算会留下微小残差。
不能简单地把所有小于某个阈值的数都设为零,否则 sin(1e-16) 这样的有效微小结果也会丢失。本项目保留数字的原始文本,并对能够用安全整数分数表示的角度分析其是否为 π/2 或90°的整数倍。确认象限后直接使用精确值:
const quadrant = exactQuadrant(node.children[0], options);
if (quadrant !== undefined) {
if (node.name === 'sin') { return [0, 1, 0, -1][quadrant]; }
if (node.name === 'cos') { return [1, 0, -1, 0][quadrant]; }
if (quadrant % 2 !== 0) { throw new Error('tan 在此角度无定义'); }
return 0;
}
这种处理直接作用于求值结果,因此 sin(π) * 1e16 也得到零。无法严格确认的角度仍按一般浮点数计算,不进行全局归零。该方法修复的是可识别的特殊角度,并不意味着应用具备任意精度或完整的符号计算能力。
4.5 使用 Canvas 绘制函数曲线
绘图模式与科学模式共用表达式解析器,允许输入包含 x 和参数 a 的函数。默认对比 sin(a*x) 与 cos(x),同时提供抛物线和反比例预设。用户可以调整参数 a、缩放视图或点击图面读取函数值。
最新代码中,两条坐标轴采用统一的屏幕比例,避免函数图形被横向或纵向拉伸:
const scale = width / (2 * this.radius);
this.yRadius = height / (2 * scale);
const px = (x: number): number => width / 2 + x * scale;
const py = (y: number): number => height / 2 - y * scale;
由画布宽度和 x 轴范围确定统一比例,再根据画布高度计算 y 轴范围。这样每个单位在两条轴上的屏幕长度相同,网格为正方形,缩放后比例也保持一致。
曲线采用数值采样绘制。程序检查采样点和中点的值,在变化剧烈的位置继续细分;遇到无定义或无法可靠连接的区间时断开绘制,避免将反比例函数渐近线两侧错误连成直线。参数滑块变化后重新采样,取点时则将触摸横坐标转换为数学坐标并计算函数值。
4.6 精简界面并保留模式状态
初版界面包含副标题、优先级说明和较多操作提示。后续将这些说明移入项目文档,界面只保留必要的单位、待更新状态和错误信息,并增大结果字号、统一圆角和间距。等号使用深绿色突出显示,运算符使用浅绿色区分,清空键用暖色文字提示。
三个模式的组件保留在同一个 Stack 中,通过 Visibility 控制显示,而不是每次切换时重新创建,因此可以保留各自的输入、历史和绘图参数。数据目前保存在内存中,应用重启或页面重建后会重置。
4.7 测试与运行结果
2026年9月20日整理报告时,重新执行了当前项目的数学测试,结果如下:
| 测试文件 | 验证内容 | 本次结果 |
|---|---|---|
calculator.test.cjs | 基础运算、优先级、小数、退格、错误恢复 | 21项断言通过 |
expression.test.cjs | 科学计算、特殊角度、按键组合、定义域、函数采样和纵轴范围 | 120项断言通过 |
| 合计 | 数学逻辑自动验证 | 141项断言通过 |
典型用例包括:
| 输入或操作 | 预期结果 |
|---|---|
2 + 3 × 4 | 14 |
0.1 + 0.2 | 显示 0.3 |
DEG:sin(30)+2^3 | 8.5 |
RAD:sin(π) | 精确返回 0 |
RAD:sin(1e-16) | 保留微小结果,不误置为零 |
5! | 120 |
sqrt(-1) | 提示实数平方根不能输入负数 |
1/x | 原点无定义,不跨渐近线连线 |
开发阶段已完成构建、安装及设备交互验证。报告整理时 HDC 未检测到已连接设备,因此本次没有重新执行设备操作脚本,也没有将旧截图当作最新版界面的重新验收证据。
以下为开发阶段保存的真实设备截图。第一张为基础计算版本,第二张为绘图精简版;后续代码又增加了分区样式与两轴等比例绘制,截图用于记录开发过程,最终实现以仓库代码为准。

图1 基础计算功能验证

图2 双曲线、缩放与参数控制界面(等比例优化前)
二、问题总结与体会
1. 遇到的问题及解决方法
-
计算只按输入顺序执行,容易破坏优先级。 标准模式先处理乘除再处理加减;科学模式通过分层语法构造表达式树,并分别验证幂的右结合和负号的优先级。
-
自定义方法名与 ArkUI 内置属性重名。 初版按钮构建方法使用
key,编译时与组件基类属性冲突。将其改为calculatorKey后消除冲突,也让方法用途更加清晰。 -
sin(π)显示微小残差。 通过表达式结构识别能够精确证明的特殊角度,再返回零或正负一;保留普通微小数值,避免全局阈值归零造成新的错误。 -
曲线在无定义点附近出现错误连接。 采样时检查函数定义域与中点偏差,对高曲率区间细分,无法可靠连接时断线。绘图本质上仍是数值近似,对极窄结构和高频振荡存在采样限制。
-
画布宽高不同导致坐标轴比例不一致。 改为共享同一个缩放系数,并由实际画布尺寸计算 y 轴范围,使单位长度一致,避免图形失真。
-
切换模式后输入状态容易丢失。 保留各模式组件实例,通过可见性切换页面,避免反复销毁和重建。
-
功能增加后界面文字过多。 删除常驻教程和重复标签,把详细规则保留在 README 中。界面优先展示公式、结果、曲线以及真正需要用户处理的状态。
2. 实验体会
本次实验从 Hello World 工程出发,逐步实现标准计算、科学计算和函数绘图,使我认识到计算器不仅是按钮布局,还涉及输入状态、数学语法、数值精度和交互反馈等多个问题。
将计算逻辑与界面分离后,可以在不启动模拟器的情况下验证大量数学用例;科学计算与绘图共用解析器,也减少了两种模式之间规则不一致的问题。浮点误差的处理则说明,显示结果看起来接近零,并不代表可以直接将其改为零,需要结合表达式语义判断。
后续可以继续加入历史持久化、图像导出和更多函数分析功能;对于本次实验,先保证现有功能的正确性、界面的清晰程度和测试的可重复性更为重要。