使用 Window Management API 管理多個螢幕

取得已連接螢幕的相關資訊,並將視窗置於這些螢幕上。

Window Management API

您可以使用 Window Management API 列舉連接至機器的螢幕,並將視窗放置在特定螢幕上。

建議用途

以下是可能會使用這個 API 的網站:

  • Gimp 等多視窗圖形編輯器可將各種編輯工具放置在精確定位的視窗中。
  • 虛擬交易平台可在多個視窗中顯示市場趨勢,且每個視窗皆可以全螢幕模式查看。
  • 幻燈片應用程式可在內部主畫面上顯示演講者備忘稿,並在外部投影機上顯示簡報。

如何使用 Window Management API

問題

經過時間考驗的視窗控制方法 Window.open() 不幸無法偵測到其他畫面。雖然這個 API 的某些部分 (例如 windowFeatures DOMString 參數) 似乎有點過時,但多年下來,這個 API 仍為我們提供良好的服務。如要指定視窗的位置,您可以將座標傳遞為 lefttop (或分別為 screenXscreenY),並將所需大小傳遞為 widthheight (或分別為 innerWidthinnerHeight)。舉例來說,如要開啟 400 x 300 視窗,並將視窗置於左側 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() 取得第二個螢幕的更多資訊。呼叫這個函式會顯示權限提示,詢問我是否允許網站在螢幕上開啟及放置視窗。這個函式會傳回承諾,並以 ScreenDetailed 物件解析。在已連結 iPad 的 MacBook Pro 13 上,這包括一個 screens 欄位,其中包含兩個 ScreenDetailed 物件:

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,但您可以使用 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 屬性) 則不會觸發,或會在未支援的瀏覽器中遭到靜默忽略。

示範

如果你跟我一樣,密切關注各種加密貨幣的發展,(實際上我並沒有,因為我很愛這個星球,但為了這篇文章,請假設我有)。為了追蹤我擁有的加密貨幣,我開發了一個網頁應用程式,讓我可以在任何生活情境下觀看市場,例如在床上休息時,我可以透過單一螢幕的良好設定來觀看市場。

床尾有大型電視螢幕,作者的腿部部分可見。畫面上顯示假的加密貨幣交易平台。
放鬆心情,觀察市場。

由於加密貨幣市場隨時都可能出現劇烈波動,在這種情況下,我可以快速移動到有多螢幕設定的辦公桌。我可以按一下任何貨幣的視窗,在另一個畫面的全螢幕檢視畫面中快速查看完整詳細資料。以下是我最近拍攝的相片,是在上次 YCY 血戰期間拍攝。這讓我完全措手不及,我只好用手遮住臉

作者將雙手放在驚慌的臉上,盯著假的加密貨幣交易平台。
Panicky,目睹 YCY 血戰。

您可以試用下方嵌入的示範,或在 Glitch 上查看原始碼

安全性和權限

Chrome 團隊根據「控管強大網頁平台功能的存取權」一文中定義的核心原則,設計並實作了 Window Management API,包括使用者控制、透明度和人體工學。Window Management API 會公開與裝置連線的螢幕相關的新資訊,增加使用者的指紋表面,特別是那些與裝置持續連線的多螢幕使用者。為了緩解這項隱私權疑慮,我們僅公開常見刊登位置用途所需的最低必要畫面屬性。網站必須取得使用者授權,才能取得多螢幕資訊,並在其他螢幕上放置視窗。雖然 Chromium 會傳回詳細的畫面標籤,但瀏覽器可以自由傳回較不具描述性的標籤 (甚至是空白標籤)。

使用者控制項

使用者可以完全控制設定的曝光程度。他們可以接受或拒絕權限提示,並透過瀏覽器的網站資訊功能撤銷先前授予的權限。

企業控管

Chrome Enterprise 使用者可以控制 Window Management API 的多個層面,如 Atomic Policy Groups 設定的相關部分所述。

透明度

瀏覽器的網站資訊會顯示是否已授予使用 Window Management API 的權限,您也可以透過 Permissions API 查詢這項資訊。

權限持續性

瀏覽器會持續授予權限。您可以透過瀏覽器的網站資訊撤銷權限。

意見回饋

Chrome 團隊希望瞭解你使用 Window Management API 的體驗。

請告訴我們 API 設計

API 是否有任何功能無法正常運作?或者,您是否缺少實作想法所需的方法或屬性?對於安全性模型有疑問或意見嗎?

  • 在對應的 GitHub 存放區中提出規格問題,或在現有問題中加入您的想法。

回報實作問題

你是否發現 Chrome 實作項目有錯誤?或者實作方式與規格不同?

  • 請前往 new.crbug.com 提交錯誤。請務必提供盡可能多的詳細資料、重現問題的簡單操作說明,並在「Components」方塊中輸入「Blink>Screen>MultiScreenGlitch 可讓您輕鬆快速地分享重現內容。

顯示對 API 的支援

您打算使用 Window Management API 嗎?你的公開支持有助於 Chrome 團隊決定功能的優先順序,並向其他瀏覽器供應商顯示支援這些功能的重要性。

實用連結

特別銘謝

窗口管理 API 規格由 Victor CostanJoshua BellMike Wasserman 編輯。這個 API 是由 Mike WassermanAdrienne Walker 實作。本文由 Joe MedleyFrançois BeaufortKayce Basques 審查。感謝 Laura Torrent Puig 提供相片。