一、基本思路

1.UI界面

        首先我们肯定需要一个计算器的界面,上部显示运算结果,下部则为进行交互的按钮。

       左图为本蒟蒻 自行编写,左图为iOS系统自带计算器

2.核心计算---逆波兰表达式

        界面有了之后,我们需要通过对按钮的点击来输入要进行的计算,这里可以创建一个变量用来储存输入的表达式,本例使用字符串的形式储存。

let ing:string[]

 需注意这里使用的是字符串数组,至于原因在后续会讲到

 获得表达式之后肯定不能直接运算,我们需要对其进行一些处理,此时就要引入一个概念

逆波兰表达式

首先我们要知道什么是逆波兰表达式?

        逆波兰表达式又称后缀表达式,即每一种运算都放置在运算对象后;

        我们平时书写的像  3+5x6 ,每一种运算符都放置在运算对象中间,所以叫做中缀表达式。

上述式子改写为后缀表达式即为  3 5 6 x +

这种式子乍一看可能很难让人理解这byd到底是咋算出来答案的,而要解释原理,我们又要引出一位神秘嘉宾,那就是数据结构中的——栈

计算原理

        首先我们需要创建一个栈用于处理数据,然后遍历表达式,遇到数字则将其直接入栈,遇到运算符则提出栈中里栈顶最近的两个元素进行计算,随后将结果再次入栈,随后栈中唯一的数字变为答案。

为什么要用逆波兰表达式?

        最大的一个原因还是,在该表达式中可以忽略运算的优先级,即按顺序遍历后得到结果,不易出错

3.辅助工具

        在完成上述过程中我们肯定需要一些工具来辅助我们

        计算方面,首先我们需要把中缀表达式变为后缀,此时我们的数据都还是string类型的,我们还需要将其变为number型,再进行计算。

        UI方面,我们可以使用@Extend,@Builder等修饰器来简化代码,提高可读性

二、功能构建

        为提高代码简洁性,在此为每个功能单独构建一个类

1.栈类

export class stack{
 public st :Array<number|string> = []

 push(item:string|number){
  this.st.push(item)
 }

 pop(){
  if(this.st.length!=0){
   this.st.pop()
  }

 }

 peek(){
  return this.st.length === 0? 0:this.st[this.st.length-1]
 }

 is_Empty(){
  return this.st.length === 0
 }

 clear(){
  this.st = []
 }

 size(){
  return this.st.length
 }

 printf(){
  for(let i =0 ;i<this.st.length ;i++){
   console.log(this.st[i].toString())
  }
 }
}

其中printf()方法方便在运行时查看栈中状态  

2.字符串转数字类

export class bbutton{
  public num:string = ''


  change(item:string){
    switch (item){
      case '0':return 0;
      case '1':return 1;
      case '2':return 2;
      case '3':return 3;
      case '4':return 4;
      case '5':return 5;
      case '6':return 6;
      case '7':return 7;
      case '8':return 8;
      case '9':return 9;
    }
    return 0
  }
  get_number(num:string){
    let j = 0; let anw = 0;let k = num.length;let q = 0
    for(let i = 0;i<num.length;i++){
      if(num[i] == '.'){
        k=i
        break;
      }
    }
    for(let i = k-1 ;i>=0; i--,j++){
      if(num[i] == '-'){
        q = 1
        break;
      }
      let a = this.change(num[i])
      anw += a*Math.pow(10,j)
    }

    j = -1
    for(let i = k+1 ;i<num.length; i++,j--){
      let a = this.change(num[i])
      anw += a*Math.pow(10,j)
    }

    if(q == 1){
      return -1*anw
    }else{
      return anw
    }
    //return parseFloat(num)
  }
  get_string(){
    return this.num
  }
}

        其中get_number()先通过遍历获得小数点位置,随后以此将整个字符串分割成整数部分和小数部分,分别处理后再相加,避免了小数点的影响

 3.中缀表达式转后缀类

export class  Key_creat{


  public suffix :Array<number|string> = []

  public st = new stack()



