The full guide to create a powerful app. With a little bit bootstrap UI.
This tutorial is written for:
Node JS
- Angular 9.1.0
- Node: 13.12.0
- OS: Darwin x64
- Electron": 8.2.0,
- Electron Builder": ^22.4.1
- Python-shell: ^1.0.8
- NG Bootstrap: ^6.0.2,
Python
- Python: 3.7.0
IDE:
- VSCode
- Sublime
- Xcode
OS:
- OS Catalina 10.15.4
Install Angular
npm install -g @angular/cli^9
Create a new app with
ng new electron-app
? Would you like to add Angular routing?(y/N) N
Select the style development. Use scss in my tutorial it is better for my common examples
? Which stylesheet format would you like to use?
CSS
❯ SCSS [ https://sass-lang.com/documentation/syntax#scss ]
Sass [ https://sass-lang.com/documentation/syntax#the-indented-syntax ]
Less [ https://lesscss.org ]
Stylus [ https://stylus-lang.com ]
cd electron-app
Install electron and electron-builder via npm on your project package.json
.
npm install --save-dev electron electron-builder
Now you need a types package for angular for using electron in a module.
npm install --save @types/electron
Install python on your system. On a windows machine may you need to allow long path names. For this case select the checkbox in the installation or setup in the registry.
Install python-shell for node js projects.
npm install --save python-shell
Replace the origin scripts entry.
"scripts": {
"ng": "ng",
"start": "ng serve",
"build": "ng build",
"test": "ng test",
"lint": "ng lint",
"e2e": "ng e2e"
},
With a bunch of new script information.
"scripts": {
"ng": "ng serve",
"clean": "rimraf dist",
"prestart": "npm run build",
"start": "electron .",
"prebuild": "npm run clean",
"build": "tsc -p electron/ && ng build",
"test": "ng test",
"lint": "ng lint",
"e2e": "ng e2e",
"prerelease": "npm run build -- --prod",
"release": "electron-builder"
},
Got to the end of the scripts
node of the package.json and add the build
node beyond the root
node
(of the json).
"build": {
"appId": "com.dave.electronapp",
"productName": "Electron App",
"asar": false,
"extraResources": [ ],
"files": [
"dist/electron-app"
]
}
Asar is a package. More information on the end of this document.
The normal entry point is the src/main.ts
file.
But for electron you need a wrapper for the application.
In the common tutorials you have to add a app.js
with the basic code.
But I will show you my way for your electron/python application.
The electron application needs a starting application and settings for the window.
Create a directory in the root directory.
mkdir electron
Create a build
directory in the electron
directory.
mkdir electron/build
Create a class file electron-application.ts
in the electron/build
directory.
touch electron/build/electron-application.ts
Content of the electron/build/electron-application.ts
file:
import {
app,
BrowserWindow,
ipcMain,
dialog
} from 'electron';
import * as path from 'path';
import * as url from 'url';
export class ElectronApplication {
appWindow = null;
dialog = dialog;
ipcMain = ipcMain;
constructor() {
app.allowRendererProcessReuse = true;
// Start when application is loaded
app.on('ready', this.initWindow);
// Close when all windows are closed.
app.on('window-all-closed', this.onWindowAllClosed);
// May initwindow
app.on('activate', this.onActivate);
}
onActivate() {
if (this.appWindow === null) {
this.initWindow();
}
}
onWindowAllClosed() {
// On macOS specific close process
if (process.platform !== 'darwin') {
app.quit();
}
}
initWindow() {
this.appWindow = new BrowserWindow({
width: 1000,
height: 800,
webPreferences: {
nodeIntegration: true,
enableRemoteModule: true
}
});
// Electron Build Path
this.appWindow.loadURL(
url.format({
pathname: path.join(app.getAppPath(), 'dist/electron-app/src/app/index.html'),
protocol: 'file:',
slashes: true
})
);
this.appWindow.webContents.openDevTools();
this.appWindow.on('closed', () => {
this.appWindow = null;
});
}
}
Add a file with the name app.ts
in the electron
directory of your project.
touch electron/app.ts
Content of the file electron/app.ts
is:
import { ElectronApplication } from './build/electron-application';
const electron = new ElectronApplication();
Create a tsconfig.json
file in the electron
directory.
touch electron/tsconfig.json
Content of the file is:
{
"compileOnSave": false,
"compilerOptions": {
"baseUrl": "./",
"outDir": "../dist/electron-app",
"sourceMap": true,
"declaration": false,
"emitDecoratorMetadata": true,
"downlevelIteration": true,
"experimentalDecorators": true,
"module": "commonjs",
"moduleResolution": "node",
"importHelpers": true,
"target": "es5",
"typeRoots": [
"node_modules/@types"
],
"lib": [
"es2018",
"dom"
]
}
}
Define the main script file in the package.json
with adding the "main": "dist/electron-app/app.js",
value under the root node.
Edit the build
node in the scripts
node "build": "tsc -p electron/ && ng build",
To generate the app.js
to load the window with the angular application wrapper.
Update the angular.json on root.
Change the outputPath
node to "outputPath": "dist/electron-app/src/app",
In the scr/index.html
change the <base href="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/">
in <base href="./">
.
Change the Meta tag view port.
From:
<meta name="viewport" content="width=device-width, initial-scale=1">
To:
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
To prevent the warning:
Electron Security Warning
This renderer process has Node.js integration enabled and
attempted to load remote content. This exposes users of this app to severe security risks.
For more information and help, consult https://electronjs.org/docs/tutorial/security
Add this meta tag into the head of your dom.
<meta http-equiv="Content-Security-Policy" content="script-src 'self';">
Complete markup.
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>ElectronApp</title>
<base href="./">
<meta http-equiv="Content-Security-Policy" content="script-src 'self';">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<link rel="icon" type="image/x-icon" href="favicon.ico">
</head>
<body>
<app-root></app-root>
</body>
</html>
Start your app npm run start
.
> [email protected] prestart /Users/dave/Development/Projects/electron/electron-app
> npm run build
> [email protected] prebuild /Users/dave/Development/Projects/electron/electron-app
> npm run clean
> [email protected] clean /Users/dave/Development/Projects/electron/electron-app
> rimraf dist
> [email protected] build /Users/dave/Development/Projects/electron/electron-app
> ng build
Generating ES5 bundles for differential loading...
ES5 bundle generation complete.
chunk {polyfills} polyfills-es2015.js, polyfills-es2015.js.map (polyfills) 141 kB [initial] [rendered]
chunk {polyfills-es5} polyfills-es5.js, polyfills-es5.js.map (polyfills-es5) 656 kB [initial] [rendered]
chunk {main} main-es2015.js, main-es2015.js.map (main) 55.7 kB [initial] [rendered]
chunk {main} main-es5.js, main-es5.js.map (main) 58.1 kB [initial] [rendered]
chunk {runtime} runtime-es2015.js, runtime-es2015.js.map (runtime) 6.16 kB [entry] [rendered]
chunk {runtime} runtime-es5.js, runtime-es5.js.map (runtime) 6.16 kB [entry] [rendered]
chunk {styles} styles-es2015.js, styles-es2015.js.map (styles) 10.1 kB [initial] [rendered]
chunk {styles} styles-es5.js, styles-es5.js.map (styles) 11.3 kB [initial] [rendered]
chunk {vendor} vendor-es2015.js, vendor-es2015.js.map (vendor) 2.37 MB [initial] [rendered]
chunk {vendor} vendor-es5.js, vendor-es5.js.map (vendor) 2.77 MB [initial] [rendered]
Date: 2020-03-30T15:34:06.773Z - Hash: 71e65ccdfd2bb4111990 - Time: 16637ms
> [email protected] start /Users/dave/Development/Projects/electron/electron-app
> electron .
After that, electron start you application. And the basic Angular Application should be displayed.
If you don't want to see the Developer tools.
Go to the app.js
file and remove or comment
the this.appWindow.webContents.openDevTools();
part.
If this app will not running. Check the names and the directories of your project.
If you don't want bootstrap or anything like that, skip this section and jump to Generate a featured module. For a nice view i have installed bootstrap, jQuery and fontawsome as node modules. Install:
ng add @angular/localize
npm install --save bootstrap @ng-bootstrap/ng-bootstrap
I choose this way, because i will theming bootstrap with scss and use the modules from the ng-bootrap package.
Edit the src/app/app.module.ts
and import the NgbModule
.
import { NgbModule } from '@ng-bootstrap/ng-bootstrap';
...
imports: [
BrowserModule,
OcrConverterModule,
NgbModule
],
Open the src/styles.scss
and import the bootstrap scss.
@import "~bootstrap/scss/bootstrap";
Add a module ng generate module features/my-feature
.
Add a component ng generate component features/my-feature
.
Edit the src/app/features/my-feature/my-feature.module.ts
.
From:
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
@NgModule({
declarations: [],
imports: [
CommonModule
]
})
export class MyFeatureModule { }
To:
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { MyFeatureComponent } from './my-feature.component';
@NgModule({
declarations: [
MyFeatureComponent
],
imports: [
CommonModule
],
exports: [
MyFeatureComponent
]
})
export class MyFeatureModule { }
Edit the markup for the feature module my-feature
.
Edit the src/app/features/my-feature/my-feature.component.html
a replace all the code with this part.
<div class="container text-center">
<h1>My Electron App</h1>
<h2>Do something</h2>
<button
type="button"
class="btn btn-primary">
Run Script
</button>
</div>
Import the my-feature
module into the app model.
Edit the src/app.module.ts
:
import { MyFeatureModule } from './features/my-feature/my-feature.module';
...
imports: [
BrowserModule,
MyFeatureModule
]
...
Load the feature in the app.component.html
.
<app-my-feature></app-my-feature>
Test the app with npm run start
.
For my setup i create a python dir into my feature model directory.
mkdir -p python/features/my-feature
Add an external git repo or create your own python setup. For this example i have add a simple script.
touch python/features/my-feature/hello.py
Edit the hello.py
script and add this part.
import sys
print('Hello from Python!')
sys.stdout.flush()
For a build process the python script needs to be exported to the dist directory.
On the build command there is no script path.
npm run start
First add the python path of your feature model into the angular.json
config.
"assets": [
"src/favicon.ico",
"src/assets",
{
"glob": "**/*",
"input": "python/",
"output": "../../python"
}
],
Parallel in the package.json
you have to modify the build
node.
Add the extra resources node. This is for the final application.
Asar is a simple extensive archive format. The extra resources are deployed in this package.
"extraResources": [
{
"filter": "**/*",
"from": "python/features",
"to": "app/python/features"
}
],
Run the build process again and check the dist directory.
npm run start
Go to the src/app/features/my-feature/my-feature.component.html
and add a (click)
event function.
<button (click)="runPythonScriptClick()"
type="button"
class="btn btn-primary">
Run Script
</button>
You need an Inter Process Communication with electron and your angular application.
You have install the @types/electron
in your package?
No? Do it!
Edit the scr/app/features/my-feature/my-feature.component.ts
.
This is for testing i try to keep the code simple. Elegance comes later
import { Component, OnInit } from '@angular/core';
import { IpcRenderer } from 'electron';
@Component({
selector: 'app-my-feature',
templateUrl: './my-feature.component.html',
styleUrls: ['./my-feature.component.scss']
})
export class MyFeatureComponent implements OnInit {
private ipc: IpcRenderer;
constructor() {
if ((window as any).require) {
try {
this.ipc = (window as any).require('electron').ipcRenderer;
} catch (e) {
throw e;
}
} else {
console.warn('App not running inside Electron!');
}
}
runPythonScript() {
this.ipc.send('runMyFeatruePythonScript');
this.ipc.once('runMyFeatruePythonScriptResult', (event, data) => {
console.log(event, data);
});
}
runPythonScriptClick() {
this.runPythonScript();
}
ngOnInit(): void {
}
}
Update the electron/build/electron-application.ts
.
Add the next code snippet into the constructor.
ipcMain.on('runMyFeatruePythonScript', (event, args) => {
const dialogOptions1 = {
type: 'none',
buttons: ['OK'],
title: 'Question',
message: 'Result py script',
detail: 'IPC Works'
};
dialog.showMessageBox(null, dialogOptions1);
// event.sender.send('runMyFeatruePythonScriptRecive', 'done');
});
Now start the application with npm run start
and click the Run Script
Button.
Now you want to get the information from a IPC output in the angular application.
Modify in the electron/build/electron-application.ts
this part of code (remove the comment).
event.sender.send('runMyFeatruePythonScriptRecive', 'done');
For python it would be nice if you can use a helper for every integration of a new feature model.
We create a generic class PythonOptions
.
ng generate class lib/python/PythonOptions
Content:
export class PythonOptions<T> {
scriptPath: string;
scriptFile: string;
ipcSend: string;
ipcReceive: string;
args: T;
}
Generate a interface MyPythonArguments
.
ng generate interface features/my-feature/MyPythonArguments
export interface MyPythonArguments {
arg1?: string;
arg2: number;
arg3: boolean;
}
Integrate the interface and the common PythonArguments
in the src/app/features/my-feature/my-feature.component.ts
.
import { PythonOptions } from '../../lib/python/python-options';
import { MyPythonArguments } from './lib/my-python-arguments';
Update the runPythonScript()
function.
runPythonScript() {
const pyopts: PythonOptions<MyPythonArguments> = {
scriptPath: 'python/features/my-feature',
scriptFile: 'hello.py',
ipcSend: 'runMyFeaturePythonScript',
ipcReceive: 'runMyFeaturePythonScriptReceive',
args: {
arg1: '1337',
arg2: 123,
arg3: false
}
};
this.ipc.send(pyopts.ipcSend, pyopts);
this.ipc.once(pyopts.ipcReceive, (event, data) => {
console.log(event, data);
});
}
Update the electron/build/electron-application.ts
.
ipcMain.on('runMyFeaturePythonScript', (event, args) => {
const argsString = JSON.stringify(args);
const dialogOptions1 = {
type: 'none',
buttons: ['OK'],
title: 'Question',
message: 'Result py script',
detail: argsString
};
dialog.showMessageBox(null, dialogOptions1);
event.sender.send(args.ipcReceive, args.args);
});
At this point, our feature module call with this.ipc.send(pyopts.ipcSend, pyopts);
the ipcMain in the electron/build/electron-application.ts
ElectronApplication
class.
This is ok, but if you have more calls an other operations like electron showOpenDialog
method, you have to create a lot of these functionality.
Add a new File in our feature model for the electron registry.
Create feature registry directory
mkdir -p electron/build/features/my-feature
touch electron/build/features/my-feature/my-feature.ipc.ts
Content:
import { ipcMain, app, dialog } from 'electron';
import { PythonShell } from 'python-shell';
import * as path from 'path';
export class MyFeatureIPC {
constructor() {
ipcMain.on('runMyFeaturePythonScript', (event, arg) => {
const pyOptions = {
scriptPath: path.join(app.getAppPath(), arg.scriptPath)
};
PythonShell.run(arg.scriptFile, pyOptions, (err, results) => {
if (err) {
throw err;
}
const options = {
type: 'none',
buttons: ['OK'],
title: 'Run python Script',
message: 'Result py script ' + arg.scriptFile,
detail: results.join('-')
};
dialog.showMessageBox(null, options);
event.sender.send(arg.ipcReceive, results);
});
});
}
}
Add a new file for the registry.
touch electron/build/electron-registry.ts
Add the feature to the registry
Content:
import { MyFeatureIPC } from './features/my-feature/my-feature.ipc';
export class ElectronRegistry {
myFeature = new MyFeatureIPC();
}
Add the registry in the electron/build/electron-application.ts
and import this feature.
import { ElectronRegistry } from './electron-registry';
Add a property to the class.
registry: ElectronRegistry = null;
Replace the ipcMain.on('runMyFeaturePythonScript' ...
call with the an instance of the registry.
this.registry = new ElectronRegistry();
In the build node of the package.json you can set up the asar variable.
If you have a Error message like that:
Uncaught Exception:
Error: /Applications/Xcode.app/Contents/Developer/Library/ Frameworks/Python3.framework/Versions/3.7/Resources/Python.app/ Contents/MacOS/Python: can't open file '/Users/dave/Development/ Projects/electron/electron-app/dist/mac/Electron App.app/ Contents/Resources/app.asar/dist/electron-app/app/features/ my-feature/python/hello.py': [Errno 20] Not a directory
With "asar": false
you have a directory for your sources.
Check the path of your scripts to debug this type of error.
In the deployment process you may have a notification like that.
asar usage is disabled — this is strongly not recommended solution=enable asar and use asarUnpack to unpack files that must be externally available
For this problem with python you can add a specific directory witch is not packed in a asar package.
Remove Asar node or set it on true,
"asar": false,
Add the python directory into the asarUpack
list.
"asarUnpack": [
"app/python/features"
]
But. If you have to create a logic for asar handling.
And you can't access the python script not from the CLI.
So, I prefer the "asar": false,
Method.
If the data changes in the application, normally angular detect the changes (ChangeDetectorRef) by itself. But angular in an electron app, you have update the data with zone.js.
For this case update the src/app/features/my-feature/my-feature.component.ts
file.
Add NgZone to your component.
import { Component, OnInit, NgZone } from '@angular/core';
Initialise NgZone in the constructor.
constructor(private zone: NgZone) {
Update the data for the view with:
this.zone.run(() => {
this.myTitle = 'foo';
});
Example:
mydata: any;
runPythonScript() {
this.ipc.send(this.pyopts.ipcSend, this.pyopts);
this.ipc.once(this.pyopts.ipcReceive, (event, data) => {
console.log(event, data);
this.zone.run(() => {
this.mydata = data;
});
});
}
After debugging with npm run start
and everything is ok, build your app.
With npm run release
you will create an application.