【鸿蒙原生应用开发--ArkUI--002】CalculatorApp - 计算器应用教程

CalculatorApp - 计算器应用教程

项目介绍

项目背景

计算器是每个开发者入门时最经典的项目之一。它虽然看起来简单,但涵盖了编程中的许多核心概念:数据存储、用户交互、条件判断、数学运算等。通过构建一个功能完整的计算器应用,你将深入理解 HarmonyOS NEXT 的开发模式和 ArkUI 框架的核心概念。

应用场景

计算器应用在日常生活中有着广泛的应用场景:

  • 基本计算:日常生活中的加减乘除运算
  • 百分比计算:购物打折、税率计算等
  • 科学计算:工程计算、数学学习等
  • 财务计算:贷款计算、投资回报等

功能特性

本计算器应用实现了以下功能:

  1. 基本四则运算:支持加(+)、减(-)、乘(×)、除(÷)运算
  2. 百分比计算:将当前数字转换为百分比
  3. 正负号切换:将正数转换为负数,负数转换为正数
  4. 小数点输入:支持小数运算
  5. 清除功能:清除所有输入,重新开始计算
  6. 连续运算:支持连续输入多个运算符

最终效果

用户界面分为两个主要区域:

  • 显示区域:位于顶部,显示当前输入的数字和运算结果,采用深色背景设计
  • 按钮区域:位于底部,采用网格布局,包含数字按钮、运算符按钮和功能按钮

技术栈

  • 开发框架:HarmonyOS NEXT API 23
  • 编程语言:ArkTS
  • UI 框架:ArkUI 声明式 UI
  • 布局方式:Column + Grid

开发环境准备

1. 创建项目

首先,我们需要创建一个新的 HarmonyOS NEXT 项目。可以通过以下两种方式:

方式一:使用 DevEco Studio 创建

  1. 打开 DevEco Studio
  2. 选择 "Create HarmonyOS Project"
  3. 选择 "Empty Ability" 模板
  4. 设置项目名称为 "CalculatorApp"
  5. 选择 API 版本为 23

方式二:复制模板项目

  1. 复制 project-template 目录
  2. 重命名为 "CalculatorApp"
  3. 修改 AppScope/app.json5 中的 bundleName

2. 项目结构

创建完成后,项目结构如下:

复制代码
CalculatorApp/
├── AppScope/                    # 应用全局配置
│   ├── app.json5               # 应用级配置
│   └── resources/              # 应用级资源
├── entry/                       # 主模块
│   └── src/main/
│       ├── ets/                # ArkTS 源代码
│       │   ├── entryability/   # UIAbility 入口
│       │   └── pages/          # 页面组件
│       └── resources/          # 资源文件
├── build-profile.json5         # 构建配置
└── oh-package.json5            # 依赖配置

知识点讲解

1. @Entry 装饰器 - 页面入口详解

@Entry 装饰器是 ArkUI 框架中最重要的装饰器之一,它标记当前组件为页面的入口组件。

核心概念:

  • 每个页面只能有一个 @Entry 修饰的组件
  • @Entry 组件是页面的根组件
  • 页面的生命周期事件由 @Entry 组件处理

语法示例:

typescript 复制代码
@Entry  // 标记为页面入口组件
@Component
struct Index {
  // 页面状态变量
  @State message: string = 'Hello World';
  
  // 页面生命周期
  onPageShow(): void {
    console.info('页面显示');
  }
  
  onPageHide(): void {
    console.info('页面隐藏');
  }
  
  // UI 构建方法
  build() {
    Column() {
      Text(this.message)
    }
  }
}

生命周期说明:

  • onPageShow():页面显示时触发
  • onPageHide():页面隐藏时触发
  • onBackPress():用户点击返回按钮时触发

2. @Component 装饰器 - 自定义组件详解

@Component 装饰器用于定义自定义组件,它是 ArkUI 组件化开发的基础。

核心概念:

  • 使用 struct 关键字定义组件
  • 必须实现 build() 方法
  • 可以包含状态变量和普通变量
  • 支持组件复用

语法示例:

