HarmonyOS ArkTS 实战:实现一个标准计算器

项目效果

本文实现的是一个基于 HarmonyOS 和 ArkTS 的标准计算器。项目中使用 ArkUI 组件完成页面布局,通过 @State 管理状态数据,实现加减乘除四则运算、小数运算、清除、退格、正负切换和百分比等标准计算器功能。

最终运行效果如下:
在这里插入图片描述

页面主要包含以下内容:

  • 顶部应用标题;
  • 上方显示区域,显示计算表达式;
  • 大字体显示当前数字;
  • 下方4×5按钮网格;
  • 数字键0-9;
  • 加减乘除运算符;
  • 等于、清除、退格、小数点、正负、百分比按钮;
  • 页面整体采用 ArkUI 声明式布局。

本文重点是演示如何在 HarmonyOS 项目中使用 ArkTS 和 ArkUI 实现一个计算器单页面应用。项目代码主要写在 entry/src/main/ets/pages/Index.ets 文件中,适合作为 HarmonyOS ArkTS 入门到进阶之间的练习案例。

前言

计算器是每个手机必备的工具,也是学习编程最经典的入门项目。实现一个计算器看起来简单,但要处理好各种边界情况并不容易:连续运算、小数、除以零、运算符切换等等。

从应用开发角度来看,这个项目不依赖后端接口,也不需要数据库,但能练习 ArkTS 中的状态管理、网格布局、按钮交互、表达式计算和边界情况处理等内容。

本文基于 HarmonyOS 和 ArkTS 实现一个标准计算器。支持加减乘除四则运算,有标准计算器的所有常用功能,界面简洁,操作流畅。

这个项目的核心不是简单的加减乘除,而是正确处理各种计算状态和边界情况。每一次按钮点击,都会改变计算器的状态,这正是 ArkUI 声明式开发的基本思想。

一、项目目标

本次实践主要实现以下目标:

  • 创建 HarmonyOS ArkTS 页面;
  • 使用 @Entry@Component 定义页面组件;
  • 使用 @State 管理页面状态;
  • 实现0-9数字输入;
  • 加减乘除四则运算;
  • 小数点输入;
  • 等于计算结果;
  • 全部清除(AC);
  • 退格删除;
  • 正负号切换;
  • 百分比计算;
  • 连续运算支持;
  • 使用 @Builder 封装计算器按钮;
  • 完成一个可以运行的标准计算器页面。

这个项目虽然是单页面应用,但它实现了完整的计算器功能,处理了各种边界情况,比普通静态页面更适合练习 ArkTS。

二、技术栈

类型 内容
开发方向 HarmonyOS 应用开发
开发语言 ArkTS
UI 框架 ArkUI
SDK 版本 HarmonyOS API 23 及以上
工程模型 Stage 模型
核心组件 Text / Button / Column / Row / Grid
状态管理 @State
数据处理 四则运算 / 状态机 / 边界处理
项目入口 entry/src/main/ets/pages/Index.ets
运行平台 模拟器或真机

本项目是 HarmonyOS 原生 ArkTS 项目。页面主体由 ArkUI 组件构建,核心逻辑写在 Index.ets 文件中,不依赖后端接口,也不需要额外配置数据库。

三、为什么选择标准计算器项目

标准计算器适合作为 ArkTS 练习项目,主要有以下几个原因。

第一,经典入门项目。几乎所有编程入门都会做计算器,是检验基础能力的好项目。

第二,实用性强。计算器是日常最常用的工具之一。

第三,适合练习状态管理。计算器有多种状态:输入数字、输入运算符、显示结果等。

第四,适合练习网格布局。计算器按钮是规则的网格,练习Grid或Flex布局。

第五,适合练习边界处理。除以零、多个小数点、连续运算符等情况都需要处理。

第六,扩展空间比较大。基础功能完成后,可以继续增加科学计算、历史记录、单位换算、进制转换和主题切换。

