Pythonでも簡単にGUIは作れる

## PythonだってGUIを作りたい

Pythonで書いたプログラムを実行して使う場合、**『GUIで入出力できたら便利なのに…』**と思うときはありませんか？

誰かにプログラムを配布する場合でも、CUI(コマンドラインから入出力)はあまり親切とは言えません。
特にITスキルの高くない人にとっては、CUIは拒絶反応を起こすこともあります。

**Pythonでも簡単にGUIを作れたら…**

そんな場合、**PySimpleGui**を使ってみてはいかがでしょうか？
PySimpleGuiは**誰でも簡単にGUIを実装できる**のが特徴で、[PySimpleGui公式ドキュメント](https://pysimplegui.readthedocs.io/en/latest/)によると、PySimpleGuiはすぐに習得でき、コード量も他のGUIライブラリ(Tkinter、Qt、WxPythonなど)の1/2～1/10程度で済むとのこと。

百聞は一見にしかずなので、まずは下記のコードと実行結果をご覧ください。

```python

import PySimpleGUI as sg

sg.theme('DarkAmber')   # デザインテーマの設定

# ウィンドウに配置するコンポーネント
layout = [  [sg.Text('ここは1行目')],
            [sg.Text('ここは2行目：適当に文字を入力してください'), sg.InputText()],
            [sg.Button('OK'), sg.Button('キャンセル')] ]

# ウィンドウの生成
window = sg.Window('サンプルプログラム', layout)

# イベントループ
while True:
    event, values = window.read()
    if event == sg.WIN_CLOSED or event == 'キャンセル':
        break
    elif event == 'OK':
        print('あなたが入力した値： ', values[0])

window.close()
```

**実行結果**

![pysimplegui_sample.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/634673/c50195fe-685b-4eb8-d59b-f4ce4cc8ad17.png)

ね、簡単でしょう？
コードは大きく5つのステップで構成されています。
各ステップの詳細は後述しますので、まずは全体の流れをざっと見ていきましょう。

* * *

:::note info
[2024-06-02 追記] PySimpleGuiはバージョン5のリリースで商用利用が有償化されました。個人利用は無料ですが、**ライセンス登録が必要**になります。
最近ですと、PythonでGUIを作るフレームワークとしては私は **「Flet」の方がオススメ**です。Fletの概要や使い方について知りたい人はこちらの記事をご覧いただければと思います。
:::

https://runrunsketch.net/flet-intro/
* * *


## 準備(インストール)

PySimpleGuiは標準ライブラリではないので、まずインストールする必要があります。
下記のpipコマンドでさくっとインストールしましょう。

```
pip install pysimplegui
```

## コード
### ステップ1. インポート
PySimpleGUIをインポートします。このとき`sg`という別名をつけるのが標準的なので、本記事でもその慣例に従います。

```python

import PySimpleGUI as sg
```
### ステップ2. デザインテーマの設定

`sg.theme()`でGUIのデザインを設定します。
ここでは'DarkAmber'というテーマを設定しましたが、他にも140種類ものデザインテーマが用意されています。

```python

sg.theme('DarkAmber')
```

### ステップ3. ウィンドウの部品とレイアウト

ここがGUIの肝になりますが、ウィンドウに配置する部品（コンポーネント)とレイアウトを設定していきます。

```python

layout = [  [sg.Text('ここは1行目')],
            [sg.Text('ここは2行目：適当に文字を入力してください'), sg.InputText()],
            [sg.Button('OK'), sg.Button('キャンセル')] ]
```

コンポーネントの配置の仕方は、

```

layout = [  [1行目のコンポーネント],
            [2行目のコンポーネント],
            [3行目のコンポーネント] ]
```
というように各行に配置したい部品をカンマで区切って設定します。
直感的でわかりやすいですね。

行の中に複数の部品を配置するときは、次のようにカンマで区切って書きます。

