# Window Management API

> window.getScreenDetails() 枚举设备连接的每块屏幕，让 PWA 把窗口打开、移动或全屏到指定屏幕。本页列出 ScreenDetailed 成员、window-management 权限及其 NotAllowedError 情形，以及回退方案。

Window Management API 让页面得知设备有几块屏幕、各在什么位置，然后把窗口放到指定屏幕上。`window.screen.isExtended` 无需提示即可回答第一个问题；`window.getScreenDetails()` 请求 `window-management` 权限，并以 `ScreenDetails` 对象兑现，其 `screens` 数组为每块显示器提供一个 `ScreenDetailed`，坐标相对于多屏原点，这样 `window.open()` 的 features 与 `requestFullscreen({ screen })` 就能指向某块显示器。

Chrome 100 在桌面端发布了 `getScreenDetails()`、`Screen.isExtended` 和 `ScreenDetailed`；Edge 与 Opera 继承该实现，Android 版 Chrome 暴露相同接口。Firefox 和 Safari 任何版本都没有实现（BCD `api.Window.getScreenDetails`）。所有接口都带 `[SecureContext]`。

## 语法

```js
window.screen.isExtended

window.getScreenDetails()

screenDetails.screens
screenDetails.currentScreen

element.requestFullscreen({ screen: screenDetailed })
```

`isExtended` 是现有 `Screen` 接口上的同步 `boolean`。`getScreenDetails()` 返回 `Promise<ScreenDetails>`；对同一个 `Window` 每次调用返回同一个 `ScreenDetails` 对象，显示器增减时触发 `screenschange`，窗口移到另一块显示器或所在屏幕的属性变化时触发 `currentscreenchange`。`ScreenDetailed` 继承自 `Screen`，所以除下表成员外还有 `width`、`height`、`availWidth`、`availHeight`、`colorDepth` 和 `orientation`。

## 成员

`getScreenDetails()` 不接受参数。它返回的对象携带以下成员；`FullscreenOptions` 的 `screen` 成员是该 API 新增的唯一输入。

| 成员 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `ScreenDetails.screens` | `FrozenArray<ScreenDetailed>` | 只读 | 全部屏幕，先按 `left` 再按 `top` 排序。集合变化时换成新数组。 |
| `ScreenDetails.currentScreen` | `ScreenDetailed` | 只读 | 窗口所在的屏幕；与 `screens` 中的某一项 `===`，所以 `screens.find(s => s !== currentScreen)` 可以选出副屏。 |
| `ScreenDetailed.left`、`ScreenDetailed.top` | `long` | 只读 | 多屏原点到该屏幕左边缘、上边缘的距离，单位 CSS 像素。 |
| `ScreenDetailed.availLeft`、`ScreenDetailed.availTop` | `long` | 只读 | 同上，但针对排除任务栏与 Dock 的可用区域。在 `window.open()` 的 features 里用作 `left`/`top`。 |
| `ScreenDetailed.isPrimary` | `boolean` | 只读 | 操作系统指定的主显示器为 `true`。 |
| `ScreenDetailed.isInternal` | `boolean` | 只读 | 笔记本屏幕之类的内置面板为 `true`，外接显示器为 `false`。 |
| `ScreenDetailed.devicePixelRatio` | `float` | 只读 | 该屏幕的物理像素与 CSS 像素之比，可能不同于 `window.devicePixelRatio`。 |
| `ScreenDetailed.label` | `DOMString` | 只读 | 操作系统提供的名称，例如 `"Built-in Retina Display"` 或 `"DELL U2720Q"`；可能为空。 |
| `FullscreenOptions.screen` | `ScreenDetailed` | 否 | 要求 `requestFullscreen()` 先把窗口移到该屏幕。浏览器可以转而尊重用户偏好。 |

## 异常