  handle(cou:string[]){

    //加法
    for(let i =0;i<cou.length;i++){
      if(cou[i] == '+'){
        if(this.st.is_Empty()){
          this.st.push('+')
        }else{
          while(!this.st.is_Empty()){
            this.suffix.push(this.st.peek())
            this.st.pop()
          }
          this.st.push('+')
        }
        //减法
      }else if(cou[i] == '-'){
        if(this.st.is_Empty()){
          this.st.push('-')
        }else{
          while(!this.st.is_Empty()){
            this.suffix.push(this.st.peek())
            this.st.pop()
          }
          this.st.push('-')
        }
        //乘法
      }else if(cou[i] == 'x'){
        if(this.st.is_Empty()){
          this.st.push('x')
        }else if(this.st.peek()=='-'||this.st.peek()=='+'){
          this.st.push('x')
        }else{
          while(!this.st.is_Empty()){
            this.suffix.push(this.st.peek())
            this.st.pop()
          }
          this.st.push('x')
        }
        //除法
      }else if(cou[i] == '÷'){
        if(this.st.is_Empty()){
          this.st.push('÷')
        }else if(this.st.peek()=='-'||this.st.peek()=='+'){
          this.st.push('÷')
        }else{
          while(!this.st.is_Empty()){
            this.suffix.push(this.st.peek())
            this.st.pop()
          }
          this.st.push('÷')
        }
      }else{
        this.suffix.push(cou[i])
      }
    }
  }
  head_1(){
    while(!this.st.is_Empty()){
      this.suffix.push(this.st.peek())
      this.st.pop()
    }

  }
  get(){
    return this.suffix
  }
  clear(){
    this.st.clear()
    this.suffix = []
  }
}

        我们首先需要创建一个字符串数组用于储存后缀表达式(为将每个运算对象隔开使用字符串数组,与基本思路中提到同理),一个栈用于储存运算符。

        当遇到数字时将其直接存入数组中

        而遇到运算符则要分以下四种情况:

1.栈为空,此时直接入栈;

2.栈顶元素运算优先级(例如‘x’大于‘+’)高于或等于自己,此时提出栈顶元素放入数组中,然后与下一元素比较,直到遇到运算优先级小于自己的元素,入栈。

3.栈顶元素低于自己,此时直接入栈

 4.运算处理类

export class answer{

  public anw = new stack()
  public num = new bbutton()


  get_anw(n:string[]){
    for(let i =0 ;i<n.length;i++){
      if(n[i] == '+'){
        this.jia()
      }else if(n[i] == '-'){
        this.jian()
      }else if(n[i] == 'x'){

          this.cheng()


      }else if(n[i] == '÷'){

          this.chu()

      }else if(n[i] == '='){
        this.get()
      }else {
        this.anw.push(this.num.get_number(n[i] as string))
      }
    }
  }
  jia(){
    let b = this.anw.peek() as number

    this.anw.pop()
    let a = this.anw.peek() as number
    this.anw.pop()
    this.anw.push(a+b)
  }
  jian(){
    let b = this.anw.peek() as number
    this.anw.pop()
    let a = this.anw.peek() as number
    this.anw.pop()

    this.anw.push(a-b)
  }
  cheng(){
    let b = this.anw.peek() as number
    this.anw.pop()
    let a = this.anw.peek() as number
    this.anw.pop()

    this.anw.push(a*b)
  }
  chu(){
    let b = this.anw.peek() as number
    this.anw.pop()
    let a = this.anw.peek() as number
    this.anw.pop()
    this.anw.push(a/b)
  }



  get():string|number{
    let a:number= this.anw.peek() as number
    a = parseFloat(a.toFixed(6).toString())
    return a
  }
  clear(){
    this.anw.clear()
  }

}

        当主函数接受到需要处理的后缀表达式时,会遍历数组并运用字符串转数字类将其转化为number类型后进行计算,最后将计算结果返回。

三、主界面构建

        主界面我选择用网格布局Grid结合ForEach渲染出主界面,代码如下

@Entry
@Component
struct page {
  @State ing :string[] = []
  @State answer:string = '0'
  @State jiancha:string = ''
  @State jiancha2:string = ''
  number :string = '0'
  suffix = new Key_creat()
  anwe = new answer()
  st_num = new bbutton()
  /////
  eventType: string = '';
  bool :boolean = false
  star_x = 0
  star_y = 0
  distance = 0