typescript 复制代码
@Component
struct MyComponent {
  // 普通变量(不参与UI更新)
  private count: number = 0;
  
  // 状态变量(参与UI更新)
  @State message: string = 'Hello';
  
  build() {
    Column() {
      Text(this.message)
      Button('点击')
        .onClick(() => {
          this.count++;
          this.message = `点击了 ${this.count} 次`;
        })
    }
  }
}

组件复用:

typescript 复制代码
// 在其他组件中使用
@Entry
@Component
struct ParentPage {
  build() {
    Column() {
      MyComponent()  // 使用自定义组件
      MyComponent()  // 复用组件
    }
  }
}

3. @State 装饰器 - 状态管理详解

@State 装饰器是 ArkUI 状态管理的核心,它定义组件的内部状态。

核心概念:

  • 状态变量的值改变时,UI 会自动更新
  • 只能在 @Component 或 @Entry 组件中使用
  • 支持基本数据类型和对象类型

数据类型支持:

typescript 复制代码
// 基本类型
@State count: number = 0;
@State message: string = '';
@State isVisible: boolean = true;

// 数组类型
@State items: string[] = [];
@State numbers: number[] = [1, 2, 3];

// 对象类型
@State user: { name: string, age: number } = { name: '张三', age: 25 };

状态更新示例:

typescript 复制代码
@Component
struct Counter {
  @State count: number = 0;
  
  build() {
    Column() {
      Text(`计数: ${this.count}`)
        .fontSize(24)
      
      Row() {
        Button('-')
          .onClick(() => {
            this.count--;  // 状态改变,UI自动更新
          })
        
        Button('+')
          .onClick(() => {
            this.count++;  // 状态改变,UI自动更新
          })
      }
    }
  }
}

4. Column 组件 - 线性布局(纵向)详解

Column 组件是 ArkUI 中最常用的布局组件之一,它将子组件垂直排列。

核心属性:

typescript 复制代码
Column() {
  // 子组件
}
.width('100%')           // 宽度
.height('100%')          // 高度
.padding(16)             // 内边距
.margin(10)              // 外边距
.backgroundColor('#F8F9FA')  // 背景颜色
.borderRadius(12)        // 圆角
.justifyContent(FlexAlign.Center)  // 主轴对齐
.alignItems(HorizontalAlign.Center)  // 交叉轴对齐

对齐方式详解:

typescript 复制代码
// 主轴对齐(垂直方向)
Column() { ... }
  .justifyContent(FlexAlign.Start)      // 顶部对齐
  .justifyContent(FlexAlign.Center)     // 居中对齐
  .justifyContent(FlexAlign.End)        // 底部对齐
  .justifyContent(FlexAlign.SpaceBetween)  // 两端对齐
  .justifyContent(FlexAlign.SpaceAround)   // 均匀分布
  .justifyContent(FlexAlign.SpaceEvenly)   // 完全均匀

// 交叉轴对齐(水平方向)
Column() { ... }
  .alignItems(HorizontalAlign.Start)    // 左对齐
  .alignItems(HorizontalAlign.Center)   // 居中对齐
  .alignItems(HorizontalAlign.End)      // 右对齐

内边距和外边距:

typescript 复制代码
// 统一设置
.padding(16)  // 四个方向都是16
.margin(10)   // 四个方向都是10

// 分别设置
.padding({ left: 16, right: 16, top: 8, bottom: 8 })
.margin({ left: 10, right: 10, top: 5, bottom: 5 })

// 水平和垂直设置
.padding({ horizontal: 16, vertical: 8 })
.margin({ horizontal: 10, vertical: 5 })

5. Text 组件 - 文本显示详解

Text 组件用于显示文本内容,支持丰富的样式设置。

基本用法:

typescript 复制代码
Text('Hello World')
  .fontSize(24)              // 字体大小
  .fontWeight(FontWeight.Bold)  // 字体粗细
  .fontColor('#333333')      // 字体颜色
  .textAlign(TextAlign.Center)  // 文本对齐

字体粗细选项:

