Реализация нативного меню и системного трея в Electron/Tauri приложении

Наша компания занимается разработкой, поддержкой и обслуживанием сайтов любой сложности. От простых одностраничных сайтов до масштабных кластерных систем построенных на микро сервисах. Опыт разработчиков подтвержден сертификатами от вендоров.

Разработка и обслуживание любых видов сайтов:

Информационные сайты или веб-приложения
Сайты визитки, landing page, корпоративные сайты, онлайн каталоги, квиз, промо-сайты, блоги, новостные ресурсы, информационные порталы, форумы, агрегаторы
Сайты или веб-приложения электронной коммерции
Интернет-магазины, B2B-порталы, маркетплейсы, онлайн-обменники, кэшбэк-сайты, биржи, дропшиппинг-платформы, парсеры товаров
Веб-приложения для управления бизнес-процессами
CRM-системы, ERP-системы, корпоративные порталы, системы управления производством, парсеры информации
Сайты или веб-приложения электронных услуг
Доски объявлений, онлайн-школы, онлайн-кинотеатры, конструкторы сайтов, порталы предоставления электронных услуг, видеохостинги, тематические порталы

Это лишь некоторые из технических типов сайтов, с которыми мы работаем, и каждый из них может иметь свои специфические особенности и функциональность, а также быть адаптированным под конкретные потребности и цели клиента

Предлагаемые услуги
Показано 1 из 1 услугВсе 2065 услуг
Реализация нативного меню и системного трея в Electron/Tauri приложении
Средняя
~2-3 рабочих дня
Часто задаваемые вопросы

Наши компетенции:

Этапы разработки

Последние работы

  • image_website-b2b-advance_0.png
    Разработка сайта компании B2B ADVANCE
    1262
  • image_web-applications_feedme_466_0.webp
    Разработка веб-приложения для компании FEEDME
    1171
  • image_websites_belfingroup_462_0.webp
    Разработка веб-сайта для компании БЕЛФИНГРУПП
    874
  • image_ecommerce_furnoro_435_0.webp
    Разработка интернет магазина для компании FURNORO
    1094
  • image_crm_enviok_479_0.webp
    Разработка веб-приложения для компании Enviok
    831
  • image_bitrix-bitrix-24-1c_fixper_448_0.png
    Разработка веб-сайта для компании ФИКСПЕР
    851

Реализация нативного меню и системного трея в Electron/Tauri приложении

Нативное меню и системный трей — элементы, которые интегрируют приложение в среду ОС. Правильная реализация делает приложение «своим» на каждой платформе; неправильная — выглядит как веб-страница в рамке.

Electron: нативное меню

Меню в Electron строится из MenuItem объектов и устанавливается через Menu.setApplicationMenu.

// main/menu.js
const { Menu, MenuItem, app, shell } = require('electron');

function createAppMenu(mainWindow) {
  const isMac = process.platform === 'darwin';

  const template = [
    // macOS: первый пункт — имя приложения
    ...(isMac ? [{
      label: app.name,
      submenu: [
        { role: 'about' },
        { type: 'separator' },
        { role: 'services' },
        { type: 'separator' },
        { role: 'hide' },
        { role: 'hideOthers' },
        { role: 'unhide' },
        { type: 'separator' },
        { role: 'quit' }
      ]
    }] : []),

    {
      label: 'Файл',
      submenu: [
        {
          label: 'Новый',
          accelerator: 'CmdOrCtrl+N',
          click: () => mainWindow.webContents.send('menu:new')
        },
        {
          label: 'Открыть...',
          accelerator: 'CmdOrCtrl+O',
          click: async () => {
            const { dialog } = require('electron');
            const result = await dialog.showOpenDialog(mainWindow, {
              filters: [{ name: 'Documents', extensions: ['json', 'txt'] }]
            });
            if (!result.canceled) {
              mainWindow.webContents.send('menu:open', result.filePaths[0]);
            }
          }
        },
        { type: 'separator' },
        isMac ? { role: 'close' } : { role: 'quit' }
      ]
    },

    {
      label: 'Правка',
      submenu: [
        { role: 'undo' },
        { role: 'redo' },
        { type: 'separator' },
        { role: 'cut' },
        { role: 'copy' },
        { role: 'paste' },
        { role: 'selectAll' }
      ]
    },

    {
      label: 'Вид',
      submenu: [
        { role: 'reload' },
        { type: 'separator' },
        { role: 'resetZoom' },
        { role: 'zoomIn' },
        { role: 'zoomOut' },
        { type: 'separator' },
        { role: 'togglefullscreen' }
      ]
    },

    {
      label: 'Помощь',
      role: 'help',
      submenu: [
        {
          label: 'Документация',
          click: () => shell.openExternal('https://docs.your-app.com')
        },
        {
          label: `Версия ${app.getVersion()}`,
          enabled: false
        }
      ]
    }
  ];

  const menu = Menu.buildFromTemplate(template);
  Menu.setApplicationMenu(menu);
  return menu;
}