在本项目中,当前数字、运算符、上一个数字是核心状态。所有计算逻辑都基于这些状态。

四、功能规则说明

按钮布局:

AC +/- % ÷
7 8 9 ×
4 5 6 -
1 2 3 +
0 . =

运算规则:

  • 输入数字时连续输入组成多位数
  • 点击运算符后保存当前数字,准备输入下一个
  • 再次点击运算符时先计算之前的结果
  • 点击等于显示最终结果
  • AC清除所有状态
  • 退格删除最后一位
  • +/-切换正负号
  • %将当前数字除以100
  • 除以0显示Error

五、项目结构

本项目主要修改首页文件:

entry
└── src
    └── main
        └── ets
            └── pages
                └── Index.ets

其中:

文件 作用
Index.ets 编写页面结构、状态数据和计算逻辑

本文不涉及复杂路由,也不需要额外创建多个页面。对于练习项目来说,把主要逻辑集中在一个 Index.ets 文件中更方便理解。

六、核心实现思路

本项目的核心流程如下:

  1. 使用 @State 保存当前显示数字、上一个数字、当前运算符;
  2. 点击数字按钮追加数字;
  3. 点击运算符保存当前数字和运算符;
  4. 点击等于时根据运算符计算结果;
  5. 处理小数点,确保只有一个;
  6. 处理正负切换;
  7. 处理百分比;
  8. 处理退格;
  9. AC重置所有状态;
  10. 处理除以零等错误情况。

项目中最重要的状态变量如下:

@State display: string = '0'
@State previousValue: number = 0
@State operator: string = ''
@State shouldResetDisplay: boolean = false
@State expression: string = ''

其中:

状态变量 作用
display 当前显示的数字
previousValue 上一个操作数
operator 当前运算符
shouldResetDisplay 输入下一个数字时是否重置显示
expression 表达式显示

这些状态构成了计算器的状态机,正确处理状态转换是计算器正确运行的关键。

七、Index.ets 完整代码

打开文件:

entry/src/main/ets/pages/Index.ets

将其中内容替换为下面代码:

@Entry
@Component
struct Index {
  @State display: string = '0'
  @State previousValue: number = 0
  @State operator: string = ''
  @State shouldResetDisplay: boolean = false
  @State expression: string = ''

  private inputNumber(num: string): void {
    if (this.shouldResetDisplay) {
      this.display = num
      this.shouldResetDisplay = false
    } else {
      if (this.display === '0' && num !== '.') {
        this.display = num
      } else if (num === '.' && this.display.includes('.')) {
        return
      } else {
        this.display += num
      }
    }
  }

  private inputOperator(op: string): void {
    let currentValue = parseFloat(this.display)
    
    if (this.operator !== '' && !this.shouldResetDisplay) {
      this.calculate()
      currentValue = parseFloat(this.display)
    }
    
    this.previousValue = currentValue
    this.operator = op
    this.shouldResetDisplay = true
    this.expression = `${this.formatNumber(this.previousValue)} ${this.getOperatorSymbol(op)}`
  }

  private calculate(): void {
    if (this.operator === '') return
    
    let currentValue = parseFloat(this.display)
    let result = 0
    
    switch (this.operator) {
      case '+':
        result = this.previousValue + currentValue
        break
      case '-':
        result = this.previousValue - currentValue
        break
      case '*':
        result = this.previousValue * currentValue
        break
      case '/':
        if (currentValue === 0) {
          this.display = 'Error'
          this.operator = ''
          this.shouldResetDisplay = true
          this.expression = ''
          return
        }
        result = this.previousValue / currentValue
        break
    }
    
    this.expression = `${this.formatNumber(this.previousValue)} ${this.getOperatorSymbol(this.operator)} ${this.formatNumber(currentValue)} =`
    this.display = this.formatResult(result)
    this.operator = ''
    this.shouldResetDisplay = true
  }

