13
10

More than 1 year has passed since last update.

Windows11秒~Electronによる初めてのアプリ開発~

Last updated at Posted at 2022-12-24

序幕

2022年春。
WindowsUpdateで準備完了連絡が来たので、
Windows10からWindows11へUpdateしました。

:notebook_with_decorative_cover:

 
ところが、

タスクバーコーナー(=通知領域=タスクトレイ)時計に秒が無い!!

のです...

Windows10ではレジストリを変更すれば、
タスクトレイの時計に秒を追加することが出来たのですが、
Windows11では出来なくなってしまったらしい...

2023/05/10 追記
2023-04 x64 Windows 11 Version 22H2 の累積更新プログラム (KB5025305) の一部を含む、
2023-05 x64 Windows 11 Version 22H2 の累積更新プログラム (KB5026372) を適用すると、
Windows11でもShowSecondsInSystemClockをレジストリに設定することで、
タスクトレイの時計に秒を表示することが出来るようになったようです。

アプリを検索してみるも高機能and多機能すぎる...
時計の横に秒があればよいだけなのです...
時計の傍に秒があればよいだけなのです...
こんな感じに...

|      [タスクトレイ]
|・ω・)チラッ  [タスクトレイ]
|彡サッ!   [タスクトレイ]
|・ω・)チラッ  [タスクトレイ]
| スッ!( ・ω・)⊃(秒)トレイ]
|・ω・)    [タ(秒)トレイ]
|彡サッ!   [タ(秒)トレイ]
|      [タ(秒)トレイ]

その日は悶々としながら就寝しました。
するとマリーアントワネットが言うのです。

秒がないなら(自分で)描画したらいいじゃない

m9(・∀・)ソレダッ!!

ということで、
初めてさわるNode.jsとElectron、
素人によるアプリ開発で出来たのが、
Windows11秒(win11sec.exe)です。

2022.0222.0222.png

:notebook_with_decorative_cover:
クソアプリな点
クソアプリな点①
通知と通知領域通知領域に記載の通り、
タスクバーの通知領域をカスタマイズするにて、
クソアプリをユーザーがオンにしてくれないと、
タスクバーコーナーのオーバーフローに、
アイコンが入れられてしまうため、
肝心の秒が見えない...
クソアプリな点②
とことん低機能なのにも関わらず、
ElectronなのでChromiumを内包しているため、
ファイルサイズがビックサイズ!!
クソアプリな点③
とことん低機能なのにも関わらず、
起動の際にそのChromiumを展開するので、
起動に時間が掛かる。

注意

注意
Windows11秒というタイトルの通り、
Windows10の環境でも大丈夫だとは思いますが、
Windows11の環境を本投稿は前提としています。

注意
htmlやjsはある程度は分かるという人を本投稿は対象としています。

Node.js

とても分かりやすいです。

Electronによるアプリ開発には、
Node.js および npm(or yarn)が必要です。

Node.jsのインストール

とても分かりやすいです。

https://nodejs.org/ja/
ダウンロードしてインストールします。
Windowsの一般的なアプリのインストールと変わりませんので、
Windowsの一般的なアプリのインストールをするように、
Node.jsをインストールください。

作業場所

win11secフォルダをWindows11のデスクトップに新規作成。
win11secフォルダを開いてアドレスバーにcmdと打ってEnter。

コマンドプロンプトが自動的に開いて、
C:\Users\USERNAME\Desktop\win11sec>
となっていると思います。

注意
全角文字や半角スペースが含まれている場合は、
Cドライブ直下にwin11secフォルダとかの方が無難です。

情報
本投稿において、コマンドプロンプトにて、
以下のディレクトリの中で各種全ての作業を行ないます。
以下のディレクトリ以外に移動しての作業はありません。
C:\Users\USERNAME\Desktop\win11sec>

情報
本投稿において、
ディレクトリとフォルダは同じものとして扱います。
「ディレクトリ」と「フォルダ」の違いと共通点
Windowsにおけるフォルダーとディレクトリとは

注意
Windowsではバックスラッシュが円記号になりがちです。
C:\Users\USERNAME\Desktop\win11sec> が、
C:¥Users¥USERNAME¥Desktop¥win11sec> と、
コマンドプロンプトの表記もなってしまいます。
適時「\」を「¥」に置き換えてお読みください。
バックスラッシュと円記号の悲劇
Windowsのパス区切り文字は、なぜ逆スラッシュになったのか?

Windows11か念の為の確認をします。
winverと打ってEnter。

win11sec> winver
Windows11であればOK。

npmの準備

Node.jsのインストールが無事完了しているかの確認、および、
Node.jsのインストールの際に一緒にインストールされている、
npmの確認、さらに、
npmの初期化処理(package.jsonの作成)をします。

win11sec> node -v
バージョン番号が表示されればOK。

win11sec> npm -v
バージョン番号が表示されればOK。

win11sec> npm init -y

-yオプションを付けないと色々質問されますが、
-yオプションを付けるとデフォルトの内容で、
npmの初期化処理(package.jsonの作成)をしてくれます。

処理が終わると、
win11secフォルダにpackage.jsonが出来ています。

win11secフォルダのpackage.jsonを、
テキストエディタで開いて、
ひとまず以下に書き換えます。

package.json
{
  "name": "win11sec",
  "version": "1.0.0",
  "description": "",
  "main": "main.js",
  "scripts": {},
  "keywords": [],
  "author": "",
  "license": ""
}

package.jsonの内容をきちんと書きたい場合は、
以下参考になります。とても分かりやすいです。

Electronのインストール

Electronをnpmでインストールします。

win11sec> npm i -D electron

Electronのインストールが無事完了したか確認します。

win11sec> node_modules\.bin\electron -v
バージョン番号が表示されればOK。

情報
ダウンロードしながらインストールするので、
1時間弱とか結構時間が掛かる場合があります。

注意
エラーがインストール時に何か表示された場合は、
エラーの内容でググって解決をしてくださいませ。
エラーの内容次第では未解決で放置の場合もあり。

electron-builderのインストール

electron-builderもnpmでインストールします。

win11sec> npm i -D electron-builder

electron-builderのインストールが無事完了したか確認します。

win11sec> node_modules\.bin\electron-builder --version
バージョン番号が表示されればOK。

情報
ダウンロードしながらインストールするので、
1時間弱とか結構時間が掛かる場合があります。

注意
エラーがインストール時に何か表示された場合は、
エラーの内容でググって解決をしてくださいませ。
エラーの内容次第では未解決で放置の場合もあり。

各種インストール完了

Electronのインストールと、
electron-builderのインストールにより、
win11secのフォルダに以下変更が施されています。

①node_modulesフォルダが作成されています。
②package-lock.jsonが新規に作成されています。
③package.jsonの下部に以下が追記されています。

package.json
{
  "name": "win11sec",
  "version": "1.0.0",
  "description": "",
  "main": "main.js",
  "scripts": {},
  "keywords": [],
  "author": "",
  "license": "",
+ "devDependencies": {
+   "electron": "^xx.x.x",
+   "electron-builder": "^xx.x.x"
+ }
}

npmの参考

参考 npm fund
npm fundとは何か

Electronの参考

注意 Electron
Electronはバージョンアップで破壊的変更が結構あるようです。
Ver14.0のremoteモジュールの削除等の破壊的変更は影響が大きく、
数年前に書かれたコードは動かない可能性があります。
インターネット上のElectronの情報を参考にする際は、
contextBridgeのコードの有無がひとつの目安かなと思います(2022年現在)
contextBridgeに関しては後述します。

Hello World

では Hello World してみましょう。

注意
electron.orgのクイックスタート、および、
electron.orgのサンプルと同様の内容です。

win11secフォルダで以下ファイルを新規作成します。

index.html
main.js
preload.js
renderer.js

win11secフォルダの構成は結果以下となります。

win11sec(フォルダ)
node_modules(フォルダ)
index.html
main.js
package.json
package-lock.json
preload.js
renderer.js

テキストエディタで開いて、
各々の中身を以下の内容にしてください。

index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Hello World</title>
</head>
<body>
<p>Hello World</p>
<script src="renderer.js"></script>
</body>
</html>
renderer.js
console.log('Hello World ( renderer.js )');
preload.js
console.log('Hello World ( preload.js )');
main.js
console.log('Hello World ( main.js )');

// Electronのモジュールの読み込み
const {
  app,
  BrowserWindow,
} = require('electron');

let mainWindow = null;

// 全てのウィンドウが閉じた時に実行
app.on('window-all-closed', () => {
  if (process.platform !== 'darwin') {
    app.quit();
  }
});

// Electronの終了時に実行
app.on('quit', () => {
});

// Electronの初期化時に実行
app.whenReady().then(() => {
  // メインウインドウ関連の初期設定
  mainWindow = new BrowserWindow({
    // メインウインドウ生成時のウインドウの横幅
    width: 1600,
    // メインウインドウ生成時のウインドウの高さ
    height: 900,
    // 上記の横幅と高さがビューポートとしての値としてメインウインドウ生成
    useContentSize: true,
    // ウェブページの機能の設定
    webPreferences: {
      // 他のスクリプトがページで実行される前にロードされるスクリプトを指定
      preload: `${app.getAppPath()}/preload.js`,
    },
  });
  // メインウインドウに表示するページの読み込み
  mainWindow.loadFile('index.html');
  // ブラウザのF12のデベロッパーツールの表示
  mainWindow.webContents.openDevTools();
  // ウィンドウが閉じた時に実行
  mainWindow.on('closed', () => {
    mainWindow = null;
  });
});

ひとまず実行してみましょう。

コマンドプロンプト、
C:\Users\USERNAME\Desktop\win11sec>
にて、
win11sec> npx electron .
です。

npxは手軽な実行用のコマンドです。
npxに関しては前述のリンク等を参照ください。

上記コマンドは、
どういう表現が正しいのか分かりませんが、
npxにてnpmのelectronでカレントディレクトリ(=[.])を読んで実行、
というところでしょうか。

カレントディレクトリ(=[.])の何を読んでいるのでしょうか。
package.jsonです。そして、
package.jsonのmainにエントリポイントのファイル名が記載されています。
ですのでmain.jsを起点に動作を開始します。

package.json
  "main": "main.js",

起動したら、
デベロッパーツールのConsoleのタブをクリックしてください。

コマンドプロンプトに「Hello World ( main.js )」、
デベロッパーツールに「Hello World ( preload.js )」、
デベロッパーツールに「Hello World ( renderer.js )」、
と表示されています。
各jsのconsole.log()の出力先が分かりました。
今後のデバッグに活用ください。

