中国海洋大学2026年夏季《移动软件开发》实验5:鸿蒙开发入门及计算器开发
姓名:【王晨旭】 学号:【24020007119】
| 项目 | 内容 |
|---|---|
| 姓名和学号? | 【王晨旭】,【24020007119】 |
| 本实验属于哪门课程? | 中国海洋大学26夏《移动软件开发》 |
| 实验名称? | 实验5:鸿蒙开发入门及计算器开发 |
| 博客地址? | 【博客地址,发布后填】 |
| Github仓库地址? | 【GitHub/Gitee仓库地址,上传后填】 |
一、实验概述
本次实验来自鸿蒙官方开发者文档的计算器架构指南,主要目标是掌握 HarmonyOS ArkTS 应用的开发流程,熟悉 DevEco Studio 的使用与模拟器调试,并基于 ArkTS 声明式 UI 开发一款功能完整的计算器应用。应用在官方简易计算器的基础上进行了大量个性化创新,包括表达式求值引擎、科学计算模式、内存存储、历史记录、主题换肤等功能。
学习目标:
- 掌握 DevEco Studio 的安装、工程创建与模拟器调试;
- 熟悉 ArkTS 声明式 UI 开发范式(
@Entry、@Component、@State、@Watch、@Builder等装饰器); - 掌握计算器的核心计算逻辑与状态管理;
- 能够在官方示例基础上进行个性化功能扩展与 UI 创新。
二、实验步骤
1. 开发环境搭建
① 安装 DevEco Studio
进入华为开发者联盟下载页面(下载中心 | 华为开发者联盟-HarmonyOS开发者官网,共建鸿蒙生态),选择最新版本的 DevEco Studio 下载并安装。安装过程中保持默认选项即可,安装路径建议不要包含中文和空格。
② 创建本地模拟器
- 点击菜单栏
Tools > Device Manager,在 Local Emulator 页签中点击右下角+ New Emulator; - 选择默认的
Huawei_Phone设备模板,首次使用需点击右侧下载图标下载系统镜像; - 单击
Next,确认模拟器名称、内存(建议 4GB)和存储空间(建议 6GB),单击Finish完成创建; - 在设备管理页面点击播放键启动模拟器,等待 HarmonyOS 系统加载完成。
③ 验证环境
创建一个 Empty Ability 工程,点击工具栏绿色 ▶ Run 按钮,若模拟器中显示 Hello World,则环境搭建成功。


