The final result

Functional description

The first requirement from the product is to open the client and evoke a floating ball on the desktop. Suspension ball can carry out normal drag operation, double click to open the function panel to quickly perform some function operations in the client, right click to open the function menu to evoke the client and other operations.

Basic understanding of

Electron has actually been used in the past, but it’s basically just a way to package our project as a client, and we haven’t really used it in depth.

We all know that there are two processes in Electron, the main process and the render process. This project involves some manipulation of files, so it is no longer just a client-side shell for the project through the main process. The implementation of some functions, including the hover ball, requires communication between the two processes. Of course, we also need to plug into electron more APIS to fulfill our needs.

Implementation approach

A rough idea

  1. First of all, it is clear that the hover ball is also a page, which is different from other normal pages in the project and requires a specific window to open, and this window is transparent and without borders.
  2. Neither drag and drop, nor menu creation is possible in our rendering process, we need to communicate with the main process and implement our functions in the main process.

Specific implementation

Create a floating ball window

Regardless of the implementation of the function, we will create the floating ball first.

const { BrowserWindow, ipcMain, screen } = require('electron')
const path = require('path')

let win2 / / floating ball

function createSuspensionWindow() {
  win2 = new BrowserWindow({
      width: 120.height: 120.type: 'toolbar'.// Create a toolbar window
      frame: false.// To create a window without borders
      resizable: false.// Disable window sizing
      transparent: true.// Set transparency
      alwaysOnTop: true.// Whether the window is always displayed before other Windows
      webPreferences: {
        // nodeIntegration: true, // Whether to integrate node
        // contextIsolation: true, // Whether the context is isolated
        preload: path.join(__dirname, './preload.js') // Preload some of the node apis in electron to the window object}});// Set the initial position of the floating ball by getting the width and height of the user's screen
  const { left, top } = { left: electron.screen.getPrimaryDisplay().workAreaSize.width - 160.top: electron.screen.getPrimaryDisplay().workAreaSize.height - 160 }
  win2.setPosition(left, top) // Set the hover position
  
  win2.loadURL('Page address');

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

  win2.on('close'.() = > {
    win2 = null; })}Copy the code

Here we declare a function createSuspensionWindow to create a window with no border and a transparent background.

There’s a catch here: Electron has a lot of APIS that are implemented using the Node environment. The main process does the underlying calls through NodeJs so there’s no problem, but there’s always something wrong with the render process.

  • First question, in the React page I directly used require(‘electron’) to get the electron API. The most common solution on the web is to set the nodeIntegration parameter to true and inherit Node from the rendering process. This parameter defaults to false after Electron5.0, but I still get an error when I set true in the project. There may be other changes later, the electron update is too fast.

  • After that didn’t work, a new approach was found by writing a preload script, in which we can retrieve part of the electron API through require, and then we can hook this script and the internal logic of electron into webContents. The solution to violence seen online is to directly add the object obtained by require in the script to the window, silently, I also tried. As expected, I failed again. In the script, I did get it and add it to the Window, but accessing the Window object in the project didn’t get it. The reason is that the configuration item contextIsolation is set to true after electron12.0, which enables contextIsolation by default. Context Isolation

  • The official reason for enabling context isolation is also simple, and universally explained — for security purposes. There is also a new method, contextBridge, that exposes its API from context-isolated preloaded scripts to renderers. contextBridge

A simple preloaded script is also assigned here

const { contextBridge, ipcRenderer, shell } = require('electron');

contextBridge.exposeInMainWorld(
  'electronApi',
  {
    // Send a message
    send: (channel, data) = > {
      let validChannels = ["createSuspensionMenu"."suspensionWindowMove"."getFilePath"."downloadFile"];
      if(validChannels.includes(channel)) { ipcRenderer.send(channel, data); }},// Receive the message
    receive: (channel, func) = > {
      let validChannels = ["replyFilePath"];
      if (validChannels.includes(channel)) {
          ipcRenderer.on(channel, (event, ... args) = > func(event,...args));
      }
    },
    showItemInFolder: (url) = > {
      shell.showItemInFolder(url)
    }
  }
)
Copy the code

The suspension ball drags and moves

Floating ball page code:

import React, { Component } from 'react'

const { send } = window.electronApi

function moveEvent(e) {
  send('suspensionWindowMove', {x: e.screenX - biasX, y: e.screenY - biasY})
}

export default class Suspension extends Component {
  
  initSuspension = () = > {
    const suspensionDom = document.getElementsByClassName('suspension') [0]
    let biasX = 0;
    let biasY = 0;
    suspensionDom.addEventListener('mousedown'.function (e) {
      switch (e.button) {
        case 0:
          biasX = e.x;
          biasY = e.y;
          document.addEventListener('mousemove', moveEvent);
          break;
        case 2:
          send('createSuspensionMenu');
        break; }}); suspensionDom.addEventListener('mouseup'.function () {
      biasX = 0;
      biasY = 0;
      document.removeEventListener('mousemove', moveEvent)
    });
  }

  componentDidMount() {
    this.initSuspension()
  }

  render() {
    return (
      <div className="suspension">T</div>)}}Copy the code

Here we send the suspensionWindowMove message to the host process during mouse movement and pass the displacement of the move as a parameter to the host process. So what we’re going to do is pan the window in the main process.

Main process code:

const { ipcMain } = require('electron')
// Move the hover ball
ipcMain.on('suspensionWindowMove'.(event, message) = > {
  win2.setPosition(message.x, message.y)
});

Copy the code

Implement hover ball right click to create menu

Right clicking on the page above already passes the message createSuspensionMenu to the main process, so we just need to accept the message and create the menu in the main process.

const { app, ipcMain, Menu } = require('electron')

let win = null // Application window
let suspensionMenu = null // Hover the ball right click menu
let win2 = null // Hover window

// Create a hover right click menu
ipcMain.on('createSuspensionMenu'.(e) = > {
  if(! suspensionMenu) { suspensionMenu = Menu.buildFromTemplate([ {label: 'Open client'.click: () = > {
          if (win === null) { // Check whether the main window exists. If it is closed, create the main window
            createWindow()
          }
        }
      },
      {
        label: 'Close the hover ball'.click: () = > {
          win2.close()
        }
      },
      {
        label: 'Exit software'.click: () = >{ app.quit(); }},]); } suspensionMenu.popup({}); });Copy the code

Double-click to open the functions menu

It was a bit of a dead end at first, trying to create new Windows through communication, but it turned out to be too much trouble. On reflection, the functions of this menu are all business functions, and I don’t need to look far, I can implement it inside the page.

The final implementation is also in the page through CSS3 to display the function panel, and electron has nothing to do with the code.

conclusion

  • The electron API needs to be used in the rendering process, which can currently be implemented using a preloaded script.
  • Through the render processipcRendererAnd in the main processipcMainCommunication between two processes can be implemented to fulfill some of our business requirements.
  • There are many solutions that may not work in blogs and forum articles on the Internet, including the one used in this article, and the official documentation will prevail.

Thank you

During the process, I referred to some articles and questions that were very helpful to me. Here I would like to express my thanks to them.

Electron realizes the function of suspending ball

ContextBridge usage issues