注意
何らかのエラーが出て処理が戻ってこない場合は、
コマンドプロンプトをアクティブウィンドウにしてCTRL+Cを押します。
バッチ ジョブを終了しますか (Y/N)?
と尋ねられるので、
キーボードのYを押す事で終了します。
キーボードのNを押しても終了します (^-^A

Hello World 説明

では、各ファイルの説明です。

index.html
renderer.js
preload.js

上記三ファイルの中身の内容は、
htmlやjsを触っている人であれば、
特に難しい内容では無いと思いますので割愛します。

問題はmain.jsですね。

Hello World 説明①

const {
  app,
  BrowserWindow,
} = require('electron');
const electron = require('electron');
const app = electron.app;
const BrowserWindow = electron.BrowserWindow;

Node.jsでのElectronのモジュールの読み込みです。
前者は後者をオブジェクトの分割代入を使用して書いたコードになります。

Hello World 説明②

let mainWindow = null;

メインウインドウのインスタンスの保持用のグローバル変数の宣言です。

Hello World 説明③

app.on('window-all-closed', () => {
  if (process.platform !== 'darwin') {
    app.quit();
  }
});

一般的に Windows や Linux では、
すべてのウィンドウを終了するとアプリケーションが完全に終了

するので本処理を記述するそうです。

Hello World 説明④

app.on('quit', () => {
});

Electronの終了時に実行されます。
Hello World では処理する内容はありません。
カラの状態です。

Hello World 説明⑤-1

app.whenReady().then(() => {
});

Electronの初期化時に実行されます。

Hello World 説明⑤-2

  mainWindow = new BrowserWindow({
    width: 1600,
    height: 900,
    useContentSize: true,
    webPreferences: {
      preload: `${app.getAppPath()}/preload.js`,
    },
  });

メインウインドウのインスタンスの生成をして、
メインウインドウのインスタンスの保持用のグローバル変数に格納します。

Hello World 説明⑤-2-1

    width: 1600,
    height: 900,
    useContentSize: true,

new BrowserWindow() のオプション で以下を設定。
メインウインドウ生成時のウインドウの横幅。
メインウインドウ生成時のウインドウの高さ。
上記の横幅と高さがビューポートとしての値としてメインウインドウ生成。

Hello World 説明⑤-2-2

    // ウェブページの機能の設定
    webPreferences: {
      // 他のスクリプトがページで実行される前にロードされるスクリプトを指定
      preload: `${app.getAppPath()}/preload.js`,
    },

new BrowserWindow() のオプション で webPreferences の preload を設定。
他のスクリプトがページで実行される前にロードされるスクリプトを指定?
よく分かりませんね、後述しますので、今はそっとしておきましょう。

Hello World 説明⑥

  mainWindow.loadFile('index.html');

index.htmlを読み込んでメインウインドウに表示します。

Hello World 説明⑦

  mainWindow.webContents.openDevTools();

デベロッパーツール(DevTools)を表示します。

参考
Electronがデフォルトの状態の場合、
Electronのデベロッパーツールの開閉は、
F12 ではなく Ctrl+Shift+I です。

Hello World 説明⑧

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

ウィンドウが閉じた時に実行されます。

ウィンドウが閉じた時にnullを代入するようにして、
インスタンスを参照していない状態に念の為します。

メインウインドウやタスクバーのアイコン

Windows11秒(win11sec.exe)は、
タスクトレイのアイコンを毎秒変更することで、
タスクトレイに秒を描画しています。

Hello World が上記でできましたので、
タスクトレイのアイコンの変更の前哨戦として、
メインウインドウやタスクバーのアイコンを変更してみます。

ICOOON MONO から画像をお借りします。
ベルのフリーアイコン素材
時計の無料アイコン

上記の256pxのPNGをダウンロードして、
以下のファイル名でwin11secフォルダに保存してください。
bell.png
clock.png

結果、
win11secフォルダの中は以下となっています。

win11sec(フォルダ)
node_modules(フォルダ)
bell.png
clock.png
index.html
main.js
package.json
package-lock.json
preload.js
renderer.js

main.jsに以下を追記します。

main.js
console.log('Hello World ( main.js )');

// Electronのモジュールの読み込み
const {
  app,
  BrowserWindow,
+ nativeImage,
} = require('electron');

let mainWindow = null;

// 中略

// Electronの初期化時に実行
app.whenReady().then(() => {
  // メインウインドウ関連の初期設定
  mainWindow = new BrowserWindow({
    // 中略
    },
  });
+ // メインウインドウのアイコンの設定
+ mainWindow.setIcon('clock.png');
+ // タスクバーのアイコンにオーバーレイされる画像の設定
+ mainWindow.setOverlayIcon(nativeImage.createFromPath('bell.png'), '');
  // メインウインドウに表示するページの読み込み
  mainWindow.loadFile('index.html');
  // ブラウザのF12のデベロッパーツールの表示
  mainWindow.webContents.openDevTools();
  // ウィンドウが閉じた時に実行
  mainWindow.on('closed', () => {
    mainWindow = null;
  });
});
:notebook_with_decorative_cover:
main.js
console.log('Hello World ( main.js )');

// Electronのモジュールの読み込み
const {
  app,
  BrowserWindow,
  nativeImage,
} = require('electron');

let mainWindow = null;

// 全てのウィンドウが閉じた時に実行
app.on('window-all-closed', () => {
  if (process.platform !== 'darwin') {
    app.quit();
  }
});

// Electronの終了時に実行
app.on('quit', () => {
});

// Electronの初期化時に実行
app.whenReady().then(() => {
  // メインウインドウ関連の初期設定
  mainWindow = new BrowserWindow({
    // メインウインドウ生成時のウインドウの横幅
    width: 1600,
    // メインウインドウ生成時のウインドウの高さ
    height: 900,
    // 上記の横幅と高さがビューポートとしての値としてメインウインドウ生成
    useContentSize: true,
    // ウェブページの機能の設定
    webPreferences: {
      // 他のスクリプトがページで実行される前にロードされるスクリプトを指定
      preload: `${app.getAppPath()}/preload.js`,
    },
  });
  // メインウインドウのアイコンの設定
  mainWindow.setIcon('clock.png');
  // タスクバーのアイコンにオーバーレイされる画像の設定
  mainWindow.setOverlayIcon(nativeImage.createFromPath('bell.png'), '');
  // メインウインドウに表示するページの読み込み
  mainWindow.loadFile('index.html');
  // ブラウザのF12のデベロッパーツールの表示
  mainWindow.webContents.openDevTools();
  // ウィンドウが閉じた時に実行
  mainWindow.on('closed', () => {
    mainWindow = null;
  });
});

setIcon()の引数は、
stringによるファイル名の指定がサポートされてますが、

setOverlayIcon()の引数は、
nativeImageでなければなりません(2022年現在)

nativeImageのインスタンスを作成してくれる、
nativeImage.createFromPath()のメソッドが用意されているので、
それを利用してbell.pngの画像を設定しています。

実行してみます。
win11sec> npx electron .

メインウインドウとタスクバーのアイコンが変更されます。

備考
Windowsにおけるアイコンの画像ファイルフォーマットであるICO、
Wikipediaの ICO(ファイルフォーマット)に以下記載があったため、
256x256pxのpngを試しに指定してみたら大丈夫そうだったので、
本投稿ではアイコンにpngを使用しています。

Windows Vistaは256×256ピクセルのアイコン画像を
そのまま表示するモードもサポートしており、
(必須ではないが)圧縮したPNGフォーマットもサポートしている。

タスクトレイのアイコン

タスクトレイにアイコンを追加します。

備考
タスクバーコーナー、通知領域、システムトレイ、タスクトレイ、
などと時代時代で表記されてきました。
Windows11ではタスクバーコーナーというようですが、
本投稿ではタスクトレイと表記します。

main.jsに以下を追記します。

main.js
console.log('Hello World ( main.js )');

// Electronのモジュールの読み込み
const {
  app,
  BrowserWindow,
  nativeImage,
+ Tray,
} = require('electron');

let mainWindow = null;
+ let tray = null;

// 中略

// Electronの終了時に実行
app.on('quit', () => {
+ // タスクトレイのアイコンの削除
+ tray.destroy();
});

// Electronの初期化時に実行
app.whenReady().then(() => {
+ // タスクトレイのアイコンの作成
+ tray = new Tray(nativeImage.createFromPath('clock.png'));
+ // タスクトレイのアイコンのホバーテキストの設定
+ tray.setToolTip('windows11秒');
  // メインウインドウ関連の初期設定
  mainWindow = new BrowserWindow({
    // 中略
  });
  // 中略
});
:notebook_with_decorative_cover:
main.js
console.log('Hello World ( main.js )');

// Electronのモジュールの読み込み
const {
  app,
  BrowserWindow,
  nativeImage,
  Tray,
} = require('electron');

let mainWindow = null;
let tray = null;

// 全てのウィンドウが閉じた時に実行
app.on('window-all-closed', () => {
  if (process.platform !== 'darwin') {
    app.quit();
  }
});

// Electronの終了時に実行
app.on('quit', () => {
  // タスクトレイのアイコンの削除
  tray.destroy();
});

// Electronの初期化時に実行
app.whenReady().then(() => {
  // タスクトレイのアイコンの作成
  tray = new Tray(nativeImage.createFromPath('clock.png'));
  // タスクトレイのアイコンのホバーテキストの設定
  tray.setToolTip('windows11秒');
  // メインウインドウ関連の初期設定
  mainWindow = new BrowserWindow({
    // メインウインドウ生成時のウインドウの横幅
    width: 1600,
    // メインウインドウ生成時のウインドウの高さ
    height: 900,
    // 上記の横幅と高さがビューポートとしての値としてメインウインドウ生成
    useContentSize: true,
    // ウェブページの機能の設定
    webPreferences: {
      // 他のスクリプトがページで実行される前にロードされるスクリプトを指定
      preload: `${app.getAppPath()}/preload.js`,
    },
  });
  // メインウインドウのアイコンの設定
  mainWindow.setIcon('clock.png');
  // タスクバーのアイコンにオーバーレイされる画像の設定
  mainWindow.setOverlayIcon(nativeImage.createFromPath('bell.png'), '');
  // メインウインドウに表示するページの読み込み
  mainWindow.loadFile('index.html');
  // ブラウザのF12のデベロッパーツールの表示
  mainWindow.webContents.openDevTools();
  // ウィンドウが閉じた時に実行
  mainWindow.on('closed', () => {
    mainWindow = null;
  });
});

実行してみます。
win11sec> npx electron .

Windows11の仕様により、
タスクトレイにアイコンが表示されませんが、
タスクバーコーナーオーバーフローアイコンの表示設定を変更すれば、
タスクトレイにアイコンが表示されます。

タスクトレイのアイコンのメニュー

Windows11秒(win11sec.exe)は、
メインウインドウを非表示にすることになりますので、
メインウインドウの右上の×ボタンを押せません。

そのままですと終了できなくなってしまうので、
タスクトレイのアイコンにメニューを追加して、
終了することができるようにします。

ついでに、
Windows11のタスクバーの設定を開けるようにします。
main.jsに以下を追記します。

main.js
console.log('Hello World ( main.js )');

// Electronのモジュールの読み込み
const {
  app,
  BrowserWindow,
  nativeImage,
+ Menu,
  Tray,
+ shell,
} = require('electron');

let mainWindow = null;
let tray = null;

// 中略

// Electronの初期化時に実行
app.whenReady().then(() => {
+ // メニューの構築
+ const contextMenu = Menu.buildFromTemplate([
+   {
+     label: 'Settings',
+     click: () => {
+       shell.openExternal('ms-settings:taskbar');
+     },
+   },
+   {
+     label: 'Quit',
+     click: () => {
+       app.quit();
+     },
+   },
+ ]);
  // タスクトレイのアイコンの作成
  tray = new Tray(nativeImage.createFromPath('clock.png'));
  // タスクトレイのアイコンのホバーテキストの設定
  tray.setToolTip('windows11秒');
+ // タスクトレイのアイコンのコンテキストメニューの設定
+ tray.setContextMenu(contextMenu);
  // メインウインドウ関連の初期設定
  mainWindow = new BrowserWindow({
    // 中略
  });
  // 中略
});
:notebook_with_decorative_cover:
main.js
console.log('Hello World ( main.js )');