2. 创建工程
打开 DevEco Studio,在欢迎页单击 Create Project,选择 Application > Empty Ability 模板,单击 Next,填写工程信息:
- Project name:
Calculator - Bundle name:
com.example.calculator - Save location:自定义路径(不含中文)
- Compatible SDK:选择 5.0.0(12) 或更高版本
- Module name:
entry - Device type:勾选 Phone
单击 Finish 完成工程创建,等待 Gradle 同步完成。
3. 工程目录结构
在 entry/src/main/ets 目录下建立如下结构:
ets/
├── common/
│ ├── constants/
│ │ └── CommonConstants.ets # 常量定义(运算符、按钮类型等)
│ └── util/
│ ├── CalculateUtil.ets # 科学计算工具类
│ └── ExpressionUtil.ets # 表达式求值引擎(调度场算法)
├── entryability/
│ └── EntryAbility.ets # 应用入口 Ability
├── pages/
│ └── HomePage.ets # 计算器主页面
└── viewmodel/
├── PressKeysItem.ets # 按钮数据模型
└── PresskeysViewModel.ets # 按钮数据集(基础/科学/内存键)
同时修改 EntryAbility.ets 中的 windowStage.loadContent 为 'pages/HomePage',并在 resources/base/profile/main_pages.json 中注册 pages/HomePage 路由。
4. 常量定义(CommonConstants.ets)
集中管理所有运算符标签和按钮类型,避免硬编码:
export class CommonConstants {
// 基础运算符
static readonly ADD: string = '+';
static readonly SUB: string = '-';
static readonly MUL: string = '×';
static readonly DIV: string = '÷';
static readonly EQUAL: string = '=';
static readonly CLEAR: string = 'AC';
static readonly DELETE: string = '⌫';
static readonly DOT: string = '.';
// 科学一元函数
static readonly SQUARE: string = 'x²';
static readonly SQRT: string = '√';
static readonly SIN: string = 'sin';
static readonly COS: string = 'cos';
static readonly TAN: string = 'tan';
static readonly LN: string = 'ln';
static readonly LOG: string = 'log';
static readonly PI: string = 'π';
static readonly E_CONST: string = 'e';
// ... 更多常量
// 按钮类型
static readonly TYPE_NUM: number = 0;
static readonly TYPE_OP: number = 1;
static readonly TYPE_FUNC: number = 2;
static readonly TYPE_SCI_UNARY: number = 3;
static readonly TYPE_SCI_CONST: number = 4;
}
5. 表达式求值引擎(ExpressionUtil.ets)
这是本计算器的核心创新之一。不同于官方示例中「第一个操作数 + 运算符 + 第二个操作数」的简单两步计算模式,本项目实现了一个完整的表达式求值引擎,支持多步连续运算、括号嵌套、运算优先级和一元负号。
实现采用调度场算法(Shunting-yard Algorithm),分为两步:
① 中缀表达式转后缀(逆波兰)表达式:
static evaluate(tokens: string[]): EvalResult {
const expr: string[] = ExpressionUtil.normalize(tokens);
const output: string[] = [];
const ops: string[] = [];
let expectOperand: boolean = true;
for (let i: number = 0; i < expr.length; i++) {
const t: string = expr[i];
if (ExpressionUtil.isNumericToken(t)) {
output.push(t);
expectOperand = false;
} else if (t === '(') {
ops.push(t);
} else if (t === ')') {
// 弹出运算符直到遇到 '('
while (ops.length > 0) {
const top: string | undefined = ops.pop();
if (top === '(') break;
output.push(top);
}
} else if (ExpressionUtil.isBinaryOp(t)) {
// 按优先级弹出栈顶运算符
while (ops.length > 0 && ops[ops.length - 1] !== '(' &&
ExpressionUtil.precedence(ops[ops.length - 1]) >= ExpressionUtil.precedence(t)) {
output.push(ops.pop() as string);
}
ops.push(t);
expectOperand = true;
}
}
// 弹出剩余运算符
while (ops.length > 0) {
output.push(ops.pop() as string);
}
// ... 后缀求值
}
② 后缀表达式求值:
使用一个栈,遇到数字压栈,遇到运算符弹出两个操作数计算后将结果压栈。同时处理除零错误和浮点精度问题:
// 减少浮点误差:对齐小数位后再加减
private static safeAdd(a: number, b: number): number {
const m: number = Math.pow(10, 10);
return Math.round(a * m + b * m) / m;
}
引擎还包含 normalize() 方法,自动补全未闭合的右括号、去除末尾悬挂的运算符,使得用户输入 (2+3 时也能正确预览结果为 5。
6. 科学计算工具类(CalculateUtil.ets)
封装所有科学计算函数,每个函数接收字符串参数、返回 number,并做定义域校验:
export class CalculateUtil {
// 平方根(负数返回 NaN)
static sqrt(arg: string): number {
const v: number = parseFloat(arg);
if (v < 0) return NaN;
return Math.sqrt(v);
}
// 自然对数(非正数返回 NaN)
static ln(arg: string): number {
const v: number = parseFloat(arg);
if (v <= 0) return NaN;
return Math.log(v);
}
// 三角函数(弧度制)
static sin(arg: string): number { return Math.sin(parseFloat(arg)); }
static cos(arg: string): number { return Math.cos(parseFloat(arg)); }
static tan(arg: string): number { return Math.tan(parseFloat(arg)); }
// 平方、倒数、指数、对数等...
}
7. 按钮数据集(PresskeysViewModel.ets)
将按钮布局数据与 UI 分离,提供三组按键:
export class PresskeysViewModel {
// 内存键:MC MR M- M+
getMemoryKeys(): PressKeysItem[][] { ... }
// 基础键:AC ( ) ÷ / 789× / 456- / 123+ / 0.⌫=
getBasicKeys(): PressKeysItem[][] { ... }
// 科学键:sin cos tan ln / log 10ˣ eˣ x² / √ 1/x π e
getSciKeys(): PressKeysItem[][] { ... }
}
每个 PressKeysItem 包含 label(显示文字)和 type(按钮类型),UI 层根据 type 决定样式和点击行为。
8. 主页面实现(HomePage.ets)
主页面采用 ArkTS 声明式 UI,核心状态变量:
@Entry
@Component
struct HomePage {
@State exprTokens: string[] = []; // 表达式 token 数组
@State justEvaluated: boolean = false; // 是否刚按等号
@State isScientific: boolean = false; // 科学模式开关
@State themeIndex: number = 0; // 主题索引
@State historyList: string[] = []; // 历史记录
@State memoryValue: number = 0; // 内存值
@State pressedKey: string = ''; // 按键动效状态
}
① 实时预览: 通过 getMainDisplay() 计算属性,在用户输入过程中调用 ExpressionUtil.evaluate() 实时显示中间结果,按等号后定格最终结果。
② 输入处理: 分为 appendDigit()(数字)、appendDot()(小数点)、appendOp()(运算符)、appendParen()(括号)、onBackspace()(退格)等方法,每种输入都做了边界判断(如运算符后接负号、右括号后接数字自动补乘号等)。
③ 内存功能: 实现 M+(加当前值到内存)、M-(从内存减去当前值)、MR(读取内存值)、MC(清空内存),内存非零时显示区左上角出现 M 徽标。
④ 历史记录: 每次按等号或科学函数后自动保存完整算式到 historyList(最多 20 条),点击右上角「历史」按钮弹出覆盖层面板,支持单条删除(每条右侧 ✕ 按钮)、一键清空、长按复制单条记录。
⑤ 主题换肤: 内置三套配色主题(暖橙/青绿/雾蓝),点击顶部色块即时切换运算符颜色、背景色和卡片色。
⑥ 按键动效: 通过 @State pressedKey 配合 .scale() 和 .animation(),实现点击时按钮缩小到 0.88 倍再弹回的按压反馈。
⑦ 剪贴板复制: 长按主显示区的结果数字,自动复制到系统剪贴板并弹出 Toast 提示。
三、实验结果
应用在 HarmonyOS 模拟器中运行正常,主要功能如下:
- 基础四则运算:支持加减乘除,含括号嵌套和运算优先级(如
(2+3)×4 = 20); - 实时预览:输入表达式过程中,主显示区实时显示中间计算结果;
- 重复等号:按
=得出结果后再次按=,自动重复上一步运算(如5+3=8,再按=得11); - 科学计算模式:点击右上角「科学」按钮切换,提供 sin/cos/tan、ln/log、10ˣ/eˣ、x²、√、1/x、π、e 共 12 个科学函数;
- 内存存储:M+ / M- / MR / MC 四键完整实现,内存非零时显示 M 徽标;
- 历史记录:自动保存最近 20 条算式,弹窗支持单条删除、清空、长按复制;
- 主题换肤:三套配色主题一键切换;
- 按键动效:所有按钮点击时有缩放按压反馈;
- 错误处理:除零显示「除数不能为0」,负数开平方显示「负数不能开平方」,对数非正数显示「函数定义域错误」。



四、问题总结与体会
1. 遇到的问题及解决方法
- 问题 1:官方示例的两步计算模式(第一个数 + 运算符 + 第二个数)无法支持连续运算和括号。解决方法:自研表达式求值引擎,采用调度场算法将中缀表达式转为后缀表达式再求值,支持任意长度的连续运算、括号嵌套和一元负号。
- 问题 2:JavaScript 浮点数精度问题(如
0.1 + 0.2 = 0.30000000000000004)。解决方法:在ExpressionUtil中实现safeAdd(),将两个数扩大 10^10 倍取整后相加再缩小,消除浮点误差;乘法用toPrecision(12)截断。 - 问题 3:ArkTS 中
@State装饰的数组调用push()、unshift()等原地修改方法不会触发 UI 更新。解决方法:每次修改历史记录时都创建新数组并重新赋值(this.historyList = newList),确保响应式更新生效。 - 问题 4:
Circle组件的.fill()和.stroke()API 仅在 SDK 26+ 支持,当前兼容 SDK 为 24,编译报错。解决方法:改用Column+.backgroundColor()+.border()实现圆形色块,兼容低版本 SDK。 - 问题 5:
.scale()方法在当前 SDK 版本不接受 number 类型参数,编译报错「Argument of type 'number' is not assignable to parameter of type 'ScaleOptions'」。解决方法:改为传入对象格式.scale({ x: 0.88, y: 0.88 })。 - 问题 6:历史记录弹窗使用
@CustomDialog时,弹窗内的列表数据与主页面的historyList不同步(删除后弹窗不刷新)。解决方法:改用Stack布局 + 条件渲染的覆盖层方式,所有状态集中在主页面组件中管理,删除后直接更新@State变量,弹窗内容实时刷新。 - 问题 7:用户输入
(2+3时表达式不完整,求值引擎报错。解决方法:在ExpressionUtil.normalize()中自动补全未闭合的右括号,并去除末尾悬挂的运算符,使不完整表达式也能正确预览。
2. 收获与体会
通过本次实验,我完成了一款功能丰富的鸿蒙计算器应用,主要收获如下:
- 掌握 ArkTS 声明式 UI 开发范式:深入理解了
@Entry、@Component、@State、@Watch、@Builder等装饰器的作用和用法,学会了用声明式方式构建界面而非命令式操作 DOM。 - 理解状态驱动视图的核心思想:ArkTS 的
@State装饰器实现了数据变化自动触发 UI 刷新,通过this.xxx = newValue的重新赋值方式确保响应式更新,这与 Vue 的响应式原理类似但语法更严格。 - 掌握调度场算法的实际应用:将数据结构课程中学到的中缀转后缀算法落地到真实项目中,实现了支持括号和优先级的表达式求值,加深了对算法实用性的理解。
- 学会工程化的代码组织:将常量、工具函数、视图模型、页面分离到不同目录和文件中,通过
import模块化引用,提高了代码的可维护性和可复用性。 - 体会到边界条件处理的重要性:计算器需要处理大量边界情况——除零、负数开方、对数定义域、浮点数精度、连续运算符、未闭合括号等,每一种都需要单独判断和友好提示。
- 提升 UI/UX 设计能力:通过主题换肤、按键动效、实时预览、内存徽标、历史弹窗等细节设计,让应用从「能用」提升到「好用」,理解了用户体验在移动开发中的重要性。
- 熟悉 DevEco Studio 开发流程:从工程创建、代码编写、编译调试到模拟器运行,完整走通了鸿蒙应用的开发链路,为后续更复杂的鸿蒙应用开发打下基础。
3. 对课程安排的建议
希望后续课程能进一步介绍鸿蒙应用的更多核心能力,例如页面路由与多页面跳转、网络请求与数据持久化、动画与手势交互、以及分布式能力等。同时也希望能增加一些 ArkTS 与 TypeScript/JavaScript 差异的专项讲解,帮助我们更快地从前端开发过渡到鸿蒙开发。
更多推荐


所有评论(0)