בניית תפריטים וסמל מגש מערכת מקוריים לאפליקציות שולחן עבודה
לקוח מבקש "משהו כמו טלגרם" — תפריט מקורי של מערכת ההפעלה וסמל מגש מערכת. יישמנו זאת ב-Electron וב-Tauri עבור תריסר פרויקטים, אבל בכל פעם אנחנו נתקלים בניואנסים של אינטגרציה בין פלטפורמות. לדוגמה, ב-macOS, סמל מגש ללא Template Image הופך לריבוע שחור; ב-Windows, תפריט ההקשר עשוי שלא להגיב ללחיצה שמאלית. במאמר זה, נפרק כיצד להימנע מבעיות אלה ולהפוך את האפליקציה שלך ל"מקורית" בכל מערכת.
תפריטים מקוריים ומגשי מערכת הם מה שהופכים מעטפת אינטרנט לאפליקציית שולחן עבודה מלאה. יישום נכון גורם לאפליקציה להרגיש בבית בכל פלטפורמה; יישום גרוע נראה כמו דף אינטרנט במסגרת. אנו מטפלים בפיתוח מלא: מאב-טיפוס ועד פרסום בחנות אפליקציות. המחיר נקבע באופן אישי לאחר ניתוח, אבל פרויקטים טיפוסיים מתחילים ב-$500, וחיסכון בזמן יכול להגיע עד 60% בהשוואה לביצוע עצמי. לדוגמה, לקוח אחד חסך $1,500 באמצעות הרכיבים המובנים שלנו.
כיצד להגדיר תפריט מקורי עבור 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 };
זוהי דוגמה טיפוסית של תפריט מקורי של Electron שעובדת בין פלטפורמות.
מדוע מגש המערכת דורש טיפול מיוחד ב-macOS?
ב-macOS, סמל המגש חייב להיות Template Image — PNG בגודל 16×16 עם פיקסלים כהים על רקע שקוף. המערכת הופכת את הצבע אוטומטית כדי להתאים לנושאים כהים/בהירים. ב-Windows, סמל רגיל עובד. אנו מבצעים אוטומציה של יצירת סמלים לכל הפלטפורמות. תהליך זה אורך עד יום אחד וחוסך עד 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 };
לדוגמת תפריט הקשר של Electron, האמור לעיל מדגים אינטגרציה של מגש.
Tauri: תפריט מקורי ומגש
ב-Tauri v2, התפריט בנוי עם // 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 }; , והמגש עם Template Image. קוד Rust מהדר לבינארי מקורי, כך שהתפריט המקורי עובד ללא עיכובים. Tauri נותן הקטנה של פי 10 בגודל הבינארי בהשוואה ל-Electron. ביצועי התפריט של Tauri הם מקוריים ללא השהיית IPC, מה שהופך אותו למהיר יותר מ-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");
}זה משמש כהפניה מוצקה ליישום מגש ב-Tauri.
השוואה בין Electron ו-Tauri עבור אלמנטים מקוריים
| פרמטר | Electron | Tauri (v2) |
|---|---|---|
| גודל בינארי | 150 MB+ (Chromium) | 5–15 MB (WebKit ב-macOS/Linux, Edge ב-Windows) |
| ביצועי תפריט | מקורי אך עם השהיית IPC | מקורי, קריאה ישירה ל-Rust |
| מורכבות יישום מגש | בינונית (אירועי Node.js) | בינונית (קריאות חוזרות ב-Rust) |
| תמיכה בין פלטפורמות | מצוינת (API אחיד) | טובה (ניואנסים מסוימים ב-Linux) |
| תמיכה בסמל Template ב-macOS | כן, דורש template | כן, דומה |
טעויות יישום נפוצות והפתרונות שלהן
בעיות נפוצות שראינו בפרויקטים
| בעיה | פתרון |
|---|---|
| סמל מגש ללא Template ב-macOS | הכן PNG בגודל 16×16 עם שקיפות; שם את הקובץ עם קידומת // 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::menu::Menu). |
| ערבוב בין IPC וקווי ממשק משתמש | בדוק תמיד אם החלון קיים לפני קריאה ל-TrayIconBuilder; השתמש ב-// 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"); } . |
| מחסור במטפל סגירת חלון | הגדר דגל Template וטפל באירוע iconTemplate.png עם show(). |
| התעלמות מתפקידי macOS | הכנס mainWindow.isDestroyed() כפריט התפריט הראשון והשתמש ב-app.isQuitting לפקודות מערכת סטנדרטיות. |
כיצד תפריטים מקוריים משפרים את חוויית המשתמש
תפריטים מקוריים מספקים קיצורי מקלדת מוכרים, תומכים בשירותי מערכת (לדוגמה, שירותי macOS), ומשתלבים עם VoiceOver/Narrator לנגישות. בפרויקט אחד לאפליקציה רפואית, יישמנו תפריט עם יותר מ-20 פריטים ומגש עם התראות — זה הפחית את זמן הפעולה הטיפוסי ב-30% בהשוואה לתפריט מותאם אישית. במקרה אחר, מעבר מ-Electron ל-Tauri קיצץ את גודל הבינארי מ-180 MB ל-12 MB והאיץ את השקת האפליקציה ב-40%. משוב לקוחות הצביע על שיעור שביעות רצון של 95% מיישום המגש.
מה כלול בעבודה
- תיעוד — תיאור מבנה התפריט, מטפלי אירועים, הוראות בניית סמלים.
- גישה — מאגר עם קוד, CI/CD לבנייה רב-פלטפורמית.
- העברת ידע — סקירת קוד, ייעוץ לשינויים עתידיים.
- תמיכה — אחריות לחודש על תיקוני באגים שהתגלו לאחר המסירה.
- ביקורת מצב נוכחי — בעת מעבר מ-Electron ל-Tauri או למסגרת אחרת.
תהליך העבודה בפרויקט
- ניתוח — איסוף דרישות לתפריט ולמגש, רשימת מקשי קיצור, התנהגות במערכות הפעלה שונות.
- עיצוב — הכנת מבנה התפריט, אבות טיפוס של סמלים, מטפלי אירועים.
- יישום — כתיבת קוד ב-JavaScript (Electron) או ב-Rust (Tauri), אינטגרציה עם הקצה האחורי.
- בדיקות — אימות ב-macOS, Windows, Linux; בדיקות יחידה לאירועי IPC ומגש.
- פריסה — בנייה לכל הפלטפורמות, פרסום בחנויות אם נדרש.
זמן הפיתוח הטיפוסי לתפריט מקורי ומגש הוא 2 עד 5 ימים בהתאם למורכבות. המחיר הסופי נקבע באופן אישי לאחר ניתוח הפרויקט. קבל ייעוץ לפרויקט שלך — נעזור לך לבחור מחסנית טכנולוגית ולהכין אב-טיפוס תוך יום אחד. יש לנו ניסיון של למעלה מ-5 שנים והשלמנו למעלה מ-30 פרויקטים עם אלמנטים מקוריים לשולחן עבודה. צור קשר כדי לדון בפרויקט שלך.







