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

获取有关已连接显示屏的信息以及相对于这些显示屏的位置窗口。

窗口管理 API

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

建议的用例

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

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

如何使用 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 像素,因此移至 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() 获取有关第二个屏幕的更多信息。调用此函数时,系统会显示权限提示,询问我是否可以打开网站并在我的屏幕上放置窗口。该函数会返回使用 ScreenDetailed 对象解析的 promise。在连接了 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);
}

聚酯纤维

您无法对 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 血洗期间拍的照片。这让我完全没头绪,把手放在我脸上

这位作家用手放在惊慌失措的脸上,盯着虚假的加密货币交易公司。
Panicky 亲眼见证了 YCY 的热血洗礼。

你可以播放下方嵌入的演示,或在出现故障时查看其源代码

安全与权限

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

用户控制

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

企业控制

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

透明度

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

权限保留

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

反馈

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

向我们介绍 API 设计

是否存在 API 无法正常运行的某些问题?或者说,是否缺少某些方法或属性来实现您的想法?如果您对安全模型有疑问或意见,

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

报告实施方面的问题

您是否发现了 Chrome 实现方面的错误?或者,实现方式是否不同于规范?

显示对该 API 的支持

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

实用链接

致谢

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