// Electronのモジュールの読み込み
const {
  app,
  BrowserWindow,
  nativeImage,
  Menu,
  Tray,
  shell,
} = require('electron');

let mainWindow = null;
let tray = null;

// 全てのウィンドウが閉じた時に実行
app.on('window-all-closed', () => {
  if (process.platform !== 'darwin') {
    app.quit();
  }
});

// Electronの終了時に実行
app.on('quit', () => {
  // タスクトレイのアイコンの削除
  tray.destroy();
});

// Electronの初期化時に実行
app.whenReady().then(() => {
  // メニューの構築
  const contextMenu = Menu.buildFromTemplate([
    {
      label: 'Settings',
      click: () => {
        shell.openExternal('ms-settings:taskbar');
      },
    },
    {
      label: 'Quit',
      click: () => {
        app.quit();
      },
    },
  ]);
  // タスクトレイのアイコンの作成
  tray = new Tray(nativeImage.createFromPath('clock.png'));
  // タスクトレイのアイコンのホバーテキストの設定
  tray.setToolTip('windows11秒');
  // タスクトレイのアイコンのコンテキストメニューの設定
  tray.setContextMenu(contextMenu);
  // メインウインドウ関連の初期設定
  mainWindow = new BrowserWindow({
    // メインウインドウ生成時のウインドウの横幅
    width: 1600,
    // メインウインドウ生成時のウインドウの高さ
    height: 900,
    // 上記の横幅と高さがビューポートとしての値としてメインウインドウ生成
    useContentSize: true,
    // ウェブページの機能の設定
    webPreferences: {
      // 他のスクリプトがページで実行される前にロードされるスクリプトを指定
      preload: `${app.getAppPath()}/preload.js`,
    },
  });
  // メインウインドウのアイコンの設定
  mainWindow.setIcon('clock.png');
  // タスクバーのアイコンにオーバーレイされる画像の設定
  mainWindow.setOverlayIcon(nativeImage.createFromPath('bell.png'), '');
  // メインウインドウに表示するページの読み込み
  mainWindow.loadFile('index.html');
  // ブラウザのF12のデベロッパーツールの表示
  mainWindow.webContents.openDevTools();
  // ウィンドウが閉じた時に実行
  mainWindow.on('closed', () => {
    mainWindow = null;
  });
});

実行してみます。
win11sec> npx electron .

タスクトレイのアイコンを右クリックするとメニューが表示されます。

実行してみてください。
win11sec> start ms-settings:taskbar
Windows11のタスクバーの設定が開きます。
shell.openExternal('ms-settings:taskbar');の部分は、
これとほぼ同じことをしています。

駄文
ms-settings:personalization-start
ms-settings:personalization-start-places
ms-settings:easeofaccess-narrator
などはtoggleにfocusした状態で設定を開けます。
ms-settings:easeofaccess-narrator-isautostartenabled
に至ってはアコーディオンを開いてfocusした状態で設定を開けます。

ms-settings:taskbarでタスクバーの設定を開くことはできますが、
タスクバーコーナーのオーバーフローのアコーディオンを開いた状態にしたい...
タスクバーコーナーのオーバーフローのアコーディオンを開いた状態の指定方法は無いのでしょうか...

アイテムの正規コントロール パネル名
Command line to get "select which icons appear on the taskbar"
explorer shell:::{05d7b0f4-2121-4eff-bf6b-ed3f69b894d9}にて、
Windows11では通常使用しない設定画面であれば開けるようですが...

秒の取得

main.jsに以下を追記します。

main.js
console.log('Hello World ( main.js )');

// Electronのモジュールの読み込み
const {
  app,
  BrowserWindow,
  nativeImage,
  Menu,
  Tray,
  shell,
} = require('electron');

+ let interval = null;
+ let prevSec = null;
+ let thisSec = null;

let mainWindow = null;
let tray = null;

// 中略

// Electronの終了時に実行
app.on('quit', () => {
+ // インターバルのタイマーの繰り返し動作の取り消し
+ clearInterval(interval);
  // タスクトレイのアイコンの削除
  tray.destroy();
});

// Electronの初期化時に実行
app.whenReady().then(() => {
  // 中略
  // メインウインドウのアイコンの設定
  mainWindow.setIcon('clock.png');
  // タスクバーのアイコンにオーバーレイされる画像の設定
  mainWindow.setOverlayIcon(nativeImage.createFromPath('bell.png'), '');
  // メインウインドウに表示するページの読み込み
  mainWindow.loadFile('index.html');
  // ブラウザのF12のデベロッパーツールの表示
  mainWindow.webContents.openDevTools();
  // ウィンドウが閉じた時に実行
  mainWindow.on('closed', () => {
    mainWindow = null;
  });
+ // 1/30秒に1回インターバルで現在時刻の秒を確認
+ interval = setInterval(() => {
+   const now = new Date();
+   thisSec = now.getSeconds();
+   if (thisSec !== prevSec) {
+     console.log(thisSec);
+   }
+   prevSec = thisSec;
+ }, 33);
});
:notebook_with_decorative_cover:
main.js
console.log('Hello World ( main.js )');

// Electronのモジュールの読み込み
const {
  app,
  BrowserWindow,
  nativeImage,
  Menu,
  Tray,
  shell,
} = require('electron');

let interval = null;
let prevSec = null;
let thisSec = null;

let mainWindow = null;
let tray = null;

// 全てのウィンドウが閉じた時に実行
app.on('window-all-closed', () => {
  if (process.platform !== 'darwin') {
    app.quit();
  }
});

// Electronの終了時に実行
app.on('quit', () => {
  // インターバルのタイマーの繰り返し動作の取り消し
  clearInterval(interval);
  // タスクトレイのアイコンの削除
  tray.destroy();
});

// Electronの初期化時に実行
app.whenReady().then(() => {
  // メニューの構築
  const contextMenu = Menu.buildFromTemplate([
    {
      label: 'Settings',
      click: () => {
        shell.openExternal('ms-settings:taskbar');
      },
    },
    {
      label: 'Quit',
      click: () => {
        app.quit();
      },
    },
  ]);
  // タスクトレイのアイコンの作成
  tray = new Tray(nativeImage.createFromPath('clock.png'));
  // タスクトレイのアイコンのホバーテキストの設定
  tray.setToolTip('windows11秒');
  // タスクトレイのアイコンのコンテキストメニューの設定
  tray.setContextMenu(contextMenu);
  // メインウインドウ関連の初期設定
  mainWindow = new BrowserWindow({
    // メインウインドウ生成時のウインドウの横幅
    width: 1600,
    // メインウインドウ生成時のウインドウの高さ
    height: 900,
    // 上記の横幅と高さがビューポートとしての値としてメインウインドウ生成
    useContentSize: true,
    // ウェブページの機能の設定
    webPreferences: {
      // 他のスクリプトがページで実行される前にロードされるスクリプトを指定
      preload: `${app.getAppPath()}/preload.js`,
    },
  });
  // メインウインドウのアイコンの設定
  mainWindow.setIcon('clock.png');
  // タスクバーのアイコンにオーバーレイされる画像の設定
  mainWindow.setOverlayIcon(nativeImage.createFromPath('bell.png'), '');
  // メインウインドウに表示するページの読み込み
  mainWindow.loadFile('index.html');
  // ブラウザのF12のデベロッパーツールの表示
  mainWindow.webContents.openDevTools();
  // ウィンドウが閉じた時に実行
  mainWindow.on('closed', () => {
    mainWindow = null;
  });
  // 1/30秒に1回インターバルで現在時刻の秒を確認
  interval = setInterval(() => {
    const now = new Date();
    thisSec = now.getSeconds();
    if (thisSec !== prevSec) {
      console.log(thisSec);
    }
    prevSec = thisSec;
  }, 33);
});

実行してみます。
win11sec> npx electron .

コマンドプロンプトに秒が出力されていきます。

※Electron特有のコードではなく、
 通常のJavaScriptのコードですので説明は簡単に済ませます。

setInterval()にて33ミリ秒(≒1/30秒)に1回、
Date()およびgetSeconds()で現在の秒を取得。
秒が切り替わった瞬間のみconsole.logに秒を出力しています。

秒の画像

Canvas API で秒の画像の作成と取得をします。

main.jsのどこかに、
const canvas = document.createElement('canvas');を追記して実行すると、
document is not definedとエラーになり実行できません。ですので、
renderer.jsで行なうことになります。

秒の画像①

Canvas API でまずは数字の00の作成をします。

renderer.jsに以下を追記します。

renderer.js
console.log('Hello World ( renderer.js )');
+ // キャンバスの生成
+ const canvas = document.createElement('canvas');
+ canvas.width = 256;
+ canvas.height = 256;
+ // キャンバスのコンテキストの取得
+ const ctx = canvas.getContext('2d');
+ ctx.textAlign = 'center';
+ ctx.fillStyle = 'black';
+ ctx.font = '200px monospace';
+ ctx.fillText('00', 128, 222);
+ // htmlにcanvas要素を追加して表示
+ document.body.append(canvas);
:notebook_with_decorative_cover:
renderer.js
console.log('Hello World ( renderer.js )');
// キャンバスの生成
const canvas = document.createElement('canvas');
canvas.width = 256;
canvas.height = 256;
// キャンバスのコンテキストの取得
const ctx = canvas.getContext('2d');
ctx.textAlign = 'center';
ctx.fillStyle = 'black';
ctx.font = '200px monospace';
ctx.fillText('00', 128, 222);
// htmlにcanvas要素を追加して表示
document.body.append(canvas);

実行してみます。
win11sec> npx electron .

メインウインドウに大きく00と描画されます。

※Electron特有のコードではなく、
 通常のJavaScriptのコードですので説明は簡単に済ませます。

document.createElement()でcanvasの要素を作成。
CanvasRenderingContext2DgetContext()で取得。
CanvasRenderingContext2Dの各種設定をして、
CanvasRenderingContext2D.fillText()で数字の00を描画。
body要素にappend()で追加しています。

秒の画像②

数字の画像データを取得します。

renderer.jsに以下を追記します。

