鸿蒙OS开发--初级计算器实例

"成功不是将来才有的,而是从决定去做的那一刻起,持续累积而成。”
—— 保罗·柯艾略(Paulo Coelho)

前言

学习完成本次实例需要运用的知识:

  1. 栈的基本操作,将中缀表达式变成后缀表达式并计算结果
  2. 首选项数据存储的应用,将上次程序留下的表达式进行保存
  3. ArkUI的基础布局知识,用于布局数字和符号的按钮,以及表达式字体的大小。

本实例需要对上述知识有一定的了解,接下来是代码的实现目标:

  1. 通过按钮输入表达式,点击等于能够计算出结果,其中,当操作表达式的时候表达式字体较大,点击等于显示结果的时候字体较大,另外可以继续操作结果
  2. 首选项数据存储的应用,将上次程序留下的表达式进行保存。
  3. 识别括号和‘-’号,优先计算括号里的内容,能够进行加减乘除的运算。
  4. 实现清零和‘<’的功能。

注:本篇文章用于个人学习总结

代码实现

根据以上功能可以将代码分为两大块,第一模块就是实现布局并且能将表达式正确输入上去,第二模块就是处理表达式并计算出结果。

布局

按钮部分

按钮大体分为两种,一种是按下之后把自己的值附上去的,比如数字和加减乘除括号,另一种是按下去之后可以实现某种功能的,比如说等于号,清理以及删除。

按照上述功能我们可以将其封装成如下类:

export default  class PressKeysBean {
  flag: number;
  value: string;
  ability?:number

  constructor(flag: number, value: string,ability?:number) {
    this.flag = flag;
    this.value = value;
    this.ability=ability
  }
}

flag用来区分是否为功能键,而value用来存放值,ability就是功能键的功能加以区分。

import PressKeysBean from './PressKeysItem'

 class PressKeysBeanViewModel{
  public getPressKeys():Array<Array<PressKeysBean>>{
    return [
      [
        new PressKeysBean(1,'C',0),
        new PressKeysBean(0,'7'),
        new PressKeysBean(0,'4'),
        new PressKeysBean(0,'1'),
        new PressKeysBean(1,'<',1)
      ],
      [
        new PressKeysBean(1,'('),
        new PressKeysBean(0,'8'),
        new PressKeysBean(0,'5'),
        new PressKeysBean(0,'2'),
        new PressKeysBean(0,'0')
      ],
      [
        new PressKeysBean(1,')'),
        new PressKeysBean(0,'9'),
        new PressKeysBean(0,'6'),
        new PressKeysBean(0,'3'),
        new PressKeysBean(0,'.')
      ],
      [
        new PressKeysBean(1,'÷'),
        new PressKeysBean(1,'×'),
        new PressKeysBean(1,'-'),
        new PressKeysBean(1,'+'),
        new PressKeysBean(2,'=')
      ]
    ]
  }
}
let keysModel = new PressKeysBeanViewModel();
export default keysModel as PressKeysBeanViewModel;

至此,我们的十六个键按照功能划分封装好了,接下来就是布局了!

布局可以用Grid布局,也可以用线性布局进行两层循环,这里使用后者。

