Electron 中创建透明窗口

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

你可能感兴趣的:(electron,前端,typescript)