In previous lessons, we briefly went over some features of ipc, but have never really explained how ipc works. Now, we are going to uncover the power of ipc in depth through making a multi-windowed application.
In every electron app, there is a main process which runs in the background. It is not required for an electron app to have a window, but each additional window you create is going to have its own render process.
It should be apparent that we would like communications to occur betweeen
- Main process and a render process
- Two render processes
Unfortunately, communication between two render processes is not supported in Electron. If you want to communicate between two windows A and B, you have to use IPC to send a message from window A to the main process, and write code for the main process to forward that message to window B.
The APIs of IPC is straight forward. There are 3 scenarios to communicate via IPC.
This is the only way a window can send messages to the main process.
import ipcRenderer from electron:
// In renderer process (web page).
const {ipcRenderer} = require('electron')
When this window receives a message in channel asynchronous-reply
from the main process, we output the message to the console
ipcRenderer.on('asynchronous-reply', (event, arg) => {
console.log(arg) // prints "pong"
})
Send a message to the main process in channel asynchronous-message
ipcRenderer.send('asynchronous-message', 'ping')
Use ipcMain to listen for a message form a render process and optionally send a reply
import ipcMain from electron:
// In main process.
const { ipcMain } = require('electron')
When the main process receives a message from a render process in channel asynchronous-message
, we print the message to the console and send a response 'pong' through the channel asynchronous-reply
ipcMain.on('asynchronous-message', (event, arg) => {
console.log(arg) // prints "ping"
event.sender.send('asynchronous-reply', 'pong')
})
If you have a reference of a window in your main process, you can access the webContent of that window to initiate ipc to that window's render process
let win
be a reference to a newly create window
// In main process
let win = new BrowserWindow({width: 800, height: 600})
win.loadFile('src/index.html')
did-finish-load
is an even that is called when the window has finished loading the page. In this case, we want to send whoooooooh!
to that window's render process through channel ping
win.webContents.on('did-finish-load', () => {
win.webContents.send('ping', 'whoooooooh!')
})
Note: The remote API actually allows you to access the main electron API (i.e. creating windows, notifications, menus) inside your render processes. However, they are synchronous
and blocking, because they use a synchronous ipc under the hood. Using remote
is not recommended, because your application UI will freeze everytime you use it.
To make the development of multi-windowed applications easier, you should manage all windows of your application inside your main process by having a map to store the reference of each window. All creations and deletions of windows should be handled by the main process, and all messages between windows should be and can only be sent through the main process. Essentially, the main process acts as a package distribution center. Imagine if you want to send a package to a friend, you would give the package to Fedex, and Fedex will deliver that package to your friend's house.
In your main process, you should define an object to keep track of all windows in your application
let windows = {}
Define a createWindow
function to handle all windows creation and keep their references in windows
object we just created
function createWindow(params){
const win = new BrowserWindow(params)
let winId = win.id;
/*
Each window has a unique id.
We can use this unique id as the key to store the window reference in `windows` object
*/
windows[winId] = win
/*
This is fired when the window `win` is closed.
We want to remove the reference to this window from `windows` object
*/
win.on('closed', () => {
delete windows[winId]
})
// return the newly created window so we can modify it right after calling this function
return win;
}
We are going to create a cryptocurrency tracking app that allows users to create always-on-top floating windows
You are probably already familiar with the setup at this point. Navigate to lesson-5/starter-code
and run
npm install
in your terminal to get your project ready.
Project Structure:
You should only be modifying code inside the src
folder
main.js
- The main electron application scriptindex.html
- The web view for the main windowdashboard.js
- External Javascript for the main windowstyle.css
- External stylesheet for the main windowpopout.html
- The web view for the popout windowpopout.js
- External Javascript for the popout windowstyle_popout.css
- External stylesheet for the popout window
Open src/main.js
in VS Code, and replace TODO: PART 4 - A
with the following code
let window = createWindow({ title: 'Crypto Dashboard', width: 800, height: 600, resizable: false })
window.loadFile('./src/index.html')
window.webContents.on('did-finish-load', () => {
window.webContents.send('crypto-data', cryptoData)
})
which creates a 800x600
unresizable window with the title Crypto Dashboard
. It's UI loaded from ./src/index.html
. In addition, we are sending the window the cryptocurrency data through the channel crypto-data
as soon as the window is ready. If you skim through the watchCrypto
function in the same js file, you can see that it is sending cryptoData
to all windows every 5 seconds.
We are using the createWindow
function we defined in Part 2
. Since this function is already included in main.js
, you can use it directly. did-finish-load
is a special reserved event by electron that is triggered when the respective window is loaded and ready.
Now we need to read and process the message in the main window's render process. Open dashboard.js
and add the following code after TODO: PART 4 - B
:
const ipc = require('electron').ipcRenderer
ipc.on('crypto-data', function (event, arg) {
refreshTable(arg)
})
Here we are simply receiving the data we sent from main.js
. The actual data processing is already done for you. You can take a look at refreshTable
if you are interested.
Run your application with npm start
inside your working directory (The same folder where your package.json
is, not src
folder!). You should see something like this:
In this final part of the tutorial, we are sending a message from the a render process and processing it in main.js
Open dashboard.js
and replace TODO: PART 5 - A
with the following code
ipc.send('crypto-popout', btnId)
This piece of code is inside a function that is called when a specific popout button is clicked. By sending an ipc message through channel crypto-popout
, we are letting main.js
know when and which button has been clicked by the user.
To receive and process the message, Open main.js
and find TODO: PART 5 - B
ipcMain.on('crypto-popout', (event, arg) => {
let targetCoin
for(const coinId in cryptoData.data){
if(cryptoData.data[coinId].website_slug === arg){
targetCoin = cryptoData.data[coinId]
break
}
}
let popout = createWindow({ title: `${targetCoin.name} Popout`, width: 500, height: 100, frame: false, transparent: true, resizable: false, alwaysOnTop: true })
popout.loadFile('./src/popout.html')
popout.webContents.on('did-finish-load', () => {
popout.send('crypto-info', arg)
popout.send('crypto-data', cryptoData)
})
})
The for loop in the beginning is simply matching the btnId
we sent to a specific crypto coin data. After we found the respective coin associated with the button, we create a popout window, and send that window which coin data it should display along with the entire cryptoData
we have.
The properties we passed in createWindow
are pretty self-explanatory. We are creating a frameless transparent window that always stay on top of all other windows.
We are almost done. All that's left is to read the cryptoData
from the popup window and display it.
Open to popout.js
and use the following code at TODO: PART 5 - C
const ipc = require('electron').ipcRenderer
let targetCoinTag
ipc.on('crypto-info', function (event, arg) {
targetCoinTag = arg
})
ipc.on('crypto-data', function (event, arg) {
let coin
for(const coinId in arg.data){
if(arg.data[coinId].website_slug === targetCoinTag){
coin = arg.data[coinId]
break
}
}
const img = './coins/64x64/' + coin.website_slug + '.png'
const price = coin.quotes.USD.price.toFixed(2)
document.getElementById('main').innerHTML = `
<img class="crypto-img" src="${img}">
<span class="crypto-price">$${price}</span>
`
})
Channel crypto-info
tells us which coin this popout window should display, and we render the actual data when we receive cryptoData
from the channel crypto-data
Lastly, you notice that there is no close button on the popout windows because they are frameless. However, we can just close the entire application when the user closes the main window.
Go to main.js
and TODO: PART 5 - D
:
window.on('closed', () => {
app.quit()
})
closed
is another special reserved event. In this case, we tell electron to close the app when the main window is closed.
If you run the application and click the popout buttons, you get little floating windows that you can freely drag around on your screen:
By now you should hopefully be pro at electron ipc. This is a very important topic, because it also extends to web development. You can explore if you are interested