  dain :boolean =false

  fuhao :boolean = false
  arr: string[] = ['AC', '+/-', '%', '÷', '7', '8', '9', 'x', '4',
    '5', '6', '-', '1', '2', '3', '+', '0', '.', '=']



  aboutToAppear(): void {
    window.getLastWindow(getContext())
      .then(win => {
        win.setWindowLayoutFullScreen(true)
      })
  }




  layoutOptions: GridLayoutOptions = {
    regularSize: [1, 1],
    onGetRectByIndex: (index: number) => {
      if (index == 16) { // key1是“0”按键对应的index
        return [4, 4, 1, 2]
      } else {
        return [0, 0, 0, 0]
      }
    }
  }

  build() {

    Column(){
      Column({space:30}){
        Text(this.jiancha)
          .fontColor(Color.White)
          .fontSize(20)
          .height(20)
        Text(this.jiancha2)
          .fontSize(25)
          .fontColor(Color.White)
          .width('100%')
          .height(20)
          .margin({top:80})
          .textAlign(TextAlign.End)
          .opacity(0.6)
          .margin({bottom:20})
        Text(this.answer)
          .fontSize(50)
          .fontColor(Color.White)
          .height(40)
          .margin({top:30})
          .width('100%')
          .textAlign(TextAlign.End)
          .onTouch((event?: TouchEvent) => {
            if(event){
              if (event.type === TouchType.Down) {
                this.eventType = 'Down';
                this.star_x = event.touches[0].x
                this.star_y = event.touches[0].y
              }
              if (event.type === TouchType.Up) {
                this.eventType = 'Up';
                this.bool = false
              }
              if (event.type === TouchType.Move) {
                this.eventType = 'Move';
                this.distance = Math.abs(event.touches[0].x-this.star_x)

                if(this.distance>20&&this.bool == false&&this.number.length>1){
                  this.number=this.number.slice(0,-1)
                  this.answer = this.number
                  this.bool = true
                  console.log('1')
                }
                if(this.number.length == 1){
                  this.answer = this.number = '0'
                  console.log('2')
                }
              }
            }
          })
      }
      .height('25%')
      .width('100%')
      .alignItems(HorizontalAlign.End)


      Grid(undefined, this.layoutOptions){
        ForEach(this.arr,(item:string,index:number) =>{
          GridItem(){
            if(check_num(item)){
              if(item=='0'){
                Button(item,{type:ButtonType.Capsule})
                  .button_zero('#333333','#FFFFFF',item)
                  .onClick(()=>{
                    if(this.st_num.get_number(this.number) != 0||this.dain == true){
                      this.number += '0'
                    }
                    this.answer = this.number
                    this.fuhao = false
                  })

              }else if(check_num(item)){
                Button(item,{type:ButtonType.Capsule})
                  .button('#333333','#FFFFFF',item)
                  .onClick(()=>{
                    console.log(this.number)
                    if(this.number[this.number.length-1]=='0'){
                      this.number = ''
                    }
                    this.number += item
                    this.answer = this.number
                    this.fuhao = false
                  })
              }

            }else{
              if(index<3){
                Button(item,{type:ButtonType.Capsule})
                  .button('#A5A5A5','#000000',item)
                  .onClick(()=>{
                    if(item == 'AC'){
                      this.ing = []
                      this.suffix.clear()
                      this.anwe.clear()
                      this.answer = '0'
                      this.number = '0'
                      this.ing = []
                      this.jiancha = ''
                      this.jiancha2 = ''
                      this.dain = false
                      this.fuhao = false

                    }else if(item == '+/-'){
                      let a = this.st_num.get_number(this.number)
                      a*=-1
                      this.number = a.toString()
                      this.answer = this.number.slice(0,12)
                    }else if(item == '%'){
                      let a = this.st_num.get_number(this.number)
                      a/=100
                      this.number = a.toString()
                      this.answer = this.number.slice(0,12)
                    }
                  })
              }else if(item == '.'){
                Button(item,{type:ButtonType.Capsule})
                  .button('#333333','#FFFFFF',item)
                  .onClick(()=>{
                    if(this.dain == false){
                      this.number += '.'
                      this.answer = this.number
                      this.dain = true
                    }
                  })
              }else{
                Button(item,{type:ButtonType.Capsule})
                  .button('#FEA00C','#FEFEFE',item)
                  .onClick(()=>{
                    if(item == 'x'||item == '÷'){
                      st1 = 1
                    }else{
                      st1 = 0
                    }
                    this.dain = false


                    if(this.fuhao == false){
                      this.ing.push(this.number)
                      this.ing.push(item)
                    }

                    this.fuhao = true
                    this.number = '0'



                    ing = this.ing
                    if(this.check(this.ing)){
                      let a =ing.pop()
                      this.suffix.handle(ing)
                      ing.push(a as string)
                    }else{
                      this.suffix.handle(this.ing)
                    }
                    this.suffix.head_1()
                    this.anw()
                    if(st1>st2){

                    }else{
                      let a :string=this.anwe.get().toString()
                      if(a == 'Infinity'){a = '错误'}
                      this.answer = a
                    }
                    st2 = st1

                    if(item == '='){
                      this.number = this.answer
                      this.ing = []
                    }
                    this.suffix.clear()
                    this.anwe.anw.clear()
                  })
              }

            }
          }
        })
      }
      // .columnsGap(10)
      .rowsGap(20)
      .maxCount(4)
      .columnsTemplate('1fr 1fr 1fr 1fr ')
      .rowsTemplate('1fr 1fr 1fr 1fr 1fr')
      .height(500)
      .margin({top:40,bottom:10})
    }
    .backgroundColor(Color.Black)
    .height('100%')
    .width('100%')
  }
  anw(){
    let st = this.suffix.get()

    this.jiancha = ''
    for(let i =0;i<st.length;i++)
      this.jiancha+=st[i]
    this.jiancha2 = ''
    for(let i =0;i<this.ing.length;i++)
      this.jiancha2+=(this.ing[i]+' ')




    this.anwe.get_anw(st as string[])


    this.anwe.anw.printf()


  }
  check(st:string[]){
    let a = new bbutton()
    let num = st[st.length-1]
    if(num == '+'||num == '-'||num == 'x'||num == '÷'){
      return true
    }else{
      return false
    }

  }
}
@Extend(Button)
function button(back:ResourceColor,word:ResourceColor,item:string){
  .fontSize(35)
  .fontColor(word)
  .backgroundColor(back)
  .borderRadius(50)
  .height(85)
  .width(85)


}
@Extend(Button)
function button_zero(back:ResourceColor,word:ResourceColor,item:string){
  .fontSize(35)
  .fontColor(word)
  .backgroundColor(back)
  .borderRadius(50)
  .height(85)
  .width(165)

}

