Cómo crear un menú multiplataforma en Electron.js

- Andrés Cruz

EN In english

Cómo crear un menú multiplataforma en Electron.js

Crear un menú en Electron.js es una de esas tareas que parece sencilla, como lo es, Habilitar la integración con Node… hasta que empiezas a añadir submenús, roles, atajos de teclado, canales IPC y, para rematar, compatibilidad multiplataforma. En mi caso, la forma en que trabajo los menús ha evolucionado mucho: desde simples templates hasta menús completos para proyectos reales, como un editor con funciones de abrir/guardar, negritas, cursivas y títulos H1/H2 enviados al renderer.

En esta guía te voy a contar cómo crear un menú en Electron.js paso a paso, con ejemplos listos para copiar/pegar y, sobre todo, con casos reales que uso en mis proyectos y en mis cursos.

menu
menu

Qué es un menú en Electron.js y cómo funciona el template

Un menú en Electron no es más que un array de objetos con una estructura predefinida. Así de simple. Cada objeto representa un elemento del menú y puede incluir propiedades como label, submenu, click, role, type o accelerator.

Es decir, cuando hablo de “template”, hablo literalmente de algo como:

const { Menu } = require('electron')
const template = [
 {
   label: 'About us',
   submenu: [
     { label: 'About the app', click() { /* ... */ } }
   ]
 }
];

Como puedes ver, la estructura de los menús no es más que un array de objetos con una estructura predefinida; podemos definir las opciones en el menú de dos tipos, personalizadas y con roles predefinidos como presentaremos en los siguientes apartados.

Cada menú está acompañado de un submenú en el cual indicamos los ítems del menú, que pueden ser personalizados o de roles predefinidos que veremos más adelante; en definitiva y como puedes ver en la estructura anterior, un menú está compuesto de un array, por lo tanto,  podemos colocar tantos ítems en nuestro menú como necesitemos; a su vez, la propiedad de submenus es un array asi que, puedes indicar tantos items como desees.

Estructura básica: array de objetos y submenús

El patrón siempre es:

  • Nivel 1 → Grupo principal (File, Edit, View, etc.)
  • Nivel 2 → Submenús (Guardar, Abrir, Copiar…)
  • Nivel 3 → Acciones o roles

Este modelo es idéntico en todas las plataformas, aunque macOS tiene algunas particularidades (sobre todo en el menú principal).

Diferencias entre opciones personalizadas y roles

Veamos los dos tipos de opciones que tenemos

Opciones personalizadas

La idea principal de los menús es que, podamos incluir opciones personalizadas para poder usar a lo largo de la aplicación;  por ende, se usan como un evento de tipo click en JavaScript asociado a un botón (por ejemplo) pero, en este caso, en vez de un botón, es una opción en un menú:

const { Menu, shell } = require('electron')

const template = [
   {
       role: 'About Electron',
       submenu: [
           {
               label: "About the app",
               click() {
                   shell.openExternal("https://www.electronjs.org")
               }
           },

       ]
   },
]

Con el shell, podemos abrir una página web en un enlace, como si se tratase de un enlace de navegación.

Su uso es muy sencillo, y son las que empleamos cuando queremos usar algún método personalizado.

Son acciones que defines tú. Por ejemplo, abrir una URL, enviar un evento al renderer o activar un modo oscuro.

Roles predefinidos

Con los roles ya contamos con funciones específicas que podemos reutilizar fácilmente:

{ role: 'reload' },
{ role: 'forceReload' },
{ role: 'toggleDevTools' },
{ role: 'resetZoom' },
{ role: 'zoomIn' },
{ role: 'zoomOut' },
{ role: 'togglefullscreen' }

Los cuales permiten:

  • reload, Recargar la página.
  • forceReload, Forzar la recarga de la página (vienen siendo un host reloading).
  • toggleDevTools, Activar la consola de desarrolladores del navegador.
  • resetZoom, Restablecer el zoom.
  • zoomIn, Zoom In, aumentar el zoom.
  • zoomOut, Zoom Out, Reducir el zoom.
  • togglefullscreen, Habilita el modo full screen de la aplicación.

También tenemos el separador para organizar los menús:

{
   type:'separator'
},

Son funciones nativas que Electron ya trae listas: reload, zoomIn, toggleDevTools, etc.

En un menú serio uso ambas. Por ejemplo, roles para cosas de sistema, y acciones personalizadas para abrir archivos o aplicar estilos.

Volviendo a la clasificación o opciones, tenemos dos:

Crear un menú personalizado en Electron (paso a paso)

Creamos un menú sencillo como:

menu.js

const { app, ipcMain, Menu, shell, BrowserWindow, globalShortcut } = require('electron')
const { open_file, save_file } = require("./editor-options")
const template = [
   {
       label: 'About us',
       click() {
           shell.openExternal("https://www.electronjs.org")
       }
   },
   {
       label: 'File',
       submenu: [
           {
               label: "Save",
               accelerator: 'CommandOrControl+Shift+S',
               click() {
                   const win = BrowserWindow.getFocusedWindow()
                   win.webContents.send('editorchannel', 'file-save')
               }
           },
           {
               label: "Open",
               accelerator: 'CommandOrControl+Shift+O',
               click() {
                   const win = BrowserWindow.getFocusedWindow()
                   open_file(win)
               }
           },
       ]
   },
   {
       label: 'Style and format',
       submenu: [
           {
               label: 'Bold',
               click() {
                   const win = BrowserWindow.getFocusedWindow()
                   win.webContents.send('editorchannel', 'style-bold')
                   console.log("Negritas")
               }
           },
           {
               label: 'Italic',
               click() {
                   const win = BrowserWindow.getFocusedWindow()
                   win.webContents.send('editor-channel', 'style-italic')
               }
           },
           {
               type: 'separator'
           },
           {
               label: 'H1',
               click() {
                   const win = BrowserWindow.getFocusedWindow()
                   win.webContents.send('editor-channel', 'style-h1')
               }
           },
           {
               label: 'H2',
               click() {
                   const win = BrowserWindow.getFocusedWindow()
                   win.webContents.send('editor-channel', 'style-h2')
               }
           },
       ]
   }
]
if (process.platform == 'win32' || process.platform == 'darwin') {
   template.push(
       {
           label: 'Default',
           submenu: [
               { role: 'reload' },
               { role: 'forceReload' },
               { role: 'toggleDevTools' },
               { role: 'resetZoom' },
               { role: 'zoomIn' },
               { role: 'zoomOut' },
               {
                   type: 'separator'
               },
               { role: 'togglefullscreen' },
           ]
       }
   )
}
ipcMain.on('editor-channel', (event, arg) => {
   console.log("Mensaje recibido del canal 'editor-channel': " + arg)
})
ipcMain.on('file-open', (event, arg) => {
   const win = BrowserWindow.getFocusedWindow()
   open_file(win)
})
ipcMain.on('file-save', (event, arg) => {
   const win = BrowserWindow.getFocusedWindow()
   save_file(win, arg)
})
app.on('ready', () => {
   globalShortcut.register('CommandOrControl+Shift+S', () => {
       const win = BrowserWindow.getFocusedWindow()
       win.webContents.send('editor-channel', 'file-save')
   })
   globalShortcut.register('CommandOrControl+Shift+O', () => {
       const win = BrowserWindow.getFocusedWindow()
       open_file(win)
   })
});
const menu = Menu.buildFromTemplate(template);
module.exports = menu

Para consumirlo, tenemos, desde el archivo principal:

const menu = require('./menu')
***
app.whenReady().then(createWindow)
Menu.setApplicationMenu(menu)

Y al ejecutar un:

npm run start

Tenemos:

menu
menu

FAQs sobre menús en Electron.js

  • ¿Dónde colocar el menu.js?
    • En el raíz del proyecto, junto al main.
  • ¿Cómo crear menús diferentes por plataforma?
    • Usando:
    • if (process.platform === 'darwin') { ... }
  • ¿Cómo combinar varios submenús sin romper el template?
    • Verifica siempre que cada submenu sea un array válido.

Conclusión

Crear un menú en Electron.js puede ser tan simple o tan avanzado como lo necesites. Lo bonito es que tienes un sistema que te permite mezclar roles nativos, acciones personalizadas, comunicación por IPC, atajos globales e integración con ventanas activas. En mi caso, uso estos patrones para aplicaciones reales —desde editores hasta utilidades— y siempre sigo el mismo flujo: template → archivo externo → roles + acciones → IPC → atajos → multiplataforma.

El siguiente paso es conocer como emplear los atajos de teclado en Electron.

Vamos a aprender a manejar los menus multiplataforma en Electron.js, conocer los tipos que son de acciones definidas por nosotros o de sistemas y recomendaciones.

Acepto recibir anuncios de interes sobre este Blog.

Andrés Cruz

EN In english