typescript 复制代码
.fontWeight(FontWeight.Lighter)  // 更细 (100-300)
.fontWeight(FontWeight.Normal)   // 正常 (400)
.fontWeight(FontWeight.Medium)   // 中等 (500)
.fontWeight(FontWeight.Bold)     // 粗体 (700)
.fontWeight(FontWeight.Bolder)   // 更粗 (900)

文本对齐方式:

typescript 复制代码
.textAlign(TextAlign.Start)    // 左对齐
.textAlign(TextAlign.Center)   // 居中对齐
.textAlign(TextAlign.End)      // 右对齐

文本溢出处理:

typescript 复制代码
Text('这是一段很长的文本内容,可能会超出显示区域')
  .maxLines(1)  // 最大行数
  .textOverflow({ overflow: TextOverflow.Ellipsis })  // 超出部分显示省略号
  .textOverflow({ overflow: TextOverflow.Clip })      // 超出部分裁剪
  .textOverflow({ overflow: TextOverflow.None })      // 不处理

行高设置:

typescript 复制代码
Text('多行文本\n第二行')
  .lineHeight(24)  // 行高

6. Button 组件 - 按钮详解

Button 组件用于创建可点击的按钮,支持文本和点击事件。

基本用法:

typescript 复制代码
Button('点击我')
  .width(120)
  .height(48)
  .fontSize(16)
  .fontWeight(FontWeight.Medium)
  .backgroundColor('#6366F1')
  .fontColor('#FFFFFF')
  .borderRadius(12)
  .onClick(() => {
    console.info('按钮被点击');
  })

按钮类型:

typescript 复制代码
// 默认按钮
Button('默认按钮')

// 胶囊按钮
Button('胶囊按钮')
  .type(ButtonType.Capsule)

// 圆形按钮
Button('圆')
  .type(ButtonType.Circle)
  .width(60)
  .height(60)

按钮状态样式:

typescript 复制代码
Button('按钮')
  .stateStyles({
    pressed: {
      .backgroundColor('#4F46E5')  // 按下状态
      .scale({ x: 0.95, y: 0.95 })
    },
    normal: {
      .backgroundColor('#6366F1')  // 正常状态
      .scale({ x: 1, y: 1 })
    },
    disabled: {
      .backgroundColor('#D1D5DB')  // 禁用状态
    }
  })

禁用按钮:

typescript 复制代码
Button('禁用按钮')
  .enabled(false)  // 设置为禁用状态
  .opacity(0.5)    // 降低透明度

7. Grid 组件 - 网格布局详解

Grid 组件用于创建网格布局,适合展示计算器的按钮排列。

核心概念:

  • columnsTemplate:定义列模板
  • rowsTemplate:定义行模板
  • columnsGap:列间距
  • rowsGap:行间距

列模板语法:

typescript 复制代码
// 4列等宽
.columnsTemplate('1fr 1fr 1fr 1fr')

// 不等宽列
.columnsTemplate('1fr 2fr 1fr')  // 中间列是其他列的2倍

// 固定宽度和弹性宽度混合
.columnsTemplate('100px 1fr 1fr')

行模板语法:

typescript 复制代码
// 5行等高
.rowsTemplate('1fr 1fr 1fr 1fr 1fr')

// 不等高行
.rowsTemplate('1fr 2fr 1fr')

完整示例:

typescript 复制代码
Grid() {
  GridItem() { Text('1') }
  GridItem() { Text('2') }
  GridItem() { Text('3') }
  GridItem() { Text('+') }
  
  GridItem() { Text('4') }
  GridItem() { Text('5') }
  GridItem() { Text('6') }
  GridItem() { Text('-') }
  
  GridItem() { Text('7') }
  GridItem() { Text('8') }
  GridItem() { Text('9') }
  GridItem() { Text('×') }
  
  GridItem() { Text('0') }
  GridItem() { Text('.') }
  GridItem() { Text('=') }
  GridItem() { Text('÷') }
}
.columnsTemplate('1fr 1fr 1fr 1fr')
.rowsTemplate('1fr 1fr 1fr 1fr')
.columnsGap(8)
.rowsGap(8)
.width('100%')
.height(300)