function check_num(item:string){
  if(item == '0'||item == '1'||item == '2'||item == '3'||item == '4'||item == '5'||item == '6'||item == '7'||item == '8'||item == '9'){
    return true
  }else{
    return false
  }
}

        其中为了更加贴近IOS系统计算器,可以构建一个GridLayoutOptions对象,使组件实现跨行跨列布局,详细用法如下

        在网格中,可以通过onGetRectByIndex返回的[rowStart,columnStart,rowSpan,columnSpan]来实现跨行跨列布局,其中rowStart和columnStart属性表示指定当前元素起始行号和起始列号,rowSpan和columnSpan属性表示指定当前元素的占用行数和占用列数。

        所以“0”按键横跨第一列和第二列,“=”按键横跨第五行和第六行,只要将“0”对应onGetRectByIndex的rowStart和columnStart设为5和0,rowSpan和columnSpan设为1和2,将“=”对应onGetRectByIndex的rowStart和columnStart设为4和3,rowSpan和columnSpan设为2和1即可。

  

layoutOptions: GridLayoutOptions = {
    regularSize: [1, 1],
    onGetRectByIndex: (index: number) => {
      if (index == 16) { // key1是“0”按键对应的index
        return [4, 4, 1, 2]
      } else {
        return [0, 0, 0, 0]
      }
    }
  }

        其中Index即为ForEach循环中的Index,对应按键‘0’,实现效果如下

        其中按键‘0’则占据两列布局,上方的‘1’则为正常的一行一列 


至此一个属于自己的专属计算器就此诞生了!

Logo

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

更多推荐