Electron Linux 風格標題欄

Electron Linux 風格標題欄
Electron Linux 風格標題欄

本文將演示如何在 Electron 中快速實現一個標題欄,可以捲起,拖動,並帶你進一步瞭解 Electron 的 bug。

隱藏默認標題欄

在你創建 BrowserWindow 的方法中,指定以下參數:

1function createWindow() {
2  if (prod) Menu.setApplicationMenu(null);
3  mainWindow = new BrowserWindow({
4    titleBarStyle: dev ? 'default' : 'hiddenInset',
5    titleBarOverlay: true,
6    frame: false,
7    // ...

編寫自己的標題欄

現在默認的標題欄已經消失了,你應該編寫一個 div,作爲自己的標題欄。這個 div 和其它 div 沒什麼兩樣,除了它要支持以下三種東西:

  1. 可以拖動
  2. 可以捲起
  3. 有一個紅綠燈組件(捲起/放下,最大化,最小化,關閉)
1 <div className="title-bar">
2  <div className="logo-and-name"><img src="public/assets/icon.ico" alt="logo" />My Application</div>
3  <div className="traffic-light">
4    <MinMaxClose /> <!-- MinMaxClose 你可以自己實現,四個按鈕,捲起/放下,最大化,最小化,關閉 --> 
5  </div>
6</div>

拖動

編寫一個 Drag 組件,它的所有 children 都可以拖動。

 1import * as React from "react";
 2import { HTMLAttributes } from "react";
 3import nconsole from "_rutils/nconsole";
 4
 5interface DragProps extends HTMLAttributes<HTMLDivElement> {
 6  children: React.ReactNode;
 7}
 8
 9function Drag(props: DragProps) {
10  const { children, ...rest } = props;
11  const stopMove = () => {
12    window.ipcAPI?.initMoveWindow(false);
13  };
14
15  const startMove = (e: React.SyntheticEvent<HTMLDivElement>) => {
16    let elementDraggable = true;
17
18    if (e.target instanceof HTMLInputElement // 輸入框,按鈕等不能拖動,可以自由添加不希望拖動的組件
19      || e.target instanceof HTMLButtonElement
20      || e.target instanceof HTMLTextAreaElement
21    ) {
22      elementDraggable = false;
23    }
24
25    if (elementDraggable) {
26      window.ipcAPI?.initMoveWindow(true);
27      window.ipcAPI?.moveWindow();
28    }
29  };
30
31  return (
32    <div
33      {...rest}
34      onMouseDown={(e) => startMove(e)}
35      onMouseUp={(e) => stopMove()}
36    >
37      { children }
38    </div>
39  );
40}
41
42export default Drag;

這個window.ipcAPI?.initMoveWindow(true); window.ipcAPI?.moveWindow(); 是什麼呢?

1function initMoveWindow(moveState: boolean) {
2  ipcRenderer.send('window-move-init', moveState);
3}
4
5function moveWindow() {
6  ipcRenderer.send('window-move');
7}

使用 ipcMain 來控制窗口移動

來看看 window-move-init and window-move 做了什麼。

  1. window-move-init 會在你點擊標題欄的瞬間調用,它告訴 electron:“準備移動!”
  2. window-move 會在你鼠標移動的每一幀調用

把以下代碼放到你的 main.ts 或者其它能夠調用 ipcMain.on 的位置。

 1
 2let winStartPosition = { x: 0, y: 0 };
 3let mouseStartPosition = { x: 0, y: 0 };
 4let size = [0, 0];
 5let ready2move = false;
 6let movingInterval: string | number | NodeJS.Timeout | null | undefined;
 7
 8ipcMain.on("window-move-init", (e, moveState: boolean) => {
 9  if (moveState) {
10    const winPosition = win.getPosition();
11    winStartPosition = { x: winPosition[0], y: winPosition[1] };
12    mouseStartPosition = screen.getCursorScreenPoint();
13    size = win.getSize();
14  } else {
15    if (movingInterval) clearInterval(movingInterval);
16    movingInterval = null;
17  }
18  ready2move = moveState;
19});
20
21ipcMain.on("window-move", (e) => {
22  if (ready2move) {
23    if (movingInterval) {
24      clearInterval(movingInterval);
25    }
26    // 使用 setInterval 是爲了解決鼠標移動太快離開窗口,導致 mouseMove 事件丟失的問題
27    movingInterval = setInterval(() => {
28      // 實時更新位置
29      const cursorPosition = screen.getCursorScreenPoint();
30      const x = winStartPosition.x + cursorPosition.x - mouseStartPosition.x;
31      const y = winStartPosition.y + cursorPosition.y - mouseStartPosition.y;
32      // 你必須用 setBounds,而不能用 setPosition,否則窗口會慢慢變大,這就是我說的 electron 的 bug,至今不修復
33      win.setBounds({ // win 就是你的 mainWindow,本示例中沒有體現
34        x,
35        y,
36        width: size[0],
37        height: size[1],
38      });
39    }, 1); // 1ms 並不會導致你的 app 性能變慢
40  } else {
41    if (movingInterval) clearInterval(movingInterval);
42    movingInterval = null;
43  }
44});

最終爲了防止出現不可預料的問題,應該在點擊右鍵或者按 ESC 的時候,取消拖動

 1export function APP() {
 2  useEffect(() => {
 3    // 某些異常場合按 ESC 停止拖動
 4    const dragFallBack = (e: KeyboardEvent) => {
 5      if (e.key === "Escape") {
 6        window.ipcAPI?.initMoveWindow(false);
 7      }
 8    };
 9    window.addEventListener("keydown", dragFallBack);
10    return () => {
11      window.removeEventListener("keydown", dragFallBack);
12    };
13  }, []);
14  
15  return (
16     <section style={{ height: "100%" }} onContextMenu={() => window.ipcAPI?.initMoveWindow(false)}>
17  ) 
18}

好了,現在試試看拖動效果吧!

捲起

捲起比較簡單,和拖動同理,也是利用 ipcMain 來控制窗口。不同的是,捲起放下使用同一個按鈕,因此你應該記錄當前是捲起或者放下。

renderer

 1<div className="traffic-light"> <!-- 在標題欄中增加捲起/放下,最大化/最小化,關閉的邏輯 -->
 2  <MinMaxClose
 3    scrollButtonStatus={trafficLightScrollButtonStatus}
 4    onScrollClick={() => {
 5      window.ipcAPI?.titleScrollToggle();
 6    }}
 7    onMinimize={() => {
 8      window.ipcAPI?.mainWindowControl('minimize');
 9    }}
10    onMaximize={() => window.ipcAPI?.mainWindowControl('maximize')}
11    onClose={() => alertAndClose()}
12  />
13</div>

ipcMain 的實現

 1let sizeForScroll = [0, 0];
 2let alreadyScrollUp = false;
 3
 4ipcMain.handle("is-window-maximized", (e) => {
 5  return win.isMaximized();
 6});
 7
 8ipcMain.handle("is-window-scrolled-up", (e) => {
 9  return win.getSize()[1] <= 50;
10});
11
12const scroll = (method: string) => {
13  if (method === "up") {
14    if (alreadyScrollUp) {
15      return;
16    }
17    sizeForScroll = win.getSize();
18    // logger.log("current size: " + sizeForScroll[0] + ", " + sizeForScroll[1]);
19    win.setSize(sizeForScroll[0], titleBarHeight, true);
20    alreadyScrollUp = true;
21    return;
22  }
23  alreadyScrollUp = false;
24  // 使用當前寬度,和捲起前的高度
25  win.setSize(win.getSize()[0], sizeForScroll[1], true);
26};
27
28// 滾輪調用,會瞬間多次調用
29ipcMain.on("title-scroll", (e, method: string) => {
30  scroll(method);
31});
32
33// 按鈕調用
34ipcMain.on("title-scroll-toggle", (e) => {
35  if (alreadyScrollUp) {
36    // 放下
37    alreadyScrollUp = false;
38    // 使用當前寬度,和捲起前的高度
39    win.setSize(win.getSize()[0], sizeForScroll[1], true);
40    return;
41  }
42  // 捲起
43  sizeForScroll = win.getSize();
44  // logger.log("current size: " + sizeForScroll[0] + ", " + sizeForScroll[1]);
45  win.setSize(sizeForScroll[0], titleBarHeight, true);
46  alreadyScrollUp = true;
47});

完成

好了。以上並不是完整的代碼,但是它包含了一種解決問題的思路。到此,你應該可以實現封面圖中的效果了。