8. GridItem 组件 - 网格项详解

GridItem 是 Grid 的子组件,每个 GridItem 占据一个网格单元。

基本用法:

typescript 复制代码
GridItem() {
  // 子组件内容
  Button('7')
    .width('100%')
    .height('100%')
}

跨行跨列:

typescript 复制代码
// 跨2列
GridItem() {
  Text('跨2列')
}
.columnStart(1)
.columnEnd(2)

// 跨2行
GridItem() {
  Text('跨2行')
}
.rowStart(1)
.rowEnd(2)

9. Row 组件 - 线性布局(横向)详解

Row 组件将子组件水平排列。

基本用法:

typescript 复制代码
Row() {
  Text('左侧')
  Blank()  // 填充中间空间
  Text('右侧')
}
.width('100%')
.padding(16)

对齐方式:

typescript 复制代码
// 主轴对齐(水平方向)
Row() { ... }
  .justifyContent(FlexAlign.Start)      // 左对齐
  .justifyContent(FlexAlign.Center)     // 居中对齐
  .justifyContent(FlexAlign.End)        // 右对齐
  .justifyContent(FlexAlign.SpaceBetween)  // 两端对齐

// 交叉轴对齐(垂直方向)
Row() { ... }
  .alignItems(VerticalAlign.Top)        // 顶部对齐
  .alignItems(VerticalAlign.Center)     // 居中对齐
  .alignItems(VerticalAlign.Bottom)     // 底部对齐

10. Blank 组件 - 空白填充详解

Blank 组件用于在 Row 或 Column 中填充剩余空间。

使用场景:

typescript 复制代码
// 场景1:左右布局
Row() {
  Text('标题')
  Blank()
  Button('操作')
}
.width('100%')

// 场景2:中间内容居中
Row() {
  Blank()
  Text('居中内容')
  Blank()
}
.width('100%')

// 场景3:多元素分布
Row() {
  Text('左')
  Blank()
  Text('中')
  Blank()
  Text('右')
}
.width('100%')

完整代码解析

页面结构设计

我们的计算器页面采用垂直布局,分为两个主要区域:

typescript 复制代码
@Entry
@Component
struct Index {
  // 状态变量定义
  @State display: string = '0';           // 显示的数字
  @State firstOperand: number = 0;        // 第一个操作数
  @State operator: string = '';           // 当前运算符
  @State waitingForSecond: boolean = false; // 是否等待第二个操作数

  build() {
    Column() {
      // 1. 显示区域(深色背景)
      Column() {
        // 运算符和第一个操作数显示
        Text(this.operator ? `${this.firstOperand} ${this.operator}` : '')
          .fontSize(14)
          .fontColor('#999999')
          .width('100%')
          .textAlign(TextAlign.End)
          .padding({ right: 24, top: 8 })

        // 当前数字显示
        Text(this.display)
          .fontSize(56)
          .fontWeight(FontWeight.Bold)
          .fontColor('#FFFFFF')
          .textAlign(TextAlign.End)
          .width('100%')
          .padding({ right: 24, bottom: 24 })
          .maxLines(1)
          .textOverflow({ overflow: TextOverflow.Ellipsis })
      }
      .width('100%')
      .height(140)
      .backgroundColor('#1A1A2E')
      .borderRadius(24)
      .margin({ bottom: 16 })

      // 2. 按钮网格区域
      Grid() {
        // 按钮内容
      }
      .columnsTemplate('1fr 1fr 1fr 1fr')
      .rowsTemplate('1fr 1fr 1fr 1fr 1fr')
      .columnsGap(8)
      .rowsGap(8)
      .width('100%')
      .layoutWeight(1)
    }
    .padding(16)
    .width('100%')
    .height('100%')
    .backgroundColor('#F8F9FA')
  }
}

显示区域详解

显示区域采用深色背景设计,分为两行:

  • 第一行:显示运算符和第一个操作数(较小字体,灰色)
  • 第二行:显示当前输入的数字(大字体,白色)
