Finestre Modali e Dialog
Electron offre sia finestre modali personalizzate che dialog nativi del sistema operativo per interagire con l’utente in modo familiare e coerente con la piattaforma.
Finestre Modali
Una finestra modale blocca l’interazione con la finestra genitore finché non viene chiusa:
function createModalWindow(parentWindow) {
const modal = new BrowserWindow({
width: 500,
height: 350,
parent: parentWindow, // Obbligatorio per le modali
modal: true, // Blocca la finestra genitore
show: false,
resizable: false,
minimizable: false,
maximizable: false,
webPreferences: {
preload: path.join(__dirname, 'preload.js'),
},
});
modal.loadFile('modal.html');
modal.once('ready-to-show', () => {
modal.show();
});
return modal;
}
Dialog Nativi
Il modulo dialog fornisce accesso ai dialog nativi del sistema operativo.
Dialog Apri File
const { dialog } = require('electron');
async function openFile(parentWindow) {
const result = await dialog.showOpenDialog(parentWindow, {
title: 'Seleziona un file',
defaultPath: app.getPath('documents'),
buttonLabel: 'Apri',
filters: [
{ name: 'Documenti', extensions: ['txt', 'md', 'doc'] },
{ name: 'Immagini', extensions: ['png', 'jpg', 'gif'] },
{ name: 'Tutti i file', extensions: ['*'] },
],
properties: [
'openFile', // Seleziona file
// 'openDirectory', // Seleziona cartelle
// 'multiSelections',// Selezione multipla
// 'showHiddenFiles',// Mostra file nascosti
],
});
if (result.canceled) return null;
return result.filePaths[0];
}
Dialog Salva File
async function saveFile(parentWindow, content) {
const result = await dialog.showSaveDialog(parentWindow, {
title: 'Salva file',
defaultPath: path.join(app.getPath('documents'), 'documento.txt'),
buttonLabel: 'Salva',
filters: [
{ name: 'File di testo', extensions: ['txt'] },
{ name: 'Markdown', extensions: ['md'] },
],
});
if (result.canceled) return false;
const fs = require('node:fs');
fs.writeFileSync(result.filePath, content, 'utf-8');
return true;
}
Dialog Messaggi
// Messaggio informativo
async function showInfo(parentWindow) {
await dialog.showMessageBox(parentWindow, {
type: 'info', // 'none', 'info', 'error', 'question', 'warning'
title: 'Informazione',
message: 'Operazione completata',
detail: 'Il file è stato salvato con successo.',
buttons: ['OK'],
});
}
// Conferma con scelta
async function showConfirm(parentWindow) {
const result = await dialog.showMessageBox(parentWindow, {
type: 'question',
title: 'Conferma',
message: 'Vuoi salvare le modifiche?',
detail: 'Le modifiche non salvate andranno perse.',
buttons: ['Salva', 'Non salvare', 'Annulla'],
defaultId: 0, // Bottone selezionato di default
cancelId: 2, // Bottone che funge da "annulla"
});
// result.response è l'indice del bottone premuto
switch (result.response) {
case 0: return 'save';
case 1: return 'discard';
case 2: return 'cancel';
}
}
// Errore
async function showError(parentWindow, error) {
await dialog.showMessageBox(parentWindow, {
type: 'error',
title: 'Errore',
message: 'Si è verificato un errore',
detail: error.message,
buttons: ['OK'],
});
}
Dialog Errore (Sincrono)
Per errori critici che devono bloccare l’esecuzione:
// Questo dialog è sincrono e può essere usato anche prima che app sia ready
dialog.showErrorBox('Errore Critico', 'L\'applicazione non può essere avviata.');
Integrazione con IPC
Esponi i dialog al renderer tramite il pattern IPC standard:
// main.js
ipcMain.handle('dialog:openFile', async (event) => {
const win = BrowserWindow.fromWebContents(event.sender);
return openFile(win);
});
ipcMain.handle('dialog:saveFile', async (event, content) => {
const win = BrowserWindow.fromWebContents(event.sender);
return saveFile(win, content);
});
ipcMain.handle('dialog:confirm', async (event, options) => {
const win = BrowserWindow.fromWebContents(event.sender);
const result = await dialog.showMessageBox(win, {
type: options.type || 'question',
title: options.title,
message: options.message,
buttons: options.buttons || ['OK', 'Annulla'],
defaultId: 0,
cancelId: 1,
});
return result.response;
});
// preload.js
contextBridge.exposeInMainWorld('electronAPI', {
openFile: () => ipcRenderer.invoke('dialog:openFile'),
saveFile: (content) => ipcRenderer.invoke('dialog:saveFile', content),
confirm: (options) => ipcRenderer.invoke('dialog:confirm', options),
});
// renderer.js
document.getElementById('delete-btn').addEventListener('click', async () => {
const response = await window.electronAPI.confirm({
type: 'warning',
title: 'Conferma eliminazione',
message: 'Sei sicuro di voler eliminare questo elemento?',
buttons: ['Elimina', 'Annulla'],
});
if (response === 0) {
// L'utente ha confermato l'eliminazione
deleteItem();
}
});
Dialog Seleziona Cartella
async function selectDirectory(parentWindow) {
const result = await dialog.showOpenDialog(parentWindow, {
title: 'Seleziona cartella',
properties: ['openDirectory', 'createDirectory'],
});
if (result.canceled) return null;
return result.filePaths[0];
}
Conferma Prima della Chiusura
Un pattern comune è chiedere conferma quando l’utente tenta di chiudere l’app con modifiche non salvate:
let hasUnsavedChanges = false;
ipcMain.on('content:changed', () => {
hasUnsavedChanges = true;
});
ipcMain.on('content:saved', () => {
hasUnsavedChanges = false;
});
mainWindow.on('close', async (event) => {
if (hasUnsavedChanges) {
event.preventDefault();
const result = await dialog.showMessageBox(mainWindow, {
type: 'warning',
title: 'Modifiche non salvate',
message: 'Hai modifiche non salvate. Cosa vuoi fare?',
buttons: ['Salva e chiudi', 'Chiudi senza salvare', 'Annulla'],
defaultId: 0,
cancelId: 2,
});
if (result.response === 0) {
// Salva e chiudi
mainWindow.webContents.send('save-and-close');
} else if (result.response === 1) {
// Chiudi senza salvare
hasUnsavedChanges = false;
mainWindow.close();
}
// Se Annulla, non fare nulla (la finestra resta aperta)
}
});