Two ways to react on the Electron ‘close’ event

I have an Electron application that needs to save some data when it’s closed by the user (e.g. just after the user clicked on the “Close” button). For this example, we want to store the resolution (width and height of the application in pixels) to a config file. The next time, the application starts, these values should be read and the application resized accordingly.

As you probably know, Electron has one main process and several renderer process for the browser windows. The main.ts file hosts the main process implementation. There, we create a new BrowserWindow instance, defining a minimal size, initial width, and height.

The use of show: false first and win.show() later-on makes the startup smoother.

let win: BrowserWindow;

function createWindow() {
  win = new BrowserWindow({
    show: false,
    backgroundColor: '#FFFFFF',
    'height': 768,
    'width': 1024,
    'minHeight': 768,
    'minWidth': 1024
  });

  win.once('ready-to-show', () => {
    win.show();
  });

  win.on('closed', _ => {
    win = null;
  });
}

The screen size is saved to a yaml file and read into a model class at start-up.

export class ClientConfig {
    constructor(
        public screenWidth = 1024,
        public screenHeight = 768
    ) { }
}

In the following, I will present both approaches I tried.

Solution 1: Renderer Process

The BrowserWindow emits events when an App Command is invoked. The ones we are interested in are ‘close’ and ‘resize’. The BrowserWindow instance is part of the main electron process. From the renderer process, we can access it via the remote interface.

By adding a listener on the resize event, the screen resolution is written to a file every time the application is resized – which is a bit of an overhead. What we actually want to achieve is that these values are written once, right when the application is to be closed.

const { remote } = require('electron');

constructor(private clientConfig: ClientConfigService) {
    // write screen size to client config file
    remote.getCurrentWindow().on('resize', () => {
      const resolution = remote.getCurrentWindow().getSize();
      this.clientConfig.ScreenWidth = resolution[0] || 1024;
      this.clientConfig.ScreenHeight = resolution[1] || 768;
    });
}

 

The Electron API for Browser Window mentions a close event, but it seems this is done by the main process, not the renderer process. Listening on this event in the renderer process was not successful, the event was never triggered.

remote.getCurrentWindow().on('close', (e) => {
  // store screen width and height
});

 

On application start up, we read in our configuration from the configuration file. The BrowserWindow’s size can be set using the setSize() method.

ngOnInit(): void {
    // load screen size from client config file
    const width = this.clientConfig.ScreenWidth || 1024;
    const height = this.clientConfig.ScreenHeight || 768;
    remote.getCurrentWindow().setSize(width, height);
}

 

Solution 2: IPC between main and renderer process

The second approach is to use both the main and the renderer process. On the main process, the BrowserWindow listens on the close event. When it happens, it sends a message via webContents to the renderer process. It also prevents the application from being immediately closed by calling event.preventDefault().

win.on('close', (e) => {
    if (win) {
      e.preventDefault();
      win.webContents.send('app-close');
    }
});

 

The renderer process is always listening on IPC messages from the main process. The ipcRenderer module provides a few methods to send synchronous and asynchronous messages from the render (web page) to the main process. When it receives the app-close event notification, it saves the data, then sends the main process an IPC message (e.g. closed);

ipcRenderer.on('app-close', _ => {
      const resolution = remote.getCurrentWindow().getSize();
      this.clientConfig.ScreenWidth = resolution[0] || 1024;
      this.clientConfig.ScreenHeight = resolution[1] || 768;

      ipcRenderer.send('closed');
});

 

The main process has previously set a hook to listen to the renderer IPC messages (ipcMain.on), so when the closed message arrives, it finally closes the program (e.g. via app.quit()). The ipcMain module handles asynchronous and synchronous messages sent from a renderer process.

ipcMain.on('closed', _ => {
  win = null;
  if (process.platform !== 'darwin') {
    app.quit();
  }
});

Calling app.quit() sends another close event to the BrowserWindow, so it will loop unless you prevent it!

 

Overall, I prefer the second solution as it feels cleaner and the file is only written once and not every time the application is resized. This is also the way I implemented it.