typescript 复制代码
// 显示区域
Column() {
  // 第一行:运算符和第一个操作数
  Text(this.operator ? `${this.firstOperand} ${this.operator}` : '')
    .fontSize(14)
    .fontColor('#999999')
    .width('100%')
    .textAlign(TextAlign.End)
    .padding({ right: 24, top: 8 })

  // 第二行:当前数字
  Text(this.display)
    .fontSize(56)
    .fontWeight(FontWeight.Bold)
    .fontColor('#FFFFFF')
    .textAlign(TextAlign.End)
    .width('100%')
    .padding({ right: 24, bottom: 24 })
    .maxLines(1)
    .textOverflow({ overflow: TextOverflow.Ellipsis })
}
.width('100%')
.height(140)  // 固定高度
.backgroundColor('#1A1A2E')  // 深色背景
.borderRadius(24)  // 圆角
.margin({ bottom: 16 })

按钮网格详解

按钮网格采用 4列5行 的布局,包含:

  • 第一行:功能按钮(C、±、%)和除号
  • 第2-4行:数字按钮和运算符
  • 第5行:0、小数点和等号
typescript 复制代码
Grid() {
  // 第一行:功能按钮
  GridItem() { this.FunctionButton('C') }
  GridItem() { this.FunctionButton('±') }
  GridItem() { this.FunctionButton('%') }
  GridItem() { this.OperatorButton('÷') }

  // 第二行:7、8、9 和乘号
  GridItem() { this.NumberButton('7') }
  GridItem() { this.NumberButton('8') }
  GridItem() { this.NumberButton('9') }
  GridItem() { this.OperatorButton('×') }

  // 第三行:4、5、6 和减号
  GridItem() { this.NumberButton('4') }
  GridItem() { this.NumberButton('5') }
  GridItem() { this.NumberButton('6') }
  GridItem() { this.OperatorButton('-') }

  // 第四行:1、2、3 和加号
  GridItem() { this.NumberButton('1') }
  GridItem() { this.NumberButton('2') }
  GridItem() { this.NumberButton('3') }
  GridItem() { this.OperatorButton('+') }

  // 第五行:0、小数点和等号
  GridItem() { this.NumberButton('0') }
  GridItem() { this.NumberButton('.') }
  GridItem() { this.EqualsButton() }
}
.columnsTemplate('1fr 1fr 1fr 1fr')  // 4列等宽
.rowsTemplate('1fr 1fr 1fr 1fr 1fr') // 5行等高
.columnsGap(8)  // 列间距
.rowsGap(8)     // 行间距
.width('100%')
.layoutWeight(1)  // 占据剩余空间

事件处理逻辑

事件处理是计算器的核心逻辑,需要处理多种情况:

typescript 复制代码
handleButtonPress(text: string): void {
  // 1. 处理数字输入
  if (text >= '0' && text <= '9') {
    if (this.waitingForSecond) {
      // 如果正在等待第二个操作数,直接替换显示
      this.display = text;
      this.waitingForSecond = false;
    } else {
      // 否则追加到当前数字
      this.display = this.display === '0' ? text : this.display + text;
    }
  }
  // 2. 处理小数点
  else if (text === '.') {
    // 确保只有一个小数点
    if (!this.display.includes('.')) {
      this.display += '.';
    }
  }
  // 3. 处理清除
  else if (text === 'C') {
    this.display = '0';
    this.firstOperand = 0;
    this.operator = '';
    this.waitingForSecond = false;
  }
  // 4. 处理正负号切换
  else if (text === '±') {
    this.display = String(-parseFloat(this.display));
  }
  // 5. 处理百分比
  else if (text === '%') {
    this.display = String(parseFloat(this.display) / 100);
  }
  // 6. 处理运算符
  else if (text === '+' || text === '-' || text === '×' || text === '÷') {
    this.firstOperand = parseFloat(this.display);
    this.operator = text;
    this.waitingForSecond = true;
  }
  // 7. 处理等号
  else if (text === '=') {
    if (this.operator) {
      const secondOperand = parseFloat(this.display);
      let result = 0;
      
      // 根据运算符计算结果
      switch (this.operator) {
        case '+':
          result = this.firstOperand + secondOperand;
          break;
        case '-':
          result = this.firstOperand - secondOperand;
          break;
        case '×':
          result = this.firstOperand * secondOperand;
          break;
        case '÷':
          // 除法需要检查除数是否为0
          result = secondOperand !== 0 ? this.firstOperand / secondOperand : 0;
          break;
      }
      
      this.display = String(result);
      this.operator = '';
      this.waitingForSecond = false;
    }
  }
}

