【鸿蒙原生应用开发--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 组件 - 网格项详解

GridItemGrid 的子组件,每个 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 组件用于在 RowColumn 中填充剩余空间。

使用场景:

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 开发的基础,掌握它们后,你将能够构建更复杂的应用。

相关推荐
Goway_Hui2 小时前
【鸿蒙原生应用开发--ArkUI--006】WeatherApp - 天气应用教程
华为·harmonyos
bylander3 小时前
【技术调研】华为《智能世界2035》白皮书调研报告
人工智能·华为
不羁的木木3 小时前
HarmonyOS文件基础服务(Core File Kit)实战演练03-文件增删改查与目录操作
pytorch·华为·harmonyos
IT大白鼠3 小时前
华为路由基础及静态路由详解
网络·华为
不羁的木木3 小时前
ArkWeb实战学习笔记02-环境搭建与基础配置
笔记·学习·harmonyos
技术路线图4 小时前
鸿蒙系统支付宝更新教程:华为应用市场操作步骤详解
华为·harmonyos
GitCode官方4 小时前
开源鸿蒙跨平台直播|15场·10大框架|首期:跨平台不是“权衡之选“,而是基础设施
人工智能·华为·开源·harmonyos·atomgit
互联网散修4 小时前
鸿蒙实战:图像滤镜工坊——ColorFilter 颜色矩阵与动态调节
harmonyos·图片颜色滤镜
UnicornDev4 小时前
【Flutter x HarmonyOS 6】设置页面的UI设计
flutter·ui·华为·harmonyos·鸿蒙