本文将详细介绍如何使用 Electron 实现一个智能侧边悬浮窗口,包含窗口初始化、拖拽移动、丰富的操作功能以及平滑的动画效果。
1. 窗口初始化
首先让我们创建一个基本的悬浮窗口,它需要具备透明、无边框、始终置顶等特性:
javascript
class SideFloatWin {
constructor(options) {
this.initConfig(options);
this.initWin();
this.openWin();
this.registerEvent();
}
initConfig(options) {
const { initUrl, env, mainWin } = options;
this.initUrl = initUrl;
this.env = env;
this.mainWin = mainWin;
this.canOpenSideFloatWin = true;
}
initWin() {
const site = this.winSiteInfo(24, 180);
this.win = new BrowserWindow({
title: '悬浮窗口',
x: site.x,
y: site.y,
width: 60,
height: 60,
show: false,
resizable: false,
frame: false,
transparent: true,
backgroundColor: '#00000000',
alwaysOnTop: true,
webPreferences: {
preload: path.join(__dirname, "preload.js"),
contextIsolation: true,
}
});
// 加载页面内容
if(this.env === "development") {
this.win.loadURL(this.initUrl + "#/side");
} else {
this.win.loadFile(this.initUrl, { hash: 'side' });
}
this.win.hide();
this.win.setMenu(null);
this.win.setSkipTaskbar(true);
// macOS 多工作空间支持
if (process.platform === 'darwin') {
this.win.setVisibleOnAllWorkspaces(true);
}
}
}
这段代码创建了一个宽高均为 60px 的圆形悬浮窗口,具有以下特性:
- 无边框透明设计
- 始终置于其他窗口之上
- 不在任务栏显示
- 支持多工作空间显示
2. 窗口移动实现
实现窗口拖拽功能需要自定义移动逻辑,因为无边框窗口无法通过系统默认方式拖动:
javascript
// 自定义窗口移动类
class CustomWindowMove {
constructor() {
this.isOpen = false;
this.win = null;
this.winStartPosition = { x: 0, y: 0, width: 0, height: 0 };
this.startPosition = { x: 0, y: 0 };
}
init(win) {
this.win = win;
}
start() {
this.isOpen = true;
// 保存窗口初始位置和大小
const winPosition = this.win.getPosition();
const winSize = this.win.getSize();
this.winStartPosition.x = winPosition[0];
this.winStartPosition.y = winPosition[1];
this.winStartPosition.width = winSize[0];
this.winStartPosition.height = winSize[1];
// 保存鼠标初始位置
const mouseStartPosition = screen.getCursorScreenPoint();
this.startPosition.x = mouseStartPosition.x;
this.startPosition.y = mouseStartPosition.y;
// 开始移动
this.move();
}
move() {
if (!this.isOpen || this.win.isDestroyed() || !this.win.isFocused()) {
this.end();
return;
}
// 计算新位置
const cursorPosition = screen.getCursorScreenPoint();
const x = this.winStartPosition.x + cursorPosition.x - this.startPosition.x;
const y = this.winStartPosition.y + cursorPosition.y - this.startPosition.y;
// 更新窗口位置
this.win.setBounds({
x: x,
y: y,
width: this.winStartPosition.width,
height: this.winStartPosition.height,
});
// 循环调用实现平滑移动
setTimeout(() => {
this.move();
}, 20);
}
end() {
this.isOpen = false;
}
}
// 在主类中集成拖拽功能
handleWinMove() {
this.floatMoveEvent = new CustomWindowMove();
this.floatMoveEvent.init(this.win);
}
// 处理拖拽事件
moveFloatWin(info) {
switch (info) {
case 'homeDragWindowStart':
this.floatMoveEvent.start();
break;
case 'homeDragWindowEnd':
this.floatMoveEvent.end();
break;
}
}
3. 窗口操作功能实现
为悬浮窗口添加丰富的交互功能,包括右键菜单、单击和双击操作:
javascript
// 注册IPC事件处理
registerEvent() {
ipcMain.on("side-float-operation", (event, info) => {
this.floatOperation(info);
});
}
// 操作处理
floatOperation(info) {
switch (info.type) {
case "move":
this.moveFloatWin(info.value);
break;
case "rightClick":
this.createMenu();
break;
case "dblClick":
this.openMainWindow();
break;
case "mouseout":
this.showWinHalf();
break;
case "mouseover":
this.showWinAll();
break;
default:
break;
}
}
// 创建右键菜单
createMenu() {
const menuList = [
{
label: '打开主窗口',
click: () => this.mainWin.show()
},
{
label: '开机启动',
type: 'checkbox',
checked: app.getLoginItemSettings().openAtLogin,
click: () => {
app.setLoginItemSettings({
openAtLogin: !app.getLoginItemSettings().openAtLogin
});
}
},
{
label: '悬浮窗功能',
type: 'checkbox',
checked: this.canOpenSideFloatWin,
click: () => {
this.canOpenSideFloatWin = !this.canOpenSideFloatWin;
if(this.canOpenSideFloatWin) {
this.openWin();
} else {
this.closeWin();
}
}
},
{
label: '功能1',
type: 'checkbox',
checked: true,
click: () => {
// 功能1切换逻辑
}
},
{
label: '功能2',
type: 'checkbox',
checked: true,
click: () => {
// 功能2切换逻辑
}
},
{
label: '退出',
click: () => app.exit()
}
];
const contextMenu = Menu.buildFromTemplate(menuList);
const mousePosition = screen.getCursorScreenPoint();
contextMenu.popup(this.win, mousePosition.x, mousePosition.y);
}
// 打开主窗口
openMainWindow() {
if(this.mainWin) {
this.mainWin.show();
}
}
在前端Vue组件中,我们需要相应实现鼠标事件的触发:
vue
<template>
<div class="side-win"
@mouseleave="mouseout"
@mouseenter="mouseover"
@mousedown.stop="moveWindow($event)"
@mouseup.stop="stopMove($event)">
<div class="side-content"
@contextmenu.prevent="rightClick"
@click.stop="clickSide"
@dblclick.stop="dblClickSide">
<div class="side-icon"></div>
</div>
</div>
</template>
<script setup>
const rightClick = () => {
window.electronAPI.sidefloatOperation({ type: "rightClick" });
}
let clickTimer = null;
const clickSide = () => {
clearTimeout(clickTimer);
clickTimer = setTimeout(() => {
// 单击操作
}, 200);
}
const dblClickSide = () => {
clearTimeout(clickTimer);
window.electronAPI.sidefloatOperation({ type: "dblClick" });
}
const mouseDowm = ref(false);
const moveWindow = (e) => {
mouseDowm.value = true;
window.electronAPI.sidefloatOperation({
type: "move",
value: "homeDragWindowStart"
});
}
const stopMove = (e) => {
mouseDowm.value = false;
window.electronAPI.sidefloatOperation({
type: "move",
value: "homeDragWindowEnd"
});
}
const mouseout = () => {
if(mouseDowm.value) return;
window.electronAPI.sidefloatOperation({ type: "mouseout" });
}
const mouseover = () => {
if(mouseDowm.value) return;
window.electronAPI.sidefloatOperation({ type: "mouseover" });
}
</script>
4. 窗口动画效果
为提升用户体验,我们为悬浮窗口添加平滑的动画效果:
javascript
// 显示完整窗口
showWinAll() {
this.moveWindowWithAnimation(this.win, this.winSiteInfo(59, 180), 200);
}
// 显示一半窗口(鼠标移出时)
showWinHalf() {
this.moveWindowWithAnimation(this.win, this.winSiteInfo(24, 180), 200);
}
// 窗口移动动画实现
moveWindowWithAnimation(window, {x, y, displayFrequency}, duration) {
const startX = window.getPosition()[0];
const startY = window.getPosition()[1];
const startTime = Date.now();
function animate() {
const currentTime = Date.now();
const elapsedTime = currentTime - startTime;
const progress = Math.min(elapsedTime / duration, 1);
// 计算当前窗口位置(使用缓动函数)
const currentX = startX + (x - startX) * progress;
const currentY = startY;
// 设置窗口位置
window.setPosition(Math.round(currentX), Math.round(currentY));
// 如果动画未完成,继续下一帧
if (progress < 1) {
setTimeout(animate, 1000 / displayFrequency / 2);
}
}
// 启动动画
animate();
}
// 计算窗口位置信息
winSiteInfo(offsetX, offsetY) {
const siteInfo = screen.getAllDisplays().reduce((p, c) => {
const {x, y, width, height} = c.bounds;
if(x + width - offsetX > p.x){
p.x = x + width - offsetX;
p.y = y + height - offsetY;
p.displayFrequency = c.displayFrequency;
}
return p;
}, {x: 0, y: 0, displayFrequency: 60});
return siteInfo;
}
最后,我们需要为悬浮窗口添加合适的样式,使其外观更加美观:
html
<style scoped>
.side-win{
width: 60px;
height: 100vh;
display: flex;
justify-content: center;
align-items: center;
cursor: pointer;
background: transparent;
}
.side-content{
display: flex;
justify-content: center;
align-items: center;
width: 48px;
height: 48px;
background: #FFFFFF;
box-shadow: 0px 3px 6px 1px rgba(0,0,0,0.16);
border-radius: 24px;
transition: all 0.3s ease;
}
.side-content:hover {
transform: scale(1.1);
box-shadow: 0px 5px 12px 2px rgba(0,0,0,0.2);
}
.side-icon{
background-image: url("@/assets/icon.png");
background-size: 100% 100%;
width: 38px;
height: 38px;
}
</style>