"成功不是将来才有的,而是从决定去做的那一刻起,持续累积而成。”
—— 保罗·柯艾略(Paulo Coelho)
前言
学习完成本次实例需要运用的知识:
- 栈的基本操作,将中缀表达式变成后缀表达式并计算结果
- 首选项数据存储的应用,将上次程序留下的表达式进行保存
- ArkUI的基础布局知识,用于布局数字和符号的按钮,以及表达式字体的大小。
本实例需要对上述知识有一定的了解,接下来是代码的实现目标:
- 通过按钮输入表达式,点击等于能够计算出结果,其中,当操作表达式的时候表达式字体较大,点击等于显示结果的时候字体较大,另外可以继续操作结果。
- 首选项数据存储的应用,将上次程序留下的表达式进行保存。
- 识别括号和‘-’号,优先计算括号里的内容,能够进行加减乘除的运算。
- 实现清零和‘<’的功能。
注:本篇文章用于个人学习总结
代码实现
根据以上功能可以将代码分为两大块,第一模块就是实现布局并且能将表达式正确输入上去,第二模块就是处理表达式并计算出结果。
布局
按钮部分
按钮大体分为两种,一种是按下之后把自己的值附上去的,比如数字和加减乘除括号,另一种是按下去之后可以实现某种功能的,比如说等于号,清理以及删除。
按照上述功能我们可以将其封装成如下类:
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('+-×÷'.includes((expr[expr.length - 1])))
return NaN // 如果表达式的最后一个字符是运算符,则返回 NaN
// 使用正则表达式匹配表达式中的数字、运算符和括号
let tokens = expr.match(/(\\d+(\\.\\d+)?|\\+|-|×|÷|\\(|\\))/g) || []; // 使用正则表达式匹配数字、小数、运算符和括号,并存储在 tokens 数组中
// 初始化 output 数组用于存储数字和运算符
let output: string[] = []
// 初始化 operations 数组用于存储运算符栈
let operations: string[] = []
// 定义一个函数来获取运算符的优先级
const getPrecedence = (op: string): number => {
// 运算符优先级映射
switch(op){
case '×':
case '÷':
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 == '(' || '+-×÷%^'.includes(prevToken))) {
// 如果 token 是 '-',并且前一个 token 是 'NaN'、'(' 或 '+'、'-'、'×'、'÷'、'%'、'^' 中的任何一个
// 将 '-' 视为一个一元运算符,相当于 '-1' 乘以一个运算符
output.push('-1')
operations.push('×')
} 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 '÷':
// 如果除数为零,则返回 NaN
if (num1 !== 0)
nums.push(num2 / num1)
else
return NaN
break
case '×':
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;
}