在 HarmonyOS PC 上实现自定义窗口样式的 Electron 应用详解

个人主页:ujainu

文章目录

    • 引言
    • 一、设计目标与用户体验考量
    • [二、Electron 主进程配置深度解析](#二、Electron 主进程配置深度解析)
      • [2.1 禁用硬件加速:确保图形稳定性](#2.1 禁用硬件加速:确保图形稳定性)
      • [2.2 窗口初始化参数详解](#2.2 窗口初始化参数详解)
      • [2.3 屏幕居中计算](#2.3 屏幕居中计算)
    • [三、自定义 UI 实现:从 CSS 到交互逻辑](#三、自定义 UI 实现:从 CSS 到交互逻辑)
      • [3.1 整体布局结构](#3.1 整体布局结构)
      • [3.2 关键 CSS 技巧](#3.2 关键 CSS 技巧)
      • [3.3 控制按钮设计](#3.3 控制按钮设计)
    • 四、主渲染进程通信(IPC)机制
    • [五、HarmonyOS PC 适配建议](#五、HarmonyOS PC 适配建议)
    • 六、扩展与优化方向
    • 完整源代码(main.js)
    • 总结

引言

随着华为鸿蒙操作系统(HarmonyOS)正式进军 PC 领域,开发者生态正迎来前所未有的机遇。尽管 HarmonyOS PC 尚未原生支持 Electron 框架,但凭借其对 Linux 应用的良好兼容能力,我们仍可在该平台上成功运行基于 Web 技术的桌面应用。

本文将聚焦于一个高度定制化的 Electron 示例:创建一个无边框、透明背景、带自定义标题栏与圆角阴影效果的窗口,并完整实现最小化、最大化/还原、关闭等系统级操作。该示例不仅展示了现代 UI 设计的最佳实践,更深入探讨了如何在 HarmonyOS PC 环境下安全、高效地构建具备原生体验的跨平台应用。

全文将围绕代码结构、Electron 配置、CSS 渲染优化及 IPC 通信机制展开详细解析,助你掌握在新兴操作系统上构建精致桌面应用的核心技能。


一、设计目标与用户体验考量

我们的目标远不止"显示文字",而是打造一个视觉精致、交互完整、符合现代设计语言的窗口:

  • 完全移除系统边框,实现自由 UI 布局
  • 启用透明背景与圆角,契合 HarmonyOS 的美学风格
  • 自定义标题栏,包含可拖拽区域与三色控制按钮(红黄绿)
  • 支持标准窗口行为:最小化、最大化/还原、关闭
  • 流畅动效:内容淡入、图标弹跳、按钮悬停反馈
  • 响应式布局,适配不同分辨率屏幕

这些特性共同构成一个接近原生应用的体验,即使运行于兼容层,也能给用户留下专业印象。


二、Electron 主进程配置深度解析

2.1 禁用硬件加速:确保图形稳定性

js 复制代码
app.disableHardwareAcceleration();

在 HarmonyOS PC 的早期阶段,GPU 驱动或合成器可能尚未完全适配 Chromium。主动禁用硬件加速可避免黑屏、闪烁或崩溃,是提升兼容性的关键一步。

⚠️ 注意:此调用必须在 app.whenReady() 之前执行,否则无效。

2.2 窗口初始化参数详解

js 复制代码
mainWindow = new BrowserWindow({
    width: 500,
    height: 400,
    x: x,
    y: y,
    frame: false,        // 移除系统边框
    transparent: true,   // 启用透明背景
    resizable: false,    // 锁定尺寸(可根据需求开启)
    movable: true,       // 允许拖动
    minimizable: true,   // 允许最小化
    closable: true,      // 允许关闭
    skipTaskbar: false,  // 在任务栏显示
    roundedCorners: true,// 启用系统级圆角(macOS/部分 Linux)
    hasShadow: true,     // 添加窗口阴影
});
  • roundedCornershasShadow 是 Electron 25+ 新增选项,在支持的平台上可增强视觉层次。
  • 虽然 resizable: false 锁定了窗口大小,但保留了 maximize 功能(通过 API 控制全屏),实现灵活切换。

2.3 屏幕居中计算

通过 screen.getPrimaryDisplay().workAreaSize 获取可用工作区,手动计算 (x, y) 坐标,确保窗口精准居中且不被任务栏遮挡。


三、自定义 UI 实现:从 CSS 到交互逻辑

3.1 整体布局结构

HTML 采用三层结构:

  • .window-container:主容器,含渐变背景、圆角、阴影
  • .title-bar:自定义标题栏,支持拖拽
  • .content:核心内容区,带动画入场效果

3.2 关键 CSS 技巧

  • -webkit-app-region: drag

    将标题栏设为可拖拽区域,模拟原生窗口移动行为。

  • -webkit-app-region: no-drag

    应用于控制按钮,防止拖拽时误触发。

  • backdrop-filter: blur(20px)

    实现毛玻璃效果(需 Chromium 支持),提升标题栏质感。

  • 渐变文本

    使用 -webkit-background-clip: text + linear-gradient 创建发光文字效果。

  • 动效设计
    fadeInUp 实现内容优雅入场,bounce 为图标添加活力,提升用户感知。

3.3 控制按钮设计

  • 采用 红(关闭)、黄(最小化)、绿(最大化) 经典配色,符合用户心智模型。
  • 悬停与点击状态使用 transform: scale() 提供触觉反馈。
  • 每个按钮添加 title 属性,提升无障碍体验。

四、主渲染进程通信(IPC)机制

由于启用了 contextIsolation(默认),渲染进程无法直接调用 BrowserWindow 方法。因此,我们通过 IPC(Inter-Process Communication) 实现安全通信:

  1. 渲染进程 :点击按钮 → ipcRenderer.send('action')
  2. 主进程 :监听事件 → 调用 mainWindow.minimize() 等 API
js 复制代码
// 渲染进程
ipcRenderer.send('close-window');

// 主进程
ipcMain.on('close-window', () => mainWindow.close());

✅ 此模式符合 Electron 安全最佳实践,避免了 nodeIntegration: true 带来的风险。


五、HarmonyOS PC 适配建议

  • 运行环境:确保已启用 Linux 兼容容器,并安装 Node.js ≥ 18。
  • 图形支持transparent: true 依赖 Alpha 通道合成,HarmonyOS PC 默认支持。
  • 字体回退 :CSS 中指定 'PingFang SC', 'Microsoft YaHei' 等中文字体,确保中文渲染清晰。
  • 未来演进:长期建议迁移至 ArkTS + Stage 模型,但 Electron 可作为快速验证或过渡方案。

六、扩展与优化方向

  • 多显示器支持 :遍历 screen.getAllDisplays() 选择最佳位置。
  • DPI 缩放适配 :根据 display.scaleFactor 动态调整窗口尺寸。
  • 深色模式:监听系统主题变化,动态切换 CSS 变量。
  • 托盘集成 :利用 Tray 模块实现后台常驻。

完整源代码(main.js)

javascript 复制代码
const { app, BrowserWindow, Tray, nativeImage, Menu, screen } = require('electron');
const path = require('path');

let mainWindow, tray;

// 1. 在 Electron 层面禁用硬件加速
app.disableHardwareAcceleration();
// 2. 在 Chromium 层面追加命令行开关,彻底禁用 GPU
// app.commandLine.appendSwitch('disable-gpu');

function createWindow() {
    // 获取屏幕尺寸
    const { width: screenWidth, height: screenHeight } = screen.getPrimaryDisplay().workAreaSize;

    // 窗口尺寸
    const windowWidth = 500;
    const windowHeight = 400;

    // 计算居中位置
    const x = (screenWidth - windowWidth) / 2;
    const y = (screenHeight - windowHeight) / 2;

    mainWindow = new BrowserWindow({
        width: windowWidth,
        height: windowHeight,
        x: x,
        y: y,
        frame: false, // 无边框
        transparent: true, // 透明背景
        resizable: false,
        movable: true,
        minimizable: true,
        closable: true,
        skipTaskbar: false,
        roundedCorners: true, // 圆角窗口
        hasShadow: true, // 添加阴影效果
    });

    // 创建 HTML 内容(包含自定义标题栏)
    const htmlContent = `
        <!DOCTYPE html>
        <html>
        <head>
            <style>
                * {
                    margin: 0;
                    padding: 0;
                    box-sizing: border-box;
                    user-select: none;
                }

                body {
                    font-family: 'PingFang SC', 'Microsoft YaHei', 'Helvetica Neue', Arial, sans-serif;
                    overflow: hidden;
                    background: transparent;
                }

                .window-container {
                    width: 100%;
                    height: 100vh;
                    background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
                    border-radius: 16px;
                    overflow: hidden;
                    display: flex;
                    flex-direction: column;
                    box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);
                }

                .title-bar {
                    height: 44px;
                    background: linear-gradient(to bottom, rgba(255, 255, 255, 0.15), rgba(255, 255, 255, 0.05));
                    backdrop-filter: blur(20px);
                    -webkit-backdrop-filter: blur(20px);
                    display: flex;
                    justify-content: space-between;
                    align-items: center;
                    padding: 0 20px;
                    -webkit-app-region: drag;
                    border-bottom: 1px solid rgba(255, 255, 255, 0.1);
                }

                .title-text {
                    color: white;
                    font-size: 15px;
                    font-weight: 600;
                    letter-spacing: 0.5px;
                    text-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
                    font-family: 'SF Pro Display', 'Segoe UI', 'PingFang SC', sans-serif;
                }

                .window-controls {
                    display: flex;
                    gap: 10px;
                    -webkit-app-region: no-drag;
                }

                .control-btn {
                    width: 14px;
                    height: 14px;
                    border-radius: 50%;
                    border: none;
                    cursor: pointer;
                    transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
                    position: relative;
                    outline: none;
                }

                .control-btn::before {
                    content: '';
                    position: absolute;
                    top: 50%;
                    left: 50%;
                    transform: translate(-50%, -50%);
                    width: 100%;
                    height: 100%;
                    border-radius: 50%;
                    background: radial-gradient(circle at 30% 30%, rgba(255, 255, 255, 0.3), transparent);
                }

                .control-btn:hover {
                    transform: scale(1.1);
                }

                .control-btn:active {
                    transform: scale(0.95);
                }

                .close-btn {
                    background: linear-gradient(135deg, #ff6b6b, #ee5a5a);
                    box-shadow: 0 2px 4px rgba(255, 107, 107, 0.4);
                }

                .close-btn:hover {
                    background: linear-gradient(135deg, #ff5252, #e04545);
                    box-shadow: 0 3px 8px rgba(255, 107, 107, 0.6);
                }

                .minimize-btn {
                    background: linear-gradient(135deg, #ffd93d, #ffcd38);
                    box-shadow: 0 2px 4px rgba(255, 217, 61, 0.4);
                }

                .minimize-btn:hover {
                    background: linear-gradient(135deg, #ffc929, #ffbd2e);
                    box-shadow: 0 3px 8px rgba(255, 217, 61, 0.6);
                }

                .maximize-btn {
                    background: linear-gradient(135deg, #6bcf7f, #4fd66a);
                    box-shadow: 0 2px 4px rgba(107, 207, 127, 0.4);
                }

                .maximize-btn:hover {
                    background: linear-gradient(135deg, #5dd66f, #45c75f);
                    box-shadow: 0 3px 8px rgba(107, 207, 127, 0.6);
                }

                .content {
                    flex: 1;
                    display: flex;
                    justify-content: center;
                    align-items: center;
                    color: white;
                    position: relative;
                }

                .content-wrapper {
                    text-align: center;
                    animation: fadeInUp 0.8s ease-out;
                }

                @keyframes fadeInUp {
                    from {
                        opacity: 0;
                        transform: translateY(30px);
                    }
                    to {
                        opacity: 1;
                        transform: translateY(0);
                    }
                }

                h1 {
                    font-size: 42px;
                    font-weight: 700;
                    background: linear-gradient(135deg, #ffffff 0%, #f0f0f0 100%);
                    -webkit-background-clip: text;
                    -webkit-text-fill-color: transparent;
                    background-clip: text;
                    text-shadow: none;
                    letter-spacing: 2px;
                    margin-bottom: 12px;
                    font-family: 'SF Pro Display', 'PingFang SC', 'Microsoft YaHei', sans-serif;
                }

                .subtitle {
                    font-size: 16px;
                    font-weight: 400;
                    color: rgba(255, 255, 255, 0.85);
                    letter-spacing: 1px;
                    font-family: 'SF Pro Text', 'PingFang SC', 'Microsoft YaHei', sans-serif;
                }

                .icon {
                    font-size: 64px;
                    margin-bottom: 20px;
                    animation: bounce 2s infinite;
                }

                @keyframes bounce {
                    0%, 100% {
                        transform: translateY(0);
                    }
                    50% {
                        transform: translateY(-10px);
                    }
                }
            </style>
        </head>
        <body>
            <div class="window-container">
                <div class="title-bar">
                    <div class="title-text">HarmonyOS PC</div>
                    <div class="window-controls">
                        <button class="control-btn minimize-btn" onclick="minimizeWindow()" title="最小化"></button>
                        <button class="control-btn maximize-btn" onclick="maximizeWindow()" title="最大化"></button>
                        <button class="control-btn close-btn" onclick="closeWindow()" title="关闭"></button>
                    </div>
                </div>
                <div class="content">
                    <div class="content-wrapper">
                        <div class="icon">🚀</div>
                        <h1>Hello HarmonyOS PC</h1>
                        <div class="subtitle">欢迎使用 HarmonyOS PC 应用</div>
                    </div>
                </div>
            </div>

            <script>
                const { ipcRenderer } = require('electron');

                function closeWindow() {
                    ipcRenderer.send('close-window');
                }

                function minimizeWindow() {
                    ipcRenderer.send('minimize-window');
                }

                function maximizeWindow() {
                    ipcRenderer.send('maximize-window');
                }
            </script>
        </body>
        </html>
    `;

    mainWindow.loadURL('data:text/html;charset=utf-8,' + encodeURIComponent(htmlContent));

    // 监听窗口控制事件
    const { ipcMain } = require('electron');

    ipcMain.on('close-window', () => {
        mainWindow.close();
    });

    ipcMain.on('minimize-window', () => {
        mainWindow.minimize();
    });

    ipcMain.on('maximize-window', () => {
        if (mainWindow.isMaximized()) {
            mainWindow.unmaximize();
        } else {
            mainWindow.maximize();
        }
    });
}

app.whenReady().then(createWindow);

// 处理 macOS 上的所有窗口关闭事件
app.on('window-all-closed', () => {
    app.quit();
});

运行界面:

图中的火箭🚀有漂浮晃动的效果

总结

本文通过一个完整的自定义窗口示例,展示了如何在 HarmonyOS PC 上利用 Electron 构建具备现代 UI 与完整交互的桌面应用。从禁用硬件加速到 IPC 通信,从 CSS 动效到圆角阴影,每一步都兼顾了功能性、安全性与视觉美感。

尽管 Electron 并非 HarmonyOS 的原生技术栈,但在生态建设初期,它为 Web 开发者提供了一条高效、低门槛的落地路径。掌握这些技巧,不仅能应对当前开发需求,也为未来向原生框架迁移积累宝贵经验。

欢迎加入开源鸿蒙PC社区:https://harmonypc.csdn.net/

相关推荐
ujainu2 小时前
Electron 极简时钟应用开发全解析:托盘驻留、精准北京时间与 HarmonyOS PC 适配实战
javascript·electron·harmonyos
盐焗西兰花2 小时前
鸿蒙学习实战之路-Share Kit系列(10/17)-目标应用接收分享(应用内处理)
学习·华为·harmonyos
江湖有缘3 小时前
基于开发者空间部署OtterWiki知识管理工具【华为开发者空间】
华为
大雷神4 小时前
HarmonyOS APP<玩转React>开源教程八:主题系统实现
react.js·开源·harmonyos
fei_sun4 小时前
【鸿蒙智能硬件】(六)使用鸿蒙app展示环境监测数据
华为·harmonyos
小圣贤君5 小时前
在 Electron 里造一个「搜书 + 下载」:从 so-novel 到 51mazi 的爬虫实践
前端·人工智能·爬虫·electron·ai写作·小说下载·网文下载
懒洋洋在睡觉6 小时前
鸿蒙 6.0横屏显示时画面旋转错误
华为·图形渲染·harmonyos
键盘鼓手苏苏6 小时前
Flutter 组件 reaxdb_dart 适配鸿蒙 HarmonyOS 实战:响应式 NoSQL 数据库,构建高性能本地持久化与分布式状态同步架构
flutter·harmonyos·鸿蒙·openharmony·reaxdb_dart
亚历克斯神6 小时前
Flutter for OpenHarmony: Flutter 三方库 mongo_dart 助力鸿蒙应用直连 NoSQL 数据库构建高效的数据流转系统(纯 Dart 驱动方案)
android·数据库·flutter·华为·nosql·harmonyos