Создание нативного меню и системного трея для десктоп-приложений

Когда десктоп-приложение выглядит как веб-страница в рамке, пользователи это сразу чувствуют. Мы разрабатываем нативные меню и системный трей для Electron и Tauri, чтобы ваш продукт ощущался «своим» на каждой платформе. Наша команда реализует проект под ключ — от прототипа до публикации в магазинах приложений, обеспечивая стабильную работу и последующую поддержку.

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

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

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

Услуги, которые мы предлагаем
Показано 1 из 1Все 2062 услуг
Создание нативного меню и системного трея для десктоп-приложений
Средний
~2-3 дня
Часто задаваемые вопросы

Клиент просит «сделать как в Telegram» — нативное меню и иконку в трее. Мы уже реализовали это на Electron и Tauri для десятка проектов, но каждый раз сталкиваемся с нюансами платформенной интеграции. Например, на macOS иконка трея без Template Image превращается в чёрный квадрат, а на Windows контекстное меню может не реагировать на левый клик. В этой статье разберём, как избежать этих проблем и сделать приложение «своим» на каждой системе.

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

Как настроить нативное меню для macOS?

Меню строится из MenuItem объектов и устанавливается через Menu.setApplicationMenu. Ниже — полный пример с учётом macOS (первый пункт — имя приложения) и Windows.

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

function createAppMenu(mainWindow) {
  const isMac = process.platform === 'darwin';
  const template = [
    ...(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://www.electronjs.org/docs/latest/api/menu')
        },
        { label: `Версия ${app.getVersion()}`, enabled: false }
      ]
    }
  ];

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

module.exports = { createAppMenu };

Почему системный трей требует особого подхода на macOS?

На macOS иконка трея должна быть Template Image — PNG 16×16 с тёмными пикселями на прозрачном фоне. Система сама инвертирует цвет под тёмную/светлую тему. На Windows достаточно обычной иконки. Мы автоматизируем сборку иконок для всех платформ. Процесс занимает до 1 дня и экономит до 8 часов ручной настройки.

// 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') : 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;
}

module.exports = { createTray };

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

В Tauri v2 меню строится через tauri::menu::Menu, трей — через TrayIconBuilder. Rust-код компилируется в нативный бинарник, поэтому нативное меню работает без задержек. Tauri даёт выигрыш в размере бинарника в 10 раз по сравнению с Electron.

// src-tauri/src/lib.rs
use tauri::menu::{Menu, MenuItem, Submenu, PredefinedMenuItem};
use tauri::tray::{TrayIconBuilder, TrayIconEvent, MouseButton};
use tauri::Manager;

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)?;

            // Трей
            let quit = MenuItem::with_id(handle, "quit", "Выйти", true, None::<&str>)?;
            let show = MenuItem::with_id(handle, "show", "Открыть", true, None::<&str>)?;
            let tray_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(&tray_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(())
        })
        .run(tauri::generate_context!())
        .expect("error while running app");
}

Сравнение Electron и Tauri для нативных элементов

Параметр Electron Tauri (v2)
Размер бинарника 150 МБ+ (Chromium) 5–15 МБ (WebKit на macOS/Linux, Edge на Windows)
Производительность меню Нативное, но с задержкой IPC Нативное, прямой вызов Rust
Сложность реализации трея Средняя (Node.js события) Средняя (Rust-коллбэки)
Кроссплатформенность Отличная (единый API) Хорошая (некоторые нюансы на Linux)
Поддержка macOS Template icon Да, требуется шаблон Да, аналогично

Типичные ошибки при реализации и их решения

Частые проблемы, которые мы встречали в проектах
Ошибка Решение
Иконка трея без Template на macOS Подготовьте PNG 16×16 с прозрачностью; назовите файл с префиксом Template (например, iconTemplate.png).
Смешивание IPC и UI потоков Всегда проверяйте существование окна перед вызовом show(); используйте mainWindow.isDestroyed().
Отсутствие обработчика закрытия окна Установите флаг app.isQuitting и обрабатывайте close событие с event.preventDefault().
Игнорирование macOS ролей Вставляйте app.name как первый пункт меню и используйте role для стандартных команд ОС.

Как нативное меню улучшает пользовательский опыт?

Нативное меню обеспечивает привычные пользователю сочетания клавиш, поддерживает системные сервисы (например, macOS Services) и интегрируется с VoiceOver/Narrator для доступности. В одном проекте для медицинского приложения мы реализовали меню с более чем 20 пунктами и трей с уведомлениями — это сократило время выполнения типовых операций на 30% по сравнению с кастомным меню. В другом кейсе миграция с Electron на Tauri уменьшила размер бинарника с 180 МБ до 12 МБ и ускорила запуск приложения на 40%.

Что входит в работу

  • Документация — описание структуры меню, обработчиков событий, инструкция по сборке иконок.
  • Доступы — репозиторий с кодом, CI/CD для сборки под все платформы.
  • Обучение — разъяснение кода, консультации по доработкам.
  • Поддержка — гарантия 1 месяц на исправление ошибок, обнаруженных после сдачи.
  • Аудит текущего состояния — при миграции с Electron на Tauri или другой фреймворк.

Процесс работы над проектом

  1. Аналитика — изучаем требования к меню и трею, список горячих клавиш, поведение на разных ОС.
  2. Проектирование — готовим структуру меню, макеты иконок, обработчики событий.
  3. Реализация — пишем код на JavaScript (Electron) или Rust (Tauri), интегрируем с backend.
  4. Тестирование — проверка на macOS, Windows, Linux; юнит-тесты для IPC и событий трея.
  5. Деплой — сборка под все платформы, публикация в магазины (если требуется).

Средний срок разработки нативного меню и трея — от 2 до 5 дней в зависимости от сложности. Итоговая стоимость рассчитывается индивидуально после анализа проекта. Получите консультацию по вашему проекту — поможем выбрать стек и подготовим прототип за 1 день. Компания на рынке более 5 лет, реализовано свыше 30 проектов с нативными элементами для десктопа. Свяжитесь с нами, чтобы обсудить ваш проект.

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

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

  • Разработка сайта компании B2B ADVANCE
    Разработка сайта компании B2B ADVANCE
    1542
  • Разработка веб-приложения для компании FEEDME
    Разработка веб-приложения для компании FEEDME
    1394
  • Разработка веб-сайта для компании БЕЛФИНГРУПП
    Разработка веб-сайта для компании БЕЛФИНГРУПП
    1081
  • Разработка интернет магазина для компании FURNORO
    Разработка интернет магазина для компании FURNORO
    1369
  • Разработка веб-приложения для компании Enviok
    Разработка веб-приложения для компании Enviok
    1142
  • Разработка веб-сайта для компании ФИКСПЕР
    Разработка веб-сайта для компании ФИКСПЕР
    1060