使用窗口管理 API 管理多个显示屏

获取有关已连接显示屏的信息,并相对于这些显示屏定位窗口。

发布时间:2020 年 9 月 14 日

Window Management API

借助 Window Management API,您可以枚举连接到机器的显示屏,并将窗口放置在特定屏幕上。

建议的应用场景

以下是一些可能使用此 API 的网站示例:

  • Gimp 这样的多窗口图形编辑器可以将各种编辑工具放置在精确定位的窗口中。
  • 虚拟交易平台可以在多个窗口中显示市场趋势,并且任何窗口都可以全屏模式查看。
  • 幻灯片应用可以在内部主屏幕上显示演讲者备注,并在外部投影仪上显示演示文稿。

如何使用 Window Management API

经过时间考验的窗口控制方法 Window.open() 遗憾的是,它无法识别额外的屏幕。虽然此 API 的某些方面看起来有点过时,例如其 windowFeatures DOMString 参数,但多年来它一直为我们提供出色的服务。如需指定窗口的位置,您可以将坐标作为 lefttop(或分别为 screenXscreenY)传递,并将所需的大小作为 widthheight(或分别为 innerWidthinnerHeight)传递。例如,如需打开一个 400x300 的窗口,该窗口距离左侧 50 像素,距离顶部 50 像素,您可以使用以下代码:

const popup = window.open(
  'https://example.com/',
  'My Popup',
  'left=50,top=50,width=400,height=300',
);

您可以通过查看 window.screen 属性来获取有关当前屏幕的信息,该属性会返回一个 Screen 对象。以下是我的 MacBook Pro 13 英寸上的输出:

window.screen;
/* Output from my MacBook Pro 13″:
  availHeight: 969
  availLeft: 0
  availTop: 25
  availWidth: 1680
  colorDepth: 30
  height: 1050
  isExtended: true
  onchange: null
  orientation: ScreenOrientation {angle: 0, type: "landscape-primary", onchange: null}
  pixelDepth: 30
  width: 1680
*/

与大多数技术从业者一样,我不得不适应 2020 年的工作现实,并设置了个人居家办公室。我的设置如图所示(如果您有兴趣,可以阅读有关我的设置的完整详细信息)。 我 MacBook 旁边的 iPad 通过随航功能连接到笔记本电脑,因此我可以随时快速将 iPad 用作第二屏幕。

放在两把椅子上的学校长椅。学校长凳上放置着一些鞋盒,上面放着一台笔记本电脑,周围环绕着两部 iPad。
多屏幕设置。

如果想充分利用更大的屏幕,我可以将代码示例中的弹出式窗口放到第二屏幕上。我的做法如下:

popup.moveTo(2500, 50);

由于无法知道第二块屏幕的尺寸,因此这只是一个粗略的估计值。window.screen 中的信息仅涵盖内置屏幕,不涵盖 iPad 屏幕。内置屏幕的报告 width1680 像素,因此移至 2500 像素可能有助于将窗口移至 iPad,因为恰好知道它位于 MacBook 的右侧。如何在一般情况下实现此目的?事实证明,有一种比猜测更好的方法。这种方式就是 Window Management API。

功能检测

如需检查是否支持 Window Management API,请使用:

if ('getScreenDetails' in window) {
  // The Window Management API is supported.
}

window-management 权限

在使用 Window Management API 之前,我必须征得用户同意。可以使用 Permissions API 查询 window-management 权限,如下所示:

let granted = false;
try {
  const { state } = await navigator.permissions.query({ name: 'window-management' });
  granted = state === 'granted';
} catch {
  // Nothing.
}

在使用同时支持旧权限名称和新权限名称的浏览器时,请务必在请求权限时使用防御性代码,如示例所示。

async function getWindowManagementPermissionState() {
  let state;
  // The new permission name.
  try {
    ({ state } = await navigator.permissions.query({
      name: "window-management",
    }));
  } catch (err) {
    return `${err.name}: ${err.message}`;
  }
  return state;
}

document.querySelector("button").addEventListener>("click", async () = {
  const state = await getWindowManagementPermissionState();
  document.querySelector("pre").textContent = state;
});