renderer.js
console.log('Hello World ( renderer.js )');
// キャンバスの生成
const canvas = document.createElement('canvas');
canvas.width = 256;
canvas.height = 256;
// キャンバスのコンテキストの取得
const ctx = canvas.getContext('2d');
ctx.textAlign = 'center';
ctx.fillStyle = 'black';
ctx.font = '200px monospace';
ctx.fillText('00', 128, 222);
+ // PNG画像のDataURLの保持用の変数
+ let pngDataURL = null;
+ // toDataURLでPNG画像のDataURLを保持用の変数に格納
+ // toDataURLは引数省略の場合はimage/png
+ pngDataURL = canvas.toDataURL();
// htmlにcanvas要素を追加して表示
document.body.append(canvas);
:notebook_with_decorative_cover:
renderer.js
console.log('Hello World ( renderer.js )');
// キャンバスの生成
const canvas = document.createElement('canvas');
canvas.width = 256;
canvas.height = 256;
// キャンバスのコンテキストの取得
const ctx = canvas.getContext('2d');
ctx.textAlign = 'center';
ctx.fillStyle = 'black';
ctx.font = '200px monospace';
ctx.fillText('00', 128, 222);
// PNG画像のDataURLの保持用の変数
let pngDataURL = null;
// toDataURLでPNG画像のDataURLを保持用の変数に格納
// toDataURLは引数省略の場合はimage/png
pngDataURL = canvas.toDataURL();
// htmlにcanvas要素を追加して表示
document.body.append(canvas);

toDataURL()でcanvasの要素からPNG画像を取得。
toDataURL()は引数を省略した場合はデフォルトでPNG画像。

実行してみます。
win11sec> npx electron .

デベロッパーツール Sources か Console いづれか、
Sources の Watch で pngDataURL と入力、または、
Console の Live Expression で pngDataURL と入力すると、
pngDataURL の変数の中身が、
"data:image/png;base64,なんたらかんたら"
となっているのが見て取れます。
"data:image/png;base64,なんたらかんたら"
なのでpng画像のデータになのでしょう、たぶん...

秒の画像③

00~60の61枚の数字の画像データを作成して取得します。
閏秒の扱いが不明なために60も一応作成して取得します。

renderer.jsに以下を追記します。

renderer.js
console.log('Hello World ( renderer.js )');
// キャンバスの生成
const canvas = document.createElement('canvas');
canvas.width = 256;
canvas.height = 256;
// キャンバスのコンテキストの取得
const ctx = canvas.getContext('2d');
ctx.textAlign = 'center';
ctx.fillStyle = 'black';
ctx.font = '200px monospace';
ctx.fillText('00', 128, 222);
- // PNG画像のDataURLの保持用の変数
- let pngDataURL = null;
- // toDataURLでPNG画像のDataURLを保持用の変数に格納
- // toDataURLは引数省略の場合はimage/png
- pngDataURL = canvas.toDataURL();
+ // 61枚のPNG画像のDataURLの保持用の配列の初期化
+ const pngDataURL = [];
+ // 61枚分処理
+ [...Array(61).keys()].forEach((i) => {
+   // キャンバスのコンテキストの全領域を透明色で消去
+   ctx.clearRect(0, 0, 256, 256);
+   // キャンバスのコンテキストに数値を二桁で描画
+   ctx.fillText(i.toString().padStart(2, '0'), 128, 222);
+   // toDataURLでPNG画像のDataURLを保持用の配列に格納
+   // toDataURLは引数省略の場合はimage/png
+   pngDataURL.push(canvas.toDataURL());
+ });
// htmlにcanvas要素を追加して表示
document.body.append(canvas);
:notebook_with_decorative_cover:
renderer.js
console.log('Hello World ( renderer.js )');
// キャンバスの生成
const canvas = document.createElement('canvas');
canvas.width = 256;
canvas.height = 256;
// キャンバスのコンテキストの取得
const ctx = canvas.getContext('2d');
ctx.textAlign = 'center';
ctx.fillStyle = 'black';
ctx.font = '200px monospace';
ctx.fillText('00', 128, 222);
// 61枚のPNG画像のDataURLの保持用の配列の初期化
const pngDataURL = [];
// 61枚分処理
[...Array(61).keys()].forEach((i) => {
  // キャンバスのコンテキストの全領域を透明色で消去
  ctx.clearRect(0, 0, 256, 256);
  // キャンバスのコンテキストに数値を二桁で描画
  ctx.fillText(i.toString().padStart(2, '0'), 128, 222);
  // toDataURLでPNG画像のDataURLを保持用の配列に格納
  // toDataURLは引数省略の場合はimage/png
  pngDataURL.push(canvas.toDataURL());
});
// htmlにcanvas要素を追加して表示
document.body.append(canvas);

タスクトレイのアイコンに数字を描画するためには

renderer.jsのPNG画像データの配列の内容を、
main.jsに伝えた上でmain.jsで処理をする必要があります。
数字の画像データの作成および取得はrenderer.jsでの処理でしたが、
タスクトレイにアイコンの設定をするのはmain.jsでの処理だからです。

Chromiumのマルチプロセスアーキテクチャを継承した設計とのことで、
Electronはメインプロセスとレンダラープロセスの2種類のプロセスがあります。
ただし、セキュリティ上の理由で、
メインプロセスはElectronの強力なAPIにアクセスができるのに対して、
レンダラープロセスはメインプロセスのようにはAPIにアクセスできません。

本投稿においては、
main.jsはメインプロセスで実行、
renderer.jsはレンダラープロセスで実行されるので、
renderer.jsのデータをmain.jsに伝えるためには、
レンダラープロセス⇒メインプロセスのプロセス間通信が必要になります。

プリロードスクリプト

プロセス間通信を行なうためのAPIにも、
レンダラープロセスはアクセスできません。
それでは困るので、
プリロードスクリプトに働いてもらうことになります。
プリロードスクリプトはAPIにアクセス可能、かつ、
プリロードスクリプトはレンダラープロセス開始前に、
プリロードスクリプトはレンダラープロセス内で実行されます。

本投稿においては、
プリロードスクリプト=preload.js
です。

Hello World の ⑥-2-2 で説明を保留にした下記の処理にて、
プリロードスクリプト(preload.js)を指定しています。

mainWindow.loadFile('index.html')の処理が行なわれる際に、
プリロードスクリプト(preload.js)がレンダラープロセス内で実行されます。

main.js
  // メインウインドウ関連の初期設定
  mainWindow = new BrowserWindow({
    // 中略
    // ウェブページの機能の設定
    webPreferences: {
      // 他のスクリプトがページで実行される前にロードされるスクリプトを指定
      preload: `${app.getAppPath()}/preload.js`,
    },
  });
  // 中略
  // メインウインドウに表示するページの読み込み
  mainWindow.loadFile('index.html');

contextBridge

contextBridge①

プリロードスクリプトが実行されて、
プリロードスクリプトにてAPIにアクセス可能だとしても、
レンダラープロセスは依然APIにアクセス不可のままです。

そこで、
contextBridge、および、
contextBridge.exposeInMainWorld()です。

プリロードスクリプトにて、
contextBridge.exposeInMainWorld()を行なうことで、
windowにオブジェクトとメソッドを追加します。

windowとは、
window.innerWidth
window.innerHeight
window.scrollY
などでお会いするやつですね。

contextBridge②

additionalAPIオブジェクトと、
helloContextBridgeメソッドを追加してみます。
※追加するオブジェクトおよびメソッドの名称は任意。

preload.jsrenderer.jsに以下を追記します。

preload.js
console.log('Hello World ( preload.js )');

+ const {
+   contextBridge,
+ } = require('electron');
+ 
+ contextBridge.exposeInMainWorld('additionalAPI', {
+   helloContextBridge: () => {
+     console.log('Hello Context Bridge');
+   },
+ });
renderer.js
console.log('Hello World ( renderer.js )');
// キャンバスの生成
const canvas = document.createElement('canvas');
canvas.width = 256;
canvas.height = 256;
// 中略
// htmlにcanvas要素を追加して表示
document.body.append(canvas);
+ // preload.jsで設定した関数の使用実験
+ window.additionalAPI.helloContextBridge();
:notebook_with_decorative_cover:
main.js
console.log('Hello World ( main.js )');

// Electronのモジュールの読み込み
const {
  app,
  BrowserWindow,
  nativeImage,
  Menu,
  Tray,
  shell,
} = require('electron');

let interval = null;
let prevSec = null;
let thisSec = null;

let mainWindow = null;
let tray = null;

// 全てのウィンドウが閉じた時に実行
app.on('window-all-closed', () => {
  if (process.platform !== 'darwin') {
    app.quit();
  }
});

// Electronの終了時に実行
app.on('quit', () => {
  // インターバルのタイマーの繰り返し動作の取り消し
  clearInterval(interval);
  // タスクトレイのアイコンの削除
  tray.destroy();
});

// Electronの初期化時に実行
app.whenReady().then(() => {
  // メニューの構築
  const contextMenu = Menu.buildFromTemplate([
    {
      label: 'Settings',
      click: () => {
        shell.openExternal('ms-settings:taskbar');
      },
    },
    {
      label: 'Quit',
      click: () => {
        app.quit();
      },
    },
  ]);
  // タスクトレイのアイコンの作成
  tray = new Tray(nativeImage.createFromPath('clock.png'));
  // タスクトレイのアイコンのホバーテキストの設定
  tray.setToolTip('windows11秒');
  // タスクトレイのアイコンのコンテキストメニューの設定
  tray.setContextMenu(contextMenu);
  // メインウインドウ関連の初期設定
  mainWindow = new BrowserWindow({
    // メインウインドウ生成時のウインドウの横幅
    width: 1600,
    // メインウインドウ生成時のウインドウの高さ
    height: 900,
    // 上記の横幅と高さがビューポートとしての値としてメインウインドウ生成
    useContentSize: true,
    // ウェブページの機能の設定
    webPreferences: {
      // 他のスクリプトがページで実行される前にロードされるスクリプトを指定
      preload: `${app.getAppPath()}/preload.js`,
    },
  });
  // メインウインドウのアイコンの設定
  mainWindow.setIcon('clock.png');
  // タスクバーのアイコンにオーバーレイされる画像の設定
  mainWindow.setOverlayIcon(nativeImage.createFromPath('bell.png'), '');
  // メインウインドウに表示するページの読み込み
  mainWindow.loadFile('index.html');
  // ブラウザのF12のデベロッパーツールの表示
  mainWindow.webContents.openDevTools();
  // ウィンドウが閉じた時に実行
  mainWindow.on('closed', () => {
    mainWindow = null;
  });
  // 1/30秒に1回インターバルで現在時刻の秒を確認
  interval = setInterval(() => {
    const now = new Date();
    thisSec = now.getSeconds();
    if (thisSec !== prevSec) {
      console.log(thisSec);
    }
    prevSec = thisSec;
  }, 33);
});
preload.js
console.log('Hello World ( preload.js )');

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

contextBridge.exposeInMainWorld('additionalAPI', {
  helloContextBridge: () => {
    console.log('Hello Context Bridge');
  },
});
renderer.js
console.log('Hello World ( renderer.js )');
// キャンバスの生成
const canvas = document.createElement('canvas');
canvas.width = 256;
canvas.height = 256;
// キャンバスのコンテキストの取得
const ctx = canvas.getContext('2d');
ctx.textAlign = 'center';
ctx.fillStyle = 'black';
ctx.font = '200px monospace';
ctx.fillText('00', 128, 222);
// 61枚のPNG画像のDataURLの保持用の配列の初期化
const pngDataURL = [];
// 61枚分処理
[...Array(61).keys()].forEach((i) => {
  // キャンバスのコンテキストの全領域を透明色で消去
  ctx.clearRect(0, 0, 256, 256);
  // キャンバスのコンテキストに数値を二桁で描画
  ctx.fillText(i.toString().padStart(2, '0'), 128, 222);
  // toDataURLでPNG画像のDataURLを保持用の配列に格納
  // toDataURLは引数省略の場合はimage/png
  pngDataURL.push(canvas.toDataURL());
});
// htmlにcanvas要素を追加して表示
document.body.append(canvas);
// preload.jsで設定した関数の使用実験
window.additionalAPI.helloContextBridge();

