3
5

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

Electron 18 を使って200行以内でファイルの入出力ができるようなテキストエディタを作ってみる。

Posted at

初めに

実用を目的とはしていません。Electronの構造を理解するために、どれだけ骨格を抜き出せるかを目的にしています。

というわけでスクラッチから書いてみます。

html中にstylejavascriptを直書きしているので、以下のワーニングが出ます。
Electron Security Warning (Insecure Content-Security-Policy)

以下のレポジトリにワーニングの出ない、他の機能も入ったバージョンを置いておきます。
ワーニングの消し方や結果だけを知りたい方はこちらで!
https://github.com/Satachito/electron-quick-start-MOD

npm,npxは入っている前提です。(nodeが入っていれば通常入っています)

準備

フォルダを作ってその中にpackage.jsonを作ってnpmElectronをインストールします。

$ mkdir TE
$ cd TE
package.json
{	"main": "main.js"
,	"devDependencies": { "electron": "^18.0.0-alpha.4" }
}
$ npm i

preload.js

ipcRendererをレンダラプロセスが使えるようにすることによって、メインプロセスにメッセージを送れるようにします。

contextBridgeを使って、レンダラプロセスがメインプロセスからのdatamenuメッセージを受け取れるようにしています。

  • dataメッセージはメインプロセスがファイルを開けたらその内容を送ってきます
  • menuメッセージはメインプロセスが選択されたメニューを送ってきます
preload.js
const { contextBridge, ipcRenderer } = require( 'electron' )
contextBridge.exposeInMainWorld(
	'ipcRenderer'
,	ipcRenderer
)
contextBridge.exposeInMainWorld(
	'onData'
,   $ => ipcRenderer.on( 'data', $ )
)
contextBridge.exposeInMainWorld(
	'onMenu'
,   $ => ipcRenderer.on( 'menu', $ )
)

index.html

index.htmlに編集用のtextareaとレンダラプロセスを記述します。

preload.jsで見えるようになったonMenuSaveSaveAsメッセージに反応し、編集中のデータ(TextArea.value)をメインプロセスに送ります。メインプロセスは保存したらファイル名を、しなかったらundefinedを戻します。

onDataはメインプロセスから送られてきたファイルの内容をtextareaにセットし、ファイル名をドキュメントタイトルにセットします。

ウインドウが閉じられる時、元のデータと編集中のデータが違っていたら、ev.returnValueに何かセットして、メインプロセスでBrowserWindowwebContentswill-prevent-unloadを検知できるようにします。

index.html
<textarea id=TextArea style="resize: none; width: 100%; height: 100%"></textarea>
<script type=module>
let
prevData = TextArea.value
onData(
	( ev, $, file ) => (
		prevData = $
	,	TextArea.value = $
	,	document.title = file
	)
)
onMenu(
	( ev, $ ) => {
		const
		Save = _ => ipcRenderer.invoke( _, TextArea.value ).then(
			file => file && (
				prevData = TextArea.value
			,	document.title = file
			)
		)
		switch ( $ ) {
		case 'Save':
			Save( 'save' )
			break
		case 'SaveAs':
			Save( 'saveAs' )
			break
		}
	}
)
onbeforeunload = ev => TextArea.value != prevData
?	ev.returnValue = ''	// for Chrome/Electron
:	undefined
</script>

main.js

メインプロセスです。

CreateWindowで新しいBrowserWindowを開きます。
file名が引数で渡されたら、BrowserWindowwebContentsdid-finish-loadを検知したらレンダラプロセスにファイルの内容を送ります。
利用者が編集済みのウインドウを閉じようとしたとき、webContentswill-prevent-unloadを検知します。編集を継続するか破棄するかを選ばせます。
ファイルメニューに New,Open,Save,SaveAsを付け加えます。

  • 利用者がNewを選んだらCreateWindowを呼びます。
  • 利用者がOpenを選んだらopenDialogを出してfileが選ばれたらそのfile名を引数にCreateWindowを呼びます。
  • 利用者がSaveを選んだらレンダラプロセスにmenu,saveと送ります。レンダラプロセスはonMenuでこれを検知すると、メインプロセスにSaveでデータを送ってきます。無事保存したら保存したfile名をレンダラプロセスに返します。保存しなかったらundefineが返されます。
  • 利用者がSaveAsを選んだらレンダラプロセスにmenu,saveAsと送ります。レンダラプロセスはonMenuでこれを検知すると、メインプロセスにSaveAsでデータを送ってきます。無事保存したら保存したfile名をレンダラプロセスに返します。保存しなかったらundefineが返されます。

