Essential desktop application attributes in Electron

This article describes the implementation of a few essential desktop application’s feature such as menu bar, dialog box and tray using electron.

Vijay KMS
RedBlackTree

--

Photo by Kelly Sikkema on Unsplash

Electron made life easier for a javascript developer to create a native desktop application. There are many articles and tutorials out there to teach how to start with Electron and incorporate popular javascript frameworks such as Angular, React in it. This article is not about the creation of Electron main script, Main process and Render process. Instead, here we are going to discuss the implementation of some essential attributes of the desktop application using Electron.

Menu Bar

A menu bar would be a crucial part of almost all desktop applications. We can create and customize the menu bar and its elements in Electron by using its inbuild API called Menu and its methods.

const { app, BrowserWindow, Menu } = require('electron');let mainWindow;
app.on('ready', () => {
mainWinow = new BrowserWindow({
webPreferences: {
nodeIntegration: true
}
});
mainWinow.loadURL(`file://${__dirname}/main.html`);
mainWinow.on('closed', () => { app.quit(); });
const mainMenu = Menu.buildFromTemplate(menuTemplate);
Menu.setApplicationMenu(mainMenu);
});

We can create the menu template as below and pass it as an argument into the buildFromTemplate method.

const menuTemplate = [
{
label: 'File',
submenu: [
{
label: 'open',
click() { onOpenClicked(); } //user-defined function
},
{
label: ‘save’,
click() { onSaveClicked(); } //user-defined function
},
{ type: 'separator' }, // Inbuild type to categorize item list
{ role: 'reload' } // Inbuild menu item to reload an application
]
}
];
const mainMenu = Menu.buildFromTemplate(menuTemplate);
Menu.setApplicationMenu(mainMenu);

We can customize the shortcut keys for the menu items by using the accelerator keyword.

