Electron, TypeScript, React, Material-UI で簡単なメモ帳アプリを作る

Electron、TypeScript、React、Material-UI の学習メモです。

# 概要
#### Electron
Wikipediaの記事が端的で分かりやすかったので転載。
> Electronは、GitHubが開発したオープンソースのソフトウェアフレームワークである。
> 
> [Chromium](https://ja.wikipedia.org/wiki/Chromium)と[Node.js](https://ja.wikipedia.org/wiki/Node.js)を使っており、HTML、CSS、JavaScriptのようなWeb技術で、macOS、Windows、Linuxに対応したデスクトップアプリケーションをつくることができる。
>
> https://ja.wikipedia.org/wiki/Electron_(%E3%82%BD%E3%83%95%E3%83%88%E3%82%A6%E3%82%A7%E3%82%A2)

[SPA](https://ja.wikipedia.org/wiki/%E3%82%B7%E3%83%B3%E3%82%B0%E3%83%AB%E3%83%9A%E3%83%BC%E3%82%B8%E3%82%A2%E3%83%97%E3%83%AA%E3%82%B1%E3%83%BC%E3%82%B7%E3%83%A7%E3%83%B3)などと大体同じ要領で、スタンドアロンでも動作するデスクトップアプリを作れるようになります。
Chromeの開発者ツールも利用できます。

[Electron](https://electronjs.org/)

#### TypeScript
マイクロソフトによって開発されたプログラミング言語です。
JavaScriptを[静的型付け](https://ja.wikipedia.org/wiki/%E9%9D%99%E7%9A%84%E5%9E%8B%E4%BB%98%E3%81%91)の仕様でラッピングしたようなもので、コンパイル後のソースはバイナリファイルではなく、JavaScriptのテキストファイルです。

[TypeScript](https://www.typescriptlang.org/index.html)

#### React
Facebookによって開発された、JavaScriptのライブラリです。
HTMLと、それを操作するJSのロジックをセットにした「コンポーネント」を組み合わせることで、WEBページを構築します。

[React](https://reactjs.org/)

#### Material-UI
Googleが提唱するマテリアルデザインに準拠したUIパーツを、Reactのコンポーネントとして提供しているライブラリです。
自分でCSSを用意せずとも、素敵なWEBページをすぐに構築できます。

[Material Design](https://material.io/)
[Material-UI](https://material-ui.com/)

# 環境構築
Node.js、npmが既に導入済みであることを前提に進めます。

- Windows10
- Node.js v10.15.1
- npm v6.4.1

## TypeScript
### インストール

```sh
$ npm i -D typescript
```

### 設定
#### TypeScript
プロジェクトのルートフォルダで、下記コマンドを実行して初期設定ファイルを出力します。
コンパイル後のJSのバージョンは2017にしました。

```sh
$ npx tsc --init
```

```json:tsconfig.json
{
  "compilerOptions": {
    "target": "es2017",
    "module": "commonjs",
    "lib": [
      "dom",
      "es2019"
    ],
    "jsx": "react",
    "sourceMap": true,
    "strict": true,
    "esModuleInterop": true
  }
}
```

## ESLintとPrettier
こちらの記事を参考にいたしました。ありがとうございます。
[VSCodeでESLint+@typescript-eslint+Prettierを導入する（v2.0.0修正版）](https://qiita.com/madono/items/a134e904e891c5cb1d20)

### インストール

```sh:ESLint
$ npm i -D eslint @typescript-eslint/parser @typescript-eslint/eslint-plugin
```

```sh:Prettier
$ npm i -D prettier eslint-config-prettier eslint-plugin-prettier
```

### 設定
初期設定ではダブルクォート指定の為、シングルクォートも可に上書きしました。

```json:.eslintrc.json
{
  "env": {
    "browser": true,
    "node": true,
    "es6": true
  },
  "extends": [
    "eslint:recommended",
    "plugin:@typescript-eslint/recommended",
    "plugin:@typescript-eslint/eslint-recommended",
    "plugin:prettier/recommended",
    "prettier/@typescript-eslint"
  ],
  "plugins": [
    "@typescript-eslint"
  ],
  "parser": "@typescript-eslint/parser",
  "parserOptions": {
    "ecmaVersion": 2018,
    "sourceType": "module",
    "project": "./tsconfig.json"
  },
  "rules": {
    // ...
    "prettier/prettier":[
      "warn",
      {
        "singleQuote":true
      }
    ]
  }
}
```

## ElectronとWebpack
こちらの記事を参考にいたしました。ありがとうございます。
[TypeScriptとElectron](https://qiita.com/necomeshi/items/1861ff80e689a377899a)

### インストール
WebpackでTypeScriptを扱う為のLoaderもインストールします。

```sh
$ npm i -D electron webpack webpack-cli ts-loader
```
### 設定
#### Webpack
TypeScriptのコンパイル設定を`strict`に設定している為、nodeのモジュールを`require`でインポートする際に、暗黙のanyに対してエラーが発生します。
今回は、コメントでエラーをスキップしてみました。

```js:webpack.config.js
// eslint-disable-next-line @typescript-eslint/no-var-requires
const path = require('path');

// メインプロセスの設定
const main = {
  mode: 'development',
  target: 'electron-main',
  entry: path.join(__dirname, 'src', 'main'),
  output: {
    filename: 'main.js',
    path: path.resolve(__dirname, 'dist')
  },
  module: {
    // ...
};

// レンダラープロセスの設定
const renderer = {
  mode: 'development',
  target: 'electron-renderer',
  devtool: 'inline-source-map',
  entry: path.join(__dirname, 'src', 'renderer', 'index'),
  output: {
    filename: 'index.js',
    path: path.resolve(__dirname, 'dist', 'scripts')
  },
  module: {
    // ...
};

module.exports = [main, renderer];
```

`target`は、webpackに環境を伝えるプロパティです。
[Targets | webpack](https://webpack.js.org/concepts/targets/)

#### package.json
`main`には、エントリーポイントとなるコンパイル後のファイルを指定します。
開発時はwebpackはウォッチモードで。

```json:package.json
{
  // ...
  "main": "dist/main.js",
  "scripts": {
    "start": "electron .",
    "build": "webpack --display-error-details -w",
    "production": "webpack --mode=production"
  },
  // ...
}
```

## ReactとMaterial-UI
### インストール

TypeScript用の定義ファイルも一緒に取得します。

```sh:React
$ npm i -D react react-dom @types/react @types/react-dom
```

```sh:Material-UI
$ npm i -D @material-ui/core @material-ui/icons
```

Material-UIのアイコンも取得しています。
ReactコードのコンパイルはTS側で行うため、Webpackのloaderや設定は不要です。

### React用Lintの導入
### インストール

```
$ npm i -D eslint-plugin-react eslint-plugin-react-hooks
```

### 設定
`.eslintrc.json`に設定を追記します。
`Props`の型チェックはTypeScriptで行うので、`react/prop-types`は`off`にします。

```diff:.eslintrc.json
{
  // ...
  "extends": [
    "eslint:recommended",
    "plugin:@typescript-eslint/recommended",
    "plugin:@typescript-eslint/eslint-recommended",
    "plugin:prettier/recommended",
    "prettier/@typescript-eslint",
+   "plugin:react/recommended"
  ],
  "plugins": [
+   "react",
+   "react-hooks",
    "@typescript-eslint"
  ],
  "parser": "@typescript-eslint/parser",
  "parserOptions": {
    "ecmaVersion": 2018,
    "sourceType": "module",
    "project": "./tsconfig.json",
+   "ecmaFeatures": {
+     "jsx": true
+   }
  },
  "rules": {
// ...
+   "react/prop-types": "off",
  }
}
```

## VSCode
拡張機能の[ESLint](https://marketplace.visualstudio.com/items?itemName=dbaeumer.vscode-eslint)を追加します。
`setting.json`にて、入力時にチェックし、保存時にフォーマット修正をするようにします。

```
"eslint.enable": true,
"eslint.run": "onType",
"eslint.autoFixOnSave": true,
"eslint.validate": [
    "javascript",
    "javascriptreact",
    {"language": "typescript", "autoFix": true },
    {"language": "typescriptreact", "autoFix": true }
],
```


まだまだ理解がふんわりしているのですが、とりあえず環境構築できました。
疲れた。息切れ。

[create-react-app](https://create-react-app.dev/docs/getting-started)を利用すると、もっと手順が減ります。

# Electron概要
## ElectronでHelloWorld
### ファイルの準備
プロジェクトフォルダ内に、下記のファイルとフォルダを準備します。

```
root
  ├ package.json
  ├ dist
  │   ├ index.html
  │   └ scripts
  └ src
       ├ main.ts
       └ renderer
            └ index.tsx
```

#### アプリのVIEW
後で使うMaterial-UIで利用するフォントのリンクもついでに入れておきます。

```html:/dist/index.html
<!DOCTYPE html>
<html>

<head>
  <meta charset="utf-8" />
  <title>Hello Electron</title>
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <link rel="stylesheet" href="https://fonts.googleapis.com/icon?family=Material+Icons" />
</head>

<body>
  <div id="contents"></div>

  <script type="text/javascript" src="scripts/index.js"></script>
</body>

</html>
```

```tsx:/src/renderer/index.tsx
import React from 'react';
import ReactDOM from 'react-dom';
import { Paper, Typography, makeStyles, Theme } from '@material-ui/core';

// 適用するCSSクラスの定義
const useStyles = makeStyles((theme: Theme) => ({
  root: {
    padding: theme.spacing(3)
  }
}));

// Headコンポーネントのインターフェースを定義
interface HeadProps {
  headline: string;
}

const Head: React.FC<HeadProps> = ({ headline }) => {
  return (
    <Typography variant="h3" component="h1">
      {headline}
    </Typography>
  );
};

const App: React.FC = () => {
  const classes = useStyles();

  return (
    <Paper className={classes.root}>
      <Head headline="Hello World" />
    </Paper>
  );
};

ReactDOM.render(<App />, document.getElementById('contents'));
```

React×TypeScriptのサンプルコードで、関数コンポーネントのインターフェースとして`React.SFC`を利用しているものを見かけますが、廃止予定のようですね。
[React 16.7 - React.SFC is now deprecated | stackoverflow](https://stackoverflow.com/questions/53885993/react-16-7-react-sfc-is-now-deprecated)

#### アプリの起動処理

```ts:/src/main.ts
import { BrowserWindow, app } from 'electron';

const mainURL = `file://${__dirname}/index.html`;
let mainWindow: BrowserWindow | null = null;

// アプリ起動後にWindowを立ち上げる
const createWindow = (): void => {
  mainWindow = new BrowserWindow({
    width: 600,
    height: 450,
    webPreferences: { nodeIntegration: true }
  });

  mainWindow.loadURL(mainURL);
  // 開発者ツールも同時に開く
  mainWindow.webContents.openDevTools();

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

// アプリの起動と終了
app.on('ready', createWindow);
app.on('window-all-closed', () => {
  app.quit();
});
app.on('activate', () => {
  if (mainWindow === null) {
    createWindow();
  }
});
```

`webPreferences: { nodeIntegration: true }`は、セキュリティ上好ましくない設定だそうですが、一旦これで進めます。
[リモートコンテンツで、Node.js integration を有効にしない | electron](https://electronjs.org/docs/tutorial/security#2-%E3%83%AA%E3%83%A2%E3%83%BC%E3%83%88%E3%82%B3%E3%83%B3%E3%83%86%E3%83%B3%E3%83%84%E3%81%A7%E3%80%81nodejs-integration-%E3%82%92%E6%9C%89%E5%8A%B9%E3%81%AB%E3%81%97%E3%81%AA%E3%81%84)
[【Electron】nodeIntegration: falseのまま、RendererプロセスでElectronのモジュールを使用する](https://qiita.com/nomuyoshi/items/9091abd9dc3b05c85f44)


### ChromeのDevToolの拡張機能
[公式サイト](https://electronjs.org/docs/tutorial/devtools-extension)の手順に従い、[React Developer Tools](https://chrome.google.com/webstore/detail/react-developer-tools/fmkadmapgofadopljbjfkapdkoienihi?hl=ja)をElectronの環境でも利用できるようにします。
DevToolを設定する`addDevToolsExtension`メソッドの呼び出しは、`ready`イベント後に1度だけ実行します。

```ts:/src/main.ts
import os from 'os';
import path from 'path';

// ...

app.on('ready', () => {
  createWindow();

  // ReactのDevTool追加
  BrowserWindow.addDevToolsExtension(
    path.join(
      os.homedir(),
      '/AppData/Local/Google/Chrome/User Data/Default/Extensions/fmkadmapgofadopljbjfkapdkoienihi/4.2.0_0'
    )
  );
});

```

公式サイト上では、便利なサードパーティーツール[electron-devtools-installer](https://github.com/MarshallOfSound/electron-devtools-installer)が紹介されています。
当初はこちらを利用しようとしたのですが、
ビルド時に依存パッケージの`7zip`が無いというエラーが出る → 7zipをインストールする → アプリ起動時に`dist\7zip-lite\7z.exe`が無いというエラーが出る
というところで詰まってしまい、一旦外しました。また別の機会に…。

### アプリの起動
予め`package.json`の`script`に登録したコマンドを実行します。

```sh
# ソースのコンパイル
$ npm run build

# アプリの起動
$ npm start
```
![hello_world.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/362737/10396134-894b-2c41-5341-e4eff19302af.png)

無事、DevToolと共に表示されました。

## メインプロセスとレンダラープロセス
Electronが`package.json`の`main`で指定したファイルを実行することで、アプリケーションのメインプロセスが立ち上がります。
メインプロセスはElectronアプリの基幹となり、アプリの実行中に一つだけ存在します。この時点ではまだ何も表示されていません。
メインプロセスから`BrowserWindow`のインスタンスを一つ以上生成します。
生成された`BrowserWindow`のインスタンス毎に実行されるプロセスを、レンダラープロセスと呼びます。
このレンダラープロセスは、WEBページの形式でアプリのGUIを担います。

![プロセス流れ.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/362737/bce343f0-cf73-dd16-94da-9ebb7f212b7c.png)

## プロセス間の通信
メインプロセスとレンダラープロセスの間で情報をやり取りするには、ipc(inter process communication)モジュールを用います。
メインプロセス向けに用意されたのが`ipcMain`、レンダラープロセス向けに用意されたのが`ipcRenderer`です。
これは[カスタムイベント](https://developer.mozilla.org/ja/docs/Web/Guide/Events/Creating_and_triggering_events)に似ています。
特定のイベント名（チャンネル）に紐づく情報の購読登録、およびイベントの発火（情報の送信）を行うことで、非同期的に通信を行います。

![IPC.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/362737/47a12bd6-0ae8-d464-8761-721a3f19b524.png)

```ts
// メインプロセス側の購読登録
import { ipcMain, IpcMainEvent } from 'electron';

ipcMain.on('close-window', (event: IpcMainEvent) => {
  // ...
});

// レンダラープロセス側の送信
import { ipcRenderer } from 'electron';
ipcRenderer.send('close-window');
```

メインプロセスからレンダラープロセスに送信する場合、レンダラープロセス側の購読登録は`ipcRenderer`を利用します。
一方でメインプロセス側からの送信は、送信対象となるレンダラープロセスの`BrowserWindow`のインスタンスを利用します。
これにより、他の無関係なレンダラープロセスが受信することはありません。

![IPC_2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/362737/ca1056eb-9dd1-fea1-c2eb-735e961f8142.png)

```ts
// レンダラープロセス側の購読登録
import { ipcRenderer, IpcRendererEvent } from 'electron';

ipcRenderer.on('read-file', (event: IpcRendererEvent, data) => {
  console.log(data.fileName);
  console.log(data.fileData);
});

// メインプロセス側の送信
browserWinObj.webContents.send('read-file', {
  fileName,
  fileData
});
```

# 簡単なテキストエディタを作る
## 完成図
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/362737/d3207715-a03b-ce79-ceb4-0cc72d231b84.png)

完全なソースはこちらです。
https://github.com/BeeBow6/electron-text-editor

## ファイルの準備
### ディレクトリ構成
さらにフォルダとファイルを追加します。

```diff
root
  ├ package.json
  ├ dist
  │   ├ index.html
  │   └ scripts
  └ src
       ├ main.ts
+      ├ fileIO.ts
       └ renderer
            ├ index.tsx
+           └ components
+                 ├ app.tsx
+                 └ menu.tsx
```

### Node.jsを利用したファイル操作
ファイルの読み込みと書き出しをまとめたユーティリティモジュールです。
Node.jsのモジュールを用いてファイル操作を実行します。
メインプロセスとレンダラープロセス、両方から利用できます。

![fileIO.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/362737/e083c057-e55a-8575-e2d4-f4899e45f03a.png)

```ts:/src/fileIO.ts
import fs from 'fs';

export enum FILE_EVENTS {
  OPEN_DIALOG = 'open_dialog',
  SAVE_DIALOG = 'save_dialog',
  OPEN_FILE = 'open_file',
  SAVE_FILE = 'save_file'
}

export const FILE_FILTERS: {
  name: string;
  extensions: string[];
}[] = [
  { name: 'Text', extensions: ['txt'] },
  { name: 'All Files', extensions: ['*'] }
];

export interface FileInfoType {
  fileName: string;
  fileText: string;
}

export const readFile = (fileName: string): string => {
  let fileText = '';
  try {
    fileText = fs.readFileSync(fileName, 'UTF-8');
  } catch (e) {
    console.log(e);
  }
  return fileText;
};

export const saveFile = (fileName: string, fileText: string): void => {
  try {
    fs.writeFileSync(fileName, fileText, 'UTF-8');
  } catch (e) {
    console.log(e);
  }
};
```

### メインプロセス
レンダラープロセス側で「OPEN」「SAVE AS」がクリックされた際の、メインプロセス側での処理をイベントハンドラに登録します。
[dialog](https://electronjs.org/docs/api/dialog)は、OSのダイアログを利用する為のモジュールです。
このモジュールは基本的にメインプロセス側から出しか利用できません。
[remote](https://electronjs.org/docs/api/remote)モジュールを介することで、レンダラープロセス側からも直接利用することが出来ます。
ただ、モーダル表示にするには第一引数に親ウィンドウの`BrowserWindow`オブジェクトを渡す必要があります。
その為メインプロセス側で`diaog`を操作し、結果をレンダラープロセスに送信しています。

#### 処理の流れ
![dialog.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/362737/329c6d04-93b5-5b2a-1b25-8c693f75956c.png)

```ts:/src/main.ts
import { BrowserWindow, app, dialog, ipcMain } from 'electron';
import {
  FILE_EVENTS,
  readFile,
  saveFile,
  FileInfoType,
  FILE_FILTERS
} from './fileIO';

const mainURL = `file://${__dirname}/index.html`;
let mainWindow: BrowserWindow | null = null;

const createWindow = (): void => ...
app.on('ready', () => ...
app.on('window-all-closed', () => ...
app.on('activate', () => ...

// ファイルを開く
ipcMain.on(FILE_EVENTS.OPEN_DIALOG, () => {
  if (mainWindow === null) return;
  const fileNames: string[] | undefined = dialog.showOpenDialogSync(
    mainWindow,
    {
      properties: ['openFile'],
      filters: FILE_FILTERS
    }
  );
  if (!fileNames || !fileNames.length) return;
  const fileText = readFile(fileNames[0]);
  //レンダラープロセスへ送信
  mainWindow.webContents.send(FILE_EVENTS.OPEN_FILE, {
    fileName: fileNames[0],
    fileText
  });
});

// 名前をつけて保存する
ipcMain.on(FILE_EVENTS.SAVE_DIALOG, (_, fileInfo: FileInfoType) => {
  if (mainWindow === null) return;
  const newFileName: string | undefined = dialog.showSaveDialogSync(
    mainWindow,
    {
      defaultPath: fileInfo.fileName,
      filters: FILE_FILTERS
    }
  );
  if (!newFileName) return;
  saveFile(newFileName, fileInfo.fileText);
  //レンダラープロセスへ送信
  mainWindow.webContents.send(FILE_EVENTS.SAVE_FILE, newFileName);
});

```

### レンダラープロセス
WEBで構築するのとほとんど変わりませんが、Node.jsのモジュールを利用できる点は異なります。

#### ルートコンポーネント

```tsx:/src/renderer/components/app.tsx
import { ipcRenderer } from 'electron';
import React, { useState, useEffect, useCallback } from 'react';
import { Container, TextField } from '@material-ui/core';
import { FILE_EVENTS, saveFile, FileInfoType } from '../../fileIO';
import Menu from './menu';

// ipcを利用して、メインプロセスにダイアログ表示を依頼

const openFileDialog = (): void => {
  ipcRenderer.send(FILE_EVENTS.OPEN_DIALOG);
};

const openSaveAsDialog = (fileInfo: FileInfoType): void => {
  ipcRenderer.send(FILE_EVENTS.SAVE_DIALOG, fileInfo);
};

const App: React.FC = () => {
  const [text, setText] = useState('');
  const [fileName, setFileName] = useState('');

  const handleChange = useCallback((e: React.ChangeEvent<HTMLInputElement>) => {
    setText(e.currentTarget.value);
  }, []);

  const handleFileSave = useCallback(() => {
    if (fileName) {
      saveFile(fileName, text);
    } else {
      openSaveAsDialog({
        fileName: '',
        fileText: text
      });
    }
  }, [fileName, text]);

  const handleFileSaveAs = useCallback(() => {
    openSaveAsDialog({
      fileName: fileName,
      fileText: text
    });
  }, [fileName, text]);

  // Dialog選択結果の取得
  useEffect(() => {
    // 開いたファイルの名前とデータを取得
    ipcRenderer.on(FILE_EVENTS.OPEN_FILE, (_, fileInfo: FileInfoType) => {
      setText(fileInfo.fileText);
      setFileName(fileInfo.fileName);
    });
    // 別名保存した際の名前を取得
    ipcRenderer.on(FILE_EVENTS.SAVE_FILE, (_, newFileName: string) => {
      setFileName(newFileName);
    });

    return (): void => {
      ipcRenderer.removeAllListeners(FILE_EVENTS.OPEN_FILE);
      ipcRenderer.removeAllListeners(FILE_EVENTS.SAVE_FILE);
    };
  }, []);

  return (
    <Container>
      <Menu
        onFileOpen={openFileDialog}
        onFileSave={handleFileSave}
        onFileSaveAs={handleFileSaveAs}
      />
      <TextField
        multiline
        fullWidth
        variant="outlined"
        rows={10}
        rowsMax={20}
        value={text}
        inputProps={{
          style: {
            fontSize: 14
          }
        }}
        onChange={handleChange}
        helperText={fileName || '[Untitled]'}
      />
    </Container>
  );
};

export default App;
```

#### メニューコンポーネント

```tsx:/src/renderer/components/menu.tsx
import React from 'react';
import { Button, ButtonGroup, makeStyles, Theme } from '@material-ui/core';
import { Save, FolderOpen, NoteAdd } from '@material-ui/icons';

const useStyles = makeStyles((theme: Theme) => ({
  buttonWrp: {
    margin: theme.spacing(2, 0, 3)
  },
  buttonIcon: {
    marginLeft: theme.spacing(0.5)
  }
}));

interface MenuProps {
  onFileOpen: () => void;
  onFileSave: () => void;
  onFileSaveAs: () => void;
}

const Menu: React.FC<MenuProps> = props => {
  const classes = useStyles();

  return (
    <ButtonGroup
      variant="outlined"
      color="primary"
      className={classes.buttonWrp}
    >
      <Button onClick={props.onFileOpen}>
        OPEN
        <FolderOpen className={classes.buttonIcon} />
      </Button>
      <Button onClick={props.onFileSave}>
        SAVE
        <Save className={classes.buttonIcon} />
      </Button>
      <Button onClick={props.onFileSaveAs}>
        SAVE AS
        <NoteAdd className={classes.buttonIcon} />
      </Button>
    </ButtonGroup>
  );
};

export default Menu;
```

**ボタンのアルファベット文字列**
ボタンの文字列は、小文字を指定しても大文字に変換されます。
これは、Googleが提唱する[マテリアルデザイン](https://material.io/components/buttons/#anatomy)にて、本文と区別する為にボタンは全て大文字にするべきとしている為です。
これを防ぐには、ボタンのラベルのスタイルを`textTransform:'none'`に変更します。
下記例ではテーマごと上書きしており、配下の全てのButtonコンポーネントに適用されるようにしています。
[Customizing components | Material-UI](https://material-ui.com/customization/components/)

```tsx
import { MuiThemeProvider, createMuiTheme, Button } from '@material-ui/core';

const theme = createMuiTheme({
  typography: {
    button: {
      textTransform: 'none'
    }
  }
});

const CustomTheme: React.FC = () => (
  <MuiThemeProvider theme={theme}>
    <Button variant="contained">hogehogehoge</Button>
  </MuiThemeProvider>
);
```

![ボタンキャプチャ.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/362737/be8797ff-0ca7-a527-4257-1943e548e03a.png)

#### index.tsx

```tsx:/src/renderer/index.tsx
import React from 'react';
import ReactDOM from 'react-dom';

import App from './components/app';

ReactDOM.render(<App />, document.getElementById('contents'));
```

ビルドしてエレクトロンをデバッグ実行(`electron .`)すると、最低限の機能を持ったテキストエディタが立ち上がります。

## アプリのパッケージ化
作成したアプリを、Winows向けにパッケージングします。
パッケージングには、[electron-packager](https://www.npmjs.com/package/electron-packager)を利用します。

```sh
$ npm i -D electron-packager
```
package.jsonにもパッケージ用スクリプトを追加しておきます。
左から、対象ディレクトリ("main"で指定しているもの)，アプリ名，環境(Windows x64)，Electronバージョン　です。

```diff:package.json
{
  // ...
  "main": "dist/main.js",
  "scripts": {
    "start": "electron .",
    "build": "webpack --display-error-details -w"
    // electron-packager <sourcedir> <appname> --platform=<platform> --arch=<arch> [optional flags...]
+   "package" : "electron-packager . electronTextEditor --platform=win32 --arch=x64 --electron-version=6.0.11"
  },
  // ...
}
```

上記コマンドを実行すると、「electronTextEditor-win32-x64」フォルダとその中に「electronTextEditor.exe」が出力されます。
おぉ…アプリができた…！



# 参考情報
[Building a desktop application with Electron](https://medium.com/developers-writing/building-a-desktop-application-with-electron-204203eeb658)
[Meet Material-UI — your new favorite user interface library](https://www.freecodecamp.org/news/meet-your-material-ui-your-new-favorite-user-interface-library-6349a1c88a8c/)
[TypeScript and React: Hooks](https://fettblog.eu/typescript-react/hooks/)
[electron-sample-apps/mini-code-editor/](https://github.com/hokein/electron-sample-apps/tree/master/mini-code-editor)
[any型で諦めない React.EventCallback](https://qiita.com/Takepepe/items/f1ba99a7ca7e66290f24)
[ようこそ！Electron入門](https://qiita.com/umamichi/items/6ce4f46c1458e89c4cfc)
[Electronの手習い〜Electron環境からパッケージ化まで〜](https://qiita.com/tagosaku324/items/c720499080d523bbe1d7)
[Material-UI と styled-components を組み合わせて、React のサイトを怠惰にスタイリングする。](https://qiita.com/Ouvill/items/c6761c32d31ffb11e114)