最後に引数でファイルが渡された時の処理をしています。

main.js
const
{ app, BrowserWindow, Menu, MenuItem, ipcMain, dialog } = require( 'electron' )
const
Send = ( ...$ ) => {
	const _ = BrowserWindow.getFocusedWindow()
	_ && _.send( ...$ )
}
const
CreateWindow = file => {
	const 
	$ = new BrowserWindow(
		{	width			: 1600
		,	height			: 800
		,	webPreferences	: {
				preload		: require( 'path' ).join( __dirname, 'preload.js' )
			}
		}
	)
	$.loadFile( 'index.html' )
	file && (
		$.webContents.on(
			'did-finish-load'
		,	() => $.send( 'data', require( 'fs' ).readFileSync( file, 'utf8' ), file )
		)
	,	$.webContents.file = file
	)
	$.webContents.on(
		'will-prevent-unload'
	,	ev => dialog.showMessageBoxSync(
			$
		,	{	type: 'question'
			,	buttons: [ 'Discard change and close', 'No' ]
			,	message: 'Do you really want to close this window?\nChanges you made may not be saved.'
			}
		) === 0 && ev.preventDefault()
	)
}
const
SaveAs = ( ev, $ ) => {
	const file = dialog.showSaveDialogSync(
		{	properties	: [ 'openFile', 'openDirectory' ]
		,	defaultPath	: ev.sender.file
		}
	)
	file && (
		require( 'fs' ).writeFileSync( file, $ )
	,	ev.sender.file = file
	)
	return file
}
ipcMain.handle( 'saveAs', SaveAs )
ipcMain.handle(
	'save'
,	( ev, $ ) => {
		const file = ev.sender.file
		return file
		?	(	require( 'fs' ).writeFileSync( file, $ )
			,	file
			)
		:	SaveAs( ev, $ )
	}
)
const
isMac = process.platform === 'darwin'
app.on(
	'window-all-closed'
,	() => isMac || app.quit()
)
app.on(
	'activate'
,	( event, hasVisibleWindows ) => hasVisibleWindows || CreateWindow()
)
app.on(
	'open-file'
,	( ev, _ ) => (
		ev.preventDefault()
	,	CreateWindow( _ )
	)
)
app.whenReady().then(
	() => {
		const
		menu = Menu.getApplicationMenu()
		const
		fileMenu = menu.items.find( $ => $.role === 'filemenu' ).submenu
		fileMenu.insert(
			0
		,	new MenuItem( 
				{	label		: 'Save as...'
				,	click		: ev => Send( 'menu', 'SaveAs' )
				}
			)
		)
		fileMenu.insert(
			0
		,	new MenuItem( 
				{	label		: 'Save'
				,	accelerator	: 'CmdOrCtrl+S'
				,	click		: ev => Send( 'menu', 'Save' )
				}
			)
		)
		fileMenu.insert( 0, new MenuItem( {	type: 'separator' } ) )
		fileMenu.insert(
			0
		,	new MenuItem( 
				{	label		: 'Open...'
				,	accelerator	: 'CmdOrCtrl+O'
				,	click		: ev => {
									const _ = dialog.showOpenDialogSync( { properties: [ 'openFile', 'openDirectory' ] } )
									_ && _.forEach( $ => CreateWindow( $ ) )
								}

				}
			)
		)
		fileMenu.insert(
			0
		,	new MenuItem( 
				{	label		: 'New'
				,	accelerator	: 'CmdOrCtrl+N'
				,	click		: ev => CreateWindow()
				}
			)
		)
		Menu.setApplicationMenu( menu )
		const _ = process.argv.slice(
			isMac
			?	process.argv[ 0 ].split( '/' ).pop()	=== 'Electron'		? 2 : 1
			:	process.argv[ 0 ].split( '\\' ).pop()	=== 'electron.exe'	? 2 : 1
		)
		_.length
		?	_.forEach( _ => CreateWindow( _ ) )
		:	CreateWindow()
	}
)

実行

$ npx electron .

最後に

パック前で以下の感じです。目的は果たせた気がします。

% wc *.js *.html package.json
     136     446    2852 main.js
      13      34     289 preload.js
      34     102     640 index.html
       3      10      77 package.json
     186     592    3858 total
3
5
0

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
3
5

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?