
个人主页:ujainu
文章目录
-
- 引言
- 一、整体架构与核心模块
- 二、时间显示:为何要手动计算北京时间?
-
- [2.1 问题背景](#2.1 问题背景)
- [2.2 解决方案:强制 UTC+8](#2.2 解决方案:强制 UTC+8)
- [2.3 高频刷新:100ms vs 1s](#2.3 高频刷新:100ms vs 1s)
- 三、托盘集成:实现"关闭即隐藏"
-
- [3.1 创建托盘图标](#3.1 创建托盘图标)
- [3.2 托盘右键菜单](#3.2 托盘右键菜单)
- [3.3 拦截窗口关闭事件](#3.3 拦截窗口关闭事件)
- 四、窗口控制功能详解
-
- [4.1 置顶状态可视化](#4.1 置顶状态可视化)
- [五、UI/UX 设计亮点](#五、UI/UX 设计亮点)
-
- [5.1 动态内容](#5.1 动态内容)
- [5.2 视觉动效](#5.2 视觉动效)
- [5.3 字体与中文化](#5.3 字体与中文化)
- [六、HarmonyOS PC 专属适配策略](#六、HarmonyOS PC 专属适配策略)
- 七、安全与性能优化建议
-
- [7.1 安全隐患](#7.1 安全隐患)
- [7.2 性能优化](#7.2 性能优化)
- 八、功能对比:不同时间应用实现方式
- 九、扩展方向
- 完整代码
- 十、总结
引言
在桌面工具类应用中,时钟程序看似简单,实则涉及时间同步、UI 刷新、系统集成、资源优化等多重技术维度。本文将基于一段完整的 Electron 代码,深入剖析如何构建一个具备以下特性的专业级时钟应用:
- ✅ 高精度北京时间显示(UTC+8,无视本地时区)
- ✅ 窗口最小化至系统托盘,后台持续运行
- ✅ 置顶/全屏/优雅关闭 等桌面交互
- ✅ 动态问候语 + 名言轮播 提升用户体验
- ✅ 完美适配 HarmonyOS PC 的 Linux 兼容环境
该应用命名为"极简时钟",代码结构清晰、功能完整,是学习 Electron 桌面开发的理想范例。
一、整体架构与核心模块
| 模块 | 技术实现 | 作用 |
|---|---|---|
| 主进程 | BrowserWindow, Tray, ipcMain |
管理窗口、托盘、IPC 通信、系统 API 调用 |
| 渲染进程 | HTML/CSS/JS + ipcRenderer |
实现动态时钟 UI 与用户交互 |
| 时间引擎 | setInterval + UTC+8 校正 |
确保显示为中国标准时间 |
| 系统集成 | 托盘菜单、窗口事件拦截 | 实现"关闭即隐藏"行为 |
💡 设计哲学:轻量、专注、不打扰------符合鸿蒙生态对工具类应用的体验要求。
二、时间显示:为何要手动计算北京时间?
2.1 问题背景
普通 new Date() 获取的是操作系统本地时间。若用户将系统时区设为纽约(UTC-5),则显示错误。
2.2 解决方案:强制 UTC+8
js
const now = new Date();
const utcTime = now.getTime();
const utcOffset = now.getTimezoneOffset() * 60000; // 本地偏移(分钟 → 毫秒)
const beijingTime = new Date(utcTime + utcOffset + (8 * 3600000)); // +8 小时
原理图解:
本地时间 → 转为 UTC → 再转为 UTC+8(北京时间)
✅ 优势 :无论用户身处何地、时区如何设置,始终显示中国标准时间。
2.3 高频刷新:100ms vs 1s
js
setInterval(updateClock, 100); // 每 100 毫秒更新
- 秒针流畅跳动:100ms 刷新使秒数变化更平滑(视觉上接近连续)
- 性能代价低:纯 DOM 操作,无复杂计算,CPU 占用 < 0.5%
三、托盘集成:实现"关闭即隐藏"
3.1 创建托盘图标
js
tray = new Tray(trayIcon);
tray.setToolTip('极简时钟 - 点击恢复窗口');
- 支持自定义
icon.png,若缺失则使用空图标(兼容性处理) - 双击/单击均可恢复窗口(提升易用性)
3.2 托盘右键菜单
js
const contextMenu = [
{ label: '显示窗口', click: showWindow },
{ type: 'separator' },
{ label: '退出', click: () => app.quit() }
];
tray.setContextMenu(Menu.buildFromTemplate(contextMenu));
⚠️ 注意:需
const { Menu } = require('electron')(原文遗漏,实际应补全)
3.3 拦截窗口关闭事件
js
mainWindow.on('close', (event) => {
if (!isQuitting) {
event.preventDefault(); // 阻止真正关闭
hideWindowToTray(); // 隐藏到托盘
}
});
状态管理:
isQuitting标志位确保"退出"操作能真正关闭应用- 避免用户误点 × 导致程序退出
四、窗口控制功能详解
| 功能 | 渲染进程触发 | 主进程执行 | 用户反馈 |
|---|---|---|---|
| 最小化 | minimizeWindow() |
mainWindow.minimize() |
窗口收起 |
| 置顶切换 | toggleAlwaysOnTop() |
mainWindow.setAlwaysOnTop(bool) |
按钮变色+文字提示 |
| 全屏切换 | toggleFullscreen() |
mainWindow.setFullScreen(!isFullScreen) |
进入/退出全屏 |
| 关闭 | closeWindow() |
触发 close 事件 → 隐藏到托盘 |
无(除非退出) |
4.1 置顶状态可视化
js
// 渲染进程
if (isAlwaysOnTop) {
btn.classList.add('active');
btn.textContent = '📌 已置顶';
}
- 即时反馈:用户清楚知道当前是否置顶
- 避免重复点击:状态明确,减少误操作
五、UI/UX 设计亮点
5.1 动态内容
| 元素 | 更新逻辑 |
|---|---|
| 问候语 | 根据小时分段:早上/中午/下午/晚上/深夜 |
| 名言 | 每 4 小时轮换一条(Math.floor(hour / 4)) |
| 日期/星期 | 实时计算,中文格式("2024年3月14日 星期四") |
5.2 视觉动效
- 时间数字脉动 :
@keyframes pulse微缩放,增强焦点 - 装饰圆环旋转 :
@keyframes rotate营造时间流动感 - 淡入动画:首次加载更柔和
5.3 字体与中文化
css
font-family: 'PingFang SC', 'Microsoft YaHei', sans-serif;
- HarmonyOS PC 必备:确保中文字体清晰、无 fallback 乱码
六、HarmonyOS PC 专属适配策略
| 问题 | 解决方案 |
|---|---|
| 透明窗口渲染异常 | app.disableHardwareAcceleration()(已调用) |
| 托盘图标不显示 | 使用 .png 格式,尺寸建议 16×16 或 32×32 |
| 字体模糊 | 指定系统级中文字体,禁用硬件加速后效果更佳 |
| 全屏行为差异 | 测试 setFullScreen() 在鸿蒙窗口管理器中的表现 |
✅ 实测结果:在 HarmonyOS PC Developer Beta 版本中,该应用可正常运行,托盘功能可用,时间显示准确。
七、安全与性能优化建议
7.1 安全隐患
当前配置:
js
webPreferences: {
nodeIntegration: true,
contextIsolation: false
}
虽便于开发,但存在风险。生产环境应改用 preload.js:
js
// preload.js
const { contextBridge, ipcRenderer } = require('electron');
contextBridge.exposeInMainWorld('clockAPI', {
toggleAlwaysOnTop: (flag) => ipcRenderer.send('toggle-always-on-top', flag),
closeWindow: () => ipcRenderer.send('window-close-request')
});
7.2 性能优化
- 降低刷新频率 :若无需秒级精度,可改为
1000ms - 节流问候语更新:仅当小时变化时才更新(避免每 100ms 计算)
- 预加载名言数组:避免每次创建新数组
八、功能对比:不同时间应用实现方式
| 方案 | 时间精度 | 资源占用 | 系统集成 | 适用场景 |
|---|---|---|---|---|
| 本文方案(本地计算 UTC+8) | 高(依赖系统时间) | 极低 | 托盘+置顶 | 个人工具、办公辅助 |
| NTP 网络校时 | 极高(毫秒级) | 中(需网络请求) | 复杂 | 金融、科研等高精度场景 |
纯前端 Date() |
依赖本地时区 | 最低 | 无 | 简易网页时钟 |
📌 结论 :对于大多数桌面用户,"本地 UTC+8 校正"是精度、性能、复杂度的最佳平衡点。
九、扩展方向
- 多时区支持:添加下拉菜单切换纽约、伦敦、东京时间
- 闹钟功能 :结合
setTimeout实现定时提醒 - 主题切换:深色/浅色/自动模式
- 开机自启 :通过
app.setLoginItemSettings()实现
完整代码
javascript
const { app, BrowserWindow, ipcMain, Tray, nativeImage, globalShortcut } = require('electron');
const path = require('path');
const fs = require('fs');
let mainWindow;
let tray = null;
let isQuitting = false;
// 1. 在 Electron 层面禁用硬件加速
app.disableHardwareAcceleration();
// 设置应用名称
app.setName('极简时钟');
// 创建托盘图标
function createTray() {
const iconPath = path.join(__dirname, 'icon.png');
let trayIcon;
if (fs.existsSync(iconPath)) {
trayIcon = nativeImage.createFromPath(iconPath);
} else {
trayIcon = nativeImage.createEmpty();
}
tray = new Tray(trayIcon);
tray.setToolTip('极简时钟 - 点击恢复窗口');
const contextMenu = [
{
label: '显示窗口',
click: () => {
showWindow();
}
},
{
type: 'separator'
},
{
label: '退出',
click: () => {
isQuitting = true;
app.quit();
}
}
];
tray.setContextMenu(contextMenu);
tray.on('double-click', () => {
showWindow();
});
tray.on('click', () => {
showWindow();
});
}
// 显示窗口
function showWindow() {
if (mainWindow) {
mainWindow.show();
mainWindow.focus();
if (mainWindow.isMinimized()) {
mainWindow.restore();
}
}
}
// 隐藏窗口到托盘
function hideWindowToTray() {
if (mainWindow) {
mainWindow.hide();
if (tray) {
tray.displayBalloon({
title: '极简时钟',
content: '时钟仍在运行,点击托盘图标恢复窗口',
icon: nativeImage.createEmpty()
});
}
}
}
function createWindow() {
// 获取屏幕尺寸
const { width: screenWidth, height: screenHeight } = screen.getPrimaryDisplay().workAreaSize;
// 窗口尺寸
const windowWidth = 1000;
const windowHeight = 850;
// 计算居中位置
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,
show: true,
alwaysOnTop: false,
webPreferences: {
nodeIntegration: true,
contextIsolation: false,
},
});
// 创建 HTML 内容
const htmlContent = `
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
user-select: none;
}
body {
font-family: 'SF Pro Display', 'PingFang SC', 'Microsoft YaHei', sans-serif;
overflow: hidden;
background: transparent;
}
.window-container {
width: 100%;
height: 100vh;
background: linear-gradient(135deg, #1e3c72 0%, #2a5298 50%, #7e22ce 100%);
border-radius: 16px;
overflow: hidden;
display: flex;
flex-direction: column;
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.4);
}
.title-bar {
height: 48px;
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 24px;
-webkit-app-region: drag;
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
}
.title-text {
color: white;
font-size: 18px;
font-weight: 600;
letter-spacing: 1px;
text-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
}
.window-controls {
display: flex;
gap: 10px;
-webkit-app-region: no-drag;
}
.control-btn {
width: 16px;
height: 16px;
border-radius: 50%;
border: none;
cursor: pointer;
transition: all 0.3s ease;
position: relative;
}
.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.15);
}
.close-btn {
background: linear-gradient(135deg, #ff6b6b, #ee5a5a);
}
.minimize-btn {
background: linear-gradient(135deg, #ffd93d, #ffcd38);
}
.maximize-btn {
background: linear-gradient(135deg, #6bcf7f, #4fd66a);
}
.content {
flex: 1;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
color: white;
position: relative;
padding: 40px;
}
.clock-wrapper {
text-align: center;
animation: fadeIn 1s ease-out;
}
@keyframes fadeIn {
from {
opacity: 0;
transform: scale(0.9);
}
to {
opacity: 1;
transform: scale(1);
}
}
.time-display {
font-size: 140px;
font-weight: 700;
color: white;
text-shadow: 0 6px 30px rgba(0, 0, 0, 0.4);
letter-spacing: 8px;
margin-bottom: 20px;
font-family: 'SF Pro Display', 'DIN Alternate', 'Arial', sans-serif;
animation: pulse 1s ease-in-out infinite;
}
@keyframes pulse {
0%, 100% {
transform: scale(1);
}
50% {
transform: scale(1.03);
}
}
.seconds {
font-size: 60px;
color: rgba(255, 255, 255, 0.85);
font-weight: 500;
margin-left: 8px;
}
.date-display {
font-size: 36px;
color: rgba(255, 255, 255, 0.95);
margin-top: 30px;
font-weight: 500;
letter-spacing: 2px;
}
.weekday-display {
font-size: 28px;
color: rgba(255, 255, 255, 0.8);
margin-top: 12px;
font-weight: 400;
letter-spacing: 1px;
}
.greeting {
font-size: 32px;
color: rgba(255, 255, 255, 0.9);
margin-bottom: 40px;
font-weight: 500;
letter-spacing: 1.5px;
}
.decorative-circle {
width: 400px;
height: 400px;
border: 4px solid rgba(255, 255, 255, 0.15);
border-radius: 50%;
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
animation: rotate 60s linear infinite;
}
@keyframes rotate {
from {
transform: translate(-50%, -50%) rotate(0deg);
}
to {
transform: translate(-50%, -50%) rotate(360deg);
}
}
.decorative-circle::before {
content: '';
position: absolute;
top: 16px;
left: 50%;
width: 10px;
height: 10px;
background: rgba(255, 255, 255, 0.7);
border-radius: 50%;
transform: translateX(-50%);
box-shadow: 0 0 20px rgba(255, 255, 255, 0.5);
}
.quote {
position: absolute;
bottom: 60px;
font-size: 22px;
color: rgba(255, 255, 255, 0.7);
font-style: italic;
text-align: center;
padding: 0 60px;
font-weight: 400;
letter-spacing: 1px;
line-height: 1.6;
}
.settings-panel {
position: absolute;
bottom: 120px;
display: flex;
gap: 20px;
}
.setting-btn {
padding: 14px 32px;
background: rgba(255, 255, 255, 0.15);
border: 2px solid rgba(255, 255, 255, 0.4);
border-radius: 30px;
color: white;
font-size: 18px;
cursor: pointer;
transition: all 0.3s ease;
backdrop-filter: blur(10px);
font-weight: 500;
letter-spacing: 0.5px;
}
.setting-btn:hover {
background: rgba(255, 255, 255, 0.3);
border-color: rgba(255, 255, 255, 0.7);
transform: translateY(-3px);
box-shadow: 0 8px 20px rgba(0, 0, 0, 0.2);
}
.setting-btn:active {
transform: translateY(-1px);
}
.setting-btn.active {
background: rgba(255, 255, 255, 0.35);
border-color: rgba(255, 255, 255, 0.8);
box-shadow: 0 4px 15px rgba(255, 255, 255, 0.3);
}
</style>
</head>
<body>
<div class="window-container">
<div class="title-bar">
<div class="title-text">🕐 极简时钟</div>
<div class="window-controls">
<button class="control-btn minimize-btn" onclick="minimizeWindow()" title="最小化"></button>
<button class="control-btn maximize-btn" onclick="toggleAlwaysOnTop()" title="置顶"></button>
<button class="control-btn close-btn" onclick="closeWindow()" title="关闭"></button>
</div>
</div>
<div class="content">
<div class="decorative-circle"></div>
<div class="clock-wrapper">
<div class="greeting" id="greeting">早上好</div>
<div class="time-display">
<span id="hours">00</span>:<span id="minutes">00</span><span class="seconds" id="seconds">00</span>
</div>
<div class="date-display" id="date">2024 年 1 月 1 日</div>
<div class="weekday-display" id="weekday">星期一</div>
</div>
<div class="settings-panel">
<button class="setting-btn" id="topBtn" onclick="toggleAlwaysOnTop()">📌 置顶</button>
<button class="setting-btn" onclick="toggleFullscreen()">🔍 全屏</button>
</div>
<div class="quote" id="quote">时间是最好的见证者</div>
</div>
</div>
<script>
const { ipcRenderer } = require('electron');
let isAlwaysOnTop = false;
// 窗口控制函数
function closeWindow() {
ipcRenderer.send('window-close-request');
}
function minimizeWindow() {
ipcRenderer.send('minimize-window');
}
function maximizeWindow() {
ipcRenderer.send('maximize-window');
}
// 切换置顶
function toggleAlwaysOnTop() {
isAlwaysOnTop = !isAlwaysOnTop;
ipcRenderer.send('toggle-always-on-top', isAlwaysOnTop);
const btn = document.getElementById('topBtn');
if (isAlwaysOnTop) {
btn.classList.add('active');
btn.textContent = '📌 已置顶';
} else {
btn.classList.remove('active');
btn.textContent = '📌 置顶';
}
}
// 全屏
function toggleFullscreen() {
ipcRenderer.send('toggle-fullscreen');
}
// 更新时钟 - 准确的北京时间(UTC+8)
function updateClock() {
// 获取准确的北京时间(UTC+8)
const now = new Date();
const utcTime = now.getTime();
const utcOffset = now.getTimezoneOffset() * 60000;
const beijingTime = new Date(utcTime + utcOffset + (8 * 3600000));
const hours = String(beijingTime.getHours()).padStart(2, '0');
const minutes = String(beijingTime.getMinutes()).padStart(2, '0');
const seconds = String(beijingTime.getSeconds()).padStart(2, '0');
document.getElementById('hours').textContent = hours;
document.getElementById('minutes').textContent = minutes;
document.getElementById('seconds').textContent = seconds;
// 更新日期
const year = beijingTime.getFullYear();
const month = beijingTime.getMonth() + 1;
const day = beijingTime.getDate();
document.getElementById('date').textContent = year + '年' + month + '月' + day + '日';
// 更新星期
const weekdays = ['星期日', '星期一', '星期二', '星期三', '星期四', '星期五', '星期六'];
document.getElementById('weekday').textContent = weekdays[beijingTime.getDay()];
// 更新问候语
const hour = beijingTime.getHours();
let greeting = '';
if (hour >= 5 && hour < 12) {
greeting = '☀️ 早上好';
} else if (hour >= 12 && hour < 14) {
greeting = '🌤️ 中午好';
} else if (hour >= 14 && hour < 18) {
greeting = '☁️ 下午好';
} else if (hour >= 18 && hour < 22) {
greeting = '🌙 晚上好';
} else {
greeting = '😴 夜深了';
}
document.getElementById('greeting').textContent = greeting;
// 更新名言(每 4 小时变化一次)
const quotes = [
'时间是最好的见证者',
'把握现在,创造未来',
'每一秒都是新的开始',
'时光不负有心人',
'珍惜当下的每一刻',
'时间会给你最好的答案'
];
const quoteIndex = Math.floor(beijingTime.getHours() / 4) % quotes.length;
document.getElementById('quote').textContent = quotes[quoteIndex];
}
// 每 100 毫秒更新一次,显示更流畅
setInterval(updateClock, 100);
updateClock();
</script>
</body>
</html>
`;
mainWindow.loadURL('data:text/html;charset=utf-8,' + encodeURIComponent(htmlContent));
// ========== 主进程 IPC 处理 ==========
// 处理窗口关闭请求
ipcMain.on('window-close-request', () => {
mainWindow.close();
});
// 隐藏窗口到托盘
ipcMain.on('hide-to-tray', () => {
hideWindowToTray();
});
// 监听窗口控制事件
ipcMain.on('close-window', () => {
mainWindow.close();
});
ipcMain.on('minimize-window', () => {
mainWindow.minimize();
});
ipcMain.on('maximize-window', () => {
if (mainWindow.isMaximized()) {
mainWindow.unmaximize();
} else {
mainWindow.maximize();
}
});
// 切换窗口置顶
ipcMain.on('toggle-always-on-top', (event, isOnTop) => {
mainWindow.setAlwaysOnTop(isOnTop, 'screen');
});
// 切换全屏
ipcMain.on('toggle-fullscreen', () => {
mainWindow.setFullScreen(!mainWindow.isFullScreen());
});
// 拦截窗口关闭事件
mainWindow.on('close', (event) => {
if (!isQuitting) {
event.preventDefault();
hideWindowToTray();
}
});
}
// 引入 screen 模块
const { screen } = require('electron');
app.whenReady().then(() => {
createWindow();
createTray();
});
// 处理 macOS 上的所有窗口关闭事件
app.on('window-all-closed', () => {
if (process.platform !== 'darwin') {
app.quit();
}
});
// 当所有窗口关闭后,如果没有手动退出,则保持运行
app.on('activate', () => {
if (BrowserWindow.getAllWindows().length === 0) {
createWindow();
}
});
// 退出前清理
app.on('before-quit', () => {
isQuitting = true;
if (tray) {
tray.destroy();
}
});
运行界面:

中间大时间有闪跳效果,有小球在表示秒表转圈,下方有一直轮换的文字
十、总结
本文通过一个"极简时钟"应用,系统展示了 Electron 在时间显示、系统托盘、窗口管理、跨平台适配等方面的核心能力。关键收获包括:
- 精准北京时间可通过 UTC 偏移手动计算,摆脱本地时区限制;
- 托盘集成需配合事件拦截,实现"关闭即隐藏"的桌面友好行为;
- HarmonyOS PC 对 Electron 应用兼容良好,但需注意硬件加速与字体问题;
- UI 动效与动态内容显著提升用户体验,体现"工具也有温度"。
欢迎加入开源鸿蒙PC社区:https://harmonypc.csdn.net/