PySimpleGUIでデスクトップアプリ(連打くん)を自作した

備忘録
つまづいたところやtipsだけをまとめる

# インストールとか
コマンドプロンプトで以下を実行してインストール
```
py -m pip install pysimplegui
```
プログラムではsgとしてインポートしておく
```test.py
import PySimpleGUI as sg
```

# テーマ設定
`sg.theme()`で設定できる
```python
sg.theme('ここにテーマの名前')
```
テーマの一覧を表示する
```python 
sg.theme_previewer()
```
![theme.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3111324/5de32a81-adf6-2222-0e98-73148739df31.png)

私の環境ではすべては表示されなかったので`scrollable=True`を追加してスクロールできるようにした
```python
sg.theme_previewer(scrollable=True)
```

# windowの設定
`keep_on_top=True`で最前面表示、`grab_anywhere=True`でどこをつかんでも動かせるようにできる
```python
win = sg.Window("テスト",layout,
                size=(300, 150),keep_on_top=True,grab_anywhere=True)
```

# 無限ループの実行
### 問題
連打くんを作るにあたって常にf9キーが押されたかどうかを判断する必要があった
ただしこちらのプログラムではtestは一回しか表示されなかった
```python
while True:
    print("test")
    event, value = win.read()
    if event == sg.WIN_CLOSED:
        break
```
### 原因と対応
`event, value = win.read()`はイベントが発生するまで待つらしい
それを防止するためにtimeoutを使う
```python
while True:#これで0.1秒ごとにtestと表示される
    print("test")
    event, value = win.read(timeout=100) #イベントが発生しなくても0.1秒で待機を抜ける
    if event == sg.WIN_CLOSED:
        break
```

# exe化とアイコン
### 準備
あらかじめフォルダを作りそこにpythonのプログラムとexeのアイコンにしたいicoファイルを移動しておく
![img.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3111324/caddef0c-ed41-30b2-d4aa-c4510118df52.png)

icon=でアイコン指定
```python
win = sg.Window("テスト",layout,icon='icon.ico',size=(300, 150))
```
しかしこのままexe化すると常にexeファイルと同じフォルダにiconファイルをおかないとアイコンが変わらないという事態が発生するため、exeファイルへの取り込みを行うためのプログラムを追加する
**importの追加**
```python
import sys
import os
```
**関数の追加**
```python
def resource_path(relative):
    if hasattr(sys, '_MEIPASS'):
        return os.path.join(sys._MEIPASS, relative)
    return os.path.join(os.path.abspath('.'), relative)
```
**sg.Window部分の変更**
```python
icon_path = resource_path('icon.ico')
win = sg.Window("テスト",layout,icon=icon_path,size=(300, 150))
```

ファイル全体としてはこんな感じ
```test.py
import PySimpleGUI as sg
import sys
import os

layout = [
    [sg.Text("Hello")]
]

def resource_path(relative):
    if hasattr(sys, '_MEIPASS'):
        return os.path.join(sys._MEIPASS, relative)
    return os.path.join(os.path.abspath('.'), relative)

icon_path = resource_path('icon.ico')
win = sg.Window("テスト",layout,icon=icon_path,size=(300, 150))

while True:
    event, value = win.read()
    if event == sg.WIN_CLOSED:
        break

win.close()
```

### exe化
exe化にはpyinstallerを使う
コマンドプロンプトでインストール
```
py -m pip install pyinstaller==5.5
```
後述の理由でバージョンは5.5にしている

「pythonのプログラムとexeのアイコンにしたいicoファイルを移動したフォルダ」でコマンドプロンプトを実行
--onefileでexeファイルを一つにまとめ、--noconsoleでコンソールを非表示にしている。また、エクスプローラー上のアイコンを変更するために--icon=icon.icoでアイコン指定。
```
pyinstaller --onefile --noconsole --icon=icon.ico test.py
```
できたexeファイルが重いときは--excludeを利用したり仮想環境を利用する

アイコンをexeファイルに取り込むために生成されたtest.specを開き、
```python
a.datas += [("icon.ico", "icon.ico", "Data")]
```
を追加する
![img2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3111324/21dea67e-121e-99f1-a7e2-82fce3bee2c1.png)
できたらコマンドプロンプトで`pyinstaller test.spec`を実行する
```
pyinstaller test.spec
```
pyinstallerのバージョンが最新だとここで`KeyError: 'Data'`というエラーが発生したのでバージョンは5.5にしている

# 蛇足
作成した連打くんのプログラムを貼っておく
```renda.py
import PySimpleGUI as sg
import pyautogui
import keyboard
import sys
import os

sg.theme('black')

layout = [
    [sg.Text("連打くん",size=(30,1), justification='center', 
             font=(None, 20, 'bold'), text_color='yellow',key='txt')],
    [sg.Text("1秒間に"),
     sg.Slider(range=(10,1000),default_value =100,resolution=10,
               orientation='h',size=(11, 15),key='repeat'),
     sg.Text("回")
    ],
    [sg.Text("f9で実行、f10で停止")],
    [sg.Checkbox("カーソル移動で停止",default=False,key='stop')]
]

def resource_path(relative):
    if hasattr(sys, '_MEIPASS'):
        return os.path.join(sys._MEIPASS, relative)
    return os.path.join(os.path.abspath('.'), relative)

icon_path = resource_path('cursor.ico')
win = sg.Window("連打くん",layout,icon=icon_path,size=(300, 185),
                keep_on_top=True,grab_anywhere=True)

while True:
    event, value = win.read(timeout=100)
    if keyboard.is_pressed('f9'):
        n = int(value['repeat']/10)
        win['txt'].update("実行中",text_color='red')
        event, value = win.read(timeout=1)
        if(value['stop']):
            pos = pyautogui.position()
            while keyboard.is_pressed('f10') == False:
                pyautogui.click(button='left', clicks=n)
                if(pos != pyautogui.position()):
                    break
        else:
            while keyboard.is_pressed('f10') == False:
                pyautogui.click(button='left', clicks=n)         
        win['txt'].update("連打くん",text_color='yellow')
    if event == sg.WIN_CLOSED:
        break

win.close()
```