悬浮球功能
功能特性
- ✅ 始终置顶显示
- ✅ 透明背景,圆形外观
- ✅ 可自由拖动位置
- ✅ 位置自动保存和恢复
- ✅ 点击恢复主窗口
- ✅ 不显示在任务栏
- ✅ 支持快捷键切换
悬浮球服务
初始化
javascript
class FloatingBallService {
/**
* 初始化悬浮球服务
* @param {BrowserWindow} mainWindow - 主窗口实例
*/
static initialize(mainWindow) {
this.mainWindow = mainWindow;
logger.info('悬浮球服务初始化完成');
}
}
// 使用
FloatingBallService.initialize(mainWindow);创建悬浮球窗口
javascript
static create() {
// 如果窗口已存在且未销毁,先销毁
if (this.floatingWindow && !this.floatingWindow.isDestroyed()) {
logger.info('销毁旧的悬浮球窗口');
this.floatingWindow.destroy();
this.floatingWindow = null;
}
const { width, height } = screen.getPrimaryDisplay().workAreaSize;
// 使用上次保存的位置,或默认位置
const defaultX = width - 80;
const defaultY = height / 2;
this.floatingWindow = new BrowserWindow({
width: 75,
height: 75,
x: this.lastPosition?.x || defaultX,
y: this.lastPosition?.y || defaultY,
frame: false, // 无边框
transparent: true, // 透明背景
alwaysOnTop: true, // 始终置顶
skipTaskbar: true, // 不显示在任务栏
resizable: false, // 不可调整大小
movable: true, // 可移动
hasShadow: true, // 显示阴影
webPreferences: {
nodeIntegration: false,
contextIsolation: true,
sandbox: false,
preload: path.join(app.getAppPath(), 'src/preload/preload-ball.js'),
},
});
// 根据环境加载不同的 URL
const url = app.isPackaged
? path.join(process.resourcesPath, 'renderer', 'ball.html')
: 'http://localhost:3000/ball.html';
this.floatingWindow.loadURL(url);
// 监听窗口移动,保存位置
this.floatingWindow.on('move', () => {
if (this.floatingWindow && !this.floatingWindow.isDestroyed()) {
const [x, y] = this.floatingWindow.getPosition();
this.lastPosition = { x, y };
}
});
// 窗口关闭时清理
this.floatingWindow.on('closed', () => {
logger.info('悬浮球窗口已关闭');
this.floatingWindow = null;
});
logger.info('悬浮球窗口创建成功');
}窗口配置说明
| 配置项 | 值 | 说明 |
|---|---|---|
width / height | 75x75 | 悬浮球尺寸 |
frame | false | 无边框窗口 |
transparent | true | 透明背景 |
alwaysOnTop | true | 始终置顶 |
skipTaskbar | true | 不在任务栏显示 |
resizable | false | 禁止调整大小 |
movable | true | 允许拖动 |
hasShadow | true | 显示阴影效果 |
核心功能
1. 拖拽移动
javascript
/**
* 拖拽移动窗口
* @param {number} screenX 鼠标屏幕X坐标
* @param {number} screenY 鼠标屏幕Y坐标
*/
static dragMove(screenX, screenY) {
if (!this.floatingWindow || this.floatingWindow.isDestroyed()) return;
const [width, height] = this.floatingWindow.getSize();
const newX = screenX - width / 2;
const newY = screenY - height / 2;
this.floatingWindow.setPosition(Math.round(newX), Math.round(newY));
this.lastPosition = { x: newX, y: newY };
}实现原理:
- 获取鼠标屏幕坐标
- 计算窗口中心位置
- 实时更新窗口位置
- 自动保存最后位置
2. 显示主窗口
javascript
/**
* 显示主窗口并隐藏悬浮球
*/
static showMainWindow() {
if (this.mainWindow && !this.mainWindow.isDestroyed()) {
this.mainWindow.show();
this.mainWindow.focus();
}
if (this.floatingWindow && !this.floatingWindow.isDestroyed()) {
this.floatingWindow.hide();
}
}3. 隐藏到悬浮球
javascript
/**
* 隐藏主窗口并显示悬浮球
*/
static hideMainWindow() {
if (this.mainWindow && !this.mainWindow.isDestroyed()) {
this.mainWindow.hide();
}
// 如果悬浮球不存在,先创建
if (!this.floatingWindow || this.floatingWindow.isDestroyed()) {
this.create();
}
if (this.floatingWindow && !this.floatingWindow.isDestroyed()) {
this.floatingWindow.show();
}
}4. 显示悬浮球
javascript
/**
* 显示悬浮球(不隐藏主窗口)
*/
static showFloatingBall() {
if (!this.floatingWindow || this.floatingWindow.isDestroyed()) {
this.create();
}
if (this.floatingWindow && !this.floatingWindow.isDestroyed()) {
this.floatingWindow.show();
}
}5. 隐藏悬浮球
javascript
/**
* 隐藏悬浮球
*/
static hideFloatingBall() {
if (this.floatingWindow && !this.floatingWindow.isDestroyed()) {
this.floatingWindow.hide();
}
}6. 关闭悬浮球
javascript
/**
* 关闭悬浮球窗口
*/
static closeFloatingBall() {
if (this.floatingWindow && !this.floatingWindow.isDestroyed()) {
this.floatingWindow.close();
this.floatingWindow = null;
}
}IPC 通信
IPC Handler 注册
javascript
// packages/main/src/ipc-handlers/floating-ball-ipc-handler.js
class FloatingBallIpcHandler {
static async register() {
// 显示悬浮球
ipcMain.handle('show-floating-ball', async () => {
FloatingBallService.showFloatingBall();
});
// 隐藏悬浮球
ipcMain.handle('hide-floating-ball', async () => {
FloatingBallService.hideFloatingBall();
});
// 隐藏主窗口到悬浮球
ipcMain.handle('hide-main-window', async () => {
FloatingBallService.hideMainWindow();
});
// 拖动悬浮球
ipcMain.on('drag-floating-ball', (event, { screenX, screenY }) => {
FloatingBallService.dragMove(screenX, screenY);
});
// 点击悬浮球恢复主窗口
ipcMain.on('floating-ball-click', () => {
FloatingBallService.showMainWindow();
});
}
static removeHandlers() {
ipcMain.removeHandler('show-floating-ball');
ipcMain.removeHandler('hide-floating-ball');
ipcMain.removeHandler('hide-main-window');
ipcMain.removeAllListeners('drag-floating-ball');
ipcMain.removeAllListeners('floating-ball-click');
}
}渲染进程调用
javascript
// 显示悬浮球
await window.electronAPI.invoke('show-floating-ball');
// 隐藏到悬浮球
await window.electronAPI.invoke('hide-main-window');
// 拖动悬浮球
window.electronAPI.send('drag-floating-ball', {
screenX: event.screenX,
screenY: event.screenY,
});
// 点击悬浮球
window.electronAPI.send('floating-ball-click');Preload 脚本
悬浮球窗口使用专用的 preload 脚本:
javascript
// packages/main/src/preload/preload-ball.js
const { contextBridge, ipcRenderer } = require('electron');
contextBridge.exposeInMainWorld('ballAPI', {
// 拖动悬浮球
dragMove: (screenX, screenY) => {
ipcRenderer.send('drag-floating-ball', { screenX, screenY });
},
// 点击悬浮球
onClick: () => {
ipcRenderer.send('floating-ball-click');
},
// 双击悬浮球
onDoubleClick: () => {
ipcRenderer.send('floating-ball-double-click');
},
});悬浮球 HTML
html
<!-- packages/renderer/ball.html -->
<!doctype html>
<html>
<head>
<meta charset="UTF-8" />
<title>悬浮球</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
width: 75px;
height: 75px;
overflow: hidden;
-webkit-app-region: drag; /* 允许拖动 */
user-select: none;
}
.floating-ball {
width: 75px;
height: 75px;
border-radius: 50%;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.3);
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
transition: transform 0.2s;
}
.floating-ball:hover {
transform: scale(1.1);
}
.floating-ball:active {
transform: scale(0.95);
}
.icon {
width: 40px;
height: 40px;
color: white;
font-size: 32px;
}
</style>
</head>
<body>
<div class="floating-ball" id="ball">
<div class="icon">⚡</div>
</div>
<script>
const ball = document.getElementById('ball');
// 点击恢复主窗口
ball.addEventListener('click', () => {
window.ballAPI.onClick();
});
// 双击事件
ball.addEventListener('dblclick', () => {
window.ballAPI.onDoubleClick();
});
// 拖动支持
let isDragging = false;
ball.addEventListener('mousedown', () => {
isDragging = true;
});
document.addEventListener('mousemove', (e) => {
if (isDragging) {
window.ballAPI.dragMove(e.screenX, e.screenY);
}
});
document.addEventListener('mouseup', () => {
isDragging = false;
});
</script>
</body>
</html>集成方式
1. 托盘菜单集成
javascript
// packages/main/src/config/menu-config.js
{
label: '收起到悬浮球',
click: () => {
FloatingBallService.showFloatingBall();
win.hide();
}
}2. 快捷键集成
javascript
// packages/main/src/shortcut/global-shortcut.js
globalShortcut.register('CmdOrCtrl+Alt+H', () => {
FloatingBallService.hideMainWindow();
});3. 窗口控制集成
javascript
// packages/main/src/handlers/other-action-handler.js
class OtherActionHandler {
constructor(win, FloatingBallService) {
this.win = win;
this.FloatingBallService = FloatingBallService;
// 监听最小化到悬浮球请求
ipcMain.on('minimize-to-floating-ball', () => {
FloatingBallService.hideMainWindow();
});
}
}位置管理
默认位置
javascript
const { width, height } = screen.getPrimaryDisplay().workAreaSize;
const defaultX = width - 80; // 右侧边缘
const defaultY = height / 2; // 垂直居中位置保存
javascript
// 监听窗口移动事件
this.floatingWindow.on('move', () => {
if (this.floatingWindow && !this.floatingWindow.isDestroyed()) {
const [x, y] = this.floatingWindow.getPosition();
this.lastPosition = { x, y }; // 保存到内存
}
});位置恢复
javascript
// 创建窗口时使用保存的位置
this.floatingWindow = new BrowserWindow({
x: this.lastPosition?.x || defaultX,
y: this.lastPosition?.y || defaultY,
// ...
});持久化存储(可选)
javascript
// 保存到本地存储
const fs = require('fs');
const positionFile = path.join(app.getPath('userData'), 'floating-ball-position.json');
// 保存位置
fs.writeFileSync(positionFile, JSON.stringify(this.lastPosition));
// 读取位置
if (fs.existsSync(positionFile)) {
this.lastPosition = JSON.parse(fs.readFileSync(positionFile, 'utf-8'));
}样式定制
自定义外观
css
/* 圆形悬浮球 */
.floating-ball {
border-radius: 50%;
}
/* 圆角矩形 */
.floating-ball {
border-radius: 15px;
}
/* 渐变背景 */
.floating-ball {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
}
/* 纯色背景 */
.floating-ball {
background: #4a90e2;
}
/* 图片背景 */
.floating-ball {
background-image: url('./icon.png');
background-size: cover;
}动画效果
css
/* 悬停动画 */
.floating-ball:hover {
transform: scale(1.1);
box-shadow: 0 6px 20px rgba(0, 0, 0, 0.4);
}
/* 点击动画 */
.floating-ball:active {
transform: scale(0.95);
}
/* 呼吸效果 */
@keyframes breathe {
0%,
100% {
opacity: 0.8;
}
50% {
opacity: 1;
}
}
.floating-ball {
animation: breathe 2s infinite;
}性能优化
1. 按需创建
javascript
// 只在需要时创建悬浮球窗口
static showFloatingBall() {
if (!this.floatingWindow || this.floatingWindow.isDestroyed()) {
this.create(); // 延迟创建
}
this.floatingWindow.show();
}2. 资源清理
javascript
// 窗口关闭时清理资源
this.floatingWindow.on('closed', () => {
this.floatingWindow = null; // 释放引用
});3. 避免重复创建
javascript
static create() {
// 如果已存在,先销毁
if (this.floatingWindow && !this.floatingWindow.isDestroyed()) {
this.floatingWindow.destroy();
this.floatingWindow = null;
}
// 创建新窗口
this.floatingWindow = new BrowserWindow({ /* ... */ });
}最佳实践
1. 状态检查
javascript
// 操作前检查窗口状态
static showFloatingBall() {
if (!this.floatingWindow || this.floatingWindow.isDestroyed()) {
this.create();
}
if (this.floatingWindow && !this.floatingWindow.isDestroyed()) {
this.floatingWindow.show();
}
}2. 错误处理
javascript
static hideMainWindow() {
try {
if (this.mainWindow && !this.mainWindow.isDestroyed()) {
this.mainWindow.hide();
}
this.showFloatingBall();
logger.info('主窗口已收起到悬浮球');
} catch (error) {
logger.error('隐藏主窗口失败:', error);
}
}3. 边界处理
javascript
// 确保悬浮球在屏幕范围内
static dragMove(screenX, screenY) {
if (!this.floatingWindow || this.floatingWindow.isDestroyed()) return;
const { width: screenWidth, height: screenHeight } =
screen.getPrimaryDisplay().workAreaSize;
const [width, height] = this.floatingWindow.getSize();
// 限制在屏幕范围内
const newX = Math.max(0, Math.min(screenX - width / 2, screenWidth - width));
const newY = Math.max(0, Math.min(screenY - height / 2, screenHeight - height));
this.floatingWindow.setPosition(Math.round(newX), Math.round(newY));
this.lastPosition = { x: newX, y: newY };
}4. 初始化时机
javascript
// 在主窗口创建后初始化
app.whenReady().then(() => {
const mainWindow = createWindow();
FloatingBallService.initialize(mainWindow);
});常见问题
Q1: 悬浮球不显示?
可能原因:
- 窗口被销毁
- 位置超出屏幕范围
- 透明度设置不当
解决方案:
javascript
// 检查窗口状态
if (!this.floatingWindow || this.floatingWindow.isDestroyed()) {
this.create();
}
// 重置到默认位置
const { width, height } = screen.getPrimaryDisplay().workAreaSize;
this.floatingWindow.setPosition(width - 80, height / 2);Q2: 悬浮球无法拖动?
原因: CSS 未设置 -webkit-app-region: drag
解决方案:
css
body {
-webkit-app-region: drag;
}Q3: 点击悬浮球无响应?
检查事项:
- Preload 脚本是否正确加载
- IPC 通信是否正常
- 事件监听是否注册
javascript
// 检查 preload 加载
console.log('ballAPI available:', typeof window.ballAPI !== 'undefined');Q4: 悬浮球位置不记忆?
实现持久化存储:
javascript
const Store = require('electron-store');
const store = new Store();
// 保存位置
this.floatingWindow.on('move', () => {
const [x, y] = this.floatingWindow.getPosition();
store.set('floatingBallPosition', { x, y });
});
// 读取位置
const savedPosition = store.get('floatingBallPosition');
if (savedPosition) {
this.lastPosition = savedPosition;
}相关文件
packages/main/src/services/floatingball-service.js- 悬浮球服务packages/main/src/ipc-handlers/floating-ball-ipc-handler.js- IPC 处理器packages/main/src/preload/preload-ball.js- Preload 脚本packages/renderer/ball.html- 悬浮球页面