const menuTemplate = [
{
label: 'File',
submenu: [
{
label: 'Quit',
accelerator: process.platform === 'win32' ? 'Ctrl+Q':'Command+Q',
click() { app.quit(); }

]
}
];

Note: process.platform === ‘darwin’ is for mac.

We can add some menu items such as developer-tool only for the development and can restrict in the production as below.

if (process.env.NODE_ENV !== 'production') {
menuTemplate.push({
label: 'View',
submenu: [
{
label: 'developer tool',
accelerator: process.platform === 'darwin' ?
'Command+Alt+I' : 'Ctrl+Shift+I',
click(item, focusedWindow) {
focusedWindow.toggleDevTools();
}

}
]
});
}

Dialog Box

Electron provides a ‘dialog’ API to use dilaog box in our application for opening and saving files, altering, show messages and errors. Let us see one by one.

Open dialog box

showOpenDialog and showOpenDialogSync are the methods used to select/open a file in a popup dialog window which will return the selected file path.

note: showOpenDialog returns Promise with an object containing canceled, filePaths and bookmarks properties.

/* html file */
<html>
<head></head>
<body>
<div>
<button type="button" onclick="onOpenFile()">Open file</button>
<button type="button" onclick="onSaveFile()">Save file</button>
</div>
<script>
const electron = require('electron');
const { ipcRenderer } = electron;

function onOpenFile() {
ipcRenderer.send('open file', 'open button clicked')
}
function onSaveFile() {
ipcRenderer.send('save file', 'Some content to write in a
file')
}

</script>
</body>
</html>

In the main Process

const { app, BrowserWindow, ipcMain, dialog } = require('electron');
let MainWindow;
app.on('ready', () => {
mainWinow = new BrowserWindow({
webPreferences: {
nodeIntegration: true
}
});
mainWinow.loadURL(`file://${__dirname}/main.html`);
mainWinow.on('closed', () => { app.quit(); });
});
ipcMain.on('open file', (event, todoOpen) => {
let options = {
title: 'open file',
buttonLabel: 'select file',
properties: ['openFile', 'multiSelections']
}
let filepath = dialog.showOpenDialogSync(options)

console.log("open return value: \n", filepath)
})

To explore the other options properties like defaultPath, filters, etc visit here.

Save Dialog Box

Similar to open dialog, showSaveDialog and showSaveDialogSync methods are used to implement the save dialog box. showSaveDialogSync returns the string on entering the file name whereas showSaveDialog return promise with an object containing canceled, filePaths and bookmarks properties.

ipcMain.on('save file', (event, data) => {
let options = {
title: "Save",
defaultPath: "c:/users/absolute/path",
buttonLabel: "Save as",
filters: [
{ name: 'All Files', extensions: ['*'] }
]
}
dialog.showSaveDialog(options).then(filename => {
const { canceled, filePath } = filename;
if (!canceled) {
fs.writeFileSync(filePath, data, err => {
if (err) {
console.log(err)
}
})
}
})
})

showSaveDialog which is an asynchronous version is recommended to avoid issues when expanding and collapsing the dialog on macOS.

Message Box

As above, the message box also has synchronous and asynchronous versions. showMessageBoxSync will return the integer-the index of the clicked button in the message box and showMessageBox will return a promise with an object containing response of type integer and checkboxChecked of type boolean which will be used if we set checkboxLabel in the message box.

dialog.showMessageBox({ 
title: "Alert Message!",
buttons: ['ok', 'report', 'know more'],
message: 'Hi there, this is an alert message',
checkboxLabel: 'I am check box',
checkboxChecked: true
}).then(msgProps => {
const { response, checkboxChecked } = msgProps
console.log("msgProps: \n", response, checkboxChecked)
})

Error Box

showErrorBox simply shows the error message to the user and returns nothing. It has only two properties such as title and content.

dialog.showErrorBox("Error Occured!", "errorMessage");

Tray

An application’s Tray can be found in the system notification area. Some important feature and shortcuts of our application can be added there for quick access. Let us see its implementation.

const electron = require('electron');
const path = require('path');
const { Tray } = electron;
const iconPath = path.join(__dirname, './desktop-icon.png');
myTray = new Tray(iconPath);
myTray.setToolTip('my app');

But before implementing Tray the app has to be ready, that is our main window has to be created first. Implementation of Tray should be inside app.on(‘ready’) function.

const electron = require('electron');
const path = require('path');
const { app, BrowserWindow, Menu, ipcMain, Tray } = electron;
let mainWindow;
app.on('ready', () => {
mainWindow = new BrowserWindow({
height: 480,
width:280,
resizable: false,
webPreferences: {
nodeIntegration: true
}
});
mainWindow.loadURL(`file://${__dirname}/main.html`);
mainWindow.on('closed', () => { app.quit(); });
const iconPath = path.join(__dirname, './windows-icon.png');
myTray = new Tray(iconPath);
myTray.setToolTip('my app');
myTray.on('click', onClick);
myTray.on('right-click', onRightClick);

});

The tray will emit the event on mouse click which will return the coordinates of the origin of the window rectangle. From that, we can manually set the position of our application’s tray.

function onClick(event, bounds) {
//click event bounds
const { x, y } = bounds;
//get window height and width
const { height, width } = mainWindow.getBounds();
if (mainWindow.isVisible()) {
mainWindow.hide();
} else {
const yPosition = process.platform === 'darwin' ? y : (y - height);
mainWindow.setBounds({
x: x - width / 2,
y: yPosition,
height: height,
width: width
});
mainWindow.show();
}
};

We can add the menu items on right click as below.

function onRightClick() {
const menuConfig = Menu.buildFromTemplate([
{
label: 'Quit',
click: () => {app.quit()}
}
]);
myTray.popUpContextMenu(menuConfig);
};

Conclusion

Creating a desktop application and its unavoidable attributes are so simple and facile in the Electron framework. I consider these three Electron APIs would essentially take part in most of the desktop applications. There are lots of other APIs available in Electron which we will discuss in the upcoming articles. Till then good luck with the Electron!

--

--