実行してみます。
win11sec> npx electron .

デベロッパーツール、
Console に「Hello Context Bridge」と表示されます。

デベロッパーツール、
Sources の Watch で window と入力。
window に additionalAPI が、
追加されているのが見えます。

DevTools > Sources > Watch
▼ window: Window
 ? additionalAPI: {helloContextBridge: ?}
 ? alert: ? alert()
 //以下略

デベロッパーツール、
Sources の Watch で window.additionalAPI と入力。
window.additionalAPI に helloContextBridge が、
追加されているのが見えます。

DevTools > Sources > Watch
▼ window.additionalAPI: Object
 ? helloContextBridge: ? ()
 ? [[Prototype]]: Object

プロセス間通信(IPC通信)

注意
レンダラープロセス⇒メインプロセスのプロセス間通信の話になります。
レンダラープロセス⇔メインプロセスのプロセス間通信や、
メインプロセス⇒レンダラープロセスのプロセス間通信は、
Windows11秒(win11sec.exe)では必要ないので取り扱いません。

main.jspreload.jsrenderer.jsに以下を追記します。

main.js
console.log('Hello World ( main.js )');

// Electronのモジュールの読み込み
const {
  app,
  BrowserWindow,
  nativeImage,
  Menu,
  Tray,
+ ipcMain,
  shell,
} = require('electron');

let interval = null;
let prevSec = null;
let thisSec = null;

let mainWindow = null;
let tray = null;

// 中略

// Electronの初期化時に実行
app.whenReady().then(() => {
  // 中略
});

+ // renderer.jsからメッセージの文字列を受信
+ ipcMain.on('send_msg_data_render_to_main', (event, data) => {
+   console.log(data);
+ });
preload.js
console.log('Hello World ( preload.js )');

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

contextBridge.exposeInMainWorld('additionalAPI', {
  helloContextBridge: () => {
    console.log('Hello Context Bridge');
  },
+ sendMsgData: (data) => {
+   ipcRenderer.send('send_msg_data_render_to_main', data);
+ },
});
renderer.js
console.log('Hello World ( renderer.js )');
// キャンバスの生成
const canvas = document.createElement('canvas');
canvas.width = 256;
canvas.height = 256;
// 中略
// htmlにcanvas要素を追加して表示
document.body.append(canvas);
// preload.jsで設定した関数の使用実験
window.additionalAPI.helloContextBridge();
+ // preload.jsで設定した関数を使用して、
+ // main.jsへとメッセージの文字列を送信
+ window.additionalAPI.sendMsgData('Hello InterProcess Communication');
:notebook_with_decorative_cover:
main.js
console.log('Hello World ( main.js )');

// Electronのモジュールの読み込み
const {
  app,
  BrowserWindow,
  nativeImage,
  Menu,
  Tray,
  ipcMain,
  shell,
} = require('electron');

let interval = null;
let prevSec = null;
let thisSec = null;

let mainWindow = null;
let tray = null;

// 全てのウィンドウが閉じた時に実行
app.on('window-all-closed', () => {
  if (process.platform !== 'darwin') {
    app.quit();
  }
});

// Electronの終了時に実行
app.on('quit', () => {
  // インターバルのタイマーの繰り返し動作の取り消し
  clearInterval(interval);
  // タスクトレイのアイコンの削除
  tray.destroy();
});

// Electronの初期化時に実行
app.whenReady().then(() => {
  // メニューの構築
  const contextMenu = Menu.buildFromTemplate([
    {
      label: 'Settings',
      click: () => {
        shell.openExternal('ms-settings:taskbar');
      },
    },
    {
      label: 'Quit',
      click: () => {
        app.quit();
      },
    },
  ]);
  // タスクトレイのアイコンの作成
  tray = new Tray(nativeImage.createFromPath('clock.png'));
  // タスクトレイのアイコンのホバーテキストの設定
  tray.setToolTip('windows11秒');
  // タスクトレイのアイコンのコンテキストメニューの設定
  tray.setContextMenu(contextMenu);
  // メインウインドウ関連の初期設定
  mainWindow = new BrowserWindow({
    // メインウインドウ生成時のウインドウの横幅
    width: 1600,
    // メインウインドウ生成時のウインドウの高さ
    height: 900,
    // 上記の横幅と高さがビューポートとしての値としてメインウインドウ生成
    useContentSize: true,
    // ウェブページの機能の設定
    webPreferences: {
      // 他のスクリプトがページで実行される前にロードされるスクリプトを指定
      preload: `${app.getAppPath()}/preload.js`,
    },
  });
  // メインウインドウのアイコンの設定
  mainWindow.setIcon('clock.png');
  // タスクバーのアイコンにオーバーレイされる画像の設定
  mainWindow.setOverlayIcon(nativeImage.createFromPath('bell.png'), '');
  // メインウインドウに表示するページの読み込み
  mainWindow.loadFile('index.html');
  // ブラウザのF12のデベロッパーツールの表示
  mainWindow.webContents.openDevTools();
  // ウィンドウが閉じた時に実行
  mainWindow.on('closed', () => {
    mainWindow = null;
  });
  // 1/30秒に1回インターバルで現在時刻の秒を確認
  interval = setInterval(() => {
    const now = new Date();
    thisSec = now.getSeconds();
    if (thisSec !== prevSec) {
      console.log(thisSec);
    }
    prevSec = thisSec;
  }, 33);
});

// renderer.jsからメッセージの文字列を受信
ipcMain.on('send_msg_data_render_to_main', (event, data) => {
  console.log(data);
});
preload.js
console.log('Hello World ( preload.js )');

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

contextBridge.exposeInMainWorld('additionalAPI', {
  helloContextBridge: () => {
    console.log('Hello Context Bridge');
  },
  sendMsgData: (data) => {
    ipcRenderer.send('send_msg_data_render_to_main', data);
  },
});
renderer.js
console.log('Hello World ( renderer.js )');
// キャンバスの生成
const canvas = document.createElement('canvas');
canvas.width = 256;
canvas.height = 256;
// キャンバスのコンテキストの取得
const ctx = canvas.getContext('2d');
ctx.textAlign = 'center';
ctx.fillStyle = 'black';
ctx.font = '200px monospace';
ctx.fillText('00', 128, 222);
// 61枚のPNG画像のDataURLの保持用の配列の初期化
const pngDataURL = [];
// 61枚分処理
[...Array(61).keys()].forEach((i) => {
  // キャンバスのコンテキストの全領域を透明色で消去
  ctx.clearRect(0, 0, 256, 256);
  // キャンバスのコンテキストに数値を二桁で描画
  ctx.fillText(i.toString().padStart(2, '0'), 128, 222);
  // toDataURLでPNG画像のDataURLを保持用の配列に格納
  // toDataURLは引数省略の場合はimage/png
  pngDataURL.push(canvas.toDataURL());
});
// htmlにcanvas要素を追加して表示
document.body.append(canvas);
// preload.jsで設定した関数の使用実験
window.additionalAPI.helloContextBridge();
// preload.jsで設定した関数を使用して、
// main.jsへとメッセージの文字列を送信
window.additionalAPI.sendMsgData('Hello InterProcess Communication');

実行してみます。
win11sec> npx electron .

コマンドプロンプトに「Hello InterProcess Communication」と表示されます。

概要説明

ipcRenderer.send('識別子', data)で送信したものを、
ipcMain.on('識別子', function(event, data){})で待ち受けして、
受信が発生した際はfunction(event, data){}で処理しています。
※electron.orgのドキュメントと表現を若干変えています。

addEventListener()

addEventListener.html
<!DOCTYPE html>
<html>
<head>
</head>
<body>
<p>addEventListener</p>
<script src="addEventListener.js"></script>
</body>
</html>
addEventListener.js
document.querySelector('p').addEventListener('click', (e) => {
  console.log(`Hello Click => x:${e.x} y:${e.y}`);
  console.log(e.type);
  console.log(e.target);
});
window.addEventListener('message', (e) => {
  console.log(e.data);
  console.log(e.type);
  console.log(e.target);
});
window.postMessage('Hello Post Message', '*');

唐突ですが、何の変哲もない、
通常のJavaScriptでのイベント処理です。

システム(ブラウザ)からの'識別子'のイベントの配信を、
hoge.addEventListener('識別子', (e) => {})で待ち受けして、
受信が発生した際は(e) => {}で処理しています。

受信が発生した際は(e) => {}の引数にて、
イベントオブジェクトeを受け取ります。
イベントオブジェクトeには、
イベント共通の情報、および、
マウスイベントであれば座標情報など、
メッセージイベントであればメッセージデータなど、
が格納されており、
それを使用して処理をしているわけですね。

ipcMain.on()

ipcMain.on('識別子', (event, data) => {})も似たような関数です。
ipcMain.on('識別子', (event, data) => {})で'識別子'を待ち受けして、
受信が発生した際は(event, data) => {}で処理しています。

window.addEventListener('message', (e) => {})では、
イベントオブジェクトeの中にe.dataがありますが、

ipcMain.on('識別子', (event, data) => {})では、
(event, data) => {}の第二引数にdataがあります。

第一引数のIpcMainEventObjecteventは、
Windows11秒(win11sec.exe)では特には必要ないので未使用です。

ipcRenderer.send()

ipcRenderer.send('識別子', data)は見たままです。
第一引数の'識別子'に向けて、
第二引数のdataを送信します。

Hello InterProcess Communication

'Hello InterProcess Communication'
という文字列を上記では送信しています。

他の言語出身からすると、
文字列を指定して大丈夫なのかしら?
と思ったのですが大丈夫なようです。

JavaScriptでは文字列がプリミティブなので、
で、自分としては決着しましたが、
それで正しいのかは不明です...

プロセス間通信(IPC通信)まとめ

プリロードスクリプト、および、contextBridgeは、
プロセス間通信(IPC通信)に関してセットの内容です。
Electronの過去の経緯を含めての詳細は、
下記などを別途参照くださいませ。

秒の画像のプロセス間通信(IPC通信)

秒の画像のプロセス間通信(IPC通信)①

00の画像を1枚renderer.jsからmain.jsに送信してみます。

main.jspreload.jsrenderer.jsに以下を追記します。

main.js
console.log('Hello World ( main.js )');

// 中略

// Electronの初期化時に実行
app.whenReady().then(() => {
  // 中略
});

// renderer.jsからメッセージの文字列を受信
ipcMain.on('send_msg_data_render_to_main', (event, data) => {
  console.log(data);
});

+ // renderer.jsから00番のPNG画像のDataURLを受信
+ ipcMain.on('send_png_data_render_to_main', (event, data) => {
+   console.log(data);
+ });
preload.js
console.log('Hello World ( preload.js )');

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

