DEV Community

Cover image for Electron: Creating Tray Menu
Francisco Amorim
Francisco Amorim

Posted on

Electron: Creating Tray Menu

Heya!

In the previous part we prepared the environment of our application built using Electron and React with Typescript. If you haven't seen it yet, it's better to check Part 1 first.

Part 2: Electron: Tray Menu

In this part we will focus on creating the tray system icon, such as its interaction and menu.

The idea is the application as soon as it is executed open an icon in the system tray.

With the left click we will open a window, in this window we can see every alarm created, and will be able to turn it on and off. We will be able to create new alarms, defining the hour and minutes, giving a name, choosing a sound and also a message. And last we can delete them. 😜

With the right click we will have a quick menu, where we can open the main window, turn alarms on and off directly in this menu and we can also turn off the application.

Let's start to code!! 😎

The Tray Button

We will create a folder called electron inside the src folder, from now on all files that are directed to electron will be created inside this folder. We have also to create our file for the Tray menu, giving the name TrayMenu.ts.
Create an assets folder under src, add an icon for the application, I used a Clock in case you have no ideas (16x16px).

At this point our project must have the following structure:

Project Structure

To create our tray menu, we need to import from Electron the Tray and nativeImage:

.src/electron/TrayMenu.ts

import { app, Tray, Menu, nativeImage } from 'electron';

Why this four? 🤔

  • app is where the event lifcycle of our applicatin is controled;
  • Tray is a class used add icons and context menus;
  • Menu it's a class to create Menu's;
  • nativeImage will create a native image from a url path, it's necessary to create the tray icon for OSX;

.src/electron/TrayMenu.ts

import { app, Tray, Menu, nativeImage } from 'electron';

export class TrayMenu {
  // Create a variable to store our tray
  // Public: Make it accessible outside of the class;
  // Readonly: Value can't be changed
  public readonly tray: Tray;

  // Path where should we fetch our icon;
  private iconPath: string = '/assets/clock-icon.png';

  constructor() {
    this.tray = new Tray(this.iconPath);
  }
}

As you can see the tray constructor accepts a url path, but we want to create our icon with nativeImage and for that we will create a method that will handle the creation.

.src/electron/TrayMenu.ts

import { app, Tray, Menu, nativeImage } from 'electron';

export class TrayMenu {
  // Create a variable to store our tray
  // Public: Make it accessible outside of the class;
  // Readonly: Value can't be changed
  public readonly tray: Tray;

  // Path where should we fetch our icon;
  private iconPath: string = '/assets/clock-icon.png';

  constructor() {
    this.tray = new Tray(this.createNativeImage());
  }

  createNativeImage() {
    // Since we never know where the app is installed,
    // we need to add the app base path to it.
    const path = `${app.getAppPath()}${this.iconPath}`;
    const image = nativeImage.createFromPath(path);
    // Marks the image as a template image.
    image.setTemplateImage(true);
    return image;
  }
}

Now we just need to instantiate TrayMenu as soon as the application is ready, and for that we will go to the main.ts file to instantiate.

./src/main.ts

import { app, BrowserWindow } from 'electron';
import { TrayMenu } from '@/electron/TrayMenu';

const appElements: any = {
  tray: null,
  windows: []
};

app.on('ready', () => {
  appElements.tray = new TrayMenu();
});

Note: You can delete the createWindow method in it.

At the moment we have created a constant to store the application elements that we need to instantiate. But later we will create a Manager for that.

Why do we need this manager?

For example if we do this:

app.on('ready', () => {
  const tray = new TrayMenu();
});

The icon will disappear, as the garbage collector will delete the references that are in this scope.

Webpack Optimization

We need to install the Webpack Copy Plugin to copy the all assets to our dist folder.

For that run:

npm install copy-webpack-plugin --save-dev

After installing it add the following config to our webpack.config.js:

./webpack.config.js

const electronConfiguration = {
  ...
  plugins: [
    new CopyPlugin({
      patterns: [
        { from: 'src/assets', to: 'assets' },
      ],
    }),
  ],
};

Let's check out our progress,run:

npm start

Tray

The Menu

Just as we did to create nativeImage we will create a method to create the menu.

./src/electron/TrayMenu.ts

import { app, Tray, Menu, nativeImage } from 'electron';

export class TrayMenu {

  ...

  constructor() {
    this.tray = new Tray(this.createNativeImage());
    // We need to set the context menu to our tray
    this.tray.setContextMenu(this.createMenu());
  }

  ...

  createMenu(): Menu {
    // This method will create the Menu for the tray
    const contextMenu = Menu.buildFromTemplate([
      {
        label: 'Tokei',
        type: 'normal',
        click: () => { 
          /* Later this will open the Main Window */ 
        }
      },
      {
        label: 'Quit',
        type: 'normal',
        click: () => app.quit()
      }
    ]);
    return contextMenu;
  }
}

Explaining better what each Button will do:

  • The first one will open the alarms window, when clicking you should open the window but for now it will do nothing;
  • The 'Quit' button as the label indicates will be to close the application, for that we will execute a function of the application controller to close it;

If you run our application now and click on the icon, the menu will be shown

Tray with Menu

Conclusion

Right now we have our application running with an Icon tray and the menu.
I hope you are enjoying it, leave your comment on what I should improve! And if you have any doubts, just comment, I will try to answer quickly. 😁

Repository: Tokei

Part 3 - The Alarm Window

Top comments (0)