Row({space:10}){
        ForEach(keysModel.getPressKeys(),(columnItem:Array<PressKeysBean>)=>{
          Column({space:30}){
            ForEach(columnItem,(Item:PressKeysBean)=> {
            //flag==0,表示是数字键,按下后直接将数字压进栈中
              if (Item.flag == 0) {
                Button(Item.value).style('#ffffffff','#000000').onClick(()=>{
                  this.expressions.push(Item.value)
                })
                //以下是加减乘除键和功能键
              } else if (Item.flag == 1) {
                Button(Item.value).style('#66ffffff','#000000').onClick(()=>{
                //等于0,是清除键,把表达式的值变成空,这里的flag,和flag1初始化。
                //flag和flag1是用来控制表达式和字体大小的,可以先不管
                  if(Item.ability==0){
                    this.expressions=[]
                    this.flag=0
                    this.flag1=0
                    //这里是删除键,点击一次在栈中弹出一个元素
                  }else if(Item.ability==1){
                    this.expressions.pop()
                    //加减乘除直接赋值
                  }else {
                    this.expressions.push(Item.value)
                    if(this.flag1==1){
                      this.expressions=[]
                      this.expressions.push(this.result.toString())
                      this.expressions.push(Item.value)
                      this.flag1=0
                    }
                  }
                })
                //这里是等于键,执行的功能是将表达式的结果进行解析赋值出结果
              }else if(Item.flag==2){
                Button(Item.value).style('#ff20b4cf','#ffffffff').onClick(()=>{
                  this.result=getCalculate(this.expressions.join(''))
                  if(Number.isNaN(this.result)){
                    this.result='表达式有误'
                  }
                  this.flag=1
                  this.flag1=1
                })
              }
            })
          }.justifyContent(FlexAlign.SpaceAround)
        })
      }.justifyContent(FlexAlign.SpaceAround)
      .width(350)
      .height(310)
      .alignItems(VerticalAlign.Bottom)
      .margin({top:280})

表达式和结果部分

情况1:表达式过长

Column({space:20}){
        TextInput({text:this.expressions.join('')})
          .minFontSize(15)
          .maxFontSize(this.flag1==1?30:50)
          .heightAdaptivePolicy(TextHeightAdaptivePolicy.MAX_LINES_FIRST)
          ...

给字体划定一个区间,一开始显示maxFontSize的字体大小,如果超出文本框显示范围,会自动缩小,直到缩小到minFontSize。

情况2:一开始的时候,结果处为空

使用flag处理,初始是0,当为0时,显示’ ',当flag为1时,结果显示。

情况3:点击等于时,结果会变大,表达式会变小,操作表达式的时候,则反之。

思路:定义flag1处理,初始是0,即一开始肯定是表达式大,点击等于的时候,让flag1等于1,结果会变得更大一些,而再次按下加减乘除继续进行操作的时候,让flag1=0.让表达式变大

布局完整代码

 @State KeyColor:string=''
  @State expressions:string[]=[]
  @State result:number|string=0
  @State flag:number=1
  @State flag1:number=0
  i:number=0
  build() {
    Stack(){

      Column({space:20}){
        TextInput({text:this.expressions.join('')})
          .fontColor($r('sys.color.mask_tertiary'))
          .backgroundColor('#ffefe8e8')
          .caretStyle({color:'#ff0a4dac',width:65})
          .minFontSize(15)
          .maxFontSize(this.flag1==1?30:50)
          .heightAdaptivePolicy(TextHeightAdaptivePolicy.MAX_LINES_FIRST)
          .textAlign(TextAlign.End)



        Text(this.flag==1?this.result.toString():'')
          .textAlign(TextAlign.End)
          .width('85%')
          .fontSize(this.flag1==1?50:30)
      }

      .margin({bottom:240})



      Row({space:10}){
        ForEach(keysModel.getPressKeys(),(columnItem:Array<PressKeysBean>)=>{
          Column({space:30}){
            ForEach(columnItem,(Item:PressKeysBean)=> {
              if (Item.flag == 0) {
                Button(Item.value).style('#ffffffff','#000000').onClick(()=>{
                  this.expressions.push(Item.value)
                })
              } else if (Item.flag == 1) {
                Button(Item.value).style('#66ffffff','#000000').onClick(()=>{
                  if(Item.ability==0){
                    this.expressions=[]
                    this.flag=0
                    this.flag1=0
                  }else if(Item.ability==1){
                    this.expressions.pop()
                  }else {
                    this.expressions.push(Item.value)
                    if(this.flag1==1){
                      this.expressions=[]
                      this.expressions.push(this.result.toString())
                      this.expressions.push(Item.value)
                      this.flag1=0
                    }
                  }
                })
              }else if(Item.flag==2){
                Button(Item.value).style('#ff20b4cf','#ffffffff').onClick(()=>{
                  this.result=getCalculate(this.expressions.join(''))
                  if(Number.isNaN(this.result)){
                    this.result='表达式有误'
                  }
                  this.flag=1
                  this.flag1=1
                })
              }
            })
          }.justifyContent(FlexAlign.SpaceAround)
        })
      }.justifyContent(FlexAlign.SpaceAround)
      .width(350)
      .height(310)
      .alignItems(VerticalAlign.Bottom)
      .margin({top:280})


    }
    .width('100%')
    .height('100%')
    .backgroundColor('#ffefe8e8')
  }
}