module.exports = { createAppMenu };

Electron: контекстное меню

Контекстное меню по правому клику строится аналогично, но показывается через menu.popup():

// main/ipc-handlers.js
const { ipcMain, Menu, BrowserWindow } = require('electron');

ipcMain.on('context-menu:show', (event, params) => {
  const win = BrowserWindow.fromWebContents(event.sender);

  const template = [
    {
      label: 'Копировать',
      enabled: params.hasSelection,
      click: () => event.sender.copy()
    },
    {
      label: 'Вставить',
      click: () => event.sender.paste()
    },
    { type: 'separator' },
    ...(params.isEditable ? [{
      label: 'Проверка орфографии',
      type: 'checkbox',
      checked: params.spellCheckEnabled,
      click: () => event.sender.send('toggle-spellcheck')
    }] : []),
    {
      label: 'Инспектор (dev)',
      click: () => event.sender.inspectElement(params.x, params.y),
      visible: process.env.NODE_ENV === 'development'
    }
  ];

  Menu.buildFromTemplate(template).popup({ window: win });
});
// renderer — вызов из фронтенда
window.addEventListener('contextmenu', (e) => {
  e.preventDefault();
  window.electronAPI.showContextMenu({
    x: e.x,
    y: e.y,
    hasSelection: window.getSelection()?.toString().length > 0,
    isEditable: e.target.matches('input, textarea, [contenteditable]')
  });
});

Electron: системный трей

// main/tray.js
const { Tray, Menu, nativeImage, app } = require('electron');
const path = require('path');

let tray = null;

function createTray(mainWindow) {
  const iconPath = process.platform === 'darwin'
    ? path.join(__dirname, '../resources/tray-icon-mac.png') // Template image (чёрно-белая)
    : path.join(__dirname, '../resources/tray-icon.png');

  tray = new Tray(nativeImage.createFromPath(iconPath));
  tray.setToolTip('My Application');

  const contextMenu = Menu.buildFromTemplate([
    {
      label: 'Открыть',
      click: () => {
        mainWindow.show();
        mainWindow.focus();
      }
    },
    {
      label: 'Статус: активен',
      enabled: false,
      id: 'status-item'
    },
    { type: 'separator' },
    {
      label: 'Настройки',
      click: () => {
        mainWindow.show();
        mainWindow.webContents.send('navigate', '/settings');
      }
    },
    { type: 'separator' },
    {
      label: 'Выйти',
      click: () => {
        app.isQuitting = true;
        app.quit();
      }
    }
  ]);

  tray.setContextMenu(contextMenu);

  // Клик по иконке (не правый клик)
  tray.on('click', () => {
    mainWindow.isVisible() ? mainWindow.hide() : mainWindow.show();
  });

  // Скрывать в трей вместо закрытия
  mainWindow.on('close', (event) => {
    if (!app.isQuitting) {
      event.preventDefault();
      mainWindow.hide();
    }
  });

  return tray;
}

