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

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

Window Management API

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

建议的用例

可能使用此 API 的网站示例包括:

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

如何使用 Window Management API

问题

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

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 像素,因此,由于 知道它位于 MacBook 的右侧,因此将其移至 2500 像素或许可以将窗口移至 iPad。在一般情况下,我该如何执行此操作?事实证明,除了猜测之外,还有更好的方法。这种方式就是 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,由 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。Window Management 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 操作,但可以对其形状进行换位处理,以便针对新 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 血浴。

您可以玩一下下面嵌入的演示,也可以在 Glitch 上查看其源代码

安全与权限

Chrome 团队使用控制对强大 Web 平台功能的访问权限中定义的核心原则(包括用户控制、透明度和人体工学)设计和实现了 Window Management API。Window Management API 会公开与设备连接的屏幕的新信息,从而增加用户(尤其是与设备持续连接的多个屏幕的用户)的指纹识别 Surface。作为缓解此隐私问题的一种缓解措施,公开的屏幕属性会被限制为常见展示位置用例所需的最低限度。网站必须获得用户许可,才能获取多屏幕信息并在其他屏幕上放置窗口。虽然 Chromium 会返回详细的屏幕标签,但浏览器可以自由返回不太具描述性(甚至空白)的标签。

用户控制

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

企业控制

Chrome 企业版用户可以控制 Window Management API 的多个方面,如 Atomic Policy Groups 设置的相关部分所述。

透明度

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

权限持久性

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

反馈

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

请向我们说明 API 设计

API 是否有某些方面未按预期运行?或者,您是否缺少实现想法所需的方法或属性?对安全模型有疑问或意见?

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

报告实现存在的问题

您是否发现了 Chrome 实现中的 bug?或者实现与规范不同?

  • 请访问 new.crbug.com 提交 bug。请务必提供尽可能详细的信息、简单的重现说明,并在 Components 框中输入 Blink>Screen>MultiScreenGlitch 非常适用于分享轻松快速的重现问题。

显示对该 API 的支持

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

实用链接

致谢

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