简介:这是一份基于鸿蒙OS(HarmonyOS)开发的记事本应用完整源码项目,面向计算机类专业学生、初学者及嵌入式/移动开发入门者,适用于毕业设计、课程设计、项目演示或二次开发参考。资源包含127个文件,主体为37个Java业务逻辑代码、46个XML布局与配置文件、26个PNG图标资源,辅以Gradle构建脚本、JSON数据模板及基础工具脚本(如gradlew.bat),总大小11.63MB,结构清晰,核心功能模块位于entry/src/main/java/com/example/todolistapplication下,含Item数据类等关键组件。已有2979人学习下载,项目经实机测试可稳定运行,配套说明涵盖IDE环境搭建(DevEco Studio)、关键目录定位与扩展修改路径,便于快速上手与功能迭代。读者可直接部署运行,深入理解HarmonyOS应用生命周期、AbilitySlice页面跳转、本地数据存储等核心开发范式,并基于现有结构拓展任务管理、云同步等进阶功能。
1. 鸿蒙记事本不是“移植Windows记事本”,而是用ArkTS重构的轻量级本地文本编辑器
很多同学拿到“基于鸿蒙系统开发记事本设计与实现”这个毕设题目时,第一反应是:不就是把Windows记事本抄一遍?结果在DevEco Studio里新建项目、跑通第一个页面就卡住——UI组件不识别、文件权限报错、保存路径找不到。真相是:鸿蒙的记事本不是图形界面复刻,而是围绕分布式能力、沙箱化存储、声明式UI(ArkTS)和轻量级文件操作重新设计的终端级应用。它解决的不是“能打字”,而是“在HarmonyOS设备上安全、可靠、可扩展地管理用户私有文本数据”。适合计算机/软件工程专业大四学生,要求掌握ArkTS基础语法、Ability生命周期、ohos.file.fs API调用及HAP包构建流程;不适合零基础直接套模板,但也不需要深入内核或驱动层。项目价值不在功能多炫,而在完整走通从UI声明→状态管理→文件读写→权限申请→打包签名的鸿蒙原生开发闭环。
2. 用ArkTS+Stage模型搭建记事本最小可运行结构
鸿蒙记事本必须基于Stage模型开发,而非旧版FA模型。Stage模型强制分离UI、逻辑与数据,天然适配鸿蒙的Ability分层机制,且DevEco Studio 4.1+对Stage支持更稳定。选择ArkTS而非JavaScript,是因为其类型安全能提前捕获file.writeText()参数类型错误,避免运行时报undefined is not a function这类调试黑洞。
2.1 创建Stage模型项目并配置module.json5权限
新建项目时,在DevEco Studio中选择“Empty Ability”模板,务必勾选“Stage Model”,语言选ArkTS。项目生成后,首件事是修改module.json5——这是鸿蒙应用的“身份证”,决定能否访问文件系统:
{ "module": { "reqPermissions": [ { "name": "ohos.permission.READ_USER_STORAGE", "reason": "读取用户文档用于打开已有笔记" }, { "name": "ohos.permission.WRITE_USER_STORAGE", "reason": "保存编辑内容到用户文档目录" } ], "abilities": [ { "name": "MainAbility", "srcEntry": "./ets/entryability/EntryAbility.ts", "exported": true, "skills": [ { "entities": ["entity.system.home"], "actions": ["action.system.home"] } ] } ] } }提示:
READ_USER_STORAGE和WRITE_USER_STORAGE是访问用户文档目录(如/data/storage/el2/000/下的Documents/)的必要权限。仅声明不够,还需在代码中动态申请——鸿蒙要求敏感权限必须运行时弹窗授权,否则file.openSync()会直接抛出SecurityException。
2.2 声明式UI:用TextInput+Button构建核心编辑区
鸿蒙UI不写XML,全部用ArkTS声明。记事本主界面只需一个可滚动文本输入框和两个按钮(保存/新建),避免过度设计:
// pages/Index.ets @Entry @Component struct Index { @State textContent: string = ''; @State fileName: string = 'untitled.txt'; build() { Column({ space: 8 }) { // 标题栏 Text('鸿蒙记事本') .fontSize(20) .fontWeight(FontWeight.Bold) .width('100%') .textAlign(TextAlign.Center) .backgroundColor(Color.LightGray) // 编辑区 - 使用Scrollable包裹TextInput实现滚动 Scrollable({ scrollDirection: ScrollDirection.Vertical }) { TextInput({ placeholder: '请输入内容...', text: this.textContent, textAlign: TextAlign.Start }) .onChange((value: string) => { this.textContent = value; }) .height('70%') .backgroundColor(Color.White) } // 操作按钮组 Row({ space: 12 }) { Button('保存', { type: ButtonType.Normal }) .onClick(() => { this.saveFile(); }) .width('45%') .height(50) Button('新建', { type: ButtonType.Normal }) .onClick(() => { this.clearContent(); }) .width('45%') .height(50) } } .padding(16) .backgroundColor(Color.Gray) } // 清空内容并重置文件名 clearContent() { this.textContent = ''; this.fileName = 'untitled.txt'; } // 保存逻辑将在2.3节展开 saveFile() { // 留空,后续实现 } }2.2.1 关键细节说明
TextInput的onChange回调必须绑定this.textContent,ArkTS的响应式更新依赖@State装饰器,直接赋值textContent = 'xxx'无效;Scrollable容器是必需的——鸿蒙TextInput默认不支持内部滚动,超出高度会截断文字;Button使用ButtonType.Normal避免默认圆角和阴影,符合记事本简洁定位;width用百分比而非固定像素,适配不同屏幕密度;backgroundColor层级要清晰:Column设背景灰,TextInput设白底,形成视觉层次,避免纯白屏刺眼。
2.3 文件读写:用ohos.file.fs API操作用户文档目录
鸿蒙应用无法直接访问绝对路径,必须通过ohos.file.fs模块操作沙箱内路径。关键路径是getExternalStorageDir()返回的用户文档目录,这是唯一允许应用自由读写的公共区域。
// utils/FileUtils.ets import fs from '@ohos.file.fs'; // 获取用户文档目录路径 function getDocumentsPath(): string { const context = getContext(this) as common.UIAbilityContext; return context.resourceManager.getApplicationInfo().codePath + '/Documents/'; } // 保存文本到指定文件 export async function saveTextToFile(content: string, fileName: string): Promise<boolean> { try { // 1. 获取用户文档目录 const docPath = await fs.getRealPath('/data/storage/el2/000/Documents/'); // 2. 构建完整文件路径 const filePath = `${docPath}/${fileName}`; // 3. 打开文件(不存在则创建) const file = await fs.open(filePath, fs.OpenMode.CREATE | fs.OpenMode.READ_WRITE); // 4. 写入UTF-8编码文本 await fs.writeText(file.fd, content, { encoding: 'UTF-8' }); // 5. 关闭文件句柄 await fs.close(file.fd); console.info(`文件保存成功: ${filePath}`); return true; } catch (err) { console.error('保存失败:', err); return false; } } // 读取文本文件内容 export async function readTextFromFile(fileName: string): Promise<string> { try { const docPath = await fs.getRealPath('/data/storage/el2/000/Documents/'); const filePath = `${docPath}/${fileName}`; // 检查文件是否存在 const stat = await fs.stat(filePath); if (!stat.isDirectory && stat.size > 0) { const file = await fs.open(filePath, fs.OpenMode.READ_ONLY); const content = await fs.readText(file.fd); await fs.close(file.fd); return content; } return ''; } catch (err) { console.error('读取失败:', err); return ''; } }2.3.1 权限申请与错误处理要点
fs.getRealPath()必须传入/data/storage/el2/000/Documents/,这是鸿蒙为每个应用分配的独立用户文档路径,硬编码不可改;fs.OpenMode.CREATE | fs.OpenMode.READ_WRITE组合确保文件不存在时自动创建,存在时覆盖写入;encoding: 'UTF-8'显式指定编码,避免中文保存为乱码(鸿蒙默认编码非UTF-8);fs.stat()检查文件存在性再读取,防止fs.open()因文件不存在而抛异常;- 所有
fsAPI均为异步,必须用await,否则file.fd为undefined导致fs.writeText崩溃。
3. 实现文件列表浏览与多文档切换功能
纯文本编辑器若只能单文件操作,实用性大打折扣。鸿蒙记事本需支持查看Documents/目录下所有.txt文件,并点击切换编辑——这要求理解鸿蒙的文件系统遍历和页面状态同步机制。
3.1 动态加载Documents目录下的.txt文件列表
在Index.ets中增加文件列表状态,并在aboutToAppear生命周期中加载:
// pages/Index.ets(续) @Entry @Component struct Index { @State textContent: string = ''; @State fileName: string = 'untitled.txt'; @State fileList: string[] = []; // 存储.txt文件名数组 @State currentFileIndex: number = -1; // 当前选中文件索引 aboutToAppear() { this.loadFileList(); } // 加载Documents目录下所有.txt文件 async loadFileList() { try { const docPath = await fs.getRealPath('/data/storage/el2/000/Documents/'); const dir = await fs.opendir(docPath); let files: string[] = []; // 遍历目录条目 for await (const entry of dir) { if (entry.name.endsWith('.txt')) { files.push(entry.name); } } await fs.closedir(dir); this.fileList = files; } catch (err) { console.error('加载文件列表失败:', err); this.fileList = []; } } // 切换到指定文件 async switchToFile(index: number) { if (index < 0 || index >= this.fileList.length) return; const fileName = this.fileList[index]; this.fileName = fileName; this.currentFileIndex = index; // 异步读取内容并更新UI const content = await readTextFromFile(fileName); this.textContent = content; } }3.1.1 关键参数与性能优化点
fs.opendir()返回异步迭代器,for await语法是鸿蒙推荐的目录遍历方式,比fs.readdir()更内存友好;entry.name.endsWith('.txt')过滤非文本文件,避免误加载.log或隐藏文件;fs.closedir(dir)必须显式调用,否则文件句柄泄漏,多次刷新后触发EMFILE错误;aboutToAppear是页面即将显示时的钩子,比onPageShow更早执行,确保列表在UI渲染前已加载。
3.2 构建可滚动文件列表侧边栏
将文件列表以垂直列表形式嵌入主界面右侧,使用Flex布局实现响应式分栏:
// pages/Index.ets(续build方法) build() { Flex({ direction: FlexDirection.Row, justifyContent: FlexAlign.SpaceBetween }) { // 主编辑区(占70%宽度) Column({ space: 8 }) { // ...原有标题、TextInput、按钮代码... } .width('70%') // 文件列表侧边栏(占30%宽度) Column({ space: 4 }) { Text('文件列表') .fontSize(16) .fontWeight(FontWeight.Medium) .width('100%') .textAlign(TextAlign.Center) .backgroundColor(Color.LightGray) List({ space: 2 }) { ForEach(this.fileList, (fileName: string, index: number) => { ListItem() { Row({ space: 8 }) { Text(`${index + 1}. ${fileName}`) .fontSize(14) .flexGrow(1) Button('打开', { type: ButtonType.Text }) .onClick(() => { this.switchToFile(index); }) .width(80) .height(30) } .backgroundColor(index === this.currentFileIndex ? Color.Blue : Color.White) .borderRadius(4) .padding(8) } }, (fileName: string) => fileName) } .listHeight('100%') .width('100%') } .width('30%') .backgroundColor(Color.Gray) } .padding(16) .backgroundColor(Color.Gray) }3.2.1 UI交互细节说明
FlexDirection.Row实现左右分栏,width用百分比保证在手机/平板不同尺寸下自适应;List组件比Column更高效处理长列表,ForEach绑定fileList数组,keyGenerator用文件名避免重复key警告;ListItem内Row布局让文件名左对齐、按钮右对齐,backgroundColor高亮当前选中项;listHeight('100%')确保列表填满侧边栏高度,避免内容溢出。
4. 处理鸿蒙特有文件编码与权限问题
鸿蒙设备上打开记事本文件出现乱码,或保存后文件在PC端无法正常读取,90%源于编码不一致和路径权限配置错误。这不是Bug,而是鸿蒙沙箱机制的必然约束。
4.1 统一使用UTF-8 BOM编码避免中文乱码
鸿蒙fs.writeText()默认不添加BOM(Byte Order Mark),而Windows记事本依赖BOM识别UTF-8。解决方案是在写入前手动添加BOM头:
// utils/FileUtils.ets(修改saveTextToFile) export async function saveTextToFile(content: string, fileName: string): Promise<boolean> { try { const docPath = await fs.getRealPath('/data/storage/el2/000/Documents/'); const filePath = `${docPath}/${fileName}`; // 添加UTF-8 BOM头(EF BB BF) const bomContent = '\uFEFF' + content; const file = await fs.open(filePath, fs.OpenMode.CREATE | fs.OpenMode.READ_WRITE); await fs.writeText(file.fd, bomContent, { encoding: 'UTF-8' }); await fs.close(file.fd); return true; } catch (err) { console.error('保存失败:', err); return false; } }注意:BOM头
\uFEFF必须加在content最前面,且encoding仍设为'UTF-8'。若设为'UTF-8-BOM',鸿蒙API不识别该编码名,会回退到默认编码导致乱码。
4.2 解决“Permission denied”错误的三步检查法
当fs.open()抛出Permission denied,按顺序排查:
| 检查项 | 操作命令/位置 | 正确状态 |
|---|---|---|
| 1. module.json5权限声明 | 查看reqPermissions数组 | 必须包含READ_USER_STORAGE和WRITE_USER_STORAGE |
| 2. 运行时动态申请 | 在Ability启动后调用requestPermissionsFromUser() | 用户需在弹窗中点击“允许” |
| 3. 路径是否沙箱内 | console.log(await fs.getRealPath('/data/storage/el2/000/Documents/')) | 输出路径必须以/data/storage/el2/000/开头 |
动态申请权限的代码示例:
// 在EntryAbility.ts的onCreate中 import abilityAccessCtrl from '@ohos.abilityAccessCtrl'; async onCreate(want: Want) { super.onCreate(want); // 请求存储权限 const atManager = abilityAccessCtrl.createAtManager(); const permissions = [ 'ohos.permission.READ_USER_STORAGE', 'ohos.permission.WRITE_USER_STORAGE' ]; try { const result = await atManager.requestPermissionsFromUser(this.context, permissions); if (result.authResults.every(r => r === 0)) { console.info('权限申请成功'); } else { console.warn('权限申请被拒绝'); } } catch (err) { console.error('权限申请异常:', err); } }4.3 验证文件是否被正确保存到用户目录
最可靠的验证方式不是看日志,而是用hdc工具连接真机或模拟器,直接查看文件系统:
# 连接设备 hdc shell # 进入应用沙箱文档目录 cd /data/storage/el2/000/Documents/ # 列出文件并查看大小 ls -la # 输出示例:-rw------- 1 system system 128 Jan 1 00:00 mynote.txt # 查看文件内容(确认中文正常) cat mynote.txt # 输出应为:你好,鸿蒙!提示:
hdc shell是鸿蒙官方调试工具,比ADB更适配。若cat显示乱码,说明BOM未生效或编码参数错误;若ls无文件,说明路径写错或权限未授予。
5. 打包HAP包并部署到真机的完整流程
毕设答辩时演示必须在真机运行,不能只在模拟器。鸿蒙HAP包(HarmonyOS Ability Package)的签名和安装流程与Android APK不同,需严格遵循官方规范。
5.1 配置签名证书与Profile文件
DevEco Studio默认使用调试证书,但真机安装必须用发布证书。步骤如下:
生成密钥库:
Build > Generate Signed Hap... > Create new keystore- 密钥库密码:
HarmonyOS2024!(建议复杂密码) - 别名:
notebook-release - 别名密码:同密钥库密码
- 密钥库密码:
申请发布Profile:
- 访问 AppGallery Connect → “我的项目” → “应用服务” → “配置文件”
- 创建新Profile,包名填
com.example.notebook(与module.json5中bundleName一致) - 上传刚生成的
.p12证书文件 - 下载生成的
.hapProfile文件
关联Profile到项目:
Project Structure > Signing Configs→ 勾选“Enable signing”Profile path指向下载的.p7b文件Keystore path指向生成的.p12文件
5.2 构建Release HAP包并安装
配置完成后,执行构建:
# 在项目根目录执行 ./gradlew buildRelease生成的HAP包路径:build/outputs/default/entry-default-unsigned.hap
签名后路径:build/outputs/default/entry-default-signed.hap
安装到真机:
# 连接真机(开启USB调试) hdc install build/outputs/default/entry-default-signed.hap # 输出:install hap success # 启动应用 hdc shell aa start -a EntryAbility -b com.example.notebook5.2.1 常见安装失败原因与修复
| 错误信息 | 原因 | 修复方案 |
|---|---|---|
Failed to install hap: ERROR_INSTALL_FAILED_INVALID_APK | Profile未关联或包名不匹配 | 检查module.json5的bundleName与Profile申请时填写的包名完全一致 |
ERROR_INSTALL_FAILED_CONFLICTING_PROVIDER | 真机已安装同包名调试版 | 先hdc shell bm uninstall com.example.notebook卸载旧版 |
ERROR_INSTALL_FAILED_SIGNATURE_ERROR | 签名证书与Profile不匹配 | 重新生成密钥库,确保Profile上传的是同一.p12文件 |
5.3 毕设答辩演示技巧:3分钟展示核心亮点
答辩时评委关注“是否真懂鸿蒙特性”,而非功能多全。聚焦三个可验证点:
- 展示分布式能力:在手机上编辑文件,用
startAbility()拉起平板上的同应用,验证ohos.distributedschedule跨设备同步(需在module.json5中配置distributedCapabilities); - 演示权限控制:进入设置→应用管理→鸿蒙记事本→权限,关闭“文件和媒体”权限,再点击保存按钮,观察是否弹出“权限被拒绝”Toast提示;
- 验证沙箱隔离:用
hdc shell进入/data/storage/el2/000/Documents/,创建一个test.txt,然后在应用内打开——证明应用只能访问自己沙箱内的文件,无法越界读取其他应用数据。
提示:答辩PPT中放一张
hdc shell ls -la截图,比10页架构图更有说服力。
本文还有配套的精品资源,点击获取