Electron 中创建透明窗口

在开发 Electron 应用时,可能需要创建完全透明的窗口,比如我们要做一个屏幕内容共享的功能,在特定矩形区域内的内容才会被共享出来,而这个区域是一个透明且可被穿透的区域。

首先我们需要再主进程上创建一个矩形窗口

ts 复制代码
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

ts 复制代码
screenRegionShareWindow.setIgnoreMouseEvents(true)

设置可拖动元素

默认情况下, 无边框窗口是不可拖拽的。 应用程序需要在 CSS 中指定 -webkit-app-region: drag 来告诉 Electron 哪些区域是可拖拽的(如操作系统的标准标题栏)在可拖拽区域内部使用 -webkit-app-region: no-drag 则可以将其中部分区域排除。

要使整个窗口可拖拽, 您可以添加 -webkit-app-region: drag 作为 body 的样式:

css 复制代码
body {
  -webkit-app-region: drag;
}

前端部分实现如下:

html 复制代码
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
  </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>

主进程代码:

ts 复制代码
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; 可以监听到鼠标事件,由因为我们设置了点击穿透,因此鼠标事件会被传递到此窗口下面的窗口。

计算共享区域的大小

当窗口移动或者调整矩形区域大小时,我们需要更新窗口位置,然后获取新的屏幕像素信息。

核心代码:

ts 复制代码
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 即可。

ts 复制代码
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 之后再更新共享区域,不然在移动的时候在共享画面那能看到边框。

相关推荐
子兮曰4 天前
jev-ultrafast 深度解析:7 秒订机票的浏览器 Agent 是如何炼成的
前端·后端·agent
子兮曰4 天前
Jev 爆发一周:7 秒 Agent 背后的 System One 生态与三场争议
前端·后端·ai编程
前端小万4 天前
写公众号赚了 3000 块后,我做了一款叫 "一键成稿" 的软件
前端·微信小程序
爱勇宝4 天前
ZCode 开源 24 小时:一份没有历史的账本,回答不了"有没有偷代码"
前端·后端·chatglm (智谱)
三十而立洋4 天前
Cookie 详解:从产生到安全,一次讲透
前端·javascript
大蓝头4 天前
安装包UI美化之路-nsNiuniuSkin全新UI调试与预览方法
ui·electron·安装包·截图控件·截屏控件
卡布鲁4 天前
把一个 Vite + Vue3 应用塞进 qiankun (React + Umi3) 主站:十个坑的复盘
前端·javascript·react.js
汉堡大王95274 天前
Jev:不是聊天机器人, 而是一个智能 if 语句
前端·人工智能·后端
梦想很大很大4 天前
从运行事实到回归证据:Workrun 的 Telemetry 与 Evaluation 实践
前端·人工智能·后端
计算机魔术师4 天前
Meta Muse agent 接入 Shopify 的 Shop Pay 实现代理式购物
前端