Реалізація нативного меню та системного трея в Electron/Tauri приложенні
Нативне меню та системний трей — елементи, які інтегрують приложение в середовище ОС. Правильна реалізація робить приложение «своїм» на кожній платформі; неправильна — виглядає як веб-сторінка в рамці.
Electron: нативне меню
Меню в Electron будується з MenuItem об'єктів та встановлюється через Menu.setApplicationMenu.
// main/menu.js
const { Menu, 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: 'Документи', 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: 'Помощь',
submenu: [
{
label: 'Документація',
click: () => shell.openExternal('https://docs.your-app.com')
}
]
}
];
const menu = Menu.buildFromTemplate(template);
Menu.setApplicationMenu(menu);
}
module.exports = { createAppMenu };
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')
: 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();
}
},
{ 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 };
На 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)?,
])?;
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};
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")
.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();
}
}
_ => {}
})
.build(app)?;
Ok(())
}
Поведінка трея на різних ОС відрізняється: на macOS лівий клік звичайно показує меню, на Windows — показує/приховує вікно. Врахуйте це при проектуванні.