表达式处理

中缀表达式转后缀表达式

function getCalculate(expr: string): number {
  // 输入参数是一个字符串类型的表达式
  if('+-&times;&divide;'.includes((expr[expr.length - 1])))
    return NaN // 如果表达式的最后一个字符是运算符,则返回 NaN
  // 使用正则表达式匹配表达式中的数字、运算符和括号
  let tokens = expr.match(/(\\d+(\\.\\d+)?|\\+|-|&times;|&divide;|\\(|\\))/g) || []; // 使用正则表达式匹配数字、小数、运算符和括号,并存储在 tokens 数组中
  // 初始化 output 数组用于存储数字和运算符
  let output: string[] = []
  // 初始化 operations 数组用于存储运算符栈
  let operations: string[] = []
  // 定义一个函数来获取运算符的优先级
  const getPrecedence = (op: string): number => {
    // 运算符优先级映射
    switch(op){
      case '&times;':
      case '&divide;':
        return 2 // 乘法和除法的优先级最高
      case '+':
      case '-':
        return 1 // 加法和减法的优先级次之
      default:
        return 0 // 其他运算符的优先级为 0
    }
  }
  // 初始化前一个 token 为 'NaN'
  let prevToken: string = 'NaN';
  // 遍历 tokens 数组
  for (let token of tokens) {
    // 如果 token 可以转换为数字,则添加到 output 数组
    if (!isNaN(Number(token))) {
      output.push(token)
    } else if (token == '(') {
      // 如果 token 是 '(',则将其推入 operations 栈
      operations.push(token)
    } else if (token == '-' && (prevToken == 'NaN' || prevToken == '(' || '+-&times;&divide;%^'.includes(prevToken))) {
      // 如果 token 是 '-',并且前一个 token 是 'NaN'、'(' 或 '+'、'-'、'&times;'、'&divide;'、'%'、'^' 中的任何一个
      // 将 '-' 视为一个一元运算符,相当于 '-1' 乘以一个运算符
      output.push('-1')
      operations.push('&times;')
    } else {
      // 当 operations 栈不为空且栈顶运算符的优先级大于等于当前运算符的优先级时
      while (operations.length > 0 && getPrecedence(operations[operations.length - 1]) >= getPrecedence(token)) {
        // 弹出栈顶运算符并添加到 output 数组
        let op = operations.pop()
        if (op) {
          output.push(op)
        }
      }
      // 如果 token 不是 ')',则将其推入 operations 栈
      if (token != ')')
        operations.push(token)
    }
    // 更新前一个 token
    prevToken = token;
  }
  // 当 operations 栈不为空时,继续弹出运算符并添加到 output 数组
  while (operations.length > 0) {
    const op = operations.pop()
    if (op !== undefined)
      output.push(op)
  }
 
}

计算后缀表达式

 // 初始化 nums 数组用于存储数字
  let nums: number[] = []
  // 遍历 output 数组
  for (let value of output) {
    // 如果 value 可以转换为数字,则添加到 nums 数组
    if (!isNaN(Number(value))) {
      nums.push(Number(value))
    } else {
      // 弹出 nums 数组中的最后两个数字,执行相应的运算,并将结果推回 nums 数组
      let num1 = nums.pop() ?? 0
      let num2 = nums.pop() ?? 0
      switch (value) {
        case '+':
          nums.push(num2 + num1)
          break
        case '-':
          nums.push(num2 - num1)
          break
        case '&divide;':
          // 如果除数为零,则返回 NaN
          if (num1 !== 0)
            nums.push(num2 / num1)
          else
            return NaN
          break
        case '&times;':
          nums.push(num2 * num1)
          break
      }
    }
  }
  // 返回 nums 数组中的第一个元素,如果没有元素则返回 NaN
  return nums[0] ?? NaN;

