鸿蒙平台 Notepadqq 文本编辑器适配实战:基于 Electron 壳方案的轻量级多标签编辑器开发
Notepadqq 是 Linux 平台上经典的文本编辑器(类 Notepad++),以简洁高效著称。本文记录在鸿蒙 PC 平台上,基于 Electron 壳方案从零构建一个轻量级多标签文本编辑器的完整过程——涵盖 CodeMirror 语法高亮引擎集成、多标签状态管理、查找替换(支持正则表达式)、自定义菜单栏、鸿蒙稳定性适配等核心功能的实现细节与踩坑经验。
欢迎加入开源鸿蒙 PC 社区:https://harmonypc.csdn.net/
欢迎在 PC 社区平台申请新建项目:https://atomgit.com/OpenHarmonyPCDeveloper
AtomGit 仓库地址:https://atomgit.com/OpenHarmonyPCDeveloper/ohos_notepadqq
一、技术架构分析
1.1 整体架构
本项目采用 Electron Web 层 + 鸿蒙 HAP 壳工程 的分层架构:
- Electron Web 层:运行于 ArkWeb 引擎,提供完整的文本编辑能力(语法高亮、多标签管理、查找替换、文件读写)
- 鸿蒙 HAP 壳工程:通过 web_engine 模块加载 Web 应用,提供窗口管理、文件系统访问、屏幕自适应等系统能力
这种架构的核心优势在于:业务代码与平台完全解耦,Electron 部分可以在任何平台独立运行,鸿蒙壳仅负责适配和打包。
1.2 技术方案
| 对比项 | Notepad++ 原版 | 鸿蒙适配版 |
|---|---|---|
| 编辑器内核 | Scintilla | CodeMirror 5(轻量级替代) |
| 开发语言 | C++ | HTML/CSS/JavaScript |
| 语法高亮 | 自定义词法分析器 | CodeMirror 内置语言模式 |
| 文件操作 | Win32 API | Node.js fs 模块 + IPC |
| 部署方式 | exe 安装包 | HAP 包(鸿蒙应用) |
| 运行环境 | Windows | 鸿蒙 PC(ArkWeb 引擎) |
1.3 核心功能清单
| 功能模块 | 具体能力 |
|---|---|
| 多标签编辑 | 新建/关闭/切换标签、未保存标记、独立光标与滚动位置 |
| 语法高亮 | 10+ 种语言(HTML/JS/Python/C/Java/CSS/JSON/XML/Markdown 等) |
| 查找替换 | 正则表达式支持、区分大小写、全部替换、实时匹配计数 |
| 文件操作 | 打开/保存/另存为、文件类型过滤、重复打开检测 |
| 视图控制 | 自动换行切换、行号显示/隐藏、字体缩放(50%-200%) |
| 菜单栏 | 文件/编辑/搜索/视图四级菜单、快捷键显示 |
| 状态栏 | 光标位置、总行数、编码格式、当前语言模式 |
二、环境准备
2.1 开发环境要求
| 项目 | 版本/信息 |
|---|---|
| 操作系统 | Windows 10/11 |
| 核心框架 | Electron (Node.js + Chromium) |
| 技术栈 | HTML5/CSS3/Vanilla JavaScript |
| 编辑器 | CodeMirror 5.65.16 |
| 目标设备 | 鸿蒙 PC |
| 目标架构 | arm64-v8a |
| 开发工具 | DevEco Studio(鸿蒙官方 IDE) |
2.2 项目结构
ohos_hap/
├── electron-apps/
│ └── Notepadqq/ # Notepadqq 应用源码(开发目录)
│ ├── main.js # Electron 主进程(窗口管理 + 文件 IPC)
│ ├── renderer.js # 渲染进程(编辑器逻辑 + 查找替换)
│ ├── index.html # HTML 页面结构
│ ├── package.json # 项目配置
│ └── styles/
│ └── notepadqq.css # 深色主题样式
├── web_engine/ # 鸿蒙 web_engine 模块
│ └── src/main/resources/
│ └── resfile/resources/app/ # 部署目录(构建时打包进 HAP)
│ ├── main.js
│ ├── renderer.js
│ ├── index.html
│ ├── package.json
│ └── styles/notepadqq.css
└── electron/ # Electron 原生库
└── libs/arm64-v8a/
├── libelectron.so # Electron 核心库
├── libadapter.so # 适配层库
└── libffmpeg.so # 多媒体库
开发流程:在 electron-apps/Notepadqq/ 中开发,每次修改后同步到 web_engine/src/main/resources/resfile/resources/app/ 部署目录。
三、核心适配流程
3.1 第一步:创建 Electron 主进程
文件:electron-apps/Notepadqq/main.js
主进程负责窗口创建和文件系统 IPC 通信。与 VSCodium 相比,Notepadqq 增加了鸿蒙三防稳定性配置。
// Notepadqq - 主进程
const { app, BrowserWindow, ipcMain, dialog, screen } = require('electron');
const fs = require('fs');
const path = require('path');
// 防 GPU 白屏:禁用硬件加速,降级为软件渲染
app.disableHardwareAcceleration();
let mainWindow = null;
// 创建窗口
function createWindow() {
try {
const display = screen.getPrimaryDisplay();
const { width, height } = display.workAreaSize;
// 防 XComponent 崩溃:frame: true + transparent: false + resizable: true
mainWindow = new BrowserWindow({
width: Math.floor(width * 0.9),
height: Math.floor(height * 0.85),
frame: true,
transparent: false,
resizable: true,
webPreferences: {
nodeIntegration: true,
contextIsolation: false
}
});
mainWindow.loadFile('index.html');
} catch (e) {
console.warn('[Notepadqq] 创建窗口失败:', e.message);
}
}
app.whenReady().then(createWindow);
app.on('window-all-closed', () => {
app.quit();
});
鸿蒙三防策略说明:
| 防护目标 | 配置项 | 作用 |
|---|---|---|
| 防 XComponent 崩溃 | frame: true, transparent: false, resizable: true | 使用标准窗口模式,避免触发鸿蒙适配层缺陷 |
| 防 GPU 白屏 | disableHardwareAcceleration() | 禁用硬件加速,规避 GPU 渲染异常 |
| 防单点故障 | try-catch 包裹窗口创建 | 即使窗口创建失败也不会导致进程崩溃 |
3.2 第二步:实现 IPC 通信层
主进程通过 ipcMain.handle 注册 IPC 通道,渲染进程通过 ipcRenderer.invoke 调用。
// ========== 文件对话框 ==========
// 打开文件(支持多种文件类型过滤)
ipcMain.handle('dialog:openFile', async () => {
try {
const result = await dialog.showOpenDialog(mainWindow, {
properties: ['openFile'],
filters: [
{ name: '所有文件', extensions: ['*'] },
{ name: '文本文件', extensions: ['txt', 'log', 'md', 'csv'] },
{ name: '代码文件', extensions: ['js', 'ts', 'py', 'java', 'c', 'cpp', 'h', 'html', 'css', 'json', 'xml'] }
]
});
return result;
} catch (e) {
return { canceled: true, filePaths: [] };
}
});
// 保存文件
ipcMain.handle('dialog:saveFile', async () => {
try {
const result = await dialog.showSaveDialog(mainWindow, {
filters: [
{ name: '所有文件', extensions: ['*'] },
{ name: '文本文件', extensions: ['txt'] }
]
});
return result;
} catch (e) {
return { canceled: true, filePath: '' };
}
});
// ========== 文件读写 ==========
// 读取文件
ipcMain.handle('file:read', async (event, filePath) => {
try {
const content = fs.readFileSync(filePath, 'utf-8');
return { success: true, content };
} catch (err) {
return { success: false, error: err.message };
}
});
// 写入文件
ipcMain.handle('file:write', async (event, filePath, content) => {
try {
fs.writeFileSync(filePath, content, 'utf-8');
return { success: true };
} catch (err) {
return { success: false, error: err.message };
}
});
IPC 通道设计:
| 通道名 | 功能 | 参数 |
|---|---|---|
| dialog:openFile | 打开文件对话框 | 无 |
| dialog:saveFile | 保存文件对话框 | 无 |
| file:read | 读取文件内容 | filePath |
| file:write | 写入文件内容 | filePath, content |
| file:basename | 获取文件名 | filePath |
3.3 第三步:构建页面布局
文件:electron-apps/Notepadqq/index.html
页面采用经典的编辑器布局:菜单栏 → 标签栏 → 查找替换面板 → 编辑器区域 → 状态栏。
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>Notepadqq - 轻量级文本编辑器</title>
<link rel="stylesheet" href="styles/notepadqq.css">
<!-- CodeMirror 核心 -->
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/codemirror@5.65.16/lib/codemirror.min.css">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/codemirror@5.65.16/theme/material-darker.css">
</head>
<body>
<!-- 菜单栏 -->
<div class="menubar">
<div class="menu-item" data-menu="file">
<span class="menu-label">文件(F)</span>
<div class="menu-dropdown" id="menu-file">
<div class="menu-option" data-action="new-file">新建 <span class="shortcut">Ctrl+N</span></div>
<div class="menu-option" data-action="open-file">打开 <span class="shortcut">Ctrl+O</span></div>
<div class="menu-option" data-action="save-file">保存 <span class="shortcut">Ctrl+S</span></div>
<div class="menu-option" data-action="save-as">另存为 <span class="shortcut">Ctrl+Shift+S</span></div>
<div class="menu-separator"></div>
<div class="menu-option" data-action="close-tab">关闭标签 <span class="shortcut">Ctrl+W</span></div>
</div>
</div>
<div class="menu-item" data-menu="edit">
<span class="menu-label">编辑(E)</span>
<div class="menu-dropdown" id="menu-edit">
<div class="menu-option" data-action="undo">撤销 <span class="shortcut">Ctrl+Z</span></div>
<div class="menu-option" data-action="redo">重做 <span class="shortcut">Ctrl+Y</span></div>
<div class="menu-separator"></div>
<div class="menu-option" data-action="cut">剪切 <span class="shortcut">Ctrl+X</span></div>
<div class="menu-option" data-action="copy">复制 <span class="shortcut">Ctrl+C</span></div>
<div class="menu-option" data-action="paste">粘贴 <span class="shortcut">Ctrl+V</span></div>
<div class="menu-separator"></div>
<div class="menu-option" data-action="select-all">全选 <span class="shortcut">Ctrl+A</span></div>
</div>
</div>
<div class="menu-item" data-menu="search">
<span class="menu-label">搜索(S)</span>
<div class="menu-dropdown" id="menu-search">
<div class="menu-option" data-action="find">查找 <span class="shortcut">Ctrl+F</span></div>
<div class="menu-option" data-action="replace">替换 <span class="shortcut">Ctrl+H</span></div>
<div class="menu-separator"></div>
<div class="menu-option" data-action="find-next">查找下一个 <span class="shortcut">F3</span></div>
<div class="menu-option" data-action="find-prev">查找上一个 <span class="shortcut">Shift+F3</span></div>
</div>
</div>
<div class="menu-item" data-menu="view">
<span class="menu-label">视图(V)</span>
<div class="menu-dropdown" id="menu-view">
<div class="menu-option" data-action="toggle-wordwrap">自动换行</div>
<div class="menu-option" data-action="toggle-linenumbers">显示行号</div>
<div class="menu-separator"></div>
<div class="menu-option" data-action="zoom-in">放大 <span class="shortcut">Ctrl++</span></div>
<div class="menu-option" data-action="zoom-out">缩小 <span class="shortcut">Ctrl+-</span></div>
<div class="menu-option" data-action="zoom-reset">重置缩放 <span class="shortcut">Ctrl+0</span></div>
</div>
</div>
</div>
<!-- 标签栏(由 JS 动态管理) -->
<div class="tabbar" id="tabbar"></div>
<!-- 查找替换面板 -->
<div class="find-panel" id="findPanel" style="display:none;">
<div class="find-row">
<label>查找:</label>
<input type="text" id="findInput" class="find-input" placeholder="输入查找内容">
<button id="btnFindPrev" class="find-btn" title="上一个">▲</button>
<button id="btnFindNext" class="find-btn" title="下一个">▼</button>
<label class="find-check"><input type="checkbox" id="chkCase"> 区分大小写</label>
<label class="find-check"><input type="checkbox" id="chkRegex"> 正则</label>
<span id="findCount" class="find-count"></span>
</div>
<div class="find-row" id="replaceRow" style="display:none;">
<label>替换:</label>
<input type="text" id="replaceInput" class="find-input" placeholder="输入替换内容">
<button id="btnReplace" class="find-btn">替换</button>
<button id="btnReplaceAll" class="find-btn">全部替换</button>
</div>
<button id="btnCloseFind" class="find-close" title="关闭">×</button>
</div>
<!-- 编辑器区域 -->
<div class="editor-area" id="editorArea">
<textarea id="editor"></textarea>
</div>
<!-- 状态栏 -->
<div class="statusbar">
<div class="status-left">
<span id="status-file">未保存</span>
</div>
<div class="status-right">
<span id="status-pos">行 1, 列 1</span>
<span id="status-lines">共 1 行</span>
<span id="status-encoding">UTF-8</span>
<span id="status-lang">纯文本</span>
</div>
</div>
<!-- CodeMirror 语言模式 -->
<script src="https://cdn.jsdelivr.net/npm/codemirror@5.65.16/lib/codemirror.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/codemirror@5.65.16/mode/htmlmixed/htmlmixed.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/codemirror@5.65.16/mode/xml/xml.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/codemirror@5.65.16/mode/javascript/javascript.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/codemirror@5.65.16/mode/css/css.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/codemirror@5.65.16/mode/python/python.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/codemirror@5.65.16/mode/markdown/markdown.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/codemirror@5.65.16/mode/clike/clike.min.js"></script>
<!-- 功能插件 -->
<script src="https://cdn.jsdelivr.net/npm/codemirror@5.65.16/addon/edit/closebrackets.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/codemirror@5.65.16/addon/edit/closetag.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/codemirror@5.65.16/addon/fold/foldcode.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/codemirror@5.65.16/addon/fold/foldgutter.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/codemirror@5.65.16/addon/fold/brace-fold.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/codemirror@5.65.16/addon/search/searchcursor.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/codemirror@5.65.16/addon/selection/active-line.min.js"></script>
<!-- 应用逻辑 -->
<script src="renderer.js"></script>
</body>
</html>
布局设计要点:
- 整体采用 Flex 纵向布局(body flex-direction: column),各区域自适应分配空间
- 标签栏由 JS 动态管理,HTML 中为空容器,避免初始标签与 JS 创建逻辑冲突
- 查找替换面板默认隐藏,通过 Ctrl+F/Ctrl+H 快捷键切换显示
- 编辑器区域使用 flex: 1 自动填充剩余空间
3.4 第四步:实现多标签管理
文件:electron-apps/Notepadqq/renderer.js
多标签是编辑器的核心功能。设计思路是:每个标签维护独立的状态数据(内容、光标位置、滚动位置、语言模式),切换标签时保存当前状态并恢复目标状态。
// ========== 全局状态 ==========
let editor = null;
let tabs = [];
let activeTabId = null;
let tabIdCounter = 0;
// ========== 标签管理 ==========
function createTab(fileName = '无标题', filePath = null, content = '') {
const tabId = tabIdCounter++;
// 保存当前标签状态
if (activeTabId !== null) {
saveTabState(activeTabId);
}
// 创建标签数据
const tabData = {
id: tabId,
name: fileName,
path: filePath,
content: content,
modified: false,
cursor: { line: 0, ch: 0 },
scrollLeft: 0,
scrollTop: 0,
mode: 'text/plain'
};
tabs.push(tabData);
// 创建标签 DOM
const tabEl = document.createElement('div');
tabEl.className = 'tab';
tabEl.dataset.tab = tabId;
tabEl.innerHTML = `
<span class="tab-name">${fileName}</span>
<span class="tab-close" title="关闭">×</span>
`;
document.getElementById('tabbar').appendChild(tabEl);
// 切换到新标签
switchTab(tabId);
return tabId;
}
function switchTab(tabId) {
const tabData = tabs.find(t => t.id === tabId);
if (!tabData) return;
// 保存当前标签状态
if (activeTabId !== null && activeTabId !== tabId) {
saveTabState(activeTabId);
}
activeTabId = tabId;
// 更新标签样式
document.querySelectorAll('.tab').forEach(el => {
el.classList.toggle('active', parseInt(el.dataset.tab) === tabId);
});
// 恢复编辑器状态
editor.setValue(tabData.content);
editor.setOption('mode', tabData.mode);
editor.setCursor(tabData.cursor);
editor.scrollTo(tabData.scrollLeft, tabData.scrollTop);
editor.clearHistory();
// 更新状态栏
updateStatusBar();
updateFileStatus(tabData);
}
function saveTabState(tabId) {
const tabData = tabs.find(t => t.id === tabId);
if (!tabData) return;
tabData.content = editor.getValue();
tabData.cursor = editor.getCursor();
tabData.scrollLeft = editor.getScrollInfo().left;
tabData.scrollTop = editor.getScrollInfo().top;
tabData.mode = editor.getOption('mode');
}
多标签状态管理模型:
| 状态项 | 说明 | 保存时机 |
|---|---|---|
| content | 编辑器文本内容 | 切换标签前 |
| cursor | 光标位置(行/列) | 切换标签前 |
| scrollLeft/scrollTop | 滚动位置 | 切换标签前 |
| mode | 语言模式 | 切换标签前 |
| modified | 是否有未保存修改 | 内容变化时 |
3.5 第五步:实现查找替换
查找替换是文本编辑器的核心功能之一,支持正则表达式和区分大小写。
function findNext() {
const query = document.getElementById('findInput').value;
if (!query) return;
const caseSensitive = document.getElementById('chkCase').checked;
const isRegex = document.getElementById('chkRegex').checked;
let searchQuery;
if (isRegex) {
try {
searchQuery = new RegExp(query, caseSensitive ? 'g' : 'gi');
} catch (e) {
return; // 无效正则
}
} else {
searchQuery = query;
}
const cursor = editor.getSearchCursor(searchQuery, editor.getCursor(), { caseFold: !caseSensitive });
if (cursor.findNext()) {
editor.setSelection(cursor.from(), cursor.to());
editor.scrollIntoView({ from: cursor.from(), to: cursor.to() }, 50);
} else {
// 循环查找:从头开始
const cursor2 = editor.getSearchCursor(searchQuery, { line: 0, ch: 0 }, { caseFold: !caseSensitive });
if (cursor2.findNext()) {
editor.setSelection(cursor2.from(), cursor2.to());
editor.scrollIntoView({ from: cursor2.from(), to: cursor2.to() }, 50);
}
}
updateFindCount();
}
function replaceAll() {
const query = document.getElementById('findInput').value;
const replacement = document.getElementById('replaceInput').value;
if (!query) return;
const caseSensitive = document.getElementById('chkCase').checked;
const isRegex = document.getElementById('chkRegex').checked;
let searchQuery;
if (isRegex) {
try {
searchQuery = new RegExp(query, caseSensitive ? 'g' : 'gi');
} catch (e) {
return;
}
} else {
searchQuery = query;
}
let count = 0;
const cursor = editor.getSearchCursor(searchQuery, { line: 0, ch: 0 }, { caseFold: !caseSensitive });
while (cursor.findNext()) {
cursor.replace(replacement);
count++;
}
showAlert(`已替换 ${count} 处`);
updateFindCount();
}
查找替换功能要点:
- 使用 CodeMirror 的 getSearchCursor API 进行文本搜索
- 支持循环查找(到达末尾后自动从头开始)
- 实时显示匹配计数(输入时自动更新)
- 正则模式下自动捕获无效正则并提示
3.6 第六步:实现自定义菜单栏
菜单栏采用纯 CSS hover 触发方式,无需 JavaScript 控制显示隐藏(同时也支持点击切换)。
function initMenus() {
const menuItems = document.querySelectorAll('.menu-option');
menuItems.forEach(item => {
item.addEventListener('click', (e) => {
const action = item.dataset.action;
handleMenuAction(action);
document.querySelectorAll('.menu-item').forEach(m => m.classList.remove('open'));
});
});
// 点击菜单项切换
document.querySelectorAll('.menu-item').forEach(item => {
item.addEventListener('click', (e) => {
e.stopPropagation();
const wasOpen = item.classList.contains('open');
document.querySelectorAll('.menu-item').forEach(m => m.classList.remove('open'));
if (!wasOpen) {
item.classList.add('open');
}
});
});
// 点击其他地方关闭菜单
document.addEventListener('click', () => {
document.querySelectorAll('.menu-item').forEach(m => m.classList.remove('open'));
});
}
菜单操作通过 data-action 属性映射到具体函数:
| 菜单 | 操作 | 快捷键 |
|---|---|---|
| 文件 | 新建/打开/保存/另存为/关闭标签 | Ctrl+N/O/S/Shift+S/W |
| 编辑 | 撤销/重做/剪切/复制/粘贴/全选 | Ctrl+Z/Y/X/C/V/A |
| 搜索 | 查找/替换/查找下一个/查找上一个 | Ctrl+F/H, F3/Shift+F3 |
| 视图 | 自动换行/显示行号/放大/缩小/重置 | Ctrl++/-/0 |
四、鸿蒙稳定性适配
4.1 自定义对话框替代原生弹窗
鸿蒙 Electron 适配层对原生弹窗(confirm、alert、select 下拉等)支持存在限制,可能触发 XComponent 创建超时导致崩溃。解决方案是用纯 HTML/CSS/JS 实现的自定义对话框替代所有原生弹窗。
// 自定义确认对话框
function showConfirm(message) {
return new Promise((resolve) => {
const overlay = document.createElement('div');
overlay.className = 'dialog-overlay';
overlay.innerHTML = `
<div class="dialog-box">
<div class="dialog-message">${message}</div>
<div class="dialog-buttons">
<button class="dialog-btn dialog-btn-ok">确定</button>
<button class="dialog-btn dialog-btn-cancel">取消</button>
</div>
</div>
`;
document.body.appendChild(overlay);
const okBtn = overlay.querySelector('.dialog-btn-ok');
const cancelBtn = overlay.querySelector('.dialog-btn-cancel');
okBtn.addEventListener('click', () => {
document.body.removeChild(overlay);
resolve(true);
});
cancelBtn.addEventListener('click', () => {
document.body.removeChild(overlay);
resolve(false);
});
okBtn.focus();
});
}
// 自定义提示对话框
function showAlert(message) {
return new Promise((resolve) => {
const overlay = document.createElement('div');
overlay.className = 'dialog-overlay';
overlay.innerHTML = `
<div class="dialog-box">
<div class="dialog-message">${message}</div>
<div class="dialog-buttons">
<button class="dialog-btn dialog-btn-ok">确定</button>
</div>
</div>
`;
document.body.appendChild(overlay);
const okBtn = overlay.querySelector('.dialog-btn-ok');
okBtn.addEventListener('click', () => {
document.body.removeChild(overlay);
resolve();
});
okBtn.focus();
});
}
使用方式(异步调用):
// 关闭标签时检查未保存修改
async function closeTab(tabId) {
const tabIndex = tabs.findIndex(t => t.id === tabId);
if (tabIndex === -1) return;
const tabData = tabs[tabIndex];
// 如果有未保存的修改,提示用户
if (tabData.modified) {
const confirmed = await showConfirm(`"${tabData.name}" 有未保存的更改,是否关闭?`);
if (!confirmed) return;
}
// 移除标签数据和 DOM
tabs.splice(tabIndex, 1);
const tabEl = document.querySelector(`.tab[data-tab="${tabId}"]`);
if (tabEl) tabEl.remove();
// 如果关闭的是当前标签,切换到相邻标签
if (activeTabId === tabId) {
if (tabs.length === 0) {
createTab();
} else {
const newIndex = Math.min(tabIndex, tabs.length - 1);
switchTab(tabs[newIndex].id);
}
}
}
4.2 对话框样式
/* 自定义对话框(鸿蒙防崩溃) */
.dialog-overlay {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.5);
display: flex;
align-items: center;
justify-content: center;
z-index: 10000;
}
.dialog-box {
background: #2d2d2d;
border: 1px solid #3c3c3c;
border-radius: 6px;
padding: 20px 24px;
min-width: 300px;
max-width: 500px;
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.6);
}
.dialog-btn-ok {
background: #0e639c;
border-color: #0e639c;
color: #ffffff;
}
.dialog-btn-ok:hover {
background: #1177bb;
}
五、构建与部署
5.1 同步到 web_engine
开发完成后,将 electron-apps/Notepadqq/ 目录的全部内容同步到 web_engine 部署目录:
# 清空旧文件
Remove-Item "web_engine\src\main\resources\resfile\resources\app\*" -Recurse -Force
# 复制最新文件
Copy-Item "electron-apps\Notepadqq\*" `
-Destination "web_engine\src\main\resources\resfile\resources\app\" `
-Recurse -Force
注意:每次修改代码后都必须同步,否则构建的 HAP 包不会包含最新代码。
5.2 构建 HAP 包
在 DevEco Studio 中打开项目,选择 Build → Build Hap(s)/APP(s) → Build Hap(s),即可生成可安装的 HAP 包。