  private clearAll(): void {
    this.display = '0'
    this.previousValue = 0
    this.operator = ''
    this.shouldResetDisplay = false
    this.expression = ''
  }

  private backspace(): void {
    if (this.shouldResetDisplay || this.display === 'Error') return
    if (this.display.length === 1 || (this.display.length === 2 && this.display.startsWith('-'))) {
      this.display = '0'
    } else {
      this.display = this.display.substring(0, this.display.length - 1)
    }
  }

  private toggleSign(): void {
    if (this.display === '0' || this.display === 'Error') return
    if (this.display.startsWith('-')) {
      this.display = this.display.substring(1)
    } else {
      this.display = '-' + this.display
    }
  }

  private percent(): void {
    let value = parseFloat(this.display)
    this.display = this.formatResult(value / 100)
    this.shouldResetDisplay = true
  }

  private formatNumber(num: number): string {
    if (Number.isInteger(num)) {
      return num.toString()
    }
    return num.toString()
  }

  private formatResult(num: number): string {
    if (Number.isInteger(num)) {
      return num.toString()
    }
    return parseFloat(num.toFixed(10)).toString()
  }

  private getOperatorSymbol(op: string): string {
    switch (op) {
      case '+': return '+'
      case '-': return '-'
      case '*': return '×'
      case '/': return '÷'
      default: return op
    }
  }

  @Builder
  CalcButton(text: string, color: string, bgColor: string, onClick: () => void, flex: number = 1) {
    Button(text)
      .layoutWeight(flex)
      .height(64)
      .fontSize(24)
      .fontWeight(FontWeight.Medium)
      .fontColor(color)
      .backgroundColor(bgColor)
      .borderRadius(32)
      .onClick(onClick)
  }

  build() {
    Column() {
      Text('计算器')
        .fontSize(28)
        .fontWeight(FontWeight.Bold)
        .fontColor('#182431')
        .margin({ top: 22 })
      Text('基于 HarmonyOS ArkTS 的标准计算器')
        .fontSize(14)
        .fontColor('#6B7280')
        .margin({ top: 8, bottom: 20 })

      Column() {
        Blank()
        
        if (this.expression) {
          Text(this.expression)
            .fontSize(18)
            .fontColor('#9CA3AF')
            .width('100%')
            .textAlign(TextAlign.End)
            .margin({ bottom: 8 })
        }
        
        Text(this.display)
          .fontSize(56)
          .fontWeight(FontWeight.Light)
          .fontColor('#182431')
          .width('100%')
          .textAlign(TextAlign.End)
          .maxLines(1)
          .textOverflow({ overflow: TextOverflow.Ellipsis })
      }
      .width('100%')
      .height(180)
      .padding(20)
      .backgroundColor(Color.White)
      .borderRadius(16)
      .margin({ bottom: 16 })
      .shadow({
        radius: 8,
        color: '#08000000',
        offsetX: 0,
        offsetY: 2
      })

      Column() {
        Row() {
          this.CalcButton('AC', '#FFFFFF', '#9CA3AF', () => { this.clearAll() })
          this.CalcButton('+/-', '#FFFFFF', '#A5A5A5', () => { this.toggleSign() })
          this.CalcButton('%', '#FFFFFF', '#A5A5A5', () => { this.percent() })
          this.CalcButton('÷', '#FFFFFF', '#FF9F0A', () => { this.inputOperator('/') })
        }
        .width('100%')
        .margin({ bottom: 12 })

        Row() {
          this.CalcButton('7', '#FFFFFF', '#333333', () => { this.inputNumber('7') })
          this.CalcButton('8', '#FFFFFF', '#333333', () => { this.inputNumber('8') })
          this.CalcButton('9', '#FFFFFF', '#333333', () => { this.inputNumber('9') })
          this.CalcButton('×', '#FFFFFF', '#FF9F0A', () => { this.inputOperator('*') })
        }
        .width('100%')
        .margin({ bottom: 12 })

        Row() {
          this.CalcButton('4', '#FFFFFF', '#333333', () => { this.inputNumber('4') })
          this.CalcButton('5', '#FFFFFF', '#333333', () => { this.inputNumber('5') })
          this.CalcButton('6', '#FFFFFF', '#333333', () => { this.inputNumber('6') })
          this.CalcButton('-', '#FFFFFF', '#FF9F0A', () => { this.inputOperator('-') })
        }
        .width('100%')
        .margin({ bottom: 12 })

        Row() {
          this.CalcButton('1', '#FFFFFF', '#333333', () => { this.inputNumber('1') })
          this.CalcButton('2', '#FFFFFF', '#333333', () => { this.inputNumber('2') })
          this.CalcButton('3', '#FFFFFF', '#333333', () => { this.inputNumber('3') })
          this.CalcButton('+', '#FFFFFF', '#FF9F0A', () => { this.inputOperator('+') })
        }
        .width('100%')
        .margin({ bottom: 12 })

        Row() {
          this.CalcButton('0', '#FFFFFF', '#333333', () => { this.inputNumber('0') }, 2)
          this.CalcButton('.', '#FFFFFF', '#333333', () => { this.inputNumber('.') })
          this.CalcButton('⌫', '#FFFFFF', '#333333', () => { this.backspace() })
          this.CalcButton('=', '#FFFFFF', '#FF9F0A', () => { this.calculate() })
        }
        .width('100%')
      }
      .width('100%')
      .padding(16)
      .backgroundColor(Color.White)
      .borderRadius(16)
      .shadow({
        radius: 8,
        color: '#08000000',
        offsetX: 0,
        offsetY: 2
      })
    }
    .width('100%')
    .height('100%')
    .padding({ left: 18, right: 18 })
    .backgroundColor('#F5F7FA')
  }
}