规范为 `getScreenDetails()` 定义了一种拒绝名称，并在相关的 `minimize()`、`maximize()`、`restore()` 和 `setResizable()` 方法中复用（见 [getScreenDetails() 方法](https://w3c.github.io/window-management/#api-window-getScreenDetails-method)（w3.org））。

| 异常 | 条件 |
|---|---|
| `NotAllowedError` | 文档不被允许使用 `window-management` Permissions Policy 特性（默认允许列表为 `'self'`）；或权限请求的结果为 `"denied"`，原因是用户拒绝了提示，或者调用时没有瞬时激活而权限仍是 `"prompt"`。窗口显示状态方法在文档不是已安装 Web 应用、缺少瞬时激活或操作系统拒绝更改时也以它拒绝。 |

`isExtended` 不抛任何异常，Permissions Policy 拦截该特性时返回 `false`，所以 `false` 的含义是「单屏，或不被允许知道」。`screen` 选项若指向另一个窗口的 `ScreenDetails` 里的 `ScreenDetailed`，会被忽略而不是拒绝。

:::observed
在 Chrome 中，权限尚未授予时于用户激活处理函数之外调用 `window.getScreenDetails()`，以 `NotAllowedError: Transient activation is required to request permission.` 拒绝；用户在 en-US 界面的提示中选择 **Block**，或站点设置已是阻止状态时，同一调用以 `NotAllowedError: Permission denied.` 拒绝。两条字符串都在 Chromium 的 [`window_screen_details.cc`](https://chromium.googlesource.com/chromium/src/+/main/third_party/blink/renderer/modules/screen_details/window_screen_details.cc)（chromium.googlesource.com）中。权限状态可以通过 `navigator.permissions.query({ name: "window-management" })` 无提示读取。
:::

## 示例

每个示例都退化为单屏行为：Firefox 和 Safari 永远走不到多屏分支，Chrome 上用户拒绝权限时该分支也会被跳过。

### 在副屏上打开伴随窗口

先检查 `isExtended`（无提示），在 click 处理函数内调用 `getScreenDetails()` 以便允许弹出权限提示，任何失败都回退到普通的 `window.open()`。

```js
button.addEventListener("click", async () => {
  const url = "/presenter-notes";
  if (!("getScreenDetails" in window) || !window.screen.isExtended) {
    window.open(url, "_blank", "popup");
    return;
  }
  try {
    const details = await window.getScreenDetails();
    const target = details.screens.find((s) => s !== details.currentScreen) ?? details.currentScreen;
    const features = `popup,left=${target.availLeft},top=${target.availTop},width=${target.availWidth},height=${target.availHeight}`;
    window.open(url, "_blank", features);
  } catch (err) {
    console.warn(`${err.name}: ${err.message}`);
    window.open(url, "_blank", "popup");
  }
});
```

权限授予后，features 字符串里的 `left` 和 `top` 按多屏原点解释，所以位于主屏左侧或上方的显示器用负坐标是合法的。

### 在外接显示器上全屏演示，笔记留在笔记本屏幕

把选中的 `ScreenDetailed` 作为 `FullscreenOptions.screen` 传入。规范允许一次成功的跨屏全屏请求为紧随其后的一次 `window.open()` 免去激活要求，这正是同一次点击还能打开笔记窗口的原因。

```js
async function startPresentation(slides) {
  if (!("getScreenDetails" in window)) {
    return slides.requestFullscreen(); // 仅当前屏幕
  }
  const details = await window.getScreenDetails();
  const external = details.screens.find((s) => !s.isInternal);
  if (!external) return slides.requestFullscreen();

  await slides.requestFullscreen({ screen: external });
  const notes = details.screens.find((s) => s.isInternal) ?? details.currentScreen;
  window.open("/notes", "_blank", `popup,left=${notes.availLeft},top=${notes.availTop},width=800,height=600`);
}
```

每块屏幕都报告 `isInternal: false`（接两台显示器的台式机）时，代码回退到当前屏幕而不是猜测。

### 响应显示器的插入与移除

`screenschange` 在 `ScreenDetails` 对象上触发，而不是在 `window` 上，并且 `screens` 数组是整体替换而非原地修改。在处理函数里重新读取它，并关闭所在屏幕已消失的伴随窗口。

```js
async function watchScreens(onChange) {
  if (!("getScreenDetails" in window)) {
    window.screen.addEventListener("change", () => onChange([window.screen]));
    return;
  }
  const details = await window.getScreenDetails();
  onChange(details.screens);
  details.addEventListener("screenschange", () => onChange(details.screens));
  details.addEventListener("currentscreenchange", () => {
    document.documentElement.style.setProperty("--dpr", details.currentScreen.devicePixelRatio);
  });
}
```

单屏回退监听的是同一份规范为 `Screen` 新增的 `change` 事件，窗口当前屏幕的基本属性（尺寸、方向、像素比）变化时触发。

## 另请参阅

- [display_override](/zh/reference/manifest/display-override/)，控制这些屏幕上窗口边框样式的 manifest 成员
- [Tabbed application mode 与 tab_strip](/zh/reference/manifest/tabbed-display/)
- [桌面端的 PWA](/zh/reference/platforms/desktop/)
- [Window Management: getScreenDetails() method](https://w3c.github.io/window-management/#api-window-getScreenDetails-method)（w3.org）
- [Window Management: permission API integration](https://w3c.github.io/window-management/#permission-api-integration)（w3.org）
- [Manage several displays with the Window Management API](https://developer.chrome.com/docs/capabilities/web-apis/window-management)（developer.chrome.com）
- [Chromium: window_screen_details.cc](https://chromium.googlesource.com/chromium/src/+/main/third_party/blink/renderer/modules/screen_details/window_screen_details.cc)（chromium.googlesource.com）