在开发 Electron 应用时,可能需要创建完全透明的窗口,比如我们要做一个屏幕内容共享的功能,在特定矩形区域内的内容才会被共享出来,而这个区域是一个透明且可被穿透的区域。
首先我们需要再主进程上创建一个矩形窗口
const screenRegionShareWindow = new BrowserWindow({
width: 800,
height: 600,
// 关键!创建无边框窗口,没有窗口的某些部分(例如工具栏、控件等)
frame: false,
// 关键!创建一个完全透明的窗口
transparent: true,
minHeight: Math.ceil(workAreaSize.height * 0.3),
minWidth: Math.ceil(workAreaSize.width * 0.3),
// 窗口可移动
movable: true,
// 窗口可调整大小
resizable: true,
// 窗口不能最小化
minimizable: false,
// 窗口不能最大化
maximizable: false,
// 窗口不能进入全屏状态
fullscreenable: false,
// 窗口不能关闭
closable: true,
webPreferences: {
nodeIntegration: true,
contextIsolation: false // 否则页面无法用require
}
});
共享区域的窗口一定是透明的、可移动的、并且没有边框、可调整大小,但不能最小化和最大化,也不能进入全屏状态,窗口也不能在程序坞中关闭。
要创建一个点击穿透窗口,也就是使窗口忽略所有鼠标事件,可以调用 win.setIgnoreMouseEvents(ignore) API
screenRegionShareWindow.setIgnoreMouseEvents(true)
默认情况下, 无边框窗口是不可拖拽的。 应用程序需要在 CSS 中指定 -webkit-app-region: drag 来告诉 Electron 哪些区域是可拖拽的(如操作系统的标准标题栏)在可拖拽区域内部使用 -webkit-app-region: no-drag 则可以将其中部分区域排除。
要使整个窗口可拖拽, 您可以添加 -webkit-app-region: drag 作为 body 的样式:
body {
-webkit-app-region: drag;
}
前端部分实现如下:
DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Documenttitle>
head>
<style>
html,
body {
margin: 0;
padding: 0;
background: transparent;
}
.container {
margin: 0px;
padding: 0px;
width: 100vw;
height: 100vh;
pointer-events: none;
position: relative;
background: transparent;
}
.line {
background: #3876ff;
-webkit-app-region: drag;
cursor: move;
position: absolute;
pointer-events: auto;
}
.line1 {
height: 24px;
top: 0;
left: 0;
right: 0;
}
.line2 {
height: 4px;
bottom: 0;
left: 0;
right: 0;
}
.line3 {
width: 4px;
bottom: 0;
left: 0;
top: 0;
}
.line4 {
width: 4px;
bottom: 0;
right: 0;
top: 0;
}
#center {
position: absolute;
left: 4px;
bottom: 4px;
right: 4px;
top: 24px;
pointer-events: auto;
}
style>
<body>
<div class="container">
<div class="line line1">div>
<div class="line line2">div>
<div class="line line3">div>
<div class="line line4">div>
<div id="center">div>
div>
body>
<script>
const $ = document.querySelector.bind(document);
const centerElm = $('#center');
const ipcRenderer = require('electron').ipcRenderer;
centerElm.addEventListener(
'mouseenter',
() => {
console.log('enter');
ipcRenderer.send('ignoreMouseEvent', true);
},
false
);
centerElm.addEventListener(
'mouseleave',
() => {
console.log('leave');
ipcRenderer.send('ignoreMouseEvent', false);
},
false
);
script>
html>
主进程代码:
ipcMain.on('ignoreMouseEvent',(event, ignore)=>{
if(ignore){
screenRegionShareWindow?.setIgnoreMouseEvents(true, { forward: true });
}else{
screenRegionShareWindow?.setIgnoreMouseEvents(false);
}
});
上面代码我们设置了鼠标进入的时候只有点击事件会穿透窗口,鼠标移动事件仍会触发(forward: true的作用),当鼠标离开窗口后就不再忽略鼠标事件。
除此之外,我们还在 CSS 上的透明区域(.container)禁用鼠标事件,该元素就永远不会成为鼠标事件的target了,而给 .line
和 .center
部分设置 pointer-events: auto;
让它们还可以成为鼠标事件的 target。
.line
是窗口的边框,并且设置 -webkit-app-region: drag; 让四周的边框可以拖动并且跟随鼠标移动。
给 .center
元素设置 pointer-events: auto;
可以监听到鼠标事件,由因为我们设置了点击穿透,因此鼠标事件会被传递到此窗口下面的窗口。
当窗口移动或者调整矩形区域大小时,我们需要更新窗口位置,然后获取新的屏幕像素信息。
核心代码:
function getContentWindowPhysicalRect() {
let rect = { x: 0, y: 0, width: 0, height: 0 };
if (screenRegionShareWindow) {
const { x, y, width, height } = screenRegionShareWindow.getContentBounds();
rect = screen.dipToScreenRect(null, {
x: Math.ceil(x + 4) + 1,
y: Math.ceil(y + 24) + 1,
width: Math.floor(width - 8) - 1,
height: Math.floor(height - 28) - 1,
});
}
return rect;
}
首先调用 getContentBounds()
API 获取到窗口的位置和大小,然后调用 dipToScreenRect
将屏幕DIP(设备独立像素)矩阵转换为屏幕物理矩阵。
可以把 DIP 的单位理解为浏览器中的 px,如果屏幕像素比(DPR)是2,则代表 1px = 2个物理像素,用 dipToScreenRect 转了之后 rect 中的值会变大。
然后就是窗口移动或者缩放时调用 getContentWindowPhysicalRect 即可。
screenRegionShareWindow.on('resized', () => {
updateContentRegion();
});
screenRegionShareWindow.on('moved', () => {
updateContentRegion();
});
// 移动之前,应该先把共享暂停,然后 moved 之后再更新共享区域
screenRegionShareWindow.on('will-move', () => {
mainWindow?.webContents.send('regionSharingWindowWillChange');
});
screenRegionShareWindow.on('will-resize', () => {
mainWindow?.webContents.send('regionSharingWindowWillChange');
});
移动或者更新窗口大小之前,应该先把共享暂停,然后 moved/resized 之后再更新共享区域,不然在移动的时候在共享画面那能看到边框。