【评论-抽奖】Electron 拓展屏的初步设计和实现

1,333 阅读6分钟

在参加完8月更文的活动后,感觉整个人都被掏空了,本来打算好好的休养生息一波的,结果申请的掘金周边礼物活动竟然通过了,这真是“人在家中坐,喜从天上来”。

很荣幸能成为首批名额中的一员,感谢社区的大力支持!

这次的活动是作者可以获得两枚掘金徽章,在评论区通过抽奖的方式将这两枚徽章送给积极参与互动的朋友,这也算是借花献佛了 ( ̄▽ ̄)~*。

所以,看完文章的你千万别忘了在评论区留下你的真知灼见,说不定中奖的那个人就是你哦!

前言

我辈码猿,不是在做需求就是在接需求,让人不得不感慨一下:需求复需求,需求何其多!

这不,本来之前开发出的 Electron 客户端,基本可以满足项目需求了,自以为可以好好放松一下了,谁知客户又给整出了幺蛾子——需要支持拓展屏。

需求分析

所谓的拓展屏,相当于是从主屏窗口复制出一个窗口,显示在主屏幕之外的其他显示器上,并且这些分屏窗口除了一些控制功能(登录、登出、分屏、切换服务等)外,其余功能都和主屏窗口相同。

因为我们的 Electron 客户端的作用是提供一个壳子,里面的加载的是其他服务提供的页面,所以这里需要考虑的就只有窗口控制了,相对来说比较简单。

要点枚举

获取所有屏幕信息

程序一启动,首先获取屏幕个数及其属性:

const { screen } = require("electron");
// 存储所有连接的显示器的信息
let displaysMap = new Map();

function getAllDisplays() {
    const allDisplays = screen.getAllDisplays();
    for (let i = 0; i < allDisplays.length; i++) {
        const display = allDisplays[i];
        displaysMap.set(display.id, display);
    }
}

function getBrowserOptsByDisplay(display) {
    const { bounds, workAreaSize } = display;
    const { x, y } = bounds;
    const { width, height } = workAreaSize;
    let isPrimary = false;
    if (display.id === screen.getPrimaryDisplay().id) {
        isPrimary = true;
    }
    return { x, y, width, height, isPrimary };
}

function getBrowserOpts() {
    let opts = [];
    const displays = displaysMap.values();
    for (const display of displays) {
        opts.push(getBrowserOptsByDisplay(display));
    }
    return opts;
}

screen.on("display-added", (event, newDisplay) => {
    displaysMap.set(newDisplay.id, newDisplay);
    // 创建窗口的逻辑等
});

screen.on("display-removed", (event, oldDisplay) => {
    displaysMap.delete(oldDisplay.id);
    // 移除窗口的逻辑
});

这里需要注意的是,主屏幕一般默认是 allDisplays 的第一个,但是也有例外,取决于显示器的接入的方式。我们可通过 screen.getPrimaryDisplay() 来获取主屏对象。

根据屏幕创建窗口

根据屏幕属性(主要是位置和尺寸)创建各自对应的的窗口,我们将窗口的设置成默认占满整个屏幕:

const { BrowserWindow } = require("electron");
const path = require("path");
let windowsMap = new Map();
// 记录主窗口的id
let masterWindowId = 0;

function createWindowByOpts(opts) {
    const customOpts = Object.assign(
        {
            title: "窗口标题",
            show: false,
            icon: path.join(process.cwd(), "./static/icon/icon.png"),
            webPreferences: {
                preload: path.join(__dirname, "./src/preload.js"),
                nodeIntegration: true,
                webSecurity: false
            }
        },
        opts
    );
    let win = new BrowserWindow(customOpts);
    if (opts.isMaster) {
        masterWindowId = win.id;
        win.once("ready-to-show", () => {
            win.show();
        });
    }
    windowsMap.set(win.id, win);
}

function createWindows() {
    const allOpts = getBrowserOpts();
    for (const opts of allOpts) {
        createWindowByOpts(opts);
    }
}

可能有人会有疑问,直接最大化就好了,为什么还要给窗口设置尺寸呢?如果不设置尺寸的话,当窗口退出全屏之后,其大小为默认值 800 * 800,用户体验不太好。