六、常见问题与解决方案
Q1:启动后页面空白
问题现象:应用启动后只显示深色背景,没有编辑器
根本原因:CodeMirror CDN 资源加载失败(网络问题或 CDN 不可达)
解决方案:检查网络连接,确保能访问 cdn.jsdelivr.net。如需离线使用,可将 CodeMirror 文件下载到本地并修改 HTML 中的引用路径。
Q2:切换标签时编辑器内容闪烁
问题现象:切换标签瞬间编辑器短暂显示空白
根本原因:CodeMirror 在 setValue 后需要重新计算布局,而视图切换和状态恢复是同步执行的
解决方案:在 switchTab 中先恢复内容再更新样式,确保 CodeMirror 容器尺寸已确定后再调用 refresh()。
Q3:查找替换在正则模式下崩溃
问题现象:输入某些正则表达式后应用无响应
根本原因:无效正则表达式(如未闭合的括号)导致 new RegExp() 抛出异常
解决方案:用 try-catch 包裹正则构造,捕获异常后直接返回
if (isRegex) {
try {
searchQuery = new RegExp(query, caseSensitive ? 'g' : 'gi');
} catch (e) {
return; // 无效正则,不执行查找
}
}
Q4:关闭标签时弹出原生确认框导致崩溃
问题现象:关闭有修改的标签时应用崩溃
根本原因:使用了原生 confirm() 对话框,触发鸿蒙适配层 SubWindow 创建
解决方案:用自定义 div 对话框替代,通过 Promise 实现异步确认
七、总结
本文完整记录了 Notepadqq 文本编辑器在鸿蒙 PC 平台的适配过程。核心技术要点总结如下:
| 技术点 | 方案 |
|---|---|
| 编辑器内核 | CodeMirror 5(material-darker 主题) |
| 多标签管理 | 单编辑器实例 + 标签状态保存/恢复 |
| 查找替换 | CodeMirror getSearchCursor + 正则支持 |
| 菜单系统 | 纯 CSS hover + JS click 双模式 |
| 文件操作 | Node.js fs 模块 + IPC 跨进程通信 |
| 鸿蒙稳定性 | 三防策略 + 自定义 div 替代原生弹窗 |
| 语言高亮 | 10+ 种 CodeMirror 内置模式 |
| 主题配色 | VS Code 风格深色主题 |
核心经验:鸿蒙 Electron 适配层目前对原生弹窗(confirm、alert、select 下拉等)的支持存在限制,解决方案是从源头避免触发原生 SubWindow——用纯 HTML/CSS/JS 实现的自定义组件替代所有原生弹窗元素。这套"自定义组件替代"策略在 Subversion 客户端和 Notepadqq 编辑器中均已验证有效。
整个适配过程遵循 Electron 壳方案标准化流程:在 electron-apps/ 开发目录中编写代码,通过同步命令部署到 web_engine/ 模块,最终由鸿蒙壳工程打包为 HAP 安装包。开发者可以专注于 Web 技术栈和编辑器功能实现,无需关心平台差异。
更多推荐




所有评论(0)