使用 Window Management API 管理多个显示屏

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

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
*/

与大多数技术从业者一样,我不得不适应新的工作现实,并设置自己的个人居家办公室。我的设置如下图所示(如果您有兴趣,可以阅读有关我的设置的完整详细信息)。 我 MacBook 旁边的 iPad 通过 Sidecar 连接到笔记本电脑,因此我可以随时快速将 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 = async () => [window.screen];
  // Set to `false`, noting that this might be a lie.
  window.screen.isExtended = false;
}

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

演示

如果您像我一样,会密切关注各种加密货币的发展。(实际上我非常喜欢这个星球,但为了本文,请假设我并不喜欢。)为了跟踪我拥有的加密货币,我开发了一款 Web 应用,让我可以在各种生活场景中关注市场动态,例如在舒适的床上,我有一个不错的单屏设置。

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

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

作者双手捂着惊慌失措的脸,盯着假加密货币交易台。
惊慌失措,目睹了 YCY 血洗。

您可以试用下方嵌入的演示,也可以在 GitHub 上查看其源代码

安全与权限

Chrome 团队在设计和实现 Window Management API 时,遵循了控制对强大的 Web 平台功能的访问权限中定义的核心原则,包括用户控制、透明度和人体工程学。窗口管理 API 会公开有关连接到设备的屏幕的新信息,从而增加用户的指纹识别面,尤其是那些始终将多个屏幕连接到其设备的用户。为了缓解这一隐私问题,公开的屏幕属性仅限于常见展示位置用例所需的最少属性。网站必须获得用户许可,才能获取多屏幕信息并在其他屏幕上放置窗口。虽然 Chromium 会返回详细的屏幕标签,但浏览器可以返回描述性较差(甚至为空)的标签。

用户控制

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

企业控制

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

透明度

使用 Window Management API 的权限是否已获授予,会在浏览器的网站信息中公开,并且还可以通过 Permissions API 进行查询。

权限持久性

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

反馈

Chrome 团队希望了解您在使用 Window Management API 方面的体验。

介绍 API 设计

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

  • 在相应的 GitHub 代码库中提交规范问题,或在现有问题中添加您的想法。

报告实现方面的问题

您是否发现 Chrome 的实现存在 bug?还是实现与规范不同?

显示对 API 的支持

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

实用链接

致谢

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