contextBridge.exposeInMainWorld('additionalAPI', {
  helloContextBridge: () => {
    console.log('Hello Context Bridge');
  },
  sendMsgData: (data) => {
    ipcRenderer.send('send_msg_data_render_to_main', data);
  },
+ sendPngDataURL: (data) => {
+   ipcRenderer.send('send_png_data_render_to_main', data);
+ },
});
renderer.js
console.log('Hello World ( renderer.js )');
// キャンバスの生成
const canvas = document.createElement('canvas');
canvas.width = 256;
canvas.height = 256;
// 中段
// htmlにcanvas要素を追加して表示
document.body.append(canvas);
// preload.jsで設定した関数の使用実験
window.additionalAPI.helloContextBridge();
// preload.jsで設定した関数を使用して、
// main.jsへとメッセージの文字列を送信
window.additionalAPI.sendMsgData('Hello InterProcess Communication');
+ // preload.jsで設定した関数を使用して、
+ // main.jsへと00番のPNG画像のDataURLを送信
+ window.additionalAPI.sendPngDataURL(pngDataURL[0]);
:notebook_with_decorative_cover:
main.js
console.log('Hello World ( main.js )');

// Electronのモジュールの読み込み
const {
  app,
  BrowserWindow,
  nativeImage,
  Menu,
  Tray,
  ipcMain,
  shell,
} = require('electron');

let interval = null;
let prevSec = null;
let thisSec = null;

let mainWindow = null;
let tray = null;

// 全てのウィンドウが閉じた時に実行
app.on('window-all-closed', () => {
  if (process.platform !== 'darwin') {
    app.quit();
  }
});

// Electronの終了時に実行
app.on('quit', () => {
  // インターバルのタイマーの繰り返し動作の取り消し
  clearInterval(interval);
  // タスクトレイのアイコンの削除
  tray.destroy();
});

// Electronの初期化時に実行
app.whenReady().then(() => {
  // メニューの構築
  const contextMenu = Menu.buildFromTemplate([
    {
      label: 'Settings',
      click: () => {
        shell.openExternal('ms-settings:taskbar');
      },
    },
    {
      label: 'Quit',
      click: () => {
        app.quit();
      },
    },
  ]);
  // タスクトレイのアイコンの作成
  tray = new Tray(nativeImage.createFromPath('clock.png'));
  // タスクトレイのアイコンのホバーテキストの設定
  tray.setToolTip('windows11秒');
  // タスクトレイのアイコンのコンテキストメニューの設定
  tray.setContextMenu(contextMenu);
  // メインウインドウ関連の初期設定
  mainWindow = new BrowserWindow({
    // メインウインドウ生成時のウインドウの横幅
    width: 1600,
    // メインウインドウ生成時のウインドウの高さ
    height: 900,
    // 上記の横幅と高さがビューポートとしての値としてメインウインドウ生成
    useContentSize: true,
    // ウェブページの機能の設定
    webPreferences: {
      // 他のスクリプトがページで実行される前にロードされるスクリプトを指定
      preload: `${app.getAppPath()}/preload.js`,
    },
  });
  // メインウインドウのアイコンの設定
  mainWindow.setIcon('clock.png');
  // タスクバーのアイコンにオーバーレイされる画像の設定
  mainWindow.setOverlayIcon(nativeImage.createFromPath('bell.png'), '');
  // メインウインドウに表示するページの読み込み
  mainWindow.loadFile('index.html');
  // ブラウザのF12のデベロッパーツールの表示
  mainWindow.webContents.openDevTools();
  // ウィンドウが閉じた時に実行
  mainWindow.on('closed', () => {
    mainWindow = null;
  });
  // 1/30秒に1回インターバルで現在時刻の秒を確認
  interval = setInterval(() => {
    const now = new Date();
    thisSec = now.getSeconds();
    if (thisSec !== prevSec) {
      console.log(thisSec);
    }
    prevSec = thisSec;
  }, 33);
});

// renderer.jsからメッセージの文字列を受信
ipcMain.on('send_msg_data_render_to_main', (event, data) => {
  console.log(data);
});

// renderer.jsから00番のPNG画像のDataURLを受信
ipcMain.on('send_png_data_render_to_main', (event, data) => {
  console.log(data);
});
preload.js
console.log('Hello World ( preload.js )');

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

contextBridge.exposeInMainWorld('additionalAPI', {
  helloContextBridge: () => {
    console.log('Hello Context Bridge');
  },
  sendMsgData: (data) => {
    ipcRenderer.send('send_msg_data_render_to_main', data);
  },
  sendPngDataURL: (data) => {
    ipcRenderer.send('send_png_data_render_to_main', data);
  },
});
renderer.js
console.log('Hello World ( renderer.js )');
// キャンバスの生成
const canvas = document.createElement('canvas');
canvas.width = 256;
canvas.height = 256;
// キャンバスのコンテキストの取得
const ctx = canvas.getContext('2d');
ctx.textAlign = 'center';
ctx.fillStyle = 'black';
ctx.font = '200px monospace';
ctx.fillText('00', 128, 222);
// 61枚のPNG画像のDataURLの保持用の配列の初期化
const pngDataURL = [];
// 61枚分処理
[...Array(61).keys()].forEach((i) => {
  // キャンバスのコンテキストの全領域を透明色で消去
  ctx.clearRect(0, 0, 256, 256);
  // キャンバスのコンテキストに数値を二桁で描画
  ctx.fillText(i.toString().padStart(2, '0'), 128, 222);
  // toDataURLでPNG画像のDataURLを保持用の配列に格納
  // toDataURLは引数省略の場合はimage/png
  pngDataURL.push(canvas.toDataURL());
});
// htmlにcanvas要素を追加して表示
document.body.append(canvas);
// preload.jsで設定した関数の使用実験
window.additionalAPI.helloContextBridge();
// preload.jsで設定した関数を使用して、
// main.jsへとメッセージの文字列を送信
window.additionalAPI.sendMsgData('Hello InterProcess Communication');
// preload.jsで設定した関数を使用して、
// main.jsへと00番のPNG画像のDataURLを送信
window.additionalAPI.sendPngDataURL(pngDataURL[0]);

実行してみます。
win11sec> npx electron .

コマンドプロンプトに文字がズラズラ出力されます。
ズラズラ出力された文字の先頭が、
"data:image/png;base64,なんたらかんたら"
となっているのが見て取れます。
"data:image/png;base64,なんたらかんたら"
なのでpng画像のデータになのでしょう、たぶん...

秒の画像のプロセス間通信(IPC通信)②-1

00~60の画像を61枚renderer.jsからmain.jsに送信してみます。

main.jsrenderer.jsを以下に変更します。

main.js
console.log('Hello World ( main.js )');

// 中略

// Electronの初期化時に実行
app.whenReady().then(() => {
  // 中略
});

// renderer.jsからメッセージの文字列を受信
ipcMain.on('send_msg_data_render_to_main', (event, data) => {
  console.log(data);
});

- // renderer.jsから00番のPNG画像のDataURLを受信
+ // renderer.jsから61枚のPNG画像のDataURLを受信
ipcMain.on('send_png_data_render_to_main', (event, data) => {
  console.log(data);
});
renderer.js
console.log('Hello World ( renderer.js )');
// キャンバスの生成
const canvas = document.createElement('canvas');
canvas.width = 256;
canvas.height = 256;
// 中略
// htmlにcanvas要素を追加して表示
document.body.append(canvas);
// preload.jsで設定した関数の使用実験
window.additionalAPI.helloContextBridge();
// preload.jsで設定した関数を使用して、
// main.jsへとメッセージの文字列を送信
window.additionalAPI.sendMsgData('Hello InterProcess Communication');
- // preload.jsで設定した関数を使用して、
- // main.jsへと00番のPNG画像のDataURLを送信
- window.additionalAPI.sendPngDataURL(pngDataURL[0]);
+ // preload.jsで設定した関数を使用して、
+ // main.jsへと61枚のPNG画像のDataURLを送信
+ window.additionalAPI.sendPngDataURL(pngDataURL);
:notebook_with_decorative_cover:
renderer.js
console.log('Hello World ( renderer.js )');
// キャンバスの生成
const canvas = document.createElement('canvas');
canvas.width = 256;
canvas.height = 256;
// キャンバスのコンテキストの取得
const ctx = canvas.getContext('2d');
ctx.textAlign = 'center';
ctx.fillStyle = 'black';
ctx.font = '200px monospace';
ctx.fillText('00', 128, 222);
// 61枚のPNG画像のDataURLの保持用の配列の初期化
const pngDataURL = [];
// 61枚分処理
[...Array(61).keys()].forEach((i) => {
  // キャンバスのコンテキストの全領域を透明色で消去
  ctx.clearRect(0, 0, 256, 256);
  // キャンバスのコンテキストに数値を二桁で描画
  ctx.fillText(i.toString().padStart(2, '0'), 128, 222);
  // toDataURLでPNG画像のDataURLを保持用の配列に格納
  // toDataURLは引数省略の場合はimage/png
  pngDataURL.push(canvas.toDataURL());
});
// htmlにcanvas要素を追加して表示
document.body.append(canvas);
// preload.jsで設定した関数の使用実験
window.additionalAPI.helloContextBridge();
// preload.jsで設定した関数を使用して、
// main.jsへとメッセージの文字列を送信
window.additionalAPI.sendMsgData('Hello InterProcess Communication');
// preload.jsで設定した関数を使用して、
// main.jsへと61枚のPNG画像のDataURLを送信
window.additionalAPI.sendPngDataURL(pngDataURL);

実行してみます。
win11sec> npx electron .

コマンドプロンプトに文字がズラズラ出力されますが、
console.log()で確認する量ではないですね。。。
まあ、それはさておき、

window.additionalAPI.sendPngDataURL(pngDataURL[0]);を、
window.additionalAPI.sendPngDataURL(pngDataURL);にして大丈夫なのでしょうか。

ipcMain.on()
ipcRenderer.send()
ドキュメントは各々以下となっています。

ipcMain.on(channel, listener)
    channel string
    listener Function
        event IpcMainEvent
        ...args any[]

ipcRenderer.send(channel, ...args)
    channel string
    ...args any[]

該当箇所の引数は...args any[]となっています。

スプレッド構文でしょうか?
残余引数です。
残余引数の説明の中ほどに

残余引数はArrayインスタンスです。

と書かれています。そのため、
window.additionalAPI.sendPngDataURL(pngDataURL);で大丈夫なのでしょうか。

秒の画像のプロセス間通信(IPC通信)②-2

コマンドプロンプトに出力する文字量でないのでコメントアウトしておきます。
もう不要でしょうからついでに秒の数値の出力もコメントアウトしておきます。
代わりに(?)、debugger; を追記しておきます。

main.jsを以下に変更します。

main.js
console.log('Hello World ( main.js )');

// 中略

// Electronの初期化時に実行
app.whenReady().then(() => {
  // 中略
  // 1/30秒に1回インターバルで現在時刻の秒を確認
  interval = setInterval(() => {
    const now = new Date();
    thisSec = now.getSeconds();
    if (thisSec !== prevSec) {
-     console.log(thisSec);
+     // console.log(thisSec);
    }
    prevSec = thisSec;
  }, 33);
});

// renderer.jsからメッセージの文字列を受信
ipcMain.on('send_msg_data_render_to_main', (event, data) => {
- console.log(data);
+ // console.log(data);
});

