使用 Window Management API 管理多個螢幕

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

發布日期:2020 年 9 月 14 日

視窗管理 API

Window Management API 可讓您列舉連線至電腦的螢幕,並將視窗放置在特定螢幕上。

建議用途

可能使用這項 API 的網站包括:

  • 多視窗圖像編輯器 (如 Gimp) 可將各種編輯工具放在精確定位的視窗中。
  • 虛擬交易平台可在多個視窗中顯示市場趨勢,且所有視窗都能以全螢幕模式檢視。
  • 投影片應用程式可以在內部主螢幕上顯示演講者備忘稿,並透過外部投影機顯示簡報。

如何使用 Window Management API

經過時間考驗的視窗控制方法 Window.open() 無法辨識額外畫面,雖然這個 API 的某些方面似乎有點過時,例如 windowFeatures DOMString 參數,但多年來一直為我們提供良好的服務。如要指定視窗的位置,可以將座標分別傳遞為 left 和 top (或 screenX 和 screenY),並將所需大小分別傳遞為 width 和 height (或 innerWidth 和 innerHeight)。舉例來說,如要在距離左側 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
*/

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

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

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

popup.moveTo(2500, 50);

由於無法得知第二個螢幕的尺寸,因此這只是粗略的估計值。「資訊」window.screen只會顯示內建螢幕的資訊,不會顯示 iPad 螢幕的資訊。回報的內建螢幕 width 為 1680 像素,因此移至 2500 像素可能能將視窗移至 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 屬性。傳回 true 或 false。就我的設定而言,這會傳回 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 的其他層面 (即各種螢幕變更事件和 FullscreenOptions 的 screen 屬性) 永遠不會觸發,或會遭到不支援的瀏覽器忽略。

示範

如果您密切關注各種加密貨幣的發展,可以在應用程式中設定單一畫面,輕鬆監控市場。(我非常不希望這樣,因為我熱愛這個星球,但為了本文,請假設我希望如此。)

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

由於這是加密貨幣,市場隨時可能變得忙碌。如果發生這種情況,我可以在辦公桌上快速切換到多螢幕設定。我只要點選任一幣別的視窗,就能在對面螢幕的全螢幕檢視畫面中,快速查看完整詳細資料。這是我最近在上次 YCY 大屠殺期間拍的照片。我完全沒料到會發生這種事,當下雙手遮臉。

我驚慌失措,眼睜睜看著 YCY 慘跌。

試用示範,或在 GitHub 上查看原始碼。

安全性和權限

Chrome 團隊已根據「控管強大的網頁平台功能存取權」中定義的核心原則 (包括使用者控制、透明度和人體工學),設計及實作 Window Management API。Window Management API 會公開裝置連線螢幕的新資訊,增加使用者的指紋辨識介面,尤其是經常在裝置上連接多個螢幕的使用者。為減輕這項隱私權疑慮,公開的螢幕屬性僅限於常見刊登位置用途所需的最低限度。

網站必須取得使用者授權,才能取得多螢幕資訊,並在其他螢幕上放置視窗。Chromium 會傳回詳細的畫面標籤, 但瀏覽器可以傳回較不具描述性的標籤 (甚至是空白標籤)。

使用者控制項

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

企業控管

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

透明度

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

權限保留

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

意見回饋

API 是否有任何不符合預期的地方?或者,是否有缺少的屬性或方法,導致您無法實作想法?對安全模型有任何問題或意見嗎?

支援 API

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

資源

特別銘謝

Window Management API 規格是由 Victor Costan、Joshua Bell 和 Mike Wasserman 編輯。這個 API 是由 Mike Wasserman 和 Adrienne Walker 實作。本文由 Joe Medley、François Beaufort 和 Kayce Basques 審查。感謝 Laura Torrent Puig 提供相片。