前言

计算器是鸿蒙 ArkTS 开发入门最经典的实战案例,能够帮助零基础开发者快速掌握ArkUI 基础组件、响应式状态管理、Grid 网格布局、按钮点击事件、条件判断与基础运算逻辑

本文基于可正常编译运行的完整源码进行讲解,全程适配鸿蒙基础开发语法,无复杂高阶语法、无第三方依赖,步骤清晰、注释详尽。大家只需将代码复制到 DevEco Studio 的 Index.ets 文件中,即可一键运行。同时文中预留截图位置,适合发布技术博客、课堂作业、学习笔记使用。

技术栈:HarmonyOS NEXT、ArkTS、ArkUI 声明式UI、DevEco Studio

一、项目整体介绍

1.1 实现功能

本款简易计算器主打基础运算能力,功能简洁实用,完全贴合入门学习需求:

  1. 支持 0-9 数字连续输入;
  2. 支持小数点输入,自动禁止重复添加小数点;
  3. 实现加、减、乘、除四则基础运算;
  4. C 清空键:一键重置所有数据与界面;
  5. = 等于键:计算并展示最终运算结果;
  6. 容错处理:拦截除数为 0 异常,避免程序崩溃;
  7. 浮点精度优化:限制小数位数,解决基础浮点显示问题;
  8. 支持运算结果连续二次计算。

1.2 界面布局说明

整体页面采用纵向 Column 布局,分为两大模块:

  1. 顶部显示区:使用 Text 组件作为计算器显示屏,展示输入内容与计算结果;
  2. 底部按键区:使用 Grid + GridItem 网格布局,分为 4 列排列所有功能按钮,区分运算符、功能键、数字键样式。

二、开发环境与工程创建

2.1 环境配置

  • 开发工具:DevEco Studio 正式版
  • 运行设备:鸿蒙模拟器 / 鸿蒙真机
  • 开发语言:ArkTS
  • 布局框架:ArkUI 原生声明式布局

2.2 新建项目步骤

  1. 打开 DevEco Studio,点击 Create Project 新建工程;
  2. 模板选择 Empty Ability 空白模板,点击下一步;
  3. 自定义项目名称(例:SimpleCalculator),设备类型选择手机(Phone),语言选择 ArkTS
  4. 选择文件保存路径,完成创建,等待项目依赖自动同步。

项目创建完成后,核心编辑文件路径:

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

三、核心变量与逻辑思路讲解

在正式拆分代码前,先理解本项目用到的核心变量与整体执行逻辑,新手更容易读懂代码。

3.1 核心状态变量

// 显示屏显示文本,@State 实现数据驱动UI自动刷新

@State showText: string = "0"

// 存储第一个参与运算的数字

private firstNum: number = 0

// 存储用户选中的运算符:+、-、*、/

private operator: string = ""

// 标记是否为全新输入,区分连续输入与二次输入

private isNewInput: boolean = true
  • @State:ArkUI 响应式关键字,变量改变时页面自动刷新,是动态界面的核心;
  • firstNum:缓存第一个运算数,用于后续四则运算;
  • operator:记录运算符,判断执行哪一种计算逻辑;
  • isNewInput:核心标记,用来区分“继续输入数字”和“重新开始输入数字”两种场景。

3.2 整体执行流程

  1. 点击数字/小数点 → 判断输入状态,拼接内容并刷新显示屏;
  2. 点击运算符 → 保存当前数字为第一个运算数,记录运算符,标记为等待新输入;
  3. 继续输入第二个数字 → 正常拼接内容;
  4. 点击等于号 → 读取两个数字与运算符,执行四则运算,处理异常并展示结果;
  5. 点击 C 键 → 所有变量重置,计算器恢复初始状态。

四、完整可运行源码

直接将下方全部代码复制粘贴到 Index.ets 中,即可编译运行:

@Entry

@Component

struct Index {

  // 显示屏显示文本(响应式刷新)

  @State showText: string = "0"

  // 运算拆分变量

  private firstNum: number = 0

  private operator: string = ""

  private isNewInput: boolean = true

  // 按钮统一点击入口

  onBtnClick(value: string): void {

    // 1. 清空重置按钮 C

    if (value === "C") {

      this.showText = "0"

      this.firstNum = 0

      this.operator = ""

      this.isNewInput = true

      return

    }

    // 2. 等于号,执行计算逻辑

    if (value === "=") {

      this.calcResult()

      return

    }

    // 3. 点击运算符,记录第一个数字与运算符

    if (["+", "-", "*", "/"].includes(value)) {

      this.firstNum = parseFloat(this.showText)

      this.operator = value

      this.isNewInput = true

      return

    }

    // 4. 数字、小数点输入逻辑

    if (this.isNewInput) {

      this.showText = value === "." ? "0." : value

      this.isNewInput = false

    } else {

      // 禁止重复输入小数点

      if (value === "." && this.showText.includes(".")) {

        return

      }

      this.showText += value

    }

  }

  // 四则运算核心方法

  calcResult(): void {

    // 未选择运算符,直接终止计算

    if (this.operator === "") {

      return

    }

    const secondNum: number = parseFloat(this.showText)

    let result: number = 0

    // 根据运算符执行对应计算

    switch (this.operator) {

      case "+":

        result = this.firstNum + secondNum

        break

      case "-":

        result = this.firstNum - secondNum

        break

      case "*":

        result = this.firstNum * secondNum

        break

      case "/":

        // 容错:拦截除数为0

        if (secondNum === 0) {

          this.showText = "计算错误"

          this.firstNum = 0

          this.operator = ""

          this.isNewInput = true

          return

        }

        result = this.firstNum / secondNum

        break

    }

    // 修正浮点数精度,保留6位小数

    result = parseFloat(result.toFixed(6))

    // 更新显示,支持连续运算

    this.showText = result.toString()

    this.firstNum = result

    this.operator = ""

    this.isNewInput = true

  }

  // 页面UI布局

  build() {

    Column() {

      // 计算器显示屏

      Text(this.showText)

        .width("90%")

        .height(120)

        .fontSize(48)

        .textAlign(TextAlign.End)

        .padding({ right: 15 })

        .backgroundColor(0xF5F5F5)

        .borderRadius(12)

        .margin({ top: 30, bottom: 20 })

      // 计算器4列网格按钮区域

      Grid() {

        GridItem() { Button("C").fontSize(28).backgroundColor(0xEEEEEE) }

        .onClick(() => this.onBtnClick("C"))

        GridItem() { Button("/").fontSize(28).backgroundColor(0xFFB74D) }

        .onClick(() => this.onBtnClick("/"))

        GridItem() { Button("*").fontSize(28).backgroundColor(0xFFB74D) }

        .onClick(() => this.onBtnClick("*"))

        GridItem() { Button("-").fontSize(28).backgroundColor(0xFFB74D) }

        .onClick(() => this.onBtnClick("-"))

        GridItem() { Button("7").fontSize(28) }

        .onClick(() => this.onBtnClick("7"))

        GridItem() { Button("8").fontSize(28) }

        .onClick(() => this.onBtnClick("8"))

        GridItem() { Button("9").fontSize(28) }

        .onClick(() => this.onBtnClick("9"))

        GridItem() { Button("+").fontSize(28).backgroundColor(0xFFB74D) }

        .onClick(() => this.onBtnClick("+"))

        GridItem() { Button("4").fontSize(28) }

        .onClick(() => this.onBtnClick("4"))

        GridItem() { Button("5").fontSize(28) }

        .onClick(() => this.onBtnClick("5"))

        GridItem() { Button("6").fontSize(28) }

        .onClick(() => this.onBtnClick("6"))

        GridItem() { Button("=").fontSize(28).backgroundColor(0x42A5F5).fontColor(Color.White) }

        .onClick(() => this.onBtnClick("="))

        GridItem() { Button("1").fontSize(28) }

        .onClick(() => this.onBtnClick("1"))

        GridItem() { Button("2").fontSize(28) }

        .onClick(() => this.onBtnClick("2"))

        GridItem() { Button("3").fontSize(28) }

        .onClick(() => this.onBtnClick("3"))

        GridItem() { Button(".").fontSize(28) }

        .onClick(() => this.onBtnClick("."))

        GridItem() { Button("0").fontSize(28).width("100%") }

        .onClick(() => this.onBtnClick("0"))

      }

      .columnsTemplate("1fr 1fr 1fr 1fr")

      .rowsGap(12)

      .columnsGap(12)

      .width("90%")

      .height(420)

    }

    .width("100%")

    .height("100%")

    .justifyContent(FlexAlign.Start)

  }

}

 

五、代码模块拆分详解

5.1 状态变量模块

这一部分定义程序运行所需的所有数据,是整个计算器的数据基础:

@State showText: string = "0"

private firstNum: number = 0

private operator: string = ""

private isNewInput: boolean = true
  • showText:页面展示的内容,初始默认显示数字 0;
  • firstNum:存储第一个运算数字,初始值为 0;
  • operator:运算符字符串,初始为空;
  • isNewInput:布尔标记,默认开启全新输入状态。

5.2 按钮统一点击方法 onBtnClick

所有按钮的点击事件都会进入这个方法,通过判断按钮内容分流执行不同逻辑,结构清晰易维护:

  1. 判断清空键 C:重置所有变量,页面回到初始状态;
  2. 判断等于键 =:调用计算方法,执行运算;
  3. 判断运算符:保存当前数字和运算符,切换为新输入状态;
  4. 判断数字/小数点:结合 isNewInput 标记,区分“覆盖输入”和“拼接输入”,同时限制重复小数点。

5.3 运算核心方法 calcResult

该方法专门实现四则运算与异常处理,是计算器的核心逻辑:

  1. 先判断是否选择运算符,无运算符则不执行计算;
  2. 将显示屏字符串转为数字,得到第二个运算数;
  3. 通过 switch 匹配运算符,分别执行加、减、乘、除;
  4. 异常处理:除法时判断除数是否为 0,若为 0 则提示“计算错误”并重置状态;
  5. 精度处理:使用 toFixed(6) 限制小数位数,优化浮点计算显示问题;
  6. 运算完成后更新界面,同时把结果赋值给 firstNum,支持连续运算。

5.4 UI 布局模块 build

build 函数负责页面渲染,分为显示屏与按钮网格两部分:

  1. Text 显示屏:设置宽高、字体大小、背景色、圆角、右对齐样式,模拟真实计算器屏幕;
  2. Grid 网格布局:设置为 4 列布局,通过 rowsGapcolumnsGap 设置按钮间距;
  3. GridItem + Button:逐个定义按钮,为不同功能按钮设置区分色,每个按钮绑定点击事件;
  4. 最外层 Column 纵向布局,让显示屏在上、按键区在下,适配手机屏幕。

5.5计算器核心逻辑类

@Entry
@Component
struct Index {
    @State showText: string = "0"
    private firstNum: number = 0
    private operator: string = ""
    private isNewInput: boolean = true

    onBtnClick(value: string): void {
        if (value === "C") {
            this.showText = "0"
            this.firstNum = 0
            this.operator = ""
            this.isNewInput = true
            return
        }

        if (value === "=") {
            this.calcResult()
            return
        }

        if (["+", "-", "*", "/"].includes(value)) {
            this.firstNum = parseFloat(this.showText)
            this.operator = value
            this.isNewInput = true
            return
        }

        if (this.isNewInput) {
            this.showText = value === "." ? "0." : value
            this.isNewInput = false
        } else {
            if (value === "." && this.showText.includes(".")) return
            this.showText += value
        }
    }

    calcResult(): void {
        if (this.operator === "") return
        const secondNum: number = parseFloat(this.showText)
        let result: number = 0

        switch (this.operator) {
            case "+": result = this.firstNum + secondNum; break
            case "-": result = this.firstNum - secondNum; break
            case "*": result = this.firstNum * secondNum; break
            case "/":
                if (secondNum === 0) {
                    this.showText = "计算错误"
                    this.firstNum = 0
                    this.operator = ""
                    this.isNewInput = true
                    return
                }
                result = this.firstNum / secondNum
                break
        }

        result = parseFloat(result.toFixed(6))
        this.showText = result.toString()
        this.firstNum = result
        this.operator = ""
        this.isNewInput = true
    }
}

5.6UI布局组件

build() {
    Column() {
        Text(this.showText)
            .width("90%")
            .height(120)
            .fontSize(48)
            .textAlign(TextAlign.End)
            .padding({ right: 15 })
            .backgroundColor(0xF5F5F5)
            .borderRadius(12)
            .margin({ top: 30, bottom: 20 })

        Grid() {
            GridItem() { Button("C").fontSize(28).backgroundColor(0xEEEEEE) }
                .onClick(() => this.onBtnClick("C"))
            GridItem() { Button("/").fontSize(28).backgroundColor(0xFFB74D) }
                .onClick(() => this.onBtnClick("/"))
            GridItem() { Button("*").fontSize(28).backgroundColor(0xFFB74D) }
                .onClick(() => this.onBtnClick("*"))
            GridItem() { Button("-").fontSize(28).backgroundColor(0xFFB74D) }
                .onClick(() => this.onBtnClick("-"))

            GridItem() { Button("7").fontSize(28) }
                .onClick(() => this.onBtnClick("7"))
            GridItem() { Button("8").fontSize(28) }
                .onClick(() => this.onBtnClick("8"))
            GridItem() { Button("9").fontSize(28) }
                .onClick(() => this.onBtnClick("9"))
            GridItem() { Button("+").fontSize(28).backgroundColor(0xFFB74D) }
                .onClick(() => this.onBtnClick("+"))

            GridItem() { Button("4").fontSize(28) }
                .onClick(() => this.onBtnClick("4"))
            GridItem() { Button("5").fontSize(28) }
                .onClick(() => this.onBtnClick("5"))
            GridItem() { Button("6").fontSize(28) }
                .onClick(() => this.onBtnClick("6"))
            GridItem() { Button("=").fontSize(28).backgroundColor(0x42A5F5).fontColor(Color.White) }
                .onClick(() => this.onBtnClick("="))

            GridItem() { Button("1").fontSize(28) }
                .onClick(() => this.onBtnClick("1"))
            GridItem() { Button("2").fontSize(28) }
                .onClick(() => this.onBtnClick("2"))
            GridItem() { Button("3").fontSize(28) }
                .onClick(() => this.onBtnClick("3"))
            GridItem() { Button(".").fontSize(28) }
                .onClick(() => this.onBtnClick("."))

            GridItem() { Button("0").fontSize(28).width("100%") }
                .onClick(() => this.onBtnClick("0"))
        }
        .columnsTemplate("1fr 1fr 1fr 1fr")
        .rowsGap(12)
        .columnsGap(12)
        .width("90%")
        .height(420)
    }
    .width("100%")
    .height("100%")
    .justifyContent(FlexAlign.Start)
}

每一步都要认真看,避免出错,慢慢来,不要着急

六、项目运行与功能测试

6.1 编译运行

代码粘贴完成后,点击 DevEco Studio 顶部运行按钮,选择模拟器或已连接的鸿蒙真机,等待编译安装,应用即可正常启动。

6.2 逐项功能测试

  1. 数字输入

点击 0-9 任意数字,屏幕正常显示,支持多位数连续输入。

  1. 小数点限制

多次点击小数点按钮,页面只会保留一个小数点,不会重复添加。

  1. 四则运算测试

输入 12 + 36 点击等于,正常输出结果;输入 88 - 226 * 7 均可正常计算。

  1. 除零异常测试

输入任意数字 ÷ 0,点击等于,屏幕显示计算错误,程序不会闪退崩溃。

  1. 清空功能

点击 C 键,页面恢复初始 0,所有运算数据全部重置。

  1. 连续运算

一次计算完成后,直接点击运算符和新数字,可基于上一次结果继续运算。

七、开发常见问题与解决办法

1. 修改代码后页面不刷新

原因:展示文本变量未使用 @State 修饰。

解决:动态展示的内容必须添加 @State 关键字,开启响应式更新。

2. 可以重复输入多个小数点

原因:未做字符判断。

解决:使用 showText.includes(".") 判断当前内容是否已存在小数点,存在则禁止输入。

3. 除数为0时程序报错/闪退

原因:缺少异常判断。

解决:在除法逻辑中增加 secondNum === 0 判断,主动拦截非法运算并提示错误。

4. 运算出现长串小数(浮点精度问题)

原因:编程语言原生浮点运算特性。

解决:使用 toFixed(位数) 限制小数展示位数,再转回数字格式。

八、项目总结

本项目基于标准 ArkTS 语法与 ArkUI 基础组件开发,代码简洁、逻辑清晰、可直接运行,非常适合鸿蒙入门学习者练习。

通过本次实战,我们掌握了以下基础知识点:

  1. DevEco Studio 项目创建、代码编写与运行流程;
  2. @State 响应式状态管理的使用场景;
  3. Column、Grid、GridItem、Text、Button 等常用基础组件;
  4. 按钮点击事件、分支判断、字符串与数字转换;
  5. 程序基础容错、异常处理的编写思路。

大家可以在现有代码基础上拓展功能,例如增加正负号、退格键、更多运算方式等,进一步巩固鸿蒙开发基础。

Logo

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

更多推荐