保存表达式功能

首选项实际运用

该功能作用是,当程序退出时,你的表达式和结果均会被留在那里,下次进入程序还可以进行进一步的运算。

//创建时,获取数据
aboutToAppear(): void {
     this.getPreference().then((preference)=>{
       preference.get('exp',[]).then((v)=>{
         this.expressions=v as string[]
         this.result=getCalculate(this.expressions.join(''))
         this.flag1=1
       })
     })


   }

//销毁时,进行表达式的存储并数据持久化
   aboutToDisappear(): void {
     this.getPreference().then((p)=>{
       p.put('exp',this.expressions)
       p.flush()
     })
   }

//获取实例
   context:Context=getContext(this)
   getPreference(){
     return preferences.getPreferences(this.context,'user')
   }

总结

完成本次实例,复习了首选项数据存储,装饰器的使用,页面布局,字体大小设置,栈的使用,中缀转后缀,等众多知识,是一个综合性极强的实例,相信通过本次的学习,对于app开发的综合能力有了较大的提示!以下是完整代码:

PressKeysItem.ets

export default  class PressKeysBean {
  flag: number;
  value: string;
  ability?:number

  constructor(flag: number, value: string,ability?:number) {
    this.flag = flag;
    this.value = value;
    this.ability=ability
  }
}

PressKeysViewModel.ets

import PressKeysBean from './PressKeysItem'

 class PressKeysBeanViewModel{
  public getPressKeys():Array<Array<PressKeysBean>>{
    return [
      [
        new PressKeysBean(1,'C',0),
        new PressKeysBean(0,'7'),
        new PressKeysBean(0,'4'),
        new PressKeysBean(0,'1'),
        new PressKeysBean(1,'<',1)
      ],
      [
        new PressKeysBean(1,'('),
        new PressKeysBean(0,'8'),
        new PressKeysBean(0,'5'),
        new PressKeysBean(0,'2'),
        new PressKeysBean(0,'0')
      ],
      [
        new PressKeysBean(1,')'),
        new PressKeysBean(0,'9'),
        new PressKeysBean(0,'6'),
        new PressKeysBean(0,'3'),
        new PressKeysBean(0,'.')
      ],
      [
        new PressKeysBean(1,'÷'),
        new PressKeysBean(1,'×'),
        new PressKeysBean(1,'-'),
        new PressKeysBean(1,'+'),
        new PressKeysBean(2,'=')
      ]
    ]
  }
}
let keysModel = new PressKeysBeanViewModel();
export default keysModel as PressKeysBeanViewModel;

Page.ets

import keysModel from '../viewmodel/PressKeysViewModel';
import  PressKeysBean  from '../viewmodel/PressKeysItem';
import { preferences } from '@kit.ArkData';


@Extend(Button) function style(color:string,fontColor?:string){
  .backgroundColor(color)
  .fontColor(Color.Black).margin(5).width(70).height(50).fontSize(30).fontColor(fontColor)
}

@Entry
@Component
struct Calculator{


   aboutToAppear(): void {
     this.getPreference().then((preference)=>{
       preference.get('exp',[]).then((v)=>{
         this.expressions=v as string[]
         this.result=getCalculate(this.expressions.join(''))
         this.flag1=1
       })
     })


   }

   aboutToDisappear(): void {
     this.getPreference().then((p)=>{
       p.put('exp',this.expressions)
       p.flush()
     })
   }

   context:Context=getContext(this)
   getPreference(){
     return preferences.getPreferences(this.context,'user')
   }