// renderer.jsから61枚のPNG画像のDataURLを受信
ipcMain.on('send_png_data_render_to_main', (event, data) => {
- console.log(data);
+ // console.log(data);
+ debugger;
});
:notebook_with_decorative_cover:
main.js
console.log('Hello World ( main.js )');

// Electronのモジュールの読み込み
const {
  app,
  BrowserWindow,
  nativeImage,
  Menu,
  Tray,
  ipcMain,
  shell,
} = require('electron');

let interval = null;
let prevSec = null;
let thisSec = null;

let mainWindow = null;
let tray = null;

// 全てのウィンドウが閉じた時に実行
app.on('window-all-closed', () => {
  if (process.platform !== 'darwin') {
    app.quit();
  }
});

// Electronの終了時に実行
app.on('quit', () => {
  // インターバルのタイマーの繰り返し動作の取り消し
  clearInterval(interval);
  // タスクトレイのアイコンの削除
  tray.destroy();
});

// Electronの初期化時に実行
app.whenReady().then(() => {
  // メニューの構築
  const contextMenu = Menu.buildFromTemplate([
    {
      label: 'Settings',
      click: () => {
        shell.openExternal('ms-settings:taskbar');
      },
    },
    {
      label: 'Quit',
      click: () => {
        app.quit();
      },
    },
  ]);
  // タスクトレイのアイコンの作成
  tray = new Tray(nativeImage.createFromPath('clock.png'));
  // タスクトレイのアイコンのホバーテキストの設定
  tray.setToolTip('windows11秒');
  // タスクトレイのアイコンのコンテキストメニューの設定
  tray.setContextMenu(contextMenu);
  // メインウインドウ関連の初期設定
  mainWindow = new BrowserWindow({
    // メインウインドウ生成時のウインドウの横幅
    width: 1600,
    // メインウインドウ生成時のウインドウの高さ
    height: 900,
    // 上記の横幅と高さがビューポートとしての値としてメインウインドウ生成
    useContentSize: true,
    // ウェブページの機能の設定
    webPreferences: {
      // 他のスクリプトがページで実行される前にロードされるスクリプトを指定
      preload: `${app.getAppPath()}/preload.js`,
    },
  });
  // メインウインドウのアイコンの設定
  mainWindow.setIcon('clock.png');
  // タスクバーのアイコンにオーバーレイされる画像の設定
  mainWindow.setOverlayIcon(nativeImage.createFromPath('bell.png'), '');
  // メインウインドウに表示するページの読み込み
  mainWindow.loadFile('index.html');
  // ブラウザのF12のデベロッパーツールの表示
  mainWindow.webContents.openDevTools();
  // ウィンドウが閉じた時に実行
  mainWindow.on('closed', () => {
    mainWindow = null;
  });
  // 1/30秒に1回インターバルで現在時刻の秒を確認
  interval = setInterval(() => {
    const now = new Date();
    thisSec = now.getSeconds();
    if (thisSec !== prevSec) {
      // console.log(thisSec);
    }
    prevSec = thisSec;
  }, 33);
});

// renderer.jsからメッセージの文字列を受信
ipcMain.on('send_msg_data_render_to_main', (event, data) => {
  // console.log(data);
});

// renderer.jsから61枚のPNG画像のDataURLを受信
ipcMain.on('send_png_data_render_to_main', (event, data) => {
  // console.log(data);
  debugger;
});

実行してみます。
ただしいつもとは違う以下コマンドを実行してください。
win11sec> npx electron --inspect-brk .

コマンドプロンプトにメッセージが何か出力されるだけで、
特に何も動きませんが正しい動作です。

ブラウザのChromeを起動してchrome://inspectを開きます。
しばらく待っていると Remote Target に項目が追加され、
inspectがクリックできるようになりますのでクリックします。
ブラウザのChromeのDevToolsが起動します。
Sources を開き、また、しばらく待ちます。

"どっか知らないところの" 最初の行でスクリプトが中断しますので F8 を押します。
すると、上記で追記したdebugger;のところで止まるので、
Sources の Watch に data と入力します。

main.jsに送信されてきたdataの中身が確認できます。

"data:image/png;base64,なんたらかんたら"
が61個あるので無事送信されているのでしょう。

秒の画像のプロセス間通信(IPC通信)②-3

確認できたので、debugger; をコメントアウトしておきます。

main.js
console.log('Hello World ( main.js )');

// 中略

// Electronの初期化時に実行
app.whenReady().then(() => {
  // 中略
});

// renderer.jsからメッセージの文字列を受信
ipcMain.on('send_msg_data_render_to_main', (event, data) => {
  // console.log(data);
});

// renderer.jsから61枚のPNG画像のDataURLを受信
ipcMain.on('send_png_data_render_to_main', (event, data) => {
  // console.log(data);
- debugger;
+ // debugger;
});

タスクトレイのアイコンに秒を描画

ではタスクトレイのアイコンに秒を描画します。

main.jsを以下に変更します。

main.js
console.log('Hello World ( main.js )');

// Electronのモジュールの読み込み
const {
  app,
  BrowserWindow,
  nativeImage,
  Menu,
  Tray,
  ipcMain,
  shell,
} = require('electron');

+ const pngNativeImage = [];

let interval = null;
let prevSec = null;
let thisSec = null;

let mainWindow = null;
let tray = null;

// 中略

// Electronの初期化時に実行
app.whenReady().then(() => {
  // 中略
  // メインウインドウのアイコンの設定
  mainWindow.setIcon('clock.png');
  // タスクバーのアイコンにオーバーレイされる画像の設定
  mainWindow.setOverlayIcon(nativeImage.createFromPath('bell.png'), '');
  // メインウインドウに表示するページの読み込み
  mainWindow.loadFile('index.html');
  // ブラウザのF12のデベロッパーツールの表示
  mainWindow.webContents.openDevTools();
  // ウィンドウが閉じた時に実行
  mainWindow.on('closed', () => {
    mainWindow = null;
  });
- // 1/30秒に1回インターバルで現在時刻の秒を確認
- interval = setInterval(() => {
-   const now = new Date();
-   thisSec = now.getSeconds();
-   if (thisSec !== prevSec) {
-     // console.log(thisSec);
-   }
-   prevSec = thisSec;
- }, 33);
});

// renderer.jsからメッセージの文字列を受信
ipcMain.on('send_msg_data_render_to_main', (event, data) => {
  // console.log(data);
});

// renderer.jsから61枚のPNG画像のDataURLを受信
ipcMain.on('send_png_data_render_to_main', (event, data) => {
  // console.log(data);
  // debugger;
+ // 61枚分処理
+ data.forEach((element, index, array) => {
+   // PNG画像のDataURLからnativeImageに生成
+   pngNativeImage[index] = nativeImage.createFromDataURL(array[index]);
+ });
+ // 1/30秒に1回インターバルで現在時刻の秒を確認
+ interval = setInterval(() => {
+   const now = new Date();
+   thisSec = now.getSeconds();
+   if (thisSec !== prevSec) {
+     // console.log(thisSec);
+     // タスクトレイアイコンに現在時刻の秒の画像を設定
+     tray.setImage(pngNativeImage[now.getSeconds()]);
+   }
+   prevSec = thisSec;
+ }, 33);
});
:notebook_with_decorative_cover:
main.js
console.log('Hello World ( main.js )');

// Electronのモジュールの読み込み
const {
  app,
  BrowserWindow,
  nativeImage,
  Menu,
  Tray,
  ipcMain,
  shell,
} = require('electron');

const pngNativeImage = [];

let interval = null;
let prevSec = null;
let thisSec = null;

let mainWindow = null;
let tray = null;

// 全てのウィンドウが閉じた時に実行
app.on('window-all-closed', () => {
  if (process.platform !== 'darwin') {
    app.quit();
  }
});

// Electronの終了時に実行
app.on('quit', () => {
  // インターバルのタイマーの繰り返し動作の取り消し
  clearInterval(interval);
  // タスクトレイのアイコンの削除
  tray.destroy();
});

// Electronの初期化時に実行
app.whenReady().then(() => {
  // メニューの構築
  const contextMenu = Menu.buildFromTemplate([
    {
      label: 'Settings',
      click: () => {
        shell.openExternal('ms-settings:taskbar');
      },
    },
    {
      label: 'Quit',
      click: () => {
        app.quit();
      },
    },
  ]);
  // タスクトレイのアイコンの作成
  tray = new Tray(nativeImage.createFromPath('clock.png'));
  // タスクトレイのアイコンのホバーテキストの設定
  tray.setToolTip('windows11秒');
  // タスクトレイのアイコンのコンテキストメニューの設定
  tray.setContextMenu(contextMenu);
  // メインウインドウ関連の初期設定
  mainWindow = new BrowserWindow({
    // メインウインドウ生成時のウインドウの横幅
    width: 1600,
    // メインウインドウ生成時のウインドウの高さ
    height: 900,
    // 上記の横幅と高さがビューポートとしての値としてメインウインドウ生成
    useContentSize: true,
    // ウェブページの機能の設定
    webPreferences: {
      // 他のスクリプトがページで実行される前にロードされるスクリプトを指定
      preload: `${app.getAppPath()}/preload.js`,
    },
  });
  // メインウインドウのアイコンの設定
  mainWindow.setIcon('clock.png');
  // タスクバーのアイコンにオーバーレイされる画像の設定
  mainWindow.setOverlayIcon(nativeImage.createFromPath('bell.png'), '');
  // メインウインドウに表示するページの読み込み
  mainWindow.loadFile('index.html');
  // ブラウザのF12のデベロッパーツールの表示
  mainWindow.webContents.openDevTools();
  // ウィンドウが閉じた時に実行
  mainWindow.on('closed', () => {
    mainWindow = null;
  });
});

// renderer.jsからメッセージの文字列を受信
ipcMain.on('send_msg_data_render_to_main', (event, data) => {
  // console.log(data);
});

// renderer.jsから61枚のPNG画像のDataURLを受信
ipcMain.on('send_png_data_render_to_main', (event, data) => {
  // console.log(data);
  // debugger;
  // 61枚分処理
  data.forEach((element, index, array) => {
    // PNG画像のDataURLからnativeImageに生成
    pngNativeImage[index] = nativeImage.createFromDataURL(array[index]);
  });
  // 1/30秒に1回インターバルで現在時刻の秒を確認
  interval = setInterval(() => {
    const now = new Date();
    thisSec = now.getSeconds();
    if (thisSec !== prevSec) {
      // console.log(thisSec);
      // タスクトレイアイコンに現在時刻の秒の画像を設定
      tray.setImage(pngNativeImage[now.getSeconds()]);
    }
    prevSec = thisSec;
  }, 33);
});

実行してみます。
win11sec> npx electron .

タスクトレイに秒が描画されました。

メインウインドウの非表示

メインウインドウを非表示にしつつ、
メインウインドウの描画をコメントアウトしておきます。
メインのアイコンの設定もコメントアウトしておきます。
タスクトレイのアイコンは無色透明画像にしておきます。
ハローワールドは出力抑制せずそのままにしてあります。
これで完成です。

main.js
console.log('Hello World ( main.js )');

// 中略