浏览器可以选择在首次尝试使用新 API 的任何方法时动态显示权限提示。请阅读下文,了解详情。

window.screen.isExtended 属性

如需了解我的设备是否连接了多个屏幕,我访问了 window.screen.isExtended 属性。它会返回 truefalse。对于我的设置,它会返回 true

window.screen.isExtended;
// Returns `true` or `false`.

getScreenDetails() 方法

现在,我知道当前设置是多屏幕设置,因此可以使用 Window.getScreenDetails() 获取有关第二个屏幕的更多信息。调用此函数会显示权限提示,询问我是否允许网站在我的屏幕上打开和放置窗口。该函数会返回一个 promise,该 promise 会解析为 ScreenDetailed 对象。在连接了 iPad 的 MacBook Pro 13 上,此字段包含两个 ScreenDetailed 对象的 screens 字段:

await window.getScreenDetails();
/* Output from my MacBook Pro 13″ with the iPad attached:
{
  currentScreen: ScreenDetailed {left: 0, top: 0, isPrimary: true, isInternal: true, devicePixelRatio: 2, …}
  oncurrentscreenchange: null
  onscreenschange: null
  screens: [{
    // The MacBook Pro
    availHeight: 969
    availLeft: 0
    availTop: 25
    availWidth: 1680
    colorDepth: 30
    devicePixelRatio: 2
    height: 1050
    isExtended: true
    isInternal: true
    isPrimary: true
    label: "Built-in Retina Display"
    left: 0
    onchange: null
    orientation: ScreenOrientation {angle: 0, type: "landscape-primary", onchange: null}
    pixelDepth: 30
    top: 0
    width: 1680
  },
  {
    // The iPad
    availHeight: 999
    availLeft: 1680
    availTop: 25
    availWidth: 1366
    colorDepth: 24
    devicePixelRatio: 2
    height: 1024
    isExtended: true
    isInternal: false
    isPrimary: false
    label: "Sidecar Display (AirPlay)"
    left: 1680
    onchange: null
    orientation: ScreenOrientation {angle: 0, type: "landscape-primary", onchange: null}
    pixelDepth: 24
    top: 0
    width: 1366
  }]
}
*/

screens 数组中包含有关已连接屏幕的信息。请注意,iPad 的 left 值从 1680 开始,这正是内置显示屏的 width。这样一来,我就可以准确确定屏幕的逻辑排列方式(彼此相邻、彼此叠放等)。现在,每个屏幕都有相应的数据,用于显示它是 isInternal 屏幕还是 isPrimary 屏幕。请注意,内置屏幕不一定是主屏幕

currentScreen 字段是与当前 window.screen 对应的实时对象。该对象会在跨屏窗口放置或设备更改时更新。

screenschange 事件

现在唯一缺少的是一种检测屏幕设置何时发生变化的方法。新事件 screenschange 正是用于此目的:每当屏幕星座发生变化时,它都会触发。(请注意,事件名称中的“screens”是复数形式。)这意味着,每当插入或拔出新屏幕或现有屏幕(如果是 Sidecar,则为物理或虚拟屏幕)时,系统都会触发该事件。

您需要异步查找新屏幕的详细信息,screenschange 事件本身不提供此数据。如需查找屏幕详细信息,请使用来自缓存的 Screens 接口的实时对象。

const screenDetails = await window.getScreenDetails();
let cachedScreensLength = screenDetails.screens.length;
screenDetails.addEventListener('screenschange', (>event) = {
  if (screenDetails.screens.length !== cachedScreensLength) {
    console.log(
      `The screen count changed from ${cachedScreensLength} to ${screenDetails.screens.length}`,
    );
    cachedScreensLength = screenDetails.screens.length;
  }
});

currentscreenchange 事件

如果我只对当前屏幕(即实时对象 currentScreen 的值)的更改感兴趣,则可以监听 currentscreenchange 事件。

const screenDetails = await window.getScreenDetails();
screenDetails.addEventListener('currentscreenchange', async (>event) = {
  const details = screenDetails.currentScreen;
  console.log('The current screen has changed.', event, details);
});

change 事件