常见问题与解决方案

问题1:数字显示过长

问题描述:当输入的数字过长时,可能会超出显示区域。

解决方案:

typescript 复制代码
Text(this.display)
  .maxLines(1)  // 限制为单行
  .textOverflow({ overflow: TextOverflow.Ellipsis })  // 超出显示省略号
  .fontSize(56)  // 根据显示区域调整字体大小

问题2:除以零的处理

问题描述:除以零会导致错误。

解决方案:

typescript 复制代码
case '÷':
  result = secondOperand !== 0 ? this.firstOperand / secondOperand : 0;
  break;

问题3:多次输入小数点

问题描述:用户可能多次输入小数点。

解决方案:

typescript 复制代码
if (text === '.') {
  if (!this.display.includes('.')) {
    this.display += '.';
  }
}

扩展学习

1. 添加更多功能

可以扩展计算器的功能:

  • 科学计算(sin、cos、tan 等)
  • 历史记录
  • 内存功能(M+、M-、MR、MC)

2. 优化用户体验

  • 添加按键音效
  • 添加按键震动反馈
  • 支持手势操作

3. 适配不同设备

  • 平板适配
  • 折叠屏适配
  • 横屏模式

总结

通过本教程,你学习了:

  1. @Entry 装饰器:页面入口组件的定义和生命周期
  2. @Component 装饰器:自定义组件的定义和复用
  3. @State 装饰器:状态管理和 UI 自动更新
  4. Column 组件:垂直线性布局的使用
  5. Text 组件:文本显示和样式设置
  6. Button 组件:按钮的创建和事件处理
  7. Grid 组件:网格布局的使用
  8. GridItem 组件:网格项的定义
  9. Row 组件:水平线性布局的使用
  10. Blank 组件:空白填充的使用

这些知识点构成了 HarmonyOS NEXT 开发的基础,掌握它们后,你将能够构建更复杂的应用。

相关推荐
梦想不只是梦与想23 分钟前
HarmonyOS应用分层架构设计
harmonyos·分层架构·一次开发,多端部署
MardaWang3 小时前
当滚动逃离了框架 ——HarmonyOS Web 与原生混排滚动的冲突本质与解法
harmonyos·arkts·鸿蒙·deveco studio
tsqtsqtsq03094 小时前
DevEco Studio 介绍
harmonyos
HwJack207 小时前
【共创稿事节】HarmonyOS 7文旅展陈展厅大空间 3DGS 重建的分块策略与拼接踩坑
3d·华为·harmonyos
熊猫钓鱼>_>9 小时前
Kotlin Multiplatform for OpenHarmony 实战:为 kotlin-inject 实现依赖注入适配
开发语言·华为·kotlin·ai编程·inject·鸿蒙·openharmony
m0_738185829 小时前
Flutter 鸿蒙化实战:media_info 适配 OpenHarmony,媒体信息与缩略图
flutter·华为·harmonyos·鸿蒙·媒体
m0_7381858210 小时前
Flutter 鸿蒙化实战:just_audio 适配 OpenHarmony,功能强大的播放器
flutter·华为·harmonyos·鸿蒙
翼辉cto10 小时前
Kotlin Multiplatform 三方库 SQLDelight 的 OpenHarmony 鸿蒙化适配实战
开发语言·kotlin·harmonyos
SuperHeroWu710 小时前
TraeCode 国内版接入 DevEco CLI:用官方知识开发鸿蒙应用
ai编程·harmonyos·知识库·trae·aicoding·skills·deveco cli