Electron 無邊框視窗拖動
射手團隊成員 Sin 撰寫 ofollow,noindex">原文連結
作業系統原生的視窗樣式中規中矩,看久了卻難免會有些厭煩。所以在使用 electron
建立桌面應用的時候,有時候我們希望能完全掌控視窗的樣式,而隱藏掉系統提供的視窗邊框和標題欄等。
通過在建立視窗的時候,指定 {frame:false}
或 {titleBarStyle: 'hidden'}
(macOS only) 即可達到隱藏邊框的效果,甚至可以通過 {transparent:true}
來指定視窗透明,建立異形的視窗呈現。 具體可見官方文件 ,這裡不再贅述。

一個值得一提的問題是視窗的拖動。因為沒有標題欄,所以需要自行實現視窗的拖動區域,否則就沒法移動視窗位置了。可能的實現方案有下面幾種:
方案一: -webkit-app-region: drag;
官方文件裡有詳細說明:
預設情況下, 無框視窗是 non-draggable 的。 應用程式需要指定 `-webkit-app-region: drag` 在 CSS 中告訴Electron 哪個區域是可拖拽的 (像 OS 的標準標題欄),並且應用程式也可以使用 `-webkit-app-region: no-drag` 來排除 draggable region 中的 non-draggable 區域。
請注意, 當前只支援矩形形狀。要使整個視窗可拖拽,您可以新增 `-webkit-app-region: drag` 作為 `body` 的樣式: <body style="-webkit-app-region: drag"></body>
請注意,如果您已使整個視窗draggable,則必須將按鈕標記為 non-draggable,否則使用者將無法單擊它們: button { -webkit-app-region: no-drag; } 如果你設定自定義標題欄為 draggable,你也需要標題欄中所有的按鈕都設為 non-draggable。
試下來拖拽效果很完美。但是,文件後面提到了這種方法較為致命的一個問題:
在某些平臺上, 可拖拽區域將被視為 non-client frame, 因此當您右鍵單擊它時,系統選單將彈出。 要使上下文選單在所有平臺上都正確執行, 您永遠也不要在可拖拽區域上使用自定義上下文選單。
不僅右鍵選單,設定了這個樣式的元素幾乎無法響應所有的滑鼠事件,包括點選、拖拽等。如果需要拖拽整個視窗,就相當尷尬了。
方案二:通過響應頁面 mousemove
事件
既然我們需要頁面能夠響應滑鼠事件,那能不能就通過滑鼠事件去解決問題呢?這是作為一名前端開發人員很容易想到的方案:通過網頁的 mousemove
事件,我們可以得知當前滑鼠在網頁上的座標,並與上一次的座標進行比較,得出滑鼠的位移數值。
然後,我們可以通過當前 electron 視窗上的 getPosition
方法獲取視窗當前位置,加上滑鼠位移得出新的位置,然後通過 setPosition
方法手動移動視窗,達到拖動的效果。
是不是看上去很美?很可惜不能高興得太早。網頁上的兩次 mousemove
事件之間有一定時間間隔,這個間隔對於桌面客戶端程式設計來說有點太長了。這就導致在效能比較差的情況下,有可能出現這樣的情況:滑鼠移動過快,移出了視窗的範圍,而下一次 mousemove
還沒來得及觸發。這樣視窗就跟不上滑鼠,“掉下來”了……
方案三:electron-drag
在一度絕望的時候,發現了 electron-drag 這個庫和它天才的想法:通過一個原生 Node.js 模組,跟蹤滑鼠在整個螢幕上的位移,然後手動設定視窗的位置。
這個庫只在 Windows 和 macOS 下可用,不支援 Linux。因此,在不支援的平臺上,需要使用方案一或者方案二進行容。
一個小插曲
在 Windows 上引用 electron-drag
時可能會丟擲 Uncaught Error: A dynamic link library (DLL) initialization routine failed
的錯誤(macOS 上暫未遇到,不確定是否也有)。這是因為該庫使用 win-mouse
和 osx-mouse
這兩個原生模組進行滑鼠位置的追蹤,而 electron 和系統中安裝的 Node.js 程式標頭檔案未必相同,要使用原生模組必須使用正確版本的標頭檔案進行編譯。解決方式是安裝 electron-rebuild
重新編譯對應的模組。Windows 下的操作如下:
electron-rebuild -f -w win-mouse
最後上程式碼 (TypeScript):
export function makeDraggable(el: HTMLElement | string) { if (typeof el === 'string') { el = document.querySelector(el) as HTMLElement; } try { const drag = require('electron-drag'); if (drag.supported) { drag(el); } else { makeDraggableFallback(el); } } catch (ex) { makeDraggableFallback(el); } } function makeDraggableFallback(el: HTMLElement) { // 方案一 // el.style['-webkit-app-region'] = 'drag'; // 方案二 let dragging = false; let mouseX = 0; let mouseY = 0; el.addEventListener('mousedown', (e) => { dragging = true; const { pageX, pageY } = e; mouseX = pageX; mouseY = pageY; }); window.addEventListener('mouseup', () => { dragging = false; }); window.addEventListener('mousemove', (e: MouseEvent) => { if (dragging) { const { pageX, pageY } = e; const win = require('electron').remote.getCurrentWindow(); const pos = win.getPosition(); pos[0] = pos[0] + pageX - mouseX; pos[1] = pos[1] + pageY - mouseY; win.setPosition(pos[0], pos[1], true); } }); }