```

layout = [  [1行目のコンポーネント1, 1行目のコンポーネント2],
            [2行目のコンポーネント1],
            [3行目のコンポーネント1, 3行目のコンポーネント2, 3行目のコンポーネント3,] ]
```


### ステップ4. ウィンドウの生成

`sg.Window()`にステップ3で作ったlayoutを指定して、ウィンドウを生成します。
このとき、第1引数としてウィンドウタイトルを指定できます。

```python

window = sg.Window('サンプルプログラム', layout)
```

### ステップ5. イベントループ

イベントループの中でイベントの発生を待ちます。
`window.read()`でイベントを受信します。イベントを受信したら、イベントによって行うべき処理を選択して実行します。

サンプルコードでは、

-  ウィンドウの×ボタンが押された(`sg.WIN_CLOSED`)→ループを抜けてウィンドウを閉じる(`window.close()`)
-  'キャンセル'ボタンを押された→ループを抜けてウィンドウを閉じる(`window.close()`)
-  'OK'ボタンが押された→テキストボックスに入力した値をprint文で表示

というイベント処理を実行しています。

```python

while True:
    event, values = window.read()
    if event == sg.WIN_CLOSED or event == 'キャンセル':
        break
    elif event == 'OK':
        print('あなたが入力した値： ', values[0])

window.close()
```

## GUIを作ってみる
では実際にモノを作りながら説明していきます。

