Electron Adventures: 에피소드 34: 애플리케이션 메뉴

이전 에피소드에서 우리는 여러 명령으로 장난감 앱을 구현했습니다. 이러한 명령을 메뉴 모음에서도 사용할 수 있다면 좋지 않을까요?

글쎄, 이것은 예상보다 훨씬 더 많은 문제에 직면합니다.
  • 운영 체제(OSX와 다른 모든 것)는 응용 프로그램 메뉴에 대한 규칙이 크게 다르기 때문에 제대로 작동하려면 기본적으로 작업을 두 번 이상 수행해야 합니다
  • .
  • Electron에서 메뉴는 프론트엔드가 아닌 백엔드의 책임입니다! 즉, 모든 메뉴 상호 작용에 대해 둘 사이에서 메시지를 주고받아야 합니다
  • .
  • 프런트엔드의 상태에 따라 동적으로 메뉴를 업데이트하려면 무엇인가를 변경하려고 할 때마다 메뉴에 대한 업데이트를 백엔드로 계속 보내야 합니다
  • .
  • 메뉴에 추가할 방법이 없습니다. Menu.setApplicationMenu를 호출하면 종료, 복사, 붙여넣기, 다시 로드, 개발자 도구 등과 같은 유용한 작업이 있는 전체 기본 메뉴가 지워집니다.
  • Menu.getApplicationMenu는 수정할 수 있는 기본 메뉴를 반환하지 않습니다. 설정하지 않은 경우 null가 됩니다. 기본 메뉴에서 항목을 추가할 수 있는 방법이 없습니다. 망할 항목 전체를 교체해야 합니다. ! 이것은 부끄러운 일이며, Electron은 정말로 문제를 해결해야 합니다. 예, 결국 전체를 교체해야 하지만 이 시점에서 개발을 비참하게 만듭니다.
  • OSX의 메뉴에 단축키가 없으면 Cmd-C 또는 Cmd-Q와 같은 키보드 단축키가 더 이상 작동하지 않습니다! 이것은 다른 운영 체제가 작동하는 방식이 아니지만 OSX에서 실행하려면 여기에서 제대로 플레이해야 하며 Electron은 도움이 되지 않습니다. 문제를 무시할 수는 없습니다
  • .

    그래서 엄청난 두통.

    장점은 일단 문제를 해결하면 모든 응용 프로그램 명령을 메뉴에 넣고 모든 키보드 단축키 논리를 처리하도록 할 수 있다는 것입니다. 활성 응용 프로그램 바로 가기와 함께 보이지 않는 메뉴 항목을 추가하여 메뉴를 작게 유지하면서 바로 가기를 가질 수도 있지만 솔직히 Javascript에서 키보드 바로 가기를 처리하는 것은 로켓 과학이 아니므로 이 작업을 수행하지 않을 것입니다.

    메뉴 만들기



    I had to dig the default menu out of Electron source code 복사하여 붙여넣습니다. 기본적으로 npm 패키지도 있지만 이전 버전입니다.

    메뉴는 완전히 정적이므로 한 번만 설정하면 됩니다. 애플리케이션 상태에 따라 수정해야 한다면 이 코드는 훨씬 더 많은 작업을 수행해야 합니다.

    다음은 main/menu.js입니다.

    let { Menu } = require("electron")
    
    let isMac = process.platform === "darwin"
    let defaultMenuTemplate = [
      ...(isMac ? [{ role: "appMenu" }] : []),
      { role: "fileMenu" },
      { role: "editMenu" },
      { role: "viewMenu" },
      { role: "windowMenu" },
    ]
    
    let extraMenuTemplate = [
      {
        label: "Box",
        submenu: [
          {
            label: "Box 1",
            click: (item, window) => window.webContents.send("menuevent", "app", "changeBox", "box-1"),
          },
          {
            label: "Box 2",
            click: (item, window) => window.webContents.send("menuevent", "app", "changeBox", "box-2"),
          },
          {
            label: "Box 3",
            click: (item, window) => window.webContents.send("menuevent", "app", "changeBox", "box-3"),
          },
          {
            label: "Box 4",
            click: (item, window) => window.webContents.send("menuevent", "app", "changeBox", "box-4"),
          },
        ],
      },
      {
        label: "BoxEdit",
        submenu: [
          {
            label: "Cut",
            click: (item, window) => window.webContents.send("menuevent", "activeBox", "cut"),
          },
          {
            label: "Copy",
            click: (item, window) => window.webContents.send("menuevent", "activeBox", "copy"),
          },
          {
            label: "Paste",
            click: (item, window) => window.webContents.send("menuevent", "activeBox", "paste"),
          },
        ],
      },
    ]
    
    let menu = Menu.buildFromTemplate([
      ...defaultMenuTemplate,
      ...extraMenuTemplate ,
    ])
    
    module.exports = {menu}
    


    해당 이벤트가 바로 이벤트 버스로 가는 것처럼 보입니까? 네 그렇습니다!

    index.js




    let { app, BrowserWindow, Menu } = require("electron")
    let { menu } = require("./main/menu")
    
    function createWindow() {
      let win = new BrowserWindow({
        webPreferences: {
          preload: `${__dirname}/preload.js`,
        },
      })
      win.maximize()
      win.loadURL("http://localhost:5000/")
    }
    
    Menu.setApplicationMenu(menu)
    
    app.on("ready", createWindow)
    
    app.on("window-all-closed", () => {
      app.quit()
    })
    


    세 가지만 수정하면 됩니다.
  • menu에서 새 정적main/menu.js 가져오기
  • Menu에서 electron 수입
  • Menu.setApplicationMenu(menu)로 세트

  • preload.js



    이벤트를 목적지로 전달하기 전에 이벤트를 약간 반송해야 합니다. 따라서 먼저 사전 로드는 이벤트 핸들러를 설정하고 프런트엔드에 노출해야 합니다.

    let { contextBridge, ipcRenderer } = require("electron")
    
    let onMenuEvent = (callback) => {
      ipcRenderer.on("menuevent", callback)
    }
    
    contextBridge.exposeInMainWorld(
      "api", { onMenuEvent }
    )
    


    모든 메뉴 이벤트에 대한 핸들러가 하나뿐이므로 매우 간단합니다. 하지만 복잡하거나 동적인 작업을 수행했다면 다음과 같은 추가 코드가 필요합니다.

    contextBridge.exposeInMainWorld(
      "api", { onMenuEvent, setMenu }
    )
    


    src/App.svelte


    Keyboard 논리가 자체 구성 요소에 있는 것처럼 AppMenu도 마찬가지입니다. App는 구성 요소 트리에 추가하기만 하면 됩니다. 나머지 파일은 이전과 같습니다.

    <script>
      import AppMenu from "./AppMenu.svelte"
    </script>
    
    <div class="app">
      <Box id="box-1" />
      <Box id="box-2" />
      <Box id="box-3" />
      <Box id="box-4" />
      <Footer />
    </div>
    
    <Keyboard />
    <AppMenu />
    


    src/AppMenu.svelte



    그리고 마지막으로 menuevent 에 관심이 있다고 사전 로드에 알려야 합니다. 그런 다음 수신한 내용을 추가 처리 없이 바로 eventBus 로 보냅니다.

    <script>
      import { onMount, getContext } from "svelte"
      let { eventBus } = getContext("app")
    
      function handleMenuEvent(event, ...args) {
        eventBus.emit(...args)
      }
    
      onMount(() => {
        window.api.onMenuEvent(handleMenuEvent)
      })
    </script>
    


    앱에 따라 구성 요소가 마운트 해제될 때 일부 정리 단계를 추가해야 할 수도 있습니다. 우리는 여기서 하지 않을 것입니다.

    그것은 많은 작업이었지만 정적 기능이 있는 작은 메뉴를 위해 마침내 준비되었습니다!

    결과



    결과는 다음과 같습니다.



    다음 에피소드에서는 지난 10년 동안 최고의 UI 혁신인 명령 팔레트를 추가할 것입니다.

    평소와 같이 all the code for the episode is here .

    좋은 웹페이지 즐겨찾기