一、实验概述

本次实验来自鸿蒙官方开发者文档的计算器架构指南,主要目标是掌握 HarmonyOS ArkTS 应用的开发流程,熟悉 DevEco Studio 的使用与模拟器调试,并基于 ArkTS 声明式 UI 开发一款功能完整的计算器应用。应用在官方简易计算器的基础上进行了大量个性化创新,包括表达式求值引擎、科学计算模式、内存存储、历史记录、主题换肤等功能。

学习目标:

  • 掌握 DevEco Studio 的安装、工程创建与模拟器调试;
  • 熟悉 ArkTS 声明式 UI 开发范式(@Entry@Component@State@Watch@Builder 等装饰器);
  • 掌握计算器的核心计算逻辑与状态管理;
  • 能够在官方示例基础上进行个性化功能扩展与 UI 创新。

二、实验步骤

1. 开发环境搭建

① 安装 DevEco Studio

进入华为开发者联盟下载页面(下载中心 | 华为开发者联盟-HarmonyOS开发者官网,共建鸿蒙生态),选择最新版本的 DevEco Studio 下载并安装。安装过程中保持默认选项即可,安装路径建议不要包含中文和空格。

② 创建本地模拟器

  1. 点击菜单栏 Tools > Device Manager,在 Local Emulator 页签中点击右下角 + New Emulator
  2. 选择默认的 Huawei_Phone 设备模板,首次使用需点击右侧下载图标下载系统镜像;
  3. 单击 Next,确认模拟器名称、内存(建议 4GB)和存储空间(建议 6GB),单击 Finish 完成创建;
  4. 在设备管理页面点击播放键启动模拟器,等待 HarmonyOS 系统加载完成。

③ 验证环境

创建一个 Empty Ability 工程,点击工具栏绿色 ▶ Run 按钮,若模拟器中显示 Hello World,则环境搭建成功。

2. 创建工程

打开 DevEco Studio,在欢迎页单击 Create Project,选择 Application > Empty Ability 模板,单击 Next,填写工程信息:

  • Project nameCalculator
  • Bundle namecom.example.calculator
  • Save location:自定义路径(不含中文)
  • Compatible SDK:选择 5.0.0(12) 或更高版本
  • Module nameentry
  • 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),确保响应式更新生效。
  • 问题 4Circle 组件的 .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. 收获与体会

通过本次实验,我完成了一款功能丰富的鸿蒙计算器应用,主要收获如下:

  1. 掌握 ArkTS 声明式 UI 开发范式:深入理解了 @Entry@Component@State@Watch@Builder 等装饰器的作用和用法,学会了用声明式方式构建界面而非命令式操作 DOM。
  2. 理解状态驱动视图的核心思想:ArkTS 的 @State 装饰器实现了数据变化自动触发 UI 刷新,通过 this.xxx = newValue 的重新赋值方式确保响应式更新,这与 Vue 的响应式原理类似但语法更严格。
  3. 掌握调度场算法的实际应用:将数据结构课程中学到的中缀转后缀算法落地到真实项目中,实现了支持括号和优先级的表达式求值,加深了对算法实用性的理解。
  4. 学会工程化的代码组织:将常量、工具函数、视图模型、页面分离到不同目录和文件中,通过 import 模块化引用,提高了代码的可维护性和可复用性。
  5. 体会到边界条件处理的重要性:计算器需要处理大量边界情况——除零、负数开方、对数定义域、浮点数精度、连续运算符、未闭合括号等,每一种都需要单独判断和友好提示。
  6. 提升 UI/UX 设计能力:通过主题换肤、按键动效、实时预览、内存徽标、历史弹窗等细节设计,让应用从「能用」提升到「好用」,理解了用户体验在移动开发中的重要性。
  7. 熟悉 DevEco Studio 开发流程:从工程创建、代码编写、编译调试到模拟器运行,完整走通了鸿蒙应用的开发链路,为后续更复杂的鸿蒙应用开发打下基础。

3. 对课程安排的建议

希望后续课程能进一步介绍鸿蒙应用的更多核心能力,例如页面路由与多页面跳转、网络请求与数据持久化、动画与手势交互、以及分布式能力等。同时也希望能增加一些 ArkTS 与 TypeScript/JavaScript 差异的专项讲解,帮助我们更快地从前端开发过渡到鸿蒙开发。

Logo

作为“人工智能6S店”的官方数字引擎,为AI开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