function updateTrayStatus(text) {
  const menu = tray.getContextMenu();
  // Перестроить меню с новым статусом
  // (Electron не позволяет менять label у существующего MenuItem напрямую)
}

module.exports = { createTray };

На macOS иконка трея должна быть «Template image» — PNG 16×16 с тёмными пикселями на прозрачном фоне. Система сама инвертирует цвет под тёмную/светлую тему.

Tauri: нативное меню

// src-tauri/src/lib.rs
use tauri::menu::{Menu, MenuItem, Submenu, PredefinedMenuItem};

pub fn run() {
    tauri::Builder::default()
        .setup(|app| {
            let handle = app.handle();

            let file_menu = Submenu::with_items(handle, "Файл", true, &[
                &MenuItem::with_id(handle, "new", "Новый", true, Some("CmdOrCtrl+N"))?,
                &MenuItem::with_id(handle, "open", "Открыть...", true, Some("CmdOrCtrl+O"))?,
                &PredefinedMenuItem::separator(handle)?,
                &PredefinedMenuItem::quit(handle, Some("Выйти"))?,
            ])?;

            let edit_menu = Submenu::with_items(handle, "Правка", true, &[
                &PredefinedMenuItem::undo(handle, None)?,
                &PredefinedMenuItem::redo(handle, None)?,
                &PredefinedMenuItem::separator(handle)?,
                &PredefinedMenuItem::cut(handle, None)?,
                &PredefinedMenuItem::copy(handle, None)?,
                &PredefinedMenuItem::paste(handle, None)?,
                &PredefinedMenuItem::select_all(handle, None)?,
            ])?;

            let menu = Menu::with_items(handle, &[&file_menu, &edit_menu])?;
            app.set_menu(menu)?;

            Ok(())
        })
        .on_menu_event(|app, event| {
            match event.id().as_ref() {
                "new" => { app.emit("menu:new", ()).unwrap(); }
                "open" => { app.emit("menu:open", ()).unwrap(); }
                _ => {}
            }
        })
        .run(tauri::generate_context!())
        .expect("error while running app");
}

Tauri: системный трей

# src-tauri/Cargo.toml
[dependencies]
tauri = { version = "2", features = ["tray-icon"] }
use tauri::{
    tray::{TrayIconBuilder, TrayIconEvent, MouseButton},
    menu::{Menu, MenuItem, PredefinedMenuItem},
    Manager,
};

fn setup_tray(app: &tauri::App) -> tauri::Result<()> {
    let handle = app.handle();

    let quit = MenuItem::with_id(handle, "quit", "Выйти", true, None::<&str>)?;
    let show = MenuItem::with_id(handle, "show", "Открыть", true, None::<&str>)?;
    let menu = Menu::with_items(handle, &[&show, &PredefinedMenuItem::separator(handle)?, &quit])?;

    TrayIconBuilder::with_id("main-tray")
        .tooltip("My Application")
        .icon(app.default_window_icon().unwrap().clone())
        .menu(&menu)
        .on_menu_event(|app, event| match event.id().as_ref() {
            "quit" => app.exit(0),
            "show" => {
                if let Some(window) = app.get_webview_window("main") {
                    let _ = window.show();
                    let _ = window.set_focus();
                }
            }
            _ => {}
        })
        .on_tray_icon_event(|tray, event| {
            if let TrayIconEvent::Click { button: MouseButton::Left, .. } = event {
                let app = tray.app_handle();
                if let Some(window) = app.get_webview_window("main") {
                    if window.is_visible().unwrap_or(false) {
                        let _ = window.hide();
                    } else {
                        let _ = window.show();
                        let _ = window.set_focus();
                    }
                }
            }
        })
        .build(app)?;

    Ok(())
}

Поведение трея на разных ОС отличается: на macOS левый клик обычно показывает меню, на Windows — показывает/скрывает окно. Это стоит учитывать при проектировании — не стоит требовать правый клик для основного действия на macOS.