你是否曾经想过,当你在计算器上按下那些看似简单的按钮时,背后究竟发生了什么魔法?今天,我们就要揭开这个神秘的面纱,一起来探索如何在HarmonyOS上实现一个简单而强大的计算器!准备好你的咖啡,因为我们即将踏上一段既有趣又富有挑战性的编码之旅。
一、二话不说,先看效果图
二、基本实现思路
在我们开始编码之前,让我们先来理清思路。实现这个计算器主要分为三个步骤:
-
中缀表达式转为后缀表达式
-
计算后缀表达式
-
UI布局
听起来很简单,对吧?但是,正如古人所说:"台上一分钟,台下十年功"。让我们一步步来解析这个看似简单的过程。其实也不难,哈哈哈
1. 中缀表达式转为后缀表达式
首先,什么是中缀表达式?简单来说,就是我们平常使用的表达式,例如 3 + 4 * 2
。但是,计算机并不像我们人类那样聪明,它需要一种更明确的表达方式 - 这就是后缀表达式(逆波兰表达式)的用武之地了。
将中缀表达式转换为后缀表达式的过程,就像是在玩一场复杂的纸牌游戏。我们需要仔细考虑每个运算符的优先级,就像在决定哪张牌应该先出一样。这个过程可以通过使用栈来实现,就像我们在整理扑克牌时会用到的那样。
转换步骤
- 初始化两个栈:一个用于存放操作符(operations),另一个用于存放输出结果(output)。
- 遍历中缀表达式的每个token:
- 如果是数字,则直接放入output栈。
- 如果是左括号,则放入operations栈。
- 如果是右括号,则从operations栈弹出操作符直到遇到左括号为止。
- 如果是操作符,则根据操作符优先级处理:
- 若当前操作符优先级高于栈顶操作符,则将当前操作符放入operations栈。
- 否则,将operations栈顶的操作符弹出并放入output栈,直到当前操作符优先级高于栈顶操作符。
2. 计算后缀表达式
一旦我们得到了后缀表达式,计算就变得相对简单了。我们只需要从左到右扫描表达式,遇到数字就入栈,遇到运算符就取出栈顶的两个数进行运算,然后将结果压回栈中。这个过程就像是在玩一个反向的俄罗斯方块游戏,我们不断地堆积数字,然后用运算符消除它们。
计算步骤
- 初始化一个栈用于存放数字。
- 遍历后缀表达式的每个token:
- 如果是数字,则压入栈中。
- 如果是操作符,则从栈中弹出两个数字进行运算,并将结果压入栈中。
3. UI布局
最后,我们需要为我们的计算器穿上一件漂亮的外衣。使用HarmonyOS的UI组件,我们可以创建一个既美观又实用的界面。我们将使用Grid布局来组织按钮,使用Column和Row来排列表达式和结果显示区域。
三、核心代码实现
现在,让我们深入代码的海洋,看看这些想法是如何变成现实的。
1. 中缀表达式转为后缀表达式
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 '!':
return 6
case '^':
return 4
case '%':
return 3
case '✖':
case '➗':
return 2
case '+':
case '-':
return 1
default:
return 0
}
}
// 增加前一个token,用来判断!前是否有数字,以及判断-是否为负号
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 == '!') {
if (isNaN(Number(prevToken)))
return NaN
else
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)
}
//...
这段代码就像是一个精密的排序机器。它将表达式拆分成一个个"令牌",然后根据每个运算符的优先级,将它们重新排列成后缀表达式。注意我们如何处理负数和特殊运算符(如阶乘和幂运算),这就像是在处理一些特殊的扑克牌。
2. 计算后缀表达式
//定义一个数组栈,用来计算最终结果
let nums: number[] = []
for (let value of output) {
if (!isNaN(Number(value))) {
nums.push(Number(value))
} else if (value == '!') {
let num1 = nums.pop()
if (num1 !== undefined)
nums.push(this.factorial(num1))
} 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
case '%':
nums.push(num2 % num1)
break
case '^':
nums.push(Math.pow(num2, num1))
break
}
}
}
return nums[0] ?? NaN;
}
这部分代码就像是一个高效的装配线。它从左到右读取后缀表达式,遇到数字就放入"仓库"(栈),遇到运算符就从"仓库"中取出数字进行运算,然后将结果放回"仓库"。最后,仓库中剩下的唯一一个数字就是我们的计算结果。
3. UI布局
@Component
struct KeyGrid{
@State rowsTemp:string = '1fr 1fr 1fr 1fr 1fr'
@State rows:number = 5
@Link@Watch('aboutToAppear') isShowMore: boolean
@Link expressionFontSize:number
@Link expressionColor:string
@Link resultFontSize:number
@Link resultColor:string
onKeyPress?: (key:string) => void
deleteLast?: () => void
deleteAll?: () => void
calculateExp?: () => void
@State pressedItems: Map<string, boolean> = new Map();
aboutToAppear(): void {
this.JudgeIsMore(this.isShowMore)
}
JudgeIsMore(judge: boolean){
if( judge == true){
this.rowsTemp = '1fr 1fr 1fr 1fr 1fr 1fr'
this.rows = 6
}else{
this.rowsTemp = '1fr 1fr 1fr 1fr 1fr'
this.rows = 5
}
}
build(){
Grid(){
//展开后的按钮
if( this.isShowMore){
GridItem(){
Text('(')
}.KeyBoxStyleOther()
.onClick( () => {
if( this.onKeyPress)
this.onKeyPress('(')
})
GridItem(){
Text(')')
}.KeyBoxStyleOther()
.onClick( () => {
if( this.onKeyPress)
this.onKeyPress(')')
})
GridItem(){
Text('!')
}.KeyBoxStyleOther()
.onClick( () => {
if( this.onKeyPress)
this.onKeyPress('!')
})
GridItem(){
Text('^')
}.KeyBoxStyleOther()
.onClick( () => {
if( this.onKeyPress)
this.onKeyPress('^')
})
}
// 页面原有按键
GridItem(){
Text('AC')
}.KeyBoxStyleOther()
.onClick( () => {
if( this.deleteAll)
this.deleteAll()
})
.selected(true)
//...
我们使用Grid容器和Column和Row来创建一个层次分明的布局,以及如何使用动画来增加用户体验。
四、完整代码(希望大家点个免费的赞或关注,完整代码直接给大家了,有问题请评论)
import { Header } from '../common/components/commonComponents'
const nums:string[] = ['7', '8', '9', '4', '5', '6', '1', '2', '3', '00', '0', '.']
@Entry
@Component
struct Calculator {
@State
@Watch('calculateExp')
expression:string = ''
@State result:number = 0
@State isShowMore:boolean = false
@State expressionFontSize: number = 40
@State expressionColor: string = '#000000'
@State resultFontSize: number = 30
@State resultColor: string = '#000000'
build() {
Column(){
/*Header()
.margin({top:20})*/
Row(){
Image($r('app.media.cal_more'))
.width(30)
.height(30)
.margin(20)
.onClick( () => {
this.isShowMore = !this.isShowMore
})
}
.width('100%')
.justifyContent(FlexAlign.End)
.alignItems(VerticalAlign.Top)
Column(){
//1.计算表达式显示
Row(){
TextArea({text:this.expression})
.fontSize(this.expressionFontSize)
.fontColor(this.expressionColor)
.fontWeight(700)
.textAlign(TextAlign.End)
//.direction(Direction.Rtl)
.backgroundColor(Color.White)
.padding({bottom:20})
.animation( {
duration:500
})
}
.padding({right:20})
.justifyContent(FlexAlign.End)
.width('100%')
Divider().width('94%').opacity(1)
//2.计算结果显示
Row(){
TextArea({text:this.result.toString()})
.textAlign(TextAlign.End)
.backgroundColor(Color.White)
.fontColor(this.resultColor)
.fontSize(this.resultFontSize)
.fontWeight(700)
.animation( {
duration:500
})
}
.padding({right:20})
.justifyContent(FlexAlign.End)
.width('100%')
.height(60)
KeyGrid({isShowMore: this.isShowMore, expressionFontSize: this.expressionFontSize, expressionColor: this.expressionColor,
resultFontSize: this.resultFontSize, resultColor: this.resultColor,
onKeyPress: (key):void => this.handleKeyEvent(key), deleteLast: ():void => this.deleteLast(),
deleteAll: ():void => this.deleteAll(), calculateExp: ():void => this.calculateExp()
})
.width('100%')
.height('50%')
}
.layoutWeight(1)
.justifyContent(FlexAlign.End)
.width('100%')
}
.width('100%')
.height('100%')
.padding({bottom:50})
}
handleKeyEvent(key: string){
this.expression += key
}
deleteLast(){
this.expression = this.expression.slice(0,-1)
this.calculateExp()
if( this.expression === '' || isNaN(Number(this.expression[this.expression.length - 1])))
this.result = 0
}
deleteAll(){
this.expression = ""
this.result = 0
}
calculateExp(){
try{
this.result = this.getCalculate(this.expression)
}catch(err){
console.error(err + '计算错误')
}
this.expressionFontSize = 40
this.expressionColor = '#000000'
this.resultFontSize = 30
this.resultColor = '#888888'
}
factorial(n: number): number {
if (n <= 1) {
return 1;
} else {
return n * this.factorial(n - 1);
}
}
getCalculate(expr:string): number {
if('+-✖➗%^'.includes((expr[expr.length - 1])))
return NaN
// 使用正则划分出 整数或小数和符号的数组,修改正则表达式以支持负数
let tokens: string[] = expr.match(/(\d+(\.\d+)?|\+|-|✖|➗|\%|\(|\)|\!|\^)/g) || [];
let output: string[] = []
let operations: string[] = []
const getPrecedence = (op:string):number => {
switch(op){
case '!':
return 6
case '^':
return 4
case '%':
return 3
case '✖':
case '➗':
return 2
case '+':
case '-':
return 1
default:
return 0
}
}
// 增加前一个token,用来判断!前是否有数字,以及判断-是否为负号
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 == '!') {
if (isNaN(Number(prevToken)))
return NaN
else
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 if (value == '!') {
let num1 = nums.pop()
if (num1 !== undefined)
nums.push(this.factorial(num1))
} 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
case '%':
nums.push(num2 % num1)
break
case '^':
nums.push(Math.pow(num2, num1))
break
}
}
}
return nums[0] ?? NaN;
}
}
@Component
struct KeyGrid{
@State rowsTemp:string = '1fr 1fr 1fr 1fr 1fr'
@State rows:number = 5
@Link@Watch('aboutToAppear') isShowMore: boolean
@Link expressionFontSize:number
@Link expressionColor:string
@Link resultFontSize:number
@Link resultColor:string
onKeyPress?: (key:string) => void
deleteLast?: () => void
deleteAll?: () => void
calculateExp?: () => void
@State pressedItems: Map<string, boolean> = new Map();
aboutToAppear(): void {
this.JudgeIsMore(this.isShowMore)
}
JudgeIsMore(judge: boolean){
if( judge == true){
this.rowsTemp = '1fr 1fr 1fr 1fr 1fr 1fr'
this.rows = 6
}else{
this.rowsTemp = '1fr 1fr 1fr 1fr 1fr'
this.rows = 5
}
}
build(){
Grid(){
//展开后的按钮
if( this.isShowMore){
GridItem(){
Text('(')
}.KeyBoxStyleOther()
.onClick( () => {
if( this.onKeyPress)
this.onKeyPress('(')
})
GridItem(){
Text(')')
}.KeyBoxStyleOther()
.onClick( () => {
if( this.onKeyPress)
this.onKeyPress(')')
})
GridItem(){
Text('!')
}.KeyBoxStyleOther()
.onClick( () => {
if( this.onKeyPress)
this.onKeyPress('!')
})
GridItem(){
Text('^')
}.KeyBoxStyleOther()
.onClick( () => {
if( this.onKeyPress)
this.onKeyPress('^')
})
}
// 页面原有按键
GridItem(){
Text('AC')
}.KeyBoxStyleOther()
.onClick( () => {
if( this.deleteAll)
this.deleteAll()
})
.selected(true)
GridItem(){
Text('%')
}.KeyBoxStyleOther()
.onClick( () => {
if( this.onKeyPress)
this.onKeyPress('%')
})
GridItem(){
Image($r('app.media.calculator_delete'))
.width(20)
.height(20)
}
.KeyBoxStyleOther()
.onClick( () => {
if( this.deleteLast)
this.deleteLast()
})
GridItem(){
Text('➗')
}
.KeyBoxStyleOther()
.onClick( () => {
if( this.onKeyPress)
this.onKeyPress('➗')
})
GridItem(){
Text('✖')
}
.KeyBoxStyleOther()
.onClick( () => {
if( this.onKeyPress)
this.onKeyPress('✖')
})
.rowStart(this.rows - 4)
.columnStart(3)
.rowEnd(this.rows - 4)
.columnEnd(3)
GridItem(){
Text('➖')
}
.KeyBoxStyleOther()
.onClick( () => {
if( this.onKeyPress)
this.onKeyPress('-')
})
.rowStart(this.rows - 3)
.columnStart(3)
GridItem(){
Text('➕')
}
.KeyBoxStyleOther()
.rowStart(this.rows - 2)
.columnStart(3)
.onClick( () => {
if( this.onKeyPress)
this.onKeyPress('+')
})
// 循环渲染
ForEach(
nums,
(num:string) => {
GridItem(){
Text(num)
.fontSize(20)
.fontWeight(700)
}
.selected(true)
.KeyBoxStyle()
.backgroundColor(this.pressedItems[num] !== true ? Color.White : '#ffa2a0a0')
.onTouch((event: TouchEvent) => {
if (event.type === TouchType.Down) {
this.pressedItems[num] = true;
} else if (event.type === TouchType.Up || event.type === TouchType.Cancel) {
this.pressedItems[num] = false;
}
})
.onClick( () => {
if( this.onKeyPress)
this.onKeyPress(num)
})
})
GridItem(){
Text('=')
.fontColor(Color.White)
}.KeyBoxStyleOther()
.rowStart(this.rows - 1)
.columnStart(3)
.backgroundColor('#ffff6b14')
.onClick( () => {
if( this.calculateExp)
this.calculateExp()
this.expressionFontSize = 30
this.expressionColor = '#888888'
this.resultFontSize = 40
this.resultColor = '#000000'
})
}
.width('100%')
.height(400)
.columnsTemplate('1fr 1fr 1fr 1fr')
.rowsTemplate(this.rowsTemp)
.columnsGap(8)
.rowsGap(8)
.backgroundColor('#ffeeebeb')
.padding(20)
.animation({
duration: 300,
curve: Curve.EaseInOut,
delay: 50,
})
}
}
@Styles function KeyBoxStyleOther(){
.width(60)
.backgroundColor('#ffe0dddd')
.height(60)
.borderRadius(8)
}
@Styles function KeyBoxStyle(){
.width(60)
.backgroundColor(Color.White)
.height(60)
.borderRadius(8)
}
由于完整的代码相当长,我就不在这里完整展示了。但是,我相信通过上面的讲解,你已经对整个实现过程有了清晰的理解。完整的代码包括了我们讨论过的所有部分,以及一些额外的功能,比如处理特殊运算符(如阶乘)和更多的UI细节。
结语
就这样,我们的HarmonyOS计算器诞生了!它不仅能够进行基本的算术运算,还能处理复杂的表达式,甚至包括阶乘和幂运算。通过实现这个看似简单的应用,我们实际上涉及了许多重要的编程概念:正则表达式、栈的使用、字符串处理、UI设计等等。
记住,每一个伟大的应用都是从简单的想法开始的。今天的计算器,明天可能就是改变世界的下一个大应用!所以,继续编码,继续创造,让我们一起用HarmonyOS改变世界!
最后,如果你在实现过程中遇到了任何问题,不要气馁。就像计算器处理复杂表达式一样,解决问题的过程可能需要一步步来。保持耐心,保持好奇,你一定会成功的!