  @State KeyColor:string=''
  @State expressions:string[]=[]
  @State result:number|string=0
  @State flag:number=1
  @State flag1:number=0
  i:number=0
  build() {
    Stack(){

      Column({space:20}){
        TextInput({text:this.expressions.join('')})
          .fontColor($r('sys.color.mask_tertiary'))
          .backgroundColor('#ffefe8e8')
          .caretStyle({color:'#ff0a4dac',width:65})
          .minFontSize(15)
          .maxFontSize(this.flag1==1?30:50)
          .heightAdaptivePolicy(TextHeightAdaptivePolicy.MAX_LINES_FIRST)
          .textAlign(TextAlign.End)



        Text(this.flag==1?this.result.toString():'')
          .textAlign(TextAlign.End)
          .width('85%')
          .fontSize(this.flag1==1?50:30)
      }

      .margin({bottom:240})



      Row({space:10}){
        ForEach(keysModel.getPressKeys(),(columnItem:Array<PressKeysBean>)=>{
          Column({space:30}){
            ForEach(columnItem,(Item:PressKeysBean)=> {
              if (Item.flag == 0) {
                Button(Item.value).style('#ffffffff','#000000').onClick(()=>{
                  this.expressions.push(Item.value)
                })
              } else if (Item.flag == 1) {
                Button(Item.value).style('#66ffffff','#000000').onClick(()=>{
                  if(Item.ability==0){
                    this.expressions=[]
                    this.flag=0
                    this.flag1=0
                  }else if(Item.ability==1){
                    this.expressions.pop()
                  }else {
                    this.expressions.push(Item.value)
                    if(this.flag1==1){
                      this.expressions=[]
                      this.expressions.push(this.result.toString())
                      this.expressions.push(Item.value)
                      this.flag1=0
                    }
                  }
                })
              }else if(Item.flag==2){
                Button(Item.value).style('#ff20b4cf','#ffffffff').onClick(()=>{
                  this.result=getCalculate(this.expressions.join(''))
                  if(Number.isNaN(this.result)){
                    this.result='表达式有误'
                  }
                  this.flag=1
                  this.flag1=1
                })
              }
            })
          }.justifyContent(FlexAlign.SpaceAround)
        })
      }.justifyContent(FlexAlign.SpaceAround)
      .width(350)
      .height(310)
      .alignItems(VerticalAlign.Bottom)
      .margin({top:280})


    }
    .width('100%')
    .height('100%')
    .backgroundColor('#ffefe8e8')
  }
}


//函数部分
function getCalculate(expr:string): number {
  if('+-×÷'.includes((expr[expr.length - 1])))
    return NaN
  // 使用正则划分出 整数或小数和符号的数组,修改正则表达式以支持负数
  let tokens = expr.match(/(\d+(\.\d+)?|\+|-|×|÷|\(|\))/g) || [];
  let output: string[] = []
  let operations: string[] = []

  const getPrecedence = (op:string):number => {
    switch(op){
      case '×':
      case '÷':
        return 2
      case '+':
      case '-':
        return 1
      default:
        return 0
    }
  }


  let prevToken:string = 'NaN';
  for (let token of tokens) {
    if (!isNaN(Number(token))) {
      output.push(token)
    } else if (token == '(') {
      operations.push(token)
    }
    else if (token == '-' && (prevToken == 'NaN' || prevToken == '(' || '+-×÷%^'.includes(prevToken))) {
      // 处理负号的情况
      output.push('-1')
      operations.push('×')
    } else {
      while (operations.length > 0 && getPrecedence(operations[operations.length - 1]) >= getPrecedence(token)) {
        let op = operations.pop()
        if (op === '(')
          break
        if (op) {
          output.push(op)
        }
      }
      if (token != ')')
        operations.push(token)
    }
    prevToken = token;
  }

  while (operations.length > 0) {
    const op = operations.pop()
    if (op !== undefined)
      output.push(op)
  }

  let nums: number[] = []

  for (let value of output) {
    if (!isNaN(Number(value))) {
      nums.push(Number(value))
    }

    else {
      let num1 = nums.pop() ?? 0
      let num2 = nums.pop() ?? 0
      switch (value) {
        case '+':
          nums.push(num2 + num1)
          break
        case '-':
          nums.push(num2 - num1)
          break
        case '÷':
          if (num1 !== 0)
            nums.push(num2 / num1)
          else
            return NaN  // 除数为零
          break
        case '×':
          nums.push(num2 * num1)
          break
      }
    }
  }

  return nums[0] ?? NaN;
}


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值