最后,如果我只对具体屏幕的更改感兴趣,可以监听该屏幕的 change 事件。

const firstScreen = (await window.getScreenDetails())[0];
firstScreen.addEventListener('change', async (>event) = {
  console.log('The first screen has changed.', event, firstScreen);
});

新增全屏选项

在此之前,您可以通过名为 requestFullScreen() 的方法请求以全屏模式显示元素。该方法采用 options 参数,您可以在其中传递 FullscreenOptions。到目前为止,它的唯一属性是 navigationUI。窗口管理 API 添加了一个新的 screen 属性,可用于确定在哪个屏幕上启动全屏视图。例如,如果您想让主屏幕全屏显示:

try {
  const primaryScreen = (await getScreenDetails()).screens.filter((screen) => screen.isPrimary)[0];
  await document.body.requestFullscreen({ screen: primaryScreen });
} catch (err) {
  console.error(err.name, err.message);
}

Polyfill

无法对 Window Management API 进行 Polyfill,但您可以对它的形状进行 shim,以便仅针对新 API 进行编码:

if (!('getScreenDetails' in window)) {
  // Returning a one-element array with the current screen,
  // noting that there might be more.
  window.getScreenDetails = as>ync () = [window.screen];
  // Set to `false`, noting that this might be a lie.
  window.screen.isExtended = false;
}

API 的其他方面(即各种屏幕更改事件和 FullscreenOptionsscreen 属性)永远不会触发,或者会被不支持的浏览器默默忽略。

演示

如果您密切关注各种加密货币的发展,则可以在我的应用中通过单屏设置轻松监控市场。(我非常不喜欢,因为我热爱这个星球,但为了本文,请假设我确实不喜欢。)

床尾处的大电视屏幕,作者的腿部部分可见。屏幕上显示着一个虚假的加密货币交易平台。
放松身心,关注市场动态。

由于涉及加密货币,市场随时可能变得动荡不安。如果发生这种情况,我可以快速移到办公桌前,那里有多个屏幕。我可以点击任何币种的窗口,然后在另一屏幕上以全屏视图快速查看完整详情。这是我最近在上次 YCY 血洗期间拍摄的照片。这让我措手不及,双手捂脸

我惊慌失措地目睹了 YCY 血洗。

您可以试用演示,也可以在 GitHub 上查看其源代码

安全与权限

Chrome 团队在设计和实现 Window Management API 时,遵循了控制对强大的 Web 平台功能的访问权限中定义的核心原则,包括用户控制、透明度和人体工程学。窗口管理 API 会公开有关连接到设备的屏幕的新信息,从而增加用户的指纹识别面,尤其是那些始终将多个屏幕连接到其设备的用户。为了缓解这一隐私问题,公开的屏幕属性仅限于常见展示位置用例所需的最少属性。

网站必须获得用户许可,才能获取多屏幕信息并在其他屏幕上放置窗口。虽然 Chromium 会返回详细的屏幕标签,但浏览器可以返回描述性较差(甚至为空)的标签。

用户控制

用户可以完全掌控其设置的公开范围。他们可以接受或拒绝权限提示,还可以通过浏览器中的网站信息功能撤消之前授予的权限。

企业控制

Chrome 企业版用户可以控制 Window Management API 的多个方面,如原子政策组设置的相关部分中所述。

透明度

浏览器会在网站信息中显示是否已授予使用 Window Management API 的权限,并且还可以使用 Permissions API 查询该权限。

权限持久性

浏览器会保留权限授予。您可以通过浏览器的网站信息撤消此权限。

反馈

API 是否存在未按预期运行的情况?或者,是否有缺少的方法或属性需要您来实现自己的想法?对安全模型有疑问或意见?

显示对 API 的支持

您打算使用 Window Management API 吗?您的公开支持有助于 Chrome 团队确定功能的优先级,并向其他浏览器供应商展示支持这些功能的重要性。

资源

致谢

窗口管理 API 规范由 Victor CostanJoshua BellMike Wasserman 编辑。 该 API 由 Mike WassermanAdrienne Walker 实现。本文由 Joe MedleyFrançois BeaufortKayce Basques 审核。 感谢 Laura Torrent Puig 提供照片。