『[【自動化】PDF内の表をPythonで抜き出す](https://qiita.com/konitech913/items/4ef70e1f7753c824b40f)』の記事で紹介したコードを少し改変し、**「PDFファイルの指定したページ内の表を抜き出してcsv化するプログラム」**をGUI付きで作ってみます。

PDFファイルの指定したページ内の表をDataFrameで抜き出す部分は関数化しておきます。
今回はここは重要ではないので、説明は割愛させていただきます。
詳しくは『[【自動化】PDF内の表をPythonで抜き出す](https://qiita.com/konitech913/items/4ef70e1f7753c824b40f)』をご覧ください。

```python

import pandas as pd
import tabula

def readTableFromPDF(filePath, isLattice, targetPages):
    '''
    指定されたfilePathのPDFファイルから表を読み込みDataFramleに変換する
    '''
    dfs = tabula.read_pdf(filePath, lattice=isLattice, pages=targetPages)

    # 変換したDataFrameを表示
    for index, df in enumerate(dfs):
        print("----------表"+str(index+1)+"----------")
        print(df.head())
        
    return dfs
```

### 準備(GUIのデザイン)

まずどんなGUIにしたいのかデザインを決めましょう。
紙でも電子でもいいので、デザインを書いてみます。

私はこんなGUIを考えました。

![gui_design.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/634673/937af746-1be8-8813-63da-63bfc612ca0f.png)



### ステップ1. インポート
PySimpleGUIを`sg`という名前でインポートします。

```python

import PySimpleGUI as sg
```
### ステップ2. デザインテーマの設定

`sg.theme()`でGUIのデザインテーマを設定します。

様々なテーマが用意されていますが、`sg.theme_previewer()`という関数をコールするとテーマ一覧を見ることができます。

```python

sg.theme_previewer() # デザインテーマの一覧を表示
```

公式サイトも参考に選んでみてください。[Look and Feel Theme Explosion](https://pysimplegui.readthedocs.io/en/latest/cookbook/#themes-window-beautification)

ここではDarkTeal7というテーマで作ることにします。

```python

sg.theme('DarkTeal7')
```

### ステップ3. ウィンドウの部品とレイアウト

GUIのデザイン案を行ごとに考えてみます。
![gui_design1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/634673/f9dba0c5-405a-7402-c805-893433177cc5.png)


全部で6行あるので、下記のようなコードになるわけです。

```python
layout = [  [1行目のコンポーネント],
            [2行目のコンポーネント],
            [3行目のコンポーネント],
            [4行目のコンポーネント],
            [5行目のコンポーネント],
            [6行目のコンポーネント]  ]
```

では、1行ずつ順番に見ていきましょう。

#### 1行目
![gui_design_row1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/634673/3eb81473-1dec-0e35-c22b-ca5b9628ebd7.png)

1行目はテキストを表示しているだけですね。
テキストは`sg.Text("表示したいテキスト")`で配置します。

```python
    [sg.Text('読み取り対象のファイルとページを指定してください')]
```

#### 2行目
![gui_design_row2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/634673/9a20c904-239e-3ee7-0027-ca7136bb530c.png)


2行目は3つのコンポーネントを配置します。

■ 1つめのコンポーネント(テキスト)
テキストは1行目と同じで`sg.Text()`で配置します。
レイアウトによってはコンポーネントのサイズを指定したいこともあると思います。
その場合は、`sg.Text('表示したいテキスト', size=("横幅","縦幅"))`という形で指定します。

■ 2つめと3つめのコンポーネント(ファイルブラウザボタン)
ファイルをブラウザで選択するためのボタンです。`sg.FileBrowse('ボタンに表示したいテキスト')`で配置します。
選択したファイルのパスは`sg.Input()`で配置したボックスに表示します。

各コンポーネントには、その**コンポーネントを識別するための名前を`key='名前'`という形で与えることができます。**
「ボタンが押された」などのイベントが発生した場合、どのボタンが押されたのかを識別する必要がありますよね。
そのときに必要になるので、イベントハンドリングしたいコンポーネントには名前をつけるようにしましょう。

後ほど、ファイルのパスを取得する必要があるので、`sg.FileBrowse()`には名前をつけておきます。

```python

    [sg.Text('ファイル', size=(15, 1)), sg.Input(), sg.FileBrowse('ファイルを選択', key='inputFilePath')]
```

#### 3行目
![gui_design_row3.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/634673/11cfa89b-7d76-dfbd-a15b-24c23c41f273.png)

3行目も3つのコンポーネントを配置します。

■ 1つめのコンポーネント(テキスト)
`sg.Text()`で配置します。

■ 2つめのコンポーネント(テキストボックス)
テキストを入力するボックスは`sg.InputText()`です。
このコンポーネントにも名前をつけておきます。

■ 3つめのコンポーネント(テキスト)
`sg.Text()`で配置します。
文字数が少し多いので、2行のテキストで表示するため`size=("横幅","縦幅")`の"縦幅"に2を指定します。

`sg.Text('複数ページのときは\n3-10 のように指定してください', size=(30, 2))`

```python
    [sg.Text('ページ', size=(15, 1)), sg.InputText('', size=(10, 1), key='pages'), sg.Text('複数ページのときは\n3-10 のように指定してください', size=(30, 2))]
```

#### 4行目
![gui_design_row4.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/634673/ed336d69-fec2-8e3e-b555-520ff793cff7.png)


4行目は2つのコンポーネントを配置します。

■ 1つめのコンポーネント(テキスト)
`sg.Text()`で配置します。

■ 2つめのコンポーネント(プルダウンメニュー)
プルダウンメニューは`sg.Combo(("値1","値2",...), default_value="値n")`です。
このコンポーネントにも名前をつけておきます。

```python

    [sg.Text('表の罫線', size=(15, 1)),sg.Combo(('あり', 'なし'), default_value="あり",size=(10, 1), key='lattice')]
```

#### 5行目
![gui_design_row5.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/634673/7169a4ee-d566-0a52-16be-1998d7aabd2e.png)


5行目は2つのコンポーネントを配置します。

■ 1つめと2つめのコンポーネント(ボタン)
ボタンは`sg.Button('ボタンに表示したいテキスト')`で配置します。
ボタンを押されたときのイベントをハンドリングしたいので、これらにも`key='名前'`で名前をつけておきます。

```python

    [sg.Button('読み取り', key='read'), sg.Button('csvに保存', key='save')]
```

#### 6行目
![gui_design_row6.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/634673/5da678aa-38af-4341-8041-8dbb95baf4c3.png)

この行のコンポーネントは1つだけです。
PDFファイルから読み取った表を確認のためにここに出力します。

テキストの出力には`sg.Output(size=("横幅","縦幅"))`を使います。

```python

    [sg.Output(size=(80,20))]
```

#### コンポーネントまとめ

1～6行目までのコンポーネントをまとめて書きます。

```python

layout = [
    [sg.Text('読み取り対象のファイルとページを指定してください')],
    [sg.Text('ファイル', size=(10, 1)), sg.Input(), sg.FileBrowse('ファイルを選択', key='inputFilePath')],
    [sg.Text('ページ', size=(10, 1)), sg.InputText('', size=(10, 1), key='pages'), sg.Text('複数ページのときは\n3-10 のように指定してください', size=(30, 2))],
    [sg.Text('表の罫線', size=(10, 1)),sg.Combo(('あり', 'なし'), default_value="あり",size=(10, 1), key='lattice')],
    [sg.Button('読み取り', key='read'), sg.Button('csvに保存', key='save')],
    [sg.Output(size=(80,20))]
]
```

### ステップ4. ウィンドウの生成

ステップ3で作成したlayoutでウィンドウを生成します。
ウィンドウのタイトルは「PDFの表を抜き出すツール」とでもしておきましょう。

```python

window = sg.Window('PDFの表を抜き出すツール', layout)
```

### ステップ5. イベントループ

最後のステップです。ここもとても大事なステップですね。

イベントループの中でイベントの発生を待ちます。
`window.read()`でイベントを受信します。イベントを受信したら、イベントによって行うべき処理を選択して実行します。


```python

while True:
    event, values = window.read()

    if event == sg.WIN_CLOSED: #ウィンドウのXボタンを押したときの処理
        break
       
    if event == 'イベント名1':
        # イベント名1が発生したときの処理

    if event == 'イベント名2':
        # イベント名2が発生したときの処理

window.close()
```
window.read()の戻り値であるeventには、各コンポーネントに`key='名前'`で与えた名前が入ります。
例えば、「読み取り」ボタンには'read'という名前をつけているので、このボタンが押された場合、eventには'read'という名前が入っています。

valuesには各コンポーネントが持っている値が入っていて、これも各コンポーネントに`key='名前'`で与えた名前を指定して取り出すことができます。
例えば、4行目のプルダウンメニュー('あり' or 'なし')の値はvalues['lattice']で取得できます。

今回のイベントハンドリングは下記の2つになります。

|イベント|イベント名|行う処理|
|---|---|---|
|「読み取り」ボタンが押された|'read'|readTableFromPDF()を実行する|
|「csvに保存」ボタンが押された|'save'|readTableFromPDF()を実行して、戻り値で得られたDataFrameをcsvとして保存する|

```python

while True:
    event, values = window.read()

    if event == sg.WIN_CLOSED: #ウィンドウのXボタンを押したときの処理
        break
       
    if event == 'read': #「読み取り」ボタンが押されたときの処理
        if values['lattice'] == 'あり':
            isLattice = True
        else:
            isLattice = False
            
        readTableFromPDF(values['inputFilePath'], isLattice, values['pages'])
        
    if event == 'save': #「csvに保存」ボタンが押されたときの処理
        if values['lattice'] == 'あり':
            isLattice = True
        else:
            isLattice = False
            
        dfs = readTableFromPDF(values['inputFilePath'], isLattice, values['pages'])
        
        for index, df in enumerate(dfs):
            basename_without_ext = os.path.splitext(os.path.basename(values['inputFilePath']))[0] # PDFファイル名を抜き出す
            filename = basename_without_ext + "_"  + str(index+1) +".csv"
            df.to_csv(filename, index=None)
            print("csvファイルに保存しました:", filename)
            
        print("すべてのcsvファイル保存が完了しました")

window.close()
```
なお、ここでのprint文の結果は6行目のコンポーネントである`sg.Output()`のボックス内に表示されます。

### ここまでのGUIコードのまとめ

ステップ1～5までの全コードをまとめておきます。

```python
# ステップ1. インポート
import PySimpleGUI as sg
import os

# ステップ2. デザインテーマの設定
sg.theme('DarkTeal7')

# ステップ3. ウィンドウの部品とレイアウト
layout = [
    [sg.Text('読み取り対象のファイルとページを指定してください')],
    [sg.Text('ファイル', size=(10, 1)), sg.Input(), sg.FileBrowse('ファイルを選択', key='inputFilePath')],
    [sg.Text('ページ', size=(10, 1)), sg.InputText('', size=(10, 1), key='pages'), sg.Text('複数ページのときは\n3-10 のように指定してください', size=(30, 2))],
    [sg.Text('表の罫線', size=(10, 1)),sg.Combo(('あり', 'なし'), default_value="あり",size=(10, 1), key='lattice')],
    [sg.Button('読み取り', key='read'), sg.Button('csvに保存', key='save')],
    [sg.Output(size=(80,20))]
]

# ステップ4. ウィンドウの生成
window = sg.Window('PDFの表を抜き出すツール', layout)

# ステップ5. イベントループ
while True:
    event, values = window.read()

    if event == sg.WIN_CLOSED: #ウィンドウのXボタンを押したときの処理
        break
       
    if event == 'read': #「読み取り」ボタンが押されたときの処理
        if values['lattice'] == 'あり':
            isLattice = True
        else:
            isLattice = False
            
        readTableFromPDF(values['inputFilePath'], isLattice, values['pages'])
        
    if event == 'save': #「csvに保存」ボタンが押されたときの処理
        if values['lattice'] == 'あり':
            isLattice = True
        else:
            isLattice = False
            
        dfs = readTableFromPDF(values['inputFilePath'], isLattice, values['pages'])
        
        for index, df in enumerate(dfs):
            basename_without_ext = os.path.splitext(os.path.basename(values['inputFilePath']))[0] # PDFファイル名を抜き出す
            filename = basename_without_ext + "_"  + str(index+1) +".csv"
            df.to_csv(filename, index=None)
            print("csvファイルに保存しました:", filename)
            
        print("すべてのcsvファイル保存が完了しました")

window.close()
```


### 完成したGUI

以上のコードを実行すると、下記のようなGUIが表示されます。ほぼデザイン案の通りにできていますね。

![gui_pdfreader1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/634673/1d1eecf4-41c6-aae7-0e25-62ab590ce65a.png)

PDFファイルとページを指定して、「読み取り」をしてみます。

![gui_pdfreader2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/634673/9d723269-ac0a-271c-5fdf-fbf36f8436ae.png)

ちゃんとPDFファイル内のページを読み取れていますね！

## やっぱりGUIは楽しくなくちゃ

PySimpleGuiはその名の通りシンプルにGUIを書くことができるので、**楽しみながら**GUIを制作することができました。
ここで紹介したコンポーネントはごく基本的なものですが、他にも様々なコンポーネントを簡単に作ることができます。
PySimpleGuiの日本語ドキュメントは残念ながら少ないですが、下記のサイトを参照しながら楽しくGUIを作っていただければと思います。

[PySimpleGui公式](https://pysimplegui.readthedocs.io/en/latest/)
PySimpleGuiの公式サイト(英語)です。

[PySimpleGUIの基本的な使用方法](https://qiita.com/KatsunoriNakamura/items/376da645e52f7ef7f9ef)
記事内にリンクされているテキストは現在数少ないPySimpleGuiの日本語テキストだと思います。
私も大いに参考にさせていただきました。