使用 Window Management API 管理多個螢幕

取得已連線螢幕的相關資訊,並根據這些螢幕放置視窗。

視窗管理 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
*/

和大多數科技業人士一樣,我必須適應新的工作模式,並在家中設置個人辦公室。我的設定如下圖所示 (如有興趣,可以參閱完整設定詳細資料)。我的 iPad 透過隨行螢幕連線到 MacBook,因此需要時可以快速將 iPad 變成第二個螢幕。

校園長椅放在兩張椅子上。學校長椅上放著鞋盒,上面放著筆電,兩側則各放著一台 iPad。
多螢幕設定。

如果想善用大螢幕,我可以將上述程式碼範例中的彈出式視窗放到第二個螢幕上。我會這樣做:

popup.moveTo(2500, 50);

由於無法得知第二個螢幕的尺寸,因此這只是粗略的估計值。「資訊」應用程式 window.screen 只會顯示內建螢幕的資訊,不會顯示 iPad 螢幕的資訊。回報的內建螢幕 width1680 像素,因此移至 2500 像素可能能將視窗移至 iPad,因為知道 iPad 位於 MacBook 的右側。How can I do this in the general case? 事實證明,除了猜測,還有更好的方法。也就是 Window Management API。

特徵偵測

如要檢查是否支援 Window Management API,請使用:

if ('getScreenDetails' in window) {
  // The Window Management API is supported.
}

window-management 權限

使用 Window Management API 前,我必須先徵求使用者授權。window-management 權限可透過 Permissions API 查詢,如下所示:

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」是複數。)也就是說,每當插入或拔除新螢幕或現有螢幕 (如果是「螢幕共享」功能,則為實體或虛擬螢幕) 時,系統就會觸發事件。

請注意,您需要非同步查詢新畫面的詳細資料,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 屬性) 則永遠不會觸發,或會遭到不支援的瀏覽器忽略。

示範

如果你跟我一樣,一定會密切關注各種加密貨幣的發展。(事實上我非常愛護地球,但為了本文,請假設我並不愛護地球。)為了追蹤我擁有的加密貨幣,我開發了一個網頁應用程式,方便我在各種情況下觀察市場,例如在床上放鬆時,我有一個不錯的單螢幕設定。

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

由於這是加密貨幣,市場隨時可能變得忙碌。如果發生這種情況,我可以快速移到辦公桌,使用多螢幕設定。我可以點選任一貨幣的視窗,在對向螢幕的全螢幕檢視畫面中快速查看完整詳細資料。下圖是我在上次 YCY 大屠殺期間拍攝的近照。我完全措手不及,雙手還放在臉上

作者雙手捂著驚恐的臉,盯著虛假的加密貨幣交易平台。
目睹 YCY 大屠殺,驚慌失措。

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

安全性和權限

Chrome 團隊已根據「控管強大的網頁平台功能存取權」中定義的核心原則 (包括使用者控制、透明度和人體工學),設計及實作 Window Management API。視窗管理 API 會公開與裝置連線的螢幕相關新資訊,增加使用者的指紋辨識範圍,尤其是經常在裝置上連接多個螢幕的使用者。為減輕這項隱私權疑慮,公開的螢幕屬性僅限於常見刊登位置用途所需的最低限度。網站必須取得使用者授權,才能取得多螢幕資訊,並在其他螢幕上放置視窗。Chromium 會傳回詳細的畫面標籤,但瀏覽器可以傳回較不具描述性的標籤 (甚至是空白標籤)。

使用者控制項

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

企業控管

Chrome Enterprise 使用者可以控管 Window Management API 的多個層面,詳情請參閱原子政策群組設定的相關章節。

透明度

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

權限保留

瀏覽器會保留授予的權限。如要撤銷權限,請透過瀏覽器的網站資訊進行操作。

意見回饋

Chrome 團隊很想瞭解您使用 Window Management API 的體驗。

介紹 API 設計

API 是否有任何不符合預期的運作方式?或者,是否有缺少的屬性或方法需要實作,才能實現您的想法?對安全模型有任何問題或意見嗎?

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

回報導入問題

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

  • 前往 new.crbug.com 提出錯誤報告。請務必盡可能提供詳細資訊、重現問題的簡單操作說明,並在「Components」(元件) 方塊中輸入 Blink>Screen>MultiScreen

支援 API

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

實用連結

特別銘謝

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