【PySimpleGUI】PythonでオリジナルGUIアプリを作成

# 0. はじめに


:::
>★追記
このライブラリはライセンスが色々面倒になったみたいですね。
代替として以下のTkEasyGUIなんていかがでしょうか？
ほぼ使い勝手そのままでライセンスもMITです
https://github.com/kujirahand/tkeasygui-python/blob/main/README-ja.md






前回に引き続き連載ネタ第2弾です。
今回は前回の成果物を使ってPythonでGUIアプリを作成してみようと思います。
TkinterというPythonの標準GUIライブラリでもいいのですが、はっきり言ってPySimpleGUIの方がいいと感じたので、今回はPySimpleGUIを使用したGUI作成をやっていきます。

[【第1回】Pythonで簡単に日本語OCR](https://qiita.com/ku_a_i/items/93fdbd75edacb34ec610) 　※前回記事
**【第2回】PythonでオリジナルGUIアプリを作成**　**←今回はこの記事**
[【第3回】Pythonで作成したアプリをexe化して配布する](https://qiita.com/ku_a_i/items/76f5fb34279ce3aa6e99)


* 動作環境
  * OS : Windows10 pro
  * Python : 3.8.3
  * PySimpleGUI ：4.55.1
  * Tesseract : 5.0.0　(OCR部分)
  * pyocr : 0.8　(OCR部分)
  * jupyter notebook

# 1. どんなアプリを作る？
前回日本語OCRの記事を書いたので、これをGUI付きでアプリ化してみようと思う。

* 今回作成するGUIアプリの仕様
 * GUIのボタン操作で画像のインポート
 * インポートした画像をGUI上で確認可能
 * 内部処理は下記記事のコードを基本そのまま採用(日本語画像を読み込んでOCRする処理)
 * OCR結果をGUI上に出力

https://qiita.com/ku_a_i/items/93fdbd75edacb34ec610


**なお。今回の最終完成形は以下のようなものを想定している**


<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/373060/01e4948e-a990-28f8-81e4-7972096837d2.png" width=80%>


# 2. GUIの画面(フレームとレイアウト)を作成する

### 2-1. テーマブラウザを決める
以下URLのサンプルレシピコードを動かして、まずは配色(テーマブラウザ)を決める

※中身は詳しく解説しない。**とにかく実行すれば直感的にわかります。**
※ちなみに`sg.theme_previewer()`コマンド1行だけでも配色一覧の確認が可能なので、慣れればこっちで確認してもいい。
※PySimpleGUIは`pip install PySimpleGUI`で導入可能


https://pysimplegui.readthedocs.io/en/latest/cookbook/#recipe-theme-browser

```Python:
"""テーマカラーにどんなものがあるか？を目視で確認するサンプルコード"""

import PySimpleGUI as sg

sg.theme('Dark Brown')

layout = [[sg.Text('Theme Browser')],
          [sg.Text('Click a Theme color to see demo window')],
          [sg.Listbox(values=sg.theme_list(), size=(20, 12), key='-LIST-', enable_events=True)],
          [sg.Button('Exit')]]

window = sg.Window('Theme Browser', layout)

while True:  # Event Loop
    event, values = window.read()
    if event in (sg.WIN_CLOSED, 'Exit'):
        break
    sg.theme(values['-LIST-'][0])
    sg.popup_get_text('This is {}'.format(values['-LIST-'][0]))

window.close()
```

**実行結果**
どんなテーマカラーを指定するとどんな背景の配色のGUIにできるか？の事前確認が可能。
色々試してみて自分のイメージ通りの色か？をまず目視で確認できるので、まず最初にこれをやるのがオススメ。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/373060/9b6c59df-86b1-3670-0020-6b4f3d76988a.png" width=70%>


今回の例ではPurple(↑画像の色)を採用することにします。

### 2-2. 画面レイアウトを決める

まずは画面構成を考える必要があるが、**クドクド説明するより画像見たほうがいいと思う。**
・Windowは、アプリの全体画面(アプリタイトル＋背景部分)
・レイアウトはWindowの上に乗せる画面
・フレームはレイアウトの上に別のレイアウトを載せられるもの。。という認識でいいはず
・`レイアウト(フレーム)の中に「文字」や「ボタン」や「画像」等の部品をのせられる`
・部品の配置は`「基本的にレイアウト(フレーム)の上側から」`配置されていく
・今回はフレームを横方向に合体させてレイアウトを作成し、Windowにのせた

![レイアウト.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/373060/1d5f08d1-53ce-9cf7-b186-c2f32bc0f91a.png)



```Python:
import PySimpleGUI as sg

#先程確認して決めたテーマカラーをsg.themeで設定
sg.theme('Purple')

#sg.Frameでフレームを定義
#フレーム1(中はからっぽ、フレームサイズだけ指定)
frame1 = sg.Frame('',
    [] , size=(500, 700) #幅,高さ
)

#フレーム2(中はからっぽ、フレームサイズだけ指定)
frame2 = sg.Frame('',
    [] , size=(400, 700) #幅,高さ
)

#全体レイアウト
"""
レイアウトの中に記述する[]が「1行」を表している
今回はframe1と2を横に並べるので、同じ[]の中に記述する
"""
layout = [
    #以下[]で1行の扱いになる。カンマ区切りで横に部品を並べられる
    [
        frame1,
        frame2
    ]
]

#GUIタイトルと全体レイアウトをのせたWindowを定義する。画面サイズは省略OK
#resizableでWindowサイズをマウスで変更できるようになる
window = sg.Window('日本語OCR実行アプリ', layout, resizable=True)

#GUI表示実行部分
while True:
    # ウィンドウ表示
    event, values = window.read()

    #クローズボタンの処理
    if event is None:
        print('exit')
        break

window.close()
```

すると以下のような画面ができあがる。
部品をのせたりしたらフレームサイズに収まらないかもですが、それは後から変えればいいです。
**※以下コードは上記コードの中身に色々追記していくスタイルにします。**

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/373060/1d32d276-2404-3404-fd21-d8f6685d377f.png" width=70%>

### 2-3. ボタンとかをフレームにのせていく

後は2-2で作成した画面に自分で色々部品をのせていけばいい。
具体的にはさっき[]で空っぽだったフレーム1と2の中に記述していくことになる。
**※下記コードの「'./初期画像.png'」は自分で適当に作成する必要があります**

★今回使用した初期画像
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/373060/6cf524d1-2a26-ab4e-18bc-b911b0022e8b.png" width=50%>

```Python:
import PySimpleGUI as sg
from PIL import Image, ImageTk
import io

#先程確認して決めたテーマカラーを設定
sg.theme('Purple')

"""
公式のサンプル集に乗ってたコードをそのまま引用
maxsizeを大きくすると、大きな画像を読み込んだ際にGUIがその分大きくなる
今回はフレームサイズ以下になるように、450×450を表示最大サイズとした
"""
def get_img_data(f, maxsize=(450, 450), first=False):
    """画像を読み込む関数"""
    global status_text #画像サイズをGUI表示させるためにグローバル変数で関数外でも参照できるようにする
    img = Image.open(f)
    status_text = "%d x %d" % img.size  # オリジナルの画像サイズ
    img.thumbnail(maxsize) #アスペクト比を維持しながら、指定したサイズ以下の画像に縮小
    status_text += " (%d x %d)" % img.size  # 縮小された画像サイズ
    if first:                     # tkinter is inactive the first time
        bio = io.BytesIO()
        img.save(bio, format="PNG")
        del img
        return bio.getvalue()
    return ImageTk.PhotoImage(img)

#GUIへ初期画像を登録する(適当にパワポとかで作っておけばいい)
fname_first = './初期画像.png'
image_elem = sg.Image(data=get_img_data(fname_first, first=True))

#画像サイズを表示させる部分を変数化しておく(画像毎にアップデートさせるため)
status_elem = sg.Text(key='-STATUS-', size=(64, 1))

"""
・sg.Imageで画像部品をのせられる
・sg.Textでテキスト部品をのせられる
・sg.InputTextでテキスト入力エリアをのせる(画像Pathを表示させる部分)
・sg.FileBrowseでWindowsでよく見るファイル選択画面を出せる(InputTextの横に置けばtextを自動入力してくれる)
・sg.Submitはいわゆる「決定ボタン」。今回はOCR開始ボタンとして使った
・sg.MLineはテキスト出力エリア
・keyは、後でイベントを追加する時に参照する変数名
・後は省略できるが、サイズやフォントも各命令で指定出来る
"""
#フレーム1
frame1 = sg.Frame('',
    [
        # テキストレイアウト
        [
            sg.Text('①画像選択ボタンを押してOCRを行いたい画像を選んでね', font=('メイリオ',12))
        ],
        #画像選択ボタン ※3つカンマ区切りで書いてるのでこれらが同じ行に配置される
        [
            sg.Text("ファイル"),
            sg.InputText('ファイルを選択', key='-INPUTTEXT-', enable_events=True,), 
            sg.FileBrowse(button_text='画像選択', font=('メイリオ',8), size=(8,3), key="-FILENAME-")
        ],
        # テキストレイアウト
        [
            sg.Text("②画像を選択したらOCR開始ボタンを押してね", font=('メイリオ',12)),
        ],
        #画像サイズ表示
        [
            sg.Text("元画像サイズ(GUI表示画像サイズ) : "),
            status_elem #
        ],
        #画像表示 ※初期画面では自分で用意した適当な画像を表示
        [
            image_elem,
        ],
        #OCR開始ボタン
        [
            sg.Submit(button_text='OCR開始',
                      font=('メイリオ',8),size=(8,3),key='button_ocr')
        ]
    ], size=(500, 700)
)

#フレーム2
frame2 = sg.Frame('',
    [
        # テキストレイアウト
        [
            sg.Text("OCR結果"), 
        ],
        # MLineでテキストエリアを作成。sizeは「**列×**行」を表している
        [
            sg.MLine(font=('メイリオ',8), size=(50,60), key='-OUTPUT-'),
        ]
    ] , size=(400, 700)
)

#左と右のフレームを合体させた全体レイアウト
layout = [
    [
        frame1,
        frame2
    ]
]

#GUIタイトルと全体レイアウトをのせたWindowを定義する
window = sg.Window('日本語OCR実行アプリ', layout, resizable=True)

#GUI表示実行部分
while True:
    # ウィンドウ表示
    event, values = window.read()

    #クローズボタンの処理
    if event is None:
        print('exit')
        break

window.close()
```

実行すると、初期画面が完成していることが確認できる。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/373060/701e49d4-c8d9-f5a1-de60-53c195fb8a66.png" width=70%>


# 3. ボタンアクションを追加する

2までに画面部品は配置し終わったので、後はボタンを押したときのアクションを追加すればいい。
`OCR部分は前回記事をまんま関数化しただけです`
`while True:　以下にアクション部分を記載していきますが、2で定義した「key」を参照しながら色々書いていきます。keyでイベントが発生したことを検知してアクションさせる感じですかね`

```Python:
import PySimpleGUI as sg
from PIL import Image, ImageTk, ImageEnhance
import io
import os
import pyocr

#テーマカラーを設定
sg.theme('Purple')

#TesseractのPath情報登録
TESSERACT_PATH = 'C:\\Users\\・・・・\\Tesseract-OCR' #インストールしたTesseract-OCRのpath
TESSDATA_PATH = 'C:\\Users\\・・・・\\Tesseract-OCR\\tessdata' #tessdataのpath
os.environ["PATH"] += os.pathsep + TESSERACT_PATH
os.environ["TESSDATA_PREFIX"] = TESSDATA_PATH

def ocr_tesseract(file_path):
    """Tesseractの日本語OCR関数 ※前回記事を関数にしただけ"""
    
    #OCRエンジン取得
    tools = pyocr.get_available_tools()
    tool = tools[0]
    
    #OCRの設定 ※tesseract_layout=6が精度には重要。デフォルトは3
    builder = pyocr.builders.TextBuilder(tesseract_layout=6)
    
    #解析画像読み込み(雨ニモマケズ)
    img = Image.open(file_path) #他の拡張子でもOK
    
    #適当に画像処理(もっとうまくやれば制度上がるかもです)
    img_g = img.convert('L') #Gray変換
    enhancer= ImageEnhance.Contrast(img_g) #コントラストを上げる
    img_con = enhancer.enhance(2.0) #コントラストを上げる

    #画像からOCRで日本語を読んで、文字列として取り出す
    txt_pyocr = tool.image_to_string(img_con , lang='jpn', builder=builder)

    #半角スペースを消す ※読みやすくするため
    txt_pyocr = txt_pyocr.replace(' ', '')
    
    return txt_pyocr

def get_img_data(f, maxsize=(450, 450), first=False):
    """画像を読み込む関数"""
    global status_text #画像サイズをGUI表示させるためにグローバル変数で関数外でも参照できるようにする
    img = Image.open(f)
    status_text = "%d x %d" % img.size  # オリジナルの画像サイズ
    img.thumbnail(maxsize) #アスペクト比を維持しながら、指定したサイズ以下の画像に縮小
    status_text += " (%d x %d)" % img.size  # 縮小された画像サイズ
    if first:                     # tkinter is inactive the first time
        bio = io.BytesIO()
        img.save(bio, format="PNG")
        del img
        return bio.getvalue()
    return ImageTk.PhotoImage(img)

#GUIへ初期画像を登録する(適当にパワポとかで作ってもOK)
fname_first = './初期画像.png'
image_elem = sg.Image(data=get_img_data(fname_first, first=True))

#画像サイズを表示させる部分を変数化しておく(画像毎にアップデートさせるため)
status_elem = sg.Text(key='-STATUS-', size=(64, 1))

#フレーム1
frame1 = sg.Frame('',
    [
        # テキストレイアウト
        [
            sg.Text('①画像選択ボタンを押してOCRを行いたい画像を選んでね', font=('メイリオ',12))
        ],
        #画像選択ボタン
        [
            sg.Text("ファイル"),
            sg.InputText('ファイルを選択', key='-INPUTTEXT-', enable_events=True,), 
            sg.FileBrowse(button_text='画像選択', font=('メイリオ',8), size=(8,3), key="-FILENAME-")
        ],
        # テキストレイアウト
        [
            sg.Text("②画像を選択したらOCR開始ボタンを押してね", font=('メイリオ',12)),
        ],
        #画像サイズ表示
        [
            sg.Text("元画像サイズ(GUI表示画像サイズ) : "),
            status_elem
        ],
        #画像表示 ※初期画面では自分で用意した適当な画像を表示
        [
            image_elem,
        ],
        #OCR開始ボタン
        [
            sg.Submit(button_text='OCR開始',
                      font=('メイリオ',8),size=(8,3),key='button_ocr')
        ]
    ], size=(500, 700)
)

#フレーム2
frame2 = sg.Frame('',
    [
        # テキストレイアウト
        [
            sg.Text("OCR結果"), 
        ],
        # MLineでテキストエリアを作成。sizeは「**列×**行」を表している
        [
            sg.MLine(font=('メイリオ',8), size=(50,60), key='-OUTPUT-'),
        ]
    ] , size=(400, 700)
)

#左と右のフレームを合体させた全体レイアウト
layout = [
    [
        frame1,
        frame2
    ]
]

#GUIタイトルと全体レイアウトをのせたWindowを定義する
window = sg.Window('日本語OCR実行アプリ', layout, resizable=True)

#GUI表示実行部分
while True:
    # ウィンドウ表示 ※eventがイベント発生、valuesはその際の中身
    event, values = window.read()

    #クローズボタンの処理
    if event is None:
        print('exit')
        break
    
    #何かファイルが選択され、inputテキストエリアに書かれたpathが存在する場合のイベント処理
    if values['-FILENAME-'] != '': #-FILENAME-で選択した画像pathのpathをvaluesで取得
        if os.path.isfile(values['-INPUTTEXT-']): #選択したファイル(テキストエリアに転記)が存在した場合の処理
            global img_path #選択した画像Pathを記憶させておく
            try:
                img_path = values['-INPUTTEXT-'] #OCR用に画像Path取得
                image_elem.update(data=get_img_data(values['-INPUTTEXT-'], first=True)) #画像表示エリアをアップデート
                status_elem.update(status_text) #画像サイズ表示部分をアップデート
            except: #例外処理 ※うまく画像が読めなかったりした場合
                error_massage = values['-INPUTTEXT-'] + ' を画像として読み込めません'
                sg.popup('画像読み込みエラー', error_massage) #エラーのポップアップを表示
                image_elem.update(data=get_img_data(fname_first, first=True)) #画像を初期画像に戻す
                
    #OCR開始ボタンを押したときのアクション
    if event == 'button_ocr':
        try:
            text_ocr = ocr_tesseract(img_path) #取得した画像PathをOCR関数へ渡す
            window.FindElement('-OUTPUT-').Update(text_ocr) #フレーム2の出力テキストエリアをアップデート       
        except: #例外処理
            error_massage = img_path + ' をOCRできませんでした'
            sg.popup('OCRエラー', error_massage) #エラーのポップアップを表示

window.close()
```

これで完成である。
参考までにどんな感じか？をGIFでのせておく。
![Animation.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/373060/fda6a78f-666b-ad96-3224-bfc47659826d.gif)



# 4. おわりに

ちょっと長い記事になってしまったが、慣れてしまえばすぐにGUIが作れるようになります。
さらにPySimpleGUIQtを使えば今風にドラッグアンドドロップなんかも使えるらしく、Pythonだけでもかなりお手軽に色々できそうなので、試してみるといいかもですね。

# 5. 追記
PySimpleGUIに関してのケーススタディを追加記事として書きました。
良ければ本記事に引き続き確認していってください

https://qiita.com/ku_a_i/items/e3fe06f24741bb7d6db7


## コーディング参考リンク

・https://github.com/PySimpleGUI/PySimpleGUI/tree/master/DemoPrograms
・https://pysimplegui.readthedocs.io/en/latest/readme%20Japanese%20Version/
・https://tomomai.com/pysimplegui_opencv/