// Electronの初期化時に実行
app.whenReady().then(() => {
  // 中略
  // タスクトレイのアイコンの作成
- tray = new Tray(nativeImage.createFromPath('clock.png'));
+ tray = new Tray(nativeImage.createFromDataURL('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVQI12NgYAAAAAMAASDVlMcAAAAASUVORK5CYII='));
  // タスクトレイのアイコンのホバーテキストの設定
  tray.setToolTip('windows11秒');
  // タスクトレイのアイコンのコンテキストメニューの設定
  tray.setContextMenu(contextMenu);
  // メインウインドウ関連の初期設定
  mainWindow = new BrowserWindow({
+   // メインウインドウ生成時のウインドウの表示非表示
+   show: false,
    // メインウインドウ生成時のウインドウの横幅
    width: 1600,
    // メインウインドウ生成時のウインドウの高さ
    height: 900,
    // 上記の横幅と高さがビューポートとしての値としてメインウインドウ生成
    useContentSize: true,
    // ウェブページの機能の設定
    webPreferences: {
      // 他のスクリプトがページで実行される前にロードされるスクリプトを指定
      preload: `${app.getAppPath()}/preload.js`,
    },
  });
  // メインウインドウのアイコンの設定
- mainWindow.setIcon('clock.png');
+ // mainWindow.setIcon('clock.png');
  // タスクバーのアイコンにオーバーレイされる画像の設定
-  mainWindow.setOverlayIcon(nativeImage.createFromPath('bell.png'), '');
+ // mainWindow.setOverlayIcon(nativeImage.createFromPath('bell.png'), '');
  // メインウインドウに表示するページの読み込み
  mainWindow.loadFile('index.html');
  // ブラウザのF12のデベロッパーツールの表示
  mainWindow.webContents.openDevTools();
  // ウィンドウが閉じた時に実行
  mainWindow.on('closed', () => {
    mainWindow = null;
  });
});

// 以下略
renderer.js
console.log('Hello World ( renderer.js )');
// キャンバスの生成
const canvas = document.createElement('canvas');
canvas.width = 256;
canvas.height = 256;
// キャンバスのコンテキストの取得
const ctx = canvas.getContext('2d');
ctx.textAlign = 'center';
ctx.fillStyle = 'black';
ctx.font = '200px monospace';
- ctx.fillText('00', 128, 222);
+ // ctx.fillText('00', 128, 222);
// 61枚のPNG画像のDataURLの保持用の配列の初期化
const pngDataURL = [];
// 61枚分処理
[...Array(61).keys()].forEach((i) => {
  // キャンバスのコンテキストの全領域を透明色で消去
  ctx.clearRect(0, 0, 256, 256);
  // キャンバスのコンテキストに数値を二桁で描画
  ctx.fillText(i.toString().padStart(2, '0'), 128, 222);
  // toDataURLでPNG画像のDataURLを保持用の配列に格納
  // toDataURLは引数省略の場合はimage/png
  pngDataURL.push(canvas.toDataURL());
});
// htmlにcanvas要素を追加して表示
- document.body.append(canvas);
+ // document.body.append(canvas);
// preload.jsで設定した関数の使用実験
window.additionalAPI.helloContextBridge();
// preload.jsで設定した関数を使用して、
// main.jsへとメッセージの文字列を送信
window.additionalAPI.sendMsgData('Hello InterProcess Communication');
// preload.jsで設定した関数を使用して、
// main.jsへと61枚のPNG画像のDataURLを送信
window.additionalAPI.sendPngDataURL(pngDataURL);
:notebook_with_decorative_cover:
main.js
console.log('Hello World ( main.js )');

// Electronのモジュールの読み込み
const {
  app,
  BrowserWindow,
  nativeImage,
  Menu,
  Tray,
  ipcMain,
  shell,
} = require('electron');

const pngNativeImage = [];

let interval = null;
let prevSec = null;
let thisSec = null;

let mainWindow = null;
let tray = null;

// 全てのウィンドウが閉じた時に実行
app.on('window-all-closed', () => {
  if (process.platform !== 'darwin') {
    app.quit();
  }
});

// Electronの終了時に実行
app.on('quit', () => {
  // インターバルのタイマーの繰り返し動作の取り消し
  clearInterval(interval);
  // タスクトレイのアイコンの削除
  tray.destroy();
});

// Electronの初期化時に実行
app.whenReady().then(() => {
  // メニューの構築
  const contextMenu = Menu.buildFromTemplate([
    {
      label: 'Settings',
      click: () => {
        shell.openExternal('ms-settings:taskbar');
      },
    },
    {
      label: 'Quit',
      click: () => {
        app.quit();
      },
    },
  ]);
  // タスクトレイのアイコンの作成
  tray = new Tray(nativeImage.createFromDataURL('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVQI12NgYAAAAAMAASDVlMcAAAAASUVORK5CYII='));
  // タスクトレイのアイコンのホバーテキストの設定
  tray.setToolTip('windows11秒');
  // タスクトレイのアイコンのコンテキストメニューの設定
  tray.setContextMenu(contextMenu);
  // メインウインドウ関連の初期設定
  mainWindow = new BrowserWindow({
    // メインウインドウ生成時のウインドウの表示非表示
    show: false,
    // メインウインドウ生成時のウインドウの横幅
    width: 1600,
    // メインウインドウ生成時のウインドウの高さ
    height: 900,
    // 上記の横幅と高さがビューポートとしての値としてメインウインドウ生成
    useContentSize: true,
    // ウェブページの機能の設定
    webPreferences: {
      // 他のスクリプトがページで実行される前にロードされるスクリプトを指定
      preload: `${app.getAppPath()}/preload.js`,
    },
  });
  // メインウインドウのアイコンの設定
  // mainWindow.setIcon('clock.png');
  // タスクバーのアイコンにオーバーレイされる画像の設定
  // mainWindow.setOverlayIcon(nativeImage.createFromPath('bell.png'), '');
  // メインウインドウに表示するページの読み込み
  mainWindow.loadFile('index.html');
  // ブラウザのF12のデベロッパーツールの表示
  mainWindow.webContents.openDevTools();
  // ウィンドウが閉じた時に実行
  mainWindow.on('closed', () => {
    mainWindow = null;
  });
});

// renderer.jsからメッセージの文字列を受信
ipcMain.on('send_msg_data_render_to_main', (event, data) => {
  // console.log(data);
});

// renderer.jsから61枚のPNG画像のDataURLを受信
ipcMain.on('send_png_data_render_to_main', (event, data) => {
  // console.log(data);
  // debugger;
  // 61枚分処理
  data.forEach((element, index, array) => {
    // PNG画像のDataURLからnativeImageに生成
    pngNativeImage[index] = nativeImage.createFromDataURL(array[index]);
  });
  // 1/30秒に1回インターバルで現在時刻の秒を確認
  interval = setInterval(() => {
    const now = new Date();
    thisSec = now.getSeconds();
    if (thisSec !== prevSec) {
      // console.log(thisSec);
      // タスクトレイアイコンに現在時刻の秒の画像を設定
      tray.setImage(pngNativeImage[now.getSeconds()]);
    }
    prevSec = thisSec;
  }, 33);
});
preload.js
console.log('Hello World ( preload.js )');

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

contextBridge.exposeInMainWorld('additionalAPI', {
  helloContextBridge: () => {
    console.log('Hello Context Bridge');
  },
  sendMsgData: (data) => {
    ipcRenderer.send('send_msg_data_render_to_main', data);
  },
  sendPngDataURL: (data) => {
    ipcRenderer.send('send_png_data_render_to_main', data);
  },
});
renderer.js
console.log('Hello World ( renderer.js )');
// キャンバスの生成
const canvas = document.createElement('canvas');
canvas.width = 256;
canvas.height = 256;
// キャンバスのコンテキストの取得
const ctx = canvas.getContext('2d');
ctx.textAlign = 'center';
ctx.fillStyle = 'black';
ctx.font = '200px monospace';
// ctx.fillText('00', 128, 222);
// 61枚のPNG画像のDataURLの保持用の配列の初期化
const pngDataURL = [];
// 61枚分処理
[...Array(61).keys()].forEach((i) => {
  // キャンバスのコンテキストの全領域を透明色で消去
  ctx.clearRect(0, 0, 256, 256);
  // キャンバスのコンテキストに数値を二桁で描画
  ctx.fillText(i.toString().padStart(2, '0'), 128, 222);
  // toDataURLでPNG画像のDataURLを保持用の配列に格納
  // toDataURLは引数省略の場合はimage/png
  pngDataURL.push(canvas.toDataURL());
});
// htmlにcanvas要素を追加して表示
// document.body.append(canvas);
// preload.jsで設定した関数の使用実験
window.additionalAPI.helloContextBridge();
// preload.jsで設定した関数を使用して、
// main.jsへとメッセージの文字列を送信
window.additionalAPI.sendMsgData('Hello InterProcess Communication');
// preload.jsで設定した関数を使用して、
// main.jsへと61枚のPNG画像のDataURLを送信
window.additionalAPI.sendPngDataURL(pngDataURL);

実行ファイルの作成

electron-builderを使用して実行ファイル(exe)を作成します。
以下実行します。

win11sec> npx electron-builder --win --x64 -c.win.target=portable -c.win.icon=clock.png -c.artifactName=${productName}.${ext}

ビルドに数分掛かりますが完了すると、
distフォルダにexeが出来上がります。

おまけ(ESLint)

JavaScriptを書くのであれば、ESLintを使用すると、
コードがコーディング規約に準拠をしているか、および、
コードのインデントや字下げスタイルを確認してくれるので、
文法不備などの記述不備によるエラーを事前確認出来るので有難いですね。
手軽に試すのであればこちら

参考
JavaScriptと互換性の高い
TypeScriptとエコシステムの情報
TypeScript Frontend Ecosystem Landscape

あとがき

ノンプログラマーの素人が記述をしたコードです。
狭い利用範囲と少ない利用頻度での確認ですので、
記載内容に間違いや勘違いがあるかもしれません。
上記内容を参照の際は自己責任でお願い致します。

参考

https://www.electronjs.org/ja/docs/latest/
https://www.electronjs.org/ja/docs/latest/tutorial/quick-start

https://www.electronjs.org/ja/docs/latest/tutorial/process-model
https://www.electronjs.org/ja/docs/latest/tutorial/context-isolation
https://www.electronjs.org/ja/docs/latest/tutorial/ipc
https://www.electronjs.org/ja/docs/latest/tutorial/examples
https://www.electronjs.org/ja/docs/latest/tutorial/debugging-main-process

https://www.electronjs.org/ja/docs/latest/breaking-changes
https://www.electronjs.org/ja/docs/latest/tutorial/electron-timelines
https://www.electronjs.org/ja/docs/latest/glossary

https://www.electronjs.org/ja/docs/latest/api/app
https://www.electronjs.org/ja/docs/latest/api/browser-window
https://www.electronjs.org/ja/docs/latest/api/ipc-main
https://www.electronjs.org/ja/docs/latest/api/menu
https://www.electronjs.org/ja/docs/latest/api/native-image
https://www.electronjs.org/ja/docs/latest/api/shell
https://www.electronjs.org/ja/docs/latest/api/tray

https://www.electronjs.org/ja/docs/latest/api/context-bridge
https://www.electronjs.org/ja/docs/latest/api/ipc-renderer

13
10
2

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
13
10