将屏幕信息传给渲染进程

因为要通过点击进行分屏,所以渲染进程需要知道总共有几个屏幕。这里将所有屏幕的信息传给渲染进程:

function sendAllScreensInfoToRender() {
    const masterWindow = windowsMap.get(masterWindowId);
    if (!masterWindow) {
        return false;
    }
    masterWindow.webContents.send("all-screens-info", displaysMap.values);
}

因为分屏操作只能在主屏窗口上进行,所以,这里只需要给主屏幕对应的渲染进程发送信息即可。

将窗体信息发送至渲染进程

我们可以将窗口的一些关键信息发送至它对应的渲染进程,以减少通信次数:

function sendKeyInfoToRender(win) {
    const windowId = win.id;
    const isMaster = win.id === masterWindowId;
    const webContentsId = win.webContents.id;
    win.webContents.send("window-key-info", {windowId, isMaster, webContentsId});
}

分屏免登录

可执行分屏的前提应该是主屏已经成功登录,为了方便用户,分出去的窗口需要免登录,一打开显示的应该和主屏窗口当前的显示一致。

为了实现免登录功能,主窗口在分屏操作时,需要将自己的登录名称和密码传给分屏窗口,分屏根据用户名和密码执行一次静默登录。

分屏和主屏区分

由于涉及到授权名额的限制,一个主窗口占用一个名额,分屏不占用授权名额,因此需要对主屏和分屏进行区分。

在登录和登出参数中增加一个属性 is_master,值类型为 bool,表示是否是主屏窗口的请求。后台在接收到登录请求后进行判断:

  • 如果 is_master 为 true,则分配一个授权
  • 否则,不作任何授权占用操作

登出请求也是同样处理。

分屏隐藏则断开连接

因为页面上的数据都是通过 WS 连接推送上来的,所以每个窗口都需要建立一个 WS 连接。

一开始的时候,我在创建窗口的时候就将页面加载过来了,不管这个窗口有没有显示,这有点浪费性能。

现在的处理方案是:

  • 如果执行了分屏操作,则给对应的窗口加载页面并显示
  • 如果关闭了分屏窗口,则给它加载一个空白页面文件,以此断开 WS 连接和渲染消耗

卸载窗体内容时发送登出请求

在页面刷新或者关闭时,都会执行登出操作:

window.on("beforeunload", function () {
    const data = {username, userhandle, is_master};
    const path = "https://localhost:9001";
    window.navigator.sendBeacon(path, JSON.stringify(data));
})

给窗体加载空白页面也可触发窗体卸载事件。

TODO

分屏的初步功能已经实现,后续还需要做的优化有以下几个方面。

切换服务后的分屏显示

目前的实现是,每次切换服务后都隐藏所有分屏窗口,需要再次手动点击分屏才可加载显示。后续的优化方案是:

  • 记录正在显示的窗口id
  • 切换服务时隐藏所有分屏窗口
  • 服务切换成功后,根据记录将之前已经显示的窗口重新加载显示

主屏关闭或者移除后的处理

目前对主屏的关闭和移除未做任何处理,后续计划在主屏关闭或者移除后,将主屏窗口移动到某个分屏上。

窗口间通信优化

目前采用的方案是将主进程当做消息中转站,后续计划使用 poseMessage API 在窗口间直接通信。

总结

以上就是 Electron 客户端拓展屏的实现思路和方案,如果你有更好的实现方式,欢迎评论区留言探讨哦!

抽奖方式

本次活动的抽奖我会采用随机抽取方式,保证公平,所以人人都有机会!

抽奖时间:9月12日 22:00(之后参与评论的朋友将无法参与抽奖)

抽奖方式:随机抽取(代码执行)

心动不如行动,请在评论区为自己争取一份机会哦!

~

~本文完,感谢阅读!

~

学习有趣的知识,结识有趣的朋友,塑造有趣的灵魂!

大家好,我是〖编程三昧〗的作者 隐逸王,我的公众号是『编程三昧』,欢迎关注,希望大家多多指教!

你来,怀揣期望,我有墨香相迎! 你归,无论得失,唯以余韵相赠!

知识与技能并重,内力和外功兼修,理论和实践两手都要抓、两手都要硬!