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++ 原版鸿蒙适配版
编辑器内核ScintillaCodeMirror 5(轻量级替代)
开发语言C++HTML/CSS/JavaScript
语法高亮自定义词法分析器CodeMirror 内置语言模式
文件操作Win32 APINode.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 技术栈和编辑器功能实现,无需关心平台差异。

Logo

作为“人工智能6S店”的官方数字引擎,为AI开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