你是否曾经想过,当你在计算器上按下那些看似简单的按钮时,背后究竟发生了什么魔法?今天,我们就要揭开这个神秘的面纱,一起来探索如何在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
}
}
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 = 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'
import Decimal from '@arkts.math.Decimal';
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:Decimal = new Decimal(0)
@State theResult: string = ''
@State isShowMore:boolean = false
@State expressionFontSize: number = 40
@State expressionColor: string = '#000000'
@State resultFontSize: number = 30
@State resultColor: string = '#000000'
@State isHaveDecimal: boolean = false
@State currentNumber: string = ''
build() {
Column(){
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(){
Row(){
TextArea({text:this.expression})
.fontSize(this.expressionFontSize)
.fontColor(this.expressionColor)
.fontWeight(700)
.textAlign(TextAlign.End)
.backgroundColor(Color.White)
.padding({bottom:20})
.animation( {
duration:500
})
}
.padding({right:20})
.justifyContent(FlexAlign.End)
.width('100%')
Divider().width('94%').opacity(1)
Row(){
Text(this.theResult)
.textAlign(TextAlign.End)
.backgroundColor(Color.White)
.fontColor(this.resultColor)
.fontSize(this.resultFontSize)
.fontWeight(700)
.maxLines(1)
.textOverflow({ overflow:TextOverflow.MARQUEE })
.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) {
const operators = '+-✖➗%^';
if (operators.includes(key)) {
this.currentNumber = '';
this.isHaveDecimal = false;
if (this.expression === '' ) {
return;
}else if(operators.includes(this.expression[this.expression.length - 1])){
this.expression = this.expression.slice(0,-1)
this.expression += key;
return;
}
} else if (key === '.') {
if (this.currentNumber.includes('.')) {
return;
}
if (this.currentNumber === '' || operators.includes(this.expression[this.expression.length - 1])) {
this.currentNumber = '0';
this.expression += '0';
}
} else if (key.match(/d/)) {
if (this.currentNumber === '0' && key !== '0') {
this.currentNumber = '';
this.expression = this.expression.slice(0, -1);
}
}
this.currentNumber += key;
this.expression += key;
}
deleteLast() {
if( this.expression.length === 0){
this.result = new Decimal(0);
}
if (this.expression.endsWith('.')) {
this.currentNumber = this.currentNumber.slice(0, -1);
}
this.expression = this.expression.slice(0, -1);
const lastOperatorIndex = Math.max(
this.expression.lastIndexOf('+'),
this.expression.lastIndexOf('-'),
this.expression.lastIndexOf('✖'),
this.expression.lastIndexOf('➗'),
this.expression.lastIndexOf('%'),
this.expression.lastIndexOf('^')
);
this.currentNumber = this.expression.slice(lastOperatorIndex + 1);
this.calculateExp();
if (this.expression === '' || isNaN(Number(this.expression[this.expression.length - 1])))
this.result = new Decimal(0);
}
deleteAll() {
this.expression = "";
this.result = new Decimal(0);
this.currentNumber = '';
this.isHaveDecimal = false;
}
calculateExp(){
const operators = '+-✖➗%^';
try{
this.result = this.getCalculate(this.expression)
if( this.expression.length === 0){
this.theResult = ''
}
else if( operators.includes(this.expression[this.expression.length - 1])){
return
}
else if( this.result.toString() === 'NaN'){
this.theResult = '表达式错误'
}else {
this.theResult = this.result.toString()
}
}catch(err){
console.error(err + '计算错误')
}
this.expressionFontSize = 40
this.expressionColor = '#000000'
this.resultFontSize = 30
this.resultColor = '#888888'
}
factorial(n: Decimal): Decimal {
if (n.toNumber() <= 1) {
return new Decimal(1);
} else {
return new Decimal(new Decimal(n).mul(this.factorial(n.sub(1))));
}
}
getCalculate(expr:string): Decimal {
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
}
}
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 new Decimal(NaN)
else
operations.push(token)
} else if (token == '-' && (prevToken == 'NaN' || prevToken == '(' || '+-✖➗%^'.includes(prevToken))) {
output.push('-1')
operations.push('✖')
} else if('+-✖➗'.includes(prevToken) && '+-✖➗'.includes(token)){
this.expression = expr.slice(0,-1)
this.expression += token
operations.push(token)
}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: Decimal[] = []
for (let value of output) {
if (!isNaN(Number(value))) {
nums.push(new Decimal(Number(value)))
} else if (value == '!') {
let num1 = nums.pop()
if (num1 !== undefined)
nums.push(new Decimal(this.factorial(num1)))
} else {
let num11 = nums.pop() ?? 0
let num22 = nums.pop() ?? 0
let num1 = new Decimal(num11)
let num2 = new Decimal(num22)
switch (value) {
case '+':
nums.push(num2.add(num1))
break
case '-':
nums.push(num2.sub(num1))
break
case '➗':
if (num1.toNumber() !== 0)
nums.push(num2.div(num1))
else
return new Decimal(NaN)
break
case '✖':
nums.push(num2.mul(num1))
break
case '%':
nums.push(num2.mod(num1))
break
case '^':
nums.push(num2.pow(num1))
break
}
}
}
return nums[0] ?? new Decimal(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 = 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)
}
结语
就这样,我们的HarmonyOS计算器诞生了!它不仅能够进行基本的算术运算,还能处理复杂的表达式,甚至包括阶乘和幂运算。通过实现这个看似简单的应用,我们实际上涉及了许多重要的编程概念:正则表达式、栈的使用、字符串处理、UI设计等等。
记住,每一个伟大的应用都是从简单的想法开始的。今天的计算器,明天可能就是改变世界的下一个大应用!所以,继续编码,继续创造,让我们一起用HarmonyOS改变世界!
最后,如果你在实现过程中遇到了任何问题,不要气馁。就像计算器处理复杂表达式一样,解决问题的过程可能需要一步步来。保持耐心,保持好奇,你一定会成功的!
1、本站所有资源均从互联网上收集整理而来,仅供学习交流之用,因此不包含技术服务请大家谅解!
2、本站不提供任何实质性的付费和支付资源,所有需要积分下载的资源均为网站运营赞助费用或者线下劳务费用!
3、本站所有资源仅用于学习及研究使用,您必须在下载后的24小时内删除所下载资源,切勿用于商业用途,否则由此引发的法律纠纷及连带责任本站和发布者概不承担!
4、本站站内提供的所有可下载资源,本站保证未做任何负面改动(不包含修复bug和完善功能等正面优化或二次开发),但本站不保证资源的准确性、安全性和完整性,用户下载后自行斟酌,我们以交流学习为目的,并不是所有的源码都100%无错或无bug!如有链接无法下载、失效或广告,请联系客服处理!
5、本站资源除标明原创外均来自网络整理,版权归原作者或本站特约原创作者所有,如侵犯到您的合法权益,请立即告知本站,本站将及时予与删除并致以最深的歉意!
6、如果您也有好的资源或教程,您可以投稿发布,成功分享后有站币奖励和额外收入!
7、如果您喜欢该资源,请支持官方正版资源,以得到更好的正版服务!
8、请您认真阅读上述内容,注册本站用户或下载本站资源即您同意上述内容!
原文链接:https://www.dandroid.cn/archives/22332,转载请注明出处。
评论0