八、代码实现说明

1. 状态机设计

计算器有几个关键状态:

  • 初始状态:显示0,等待输入
  • 输入数字:追加数字到display
  • 输入运算符:保存当前值,准备输入下一个数
  • 显示结果:等于后显示结果,下次输入数字时重置
2. 连续运算

点击运算符时如果已有运算符,先计算之前的结果,再保存新的运算符,支持连续运算。

3. 边界处理
  • 除以零:显示Error
  • 多个小数点:阻止输入第二个小数点
  • 开头的0:输入第一个非0数字时替换0
  • 退格:删到0就不再删了
  • 长数字:自动截断,避免溢出
4. 按钮样式

模仿iOS计算器风格:

  • 数字键:深灰背景白字
  • 功能键:浅灰背景白字
  • 运算符:橙色背景白字
  • 圆角按钮,间距均匀
5. 表达式显示

计算时显示完整表达式,让用户清楚看到计算过程。

6. 结果格式化

去掉浮点数计算产生的多余小数位,避免显示类似0.30000000000000004的结果。

九、运行项目

代码编写完成后,在DevEco Studio中运行项目。测试加减乘除运算,测试小数、连续运算、清除、退格、正负、百分比等功能,验证除以零的错误处理。

十、开发中遇到的问题

  • 连续运算:点击第二个运算符时要先计算前面的结果
  • 浮点数精度:toFixed后转字符串去掉多余的0
  • 状态重置:等于后输入新数字要重置显示
  • 按钮宽度:0号键占两格宽度,其他按钮等宽

十一、总结

本文基于HarmonyOS和ArkTS实现了一个标准计算器。项目通过状态机管理计算状态,实现了完整的四则运算和各种计算器功能,处理了各种边界情况。这个项目展示了ArkTS中状态管理、网格布局和逻辑处理的基本方法,适合作为入门练习项目。

Logo

讨论HarmonyOS开发技术,专注于API与组件、DevEco Studio、测试、元服务和应用上架分发等。

更多推荐