移动软件开发实验五——鸿蒙开发入门及计算器开发
| 本实验属于哪门课程? | 中国海洋大学26夏《移动软件开发》 |
|---|---|
| 实验名称? | 实验5:鸿蒙开发入门及计算器开发 |
| 代码仓库地址? | https://github.com/Melia-pp/mobile_dev_test5 |
一、实验内容
1.开发环境搭建
1.安装DevEco Studio
步骤 1 进入下载页面https://developer.huawei.com/consumer/cn/download/,选择最新版本下载。

步骤 2 参考页面进行安装:https://developer.huawei.com/consumer/cn/doc/harmonyos-guides-V5/ide-software-install-V5

2.创建模拟器
整体步骤参考:https://developer.huawei.com/consumer/cn/doc/harmonyos-guides-V5/ide-emulator-create-V5
3.开发及运行环境验收
步骤 1: 打开DevEco Studio,在欢迎页单击Create Project,创建一个新工程。
步骤 2: 根据工程创建向导,选择创建Application或Atomic Service。选择Empty Ability模板,然后单击Next。

步骤 3: 填写工程相关信息,单击Finish。关于各个参数的详细介绍,请参考创建一个新的工程。

2.实现简易计算器
教程链接:https://developer.huawei.com/consumer/cn/doc/architecture-guides/calculator-0000002298744774
1.计算逻辑函数的定义
标准计算器的核心逻辑封装在 CalculateUtil.ets 中,先将中缀表达式转换为后缀表达式,再对后缀表达式求值。
(1)运算符识别与优先级
// 判断是否为运算符
isSymbol(value: string) {
if (CheckEmptyUtil.isEmpty(value)) {
return;
}
return (CommonConstants.OPERATORS.indexOf(value) !== -1);
}
// 获取运算符优先级:+ - 为 MEDIUM,× ÷ 为 HIGH
getPriority(value: string): number {
let result = 0;
switch (value) {
case SymbolicEnumeration.ADD:
case SymbolicEnumeration.MIN:
result = Priority.MEDIUM;
break;
case SymbolicEnumeration.MUL:
case SymbolicEnumeration.DIV:
result = Priority.HIGH;
break;
default:
result = Priority.LOW;
break;
}
return result;
}
(2)中缀转后缀(调度场算法)
表达式在页面层以数组形式存储(数字与运算符交替,如 ['12', '+', '3']),parseExpression 借助运算符栈将其转换为后缀队列:
parseExpression(expressions: Array<string>): string {
let outputStack: string[] = []; // 运算符栈
let outputQueue: string[] = []; // 后缀队列
expressions.forEach((item: string, index: number) => {
// 处理百分号:将 "50%" 替换为 50÷100 的结果
if (item.indexOf(CommonConstants.PERCENT_SIGN) !== -1) {
expressions[index] = (this.mulOrDiv(item.slice(0, item.length - 1),
CommonConstants.ONE_HUNDRED, CommonConstants.DIV)).toString();
}
// 表达式末尾若是运算符则弹出(如 "12+3+=" 中的末尾 +)
if ((index === len - 1) && this.isSymbol(item)) {
expressions.pop();
}
});
while (expressions.length > 0) {
let current = expressions.shift();
if (this.isSymbol(current)) {
// 栈顶优先级 >= 当前运算符时,弹栈进入后缀队列
while (outputStack.length > 0 &&
this.comparePriority(current, outputStack[outputStack.length - 1])) {
outputQueue.push(outputStack.pop());
}
outputStack.push(current);
} else {
outputQueue.push(current); // 数字直接入队
}
}
while (outputStack.length > 0) {
outputQueue.push(outputStack.pop());
}
return this.dealQueue(outputQueue);
}
(3)后缀表达式求值
dealQueue(queue: Array<string>): string {
let outputStack: string[] = [];
while (queue.length > 0) {
let current = queue.shift();
if (!this.isSymbol(current)) {
outputStack.push(current); // 数字压栈
} else {
let second = outputStack.pop(); // 弹出右操作数
let first = outputStack.pop(); // 弹出左操作数
outputStack.push(this.calResult(first, second, current));
}
}
return outputStack[0];
}
// 二元运算分发
calResult(arg1: string, arg2: string, symbol: string): string {
switch (symbol) {
case SymbolicEnumeration.ADD: result = this.add(arg1, arg2, CommonConstants.ADD); break;
case SymbolicEnumeration.MIN: result = this.add(arg1, arg2, CommonConstants.MIN); break;
case SymbolicEnumeration.MUL: result = this.mulOrDiv(arg1, arg2, CommonConstants.MUL); break;
case SymbolicEnumeration.DIV: result = this.mulOrDiv(arg1, arg2, CommonConstants.DIV); break;
}
return this.numberToScientificNotation(result);
}
2.按键与页面设计
(1)按键数据模型(PressKeysItem.ets):每个按键包含 flag(渲染类型)、宽高、文本值和可选图标资源:
export class PressKeyItem {
flag: number; // 0=图片键 1=数字黑字 2=符号蓝字 3=等号白字大按钮
width: string;
height: string;
value: string;
source?: Resource;
constructor(flag, width, height, value, source?) { ... }
}
(2)按键布局数据(PresskeysViewModel.ets):外层数组是键盘列,内层数组是每列从上到下的按键。渲染时按列遍历,因此修改按键只需增删数据,无需改动页面:
getPressKeys(): Array<Array<PressKeyItem>> {
return [
[ // 第 0 列:C、7、4、1、%
new PressKeyItem(2, '28vp', '37vp', 'C', $r('app.media.clean')),
new PressKeyItem(1, '26vp', '35vp', '7'),
...
],
[ // 第 1 列:÷、8、5、2、0
new PressKeyItem(2, '42vp', '56vp', '÷', $r('app.media.ic_div')),
...
],
...
[ // 第 3 列:del、-、+、=(最后一行大按钮)
new PressKeyItem(0, '30.48vp', '20vp', 'del', $r('app.media.ic_delete')),
...
new PressKeyItem(3, '28vp', '54vp', '=', $r('app.media.ic_equ'))
]
];
}
(3)页面渲染(HomePage.ets):通过双重 ForEach 按"列 → 键"渲染,依据 flag 决定图片/文字样式;最后一列的最后一个按键(=)特殊处理为 148 高、40 圆角、蓝色背景的大按钮,与整体键盘视觉统一:
ForEach(keysModel.getPressKeys(), (columnItem, columnItemIndex?) => {
Column() {
ForEach(columnItem, (keyItem, keyItemIndex?) => {
Column() {
if (keyItem.flag === 0) {
Image(keyItem.source) // 图片键
} else if (keyItem.flag === 3) {
Text(keyItem.value).fontSize(36).fontColor(Color.White) // = 键白字
} else {
Text(keyItem.value) // 数字黑字 / 符号蓝字
}
}
.width(64)
.height((columnItemIndex === 3 && keyItemIndex === 3) ? 148 : 64)
.borderRadius(... 40 : 32)
.backgroundColor(... equals_back_color : 白色)
...
})
}
})
3.调用逻辑
页面层通过 onClick 依据 flag 分发到"输入符号"或"输入数字",再统一交给计算工具类求值,形成完整的调用链:
// 1. 按键点击分发
.onClick(() => {
if (keyItem.flag === 0 || keyItem.flag === 2 || keyItem.flag === 3) {
this.inputSymbol(keyItem.value); // 符号(C/del/=/运算符)
} else {
this.inputNumber(keyItem.value); // 数字
}
})
// 2. 等号结算
case Symbol.EQU:
this.getResult().then((result: boolean) => {
if (!result) return;
this.inputValue = this.calValue; // 结果上屏
this.expressions = [];
this.expressions.push(this.inputValue); // 结果可继续参与运算
});
break;
// 3. 调用工具类求值
async getResult() {
let calResult = CalculateUtil.parseExpression(this.deepCopy());
if (calResult === 'NaN') {
this.calValue = this.resourceToString($r('app.string.error'));
return false;
}
this.calValue = calResult;
return true;
}
3.科学计算器的完成
1.计算逻辑函数的定义
科学计算器改用中缀表达式字符串 + 双栈直接求值算法,操作数栈与运算符栈协同工作,支持括号、一元负号、函数与右结合运算符。
(1)分词器 tokenize:把 "sin(30)+2^3" 拆成 数字/运算符/函数/括号 记号序列,并区分一元负号(neg)与二元减号:
tokenize(expression: string): Array<Token> {
let tokens: Array<Token> = [];
while (i < expression.length) {
let ch = expression[i];
if (this.isDigit(ch) || ch === '.') { // 数字(含小数)
...
tokens.push(new Token(0, num));
}
if (ch === 'π') { tokens.push(new Token(0, Math.PI.toString())); }
if (ch === 'e') {
// 若是科学计数法结果(如 1.23e+18),合并进前一个数字;否则为自然常数
...
}
if (this.isAlpha(ch)) { // 函数名 sin/cos/...
let name = '';
while (i < expression.length && this.isAlpha(expression[i])) { name += expression[i]; i++; }
if (this.isFunc(name)) { tokens.push(new Token(2, name)); }
}
if (ch === '√') { tokens.push(new Token(2, 'sqrt')); }
...
if (ch === '-') {
// 前一个是数字/右括号/%/! 时为二元减,否则为一元负号
let isBinary = prev !== undefined &&
(prev.type === 0 || prev.type === 4 || prev.value === '%' || prev.value === '!');
tokens.push(new Token(1, isBinary ? '-' : 'neg'));
}
}
return tokens;
}
(2)双栈求值 evaluate:核心循环按记号类型处理——数字压操作数栈、函数/左括号压运算符栈、右括号弹栈至左括号、运算符按优先级与结合性弹栈计算:
evaluate(expression: string): string {
let completeExpr = this.completeBrackets(expression); // 自动补全右括号
let tokens = this.tokenize(completeExpr);
let valStack: Array<number> = []; // 操作数栈
let opStack: Array<Token> = []; // 运算符栈
for (let token of tokens) {
if (token.type === 0) {
valStack.push(Number(token.value)); // 数字直接入栈
} else if (token.type === 3) {
opStack.push(token); // 左括号入栈
} else if (token.type === 4) { // 右括号:弹栈至左括号
while (opStack.length > 0 && opStack[opStack.length - 1].type !== 3) {
this.applyTop(valStack, opStack);
}
opStack.pop(); // 弹出左括号
if (opStack.length > 0 && opStack[opStack.length - 1].type === 2) {
this.applyTop(valStack, opStack); // 应用函数(sin/√/...)
}
} else {
if (token.value === '%' || token.value === '!') { // 后缀运算符立即计算
let v = valStack.pop();
valStack.push(token.value === '%' ? v / 100 : this.factorial(v));
continue;
}
// 栈顶优先级更高(或同级且当前非右结合)时先弹栈计算
while (opStack.length > 0 && opStack[opStack.length - 1].type === 1 &&
(priority(栈顶) > priority(当前) ||
(priority(栈顶) === priority(当前) && !this.rightAssoc(token.value)))) {
this.applyTop(valStack, opStack);
}
opStack.push(token);
}
}
while (opStack.length > 0) { this.applyTop(valStack, opStack); }
return this.formatResult(valStack[0]);
}
(3)函数库与结果格式化:calcFunc 实现三角函数(角度制)、对数、开方;factorial 实现阶乘;formatResult 处理去尾零与科学计数法:
calcFunc(name: string, arg: number): number {
switch (name) {
case 'sin': return Math.sin(arg * this.ANGLE_CONVERT); // 角度制
case 'cos': return Math.cos(arg * this.ANGLE_CONVERT);
case 'tan': return Math.tan(arg * this.ANGLE_CONVERT);
case 'ln': return Math.log(arg);
case 'log': return Math.log10(arg);
case 'sqrt': return Math.sqrt(arg);
default: return Number.NaN;
}
}
formatResult(value: number): string {
if (Number.isNaN(value) || !Number.isFinite(value)) return 'NaN';
if (Math.abs(value) >= 1e16 || Math.abs(value) < 1e-9) {
result = value.toExponential(10); // 极大/极小值转科学计数法
} else {
result = value.toPrecision(12); // 常规值保留 12 位有效数字
}
// 去除尾部的 0(如 123.456000 -> 123.456)
...
}
2.按键与页面设计
科学计算器采用 5 列 × 7 行布局,按键模型在标准版基础上增加 fontSize 字段,以适配"sin""x²"等多字符小字号功能键:
// ScientificKeysItem.ets
export class ScientificKeyItem {
flag: number; // 0=图片键 1=数字黑字 2=功能蓝字 3=等号大按钮
width: number;
height: number;
value: string;
fontSize: number; // 新增:功能键用小字号(20),数字/运算符用大字号(24)
source?: Resource;
}
键盘布局(ScientificKeysViewModel.ets),与标准计算器统一——白色圆角按键、蓝色符号、右下角蓝色大等号:
getSciKeys(): Array<Array<ScientificKeyItem>> {
return [
[ // 列0:C、7、4、1、0、sin、ln
new ScientificKeyItem(2, 52, 48, 'C', 24),
new ScientificKeyItem(1, 52, 48, '7', 24),
...
new ScientificKeyItem(2, 52, 48, 'sin', 20),
new ScientificKeyItem(2, 52, 48, 'ln', 20)
],
[ // 列1:del、8、5、2、.、cos、log
new ScientificKeyItem(0, 52, 48, 'del', 0, $r('app.media.ic_delete')),
...
],
... // 列2(÷、9、6、3、%、tan、1/x)、列3(×、(、)、x²、x³、√、xⁿ)
[ // 列4:-、+、π、e、±、n!、=(大按钮)
new ScientificKeyItem(2, 52, 48, '-', 24),
new ScientificKeyItem(2, 52, 48, '+', 24),
new ScientificKeyItem(1, 52, 48, 'π', 24),
new ScientificKeyItem(1, 52, 48, 'e', 24),
new ScientificKeyItem(2, 52, 48, '±', 24),
new ScientificKeyItem(2, 52, 48, 'n!', 20),
new ScientificKeyItem(3, 52, 108, '=', 36)
]
];
}
ScientificPage.ets 的渲染结构、颜色、圆角、阴影参数均与 HomePage 保持一致
3.调用逻辑
科学页所有按键点击统一进入 onKey 分发,再调用 appendInput(组装表达式)、calculate(等号结算)、tryPreview(实时预览):
onKey(keyItem: ScientificKeyItem) {
switch (keyItem.value) {
case 'C': this.expression = ''; this.result = ''; break; // 清空
case 'del': this.deleteLast(); this.tryPreview(); break; // 退格
case '=': this.calculate(); break; // 结算
default: this.appendInput(keyItem.value); this.tryPreview(); break;
}
}
// 追加输入:函数作用于当前数值段(先输 30 再点 sin -> "sin(30")
case 'sin': case 'cos': case 'tan': case 'ln': case 'log': case '√': case '1/x':
expr = this.appendFunc(expr, value);
break;
case 'x²': // 负数自动加括号:2±x² -> (-2)^2
expr = this.appendPow(expr, '^2');
break;
case '±': // 对当前数字段取反
expr = this.toggleSign(expr);
break;
// 等号结算:表达式 -> 结果,结果作为新表达式可继续运算
calculate() {
let calResult: string = ScientificUtil.evaluate(this.expression);
if (calResult === 'NaN') { this.result = 'error'; return; }
this.expression = calResult;
this.result = '';
}
// 实时预览:输入过程中即时求值显示
tryPreview() {
let calResult: string = ScientificUtil.evaluate(this.expression);
if (calResult === 'NaN') { this.result = ''; return; } // 中间态不打扰
this.result = calResult;
}
4.页面注册与跳转
HarmonyOS 新增页面必须注册到 main_pages.json,否则运行时找不到页面:
{
"src": [
"pages/HomePage",
"pages/ScientificPage"
]
}
跳转入口加在标准页标题栏右侧(HomePage.ets):
import { router } from '@kit.ArkUI';
Text($r('app.string.scientific_text'))
.fontColor('rgba(10, 89, 247, 1)')
.onClick(() => {
router.pushUrl({ url: 'pages/ScientificPage' }); // 进入科学计算器
});
科学页右上角"普通"按钮调用 router.back() 返回标准计算器。
二、问题解决与收获
1.问题与解决
1.电脑缺少Hyper-v管理器,无法启动模拟器。要解决这个问题,需要打开BIOS,启用硬件虚拟化,再在“启用或关闭Windows功能”这里勾选“虚拟机平台”和“Windows虚拟机监控程序平台”。重启电脑即可。
2.科学计算器页面的归零、除号、乘号、删除图标比例过大。后面发现其中的归零、除号和乘号图标应该是直接用蓝色文字表示的。后续将归零、除号、乘号改回蓝色文字与普通页一致;del 保留图片但改用 28×28 + objectFit(Contain) 等比显示,不再变形。
2.收获与体会
1.对于鸿蒙系统软件的开发有了一定的了解。通过本次实验了解了DevEco Studio的使用方法,包括启动模拟器、创建项目、工程结构分布等。
2.对于计算器设计需要运用到的算法也有了进一步了解,包括标准计算器的"中缀转后缀 + 后缀求值"与科学计算器的"双栈直接求值"。这两个算法都是利用栈的 L 特性处理运算符优先级。
更多推荐



所有评论(0)