HarmonyOS ArkTS 实战:实现一个标准计算器
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 文件中更方便理解。
六、核心实现思路
本项目的核心流程如下:
- 使用
@State保存当前显示数字、上一个数字、当前运算符; - 点击数字按钮追加数字;
- 点击运算符保存当前数字和运算符;
- 点击等于时根据运算符计算结果;
- 处理小数点,确保只有一个;
- 处理正负切换;
- 处理百分比;
- 处理退格;
- AC重置所有状态;
- 处理除以零等错误情况。
项目中最重要的状态变量如下:
@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中状态管理、网格布局和逻辑处理的基本方法,适合作为入门练习项目。
更多推荐

所有评论(0)