PySimpleGUIを使ってプレゼン用カウントダウンタイマーを作った

#序論
男もすなるPySimpleGUIといふものを女もしてみむとてするなり[(大嘘)](https://ja.wikipedia.org/wiki/%E5%9C%9F%E4%BD%90%E6%97%A5%E8%A8%98)

初投稿です。

#背景・目的
研究室の卒論修論発表会でzoomに表示するためのプレゼンタイマーが必要でした。
指定の発表時間が終わるとリセットされ、質問時間として継続するものです。
既存のものにはjavascriptによるブラウザ表示のもの([こちらのサイト](https://ss.scphys.kyoto-u.ac.jp/person/yonezawa/contents/program/js/timer/index.html))があったため、使わせていただきました。
本稿では覚えたてのPythonとGUIを実装するためのPySimpleGUIライブラリを使い、PC用のカウントダウン型のプレゼン用タイマーアプリを作ることを目的としました。

※zoomにて画面共有を行う場合、ブラウザ上のツールバーや通知など公の場では好ましくない(かもしれない)ものが映り得ます。上記のリンク先にて示されているように画面共有にはOBSを使用し画面の一部を切り抜いて映す、という動画配信者の如き手間がかかります。


#実装
not 情報系 and C入門挫折マンなので可読性とか諸々についてはごめんなさい。

環境：Windows10, python3.9.0
(LinuxMint20 mateでの動作を確認(2021/02/17))

始めに実装に用いるライブラリを確認しておきます。
手元にアラーム音源があればplaysoundライブラリを使って流すこともできます。無ければ章末のAppendix.を参照してください。

```
pip install PySimpleGUI, playsound
```

【アラーム音を使いたい場合】
「alarm」検索でフォーカスされるコメントアウトを解除し、while文冒頭該当箇所に音源のパスを入れてください。
以下に記載する音無し版は[ここ(Github)](https://github.com/subterraneananimism/timer/blob/main/timer_nosound.py)にも上げています。

```PresentationTimer_nosound.py
#! python3.9.0
import PySimpleGUI as sg
import time
# 時間切れ時に手元のalarm音源mp3 or wmvを流す際は以下と節々のコメントアウト解除
# from playsound import playsound

def time_as_int():
    return int(round(time.time() * 100))

# GUIテーマ。sg.theme_previewer()で使えるテーマを確認できる。
sg.theme('Reddit')

# tab1(表示部)
layout1 = [
    # 表示テキスト font設定は各々の環境を参照してください。
    [sg.Text('', size=(10,1), font=('Bauhaus 93',80), justification='c', pad=((30,30),(20,0)), key='text')],
    [sg.Text('', size=(28,1), font=('Helvetica',30), justification='c', pad=((0,0),(0,10)) ,key='add_text')],
    # 開始/停止・リセット・終了ボタン
    [sg.Button('Run', button_color=('white', 'Blue'), key='-RUN-PAUSE-'),
    sg.Button('Reset', button_color=('white', 'Green'), key='-RESET-'),
    sg.Exit(button_color=('white', 'Darkred'), key='-QUIT1-')]]

# tab2(設定部)
layout2 = [
    # 発表時間設定・質問時間設定。後者が要らなければそのままでおｋ
    # 発表部。デフォルトはカップ麺の待ち時間
    [sg.Text('*OPTIONAL*\nInput the time title:',size=(15,2), justification='c', pad=((0,0),(10,10))), sg.InputText(default_text='Presentation time' ,key='first_title')],
    [sg.Combo([i for i in range(0,60)] + [''], default_value='3',size=(5,7) , font=('Helvetica', 10), pad=((125,0),(0,0)), key='min'),
    sg.Text('min.', font=('Helvetica', 10)),
    sg.Combo([i for i in range(0,60)] + [''], default_value='0',size=(5,7), font=('Helvetica', 10), key='sec'),
    sg.Text('sec.', font=('Helvetica', 10))],
    [sg.Text('')],
    # 質問部。要らなくてもそのままでいい
    [sg.Text('*OPTIONAL*\nInput the additionnal time title:',size=(15,3) , justification='c', pad=((0,0),(0,0))), sg.InputText(default_text='Question time' ,key='second_title')],
    [sg.Combo([i for i in range(0,60)] + [''], default_value='0',size=(5,5) , font=('Helvetica', 10), pad=((125,0),(0,0)), key='add_min'),
    sg.Text('min.', font=('Helvetica', 10)),
    sg.Combo([i for i in range(0,60)] + [''], default_value='0',size=(5,5), font=('Helvetica', 10), key='add_sec'),
    sg.Text('sec.', font=('Helvetica', 10))],
    [sg.Exit(button_color=('white', 'Darkred'), key='-QUIT2-', pad=((0,0),(20,0)))]
    ]

#タブ化
layout = [[sg.TabGroup([[sg.Tab('Timer', layout1), sg.Tab('Settings', layout2)]])]]

# 表示。タイトル無し、ボタンリサイズ無し、最前面表示、どこ掴んでもいい
window = sg.Window('Running Timer', layout,
    no_titlebar=True,
    auto_size_buttons=False,
    keep_on_top=True,
    grab_anywhere=True,
    element_padding=(0, 0))

# 変数の初期化
start_time, paused_time, paused = 0, 0, True
first_flag = True
add_word = ''
hold_time, hold_flag = 0, True
# alarm_flag = False

while True:
#    if alarm_flag:
#        playsound('alarm.mp3')) # ※音源のパスが必要※
#        alarm_flag = not alarm_flag

    if not paused:  # Run状態 10msで読み込み、与えた時間とその変化を記述・追加時間がある場合の制御
        event, values = window.read(timeout=10)
        current_time = values['min'] * 100 * 60 + values['sec'] * 100
        add_time = values['add_min'] * 100 * 60 + values['add_sec'] * 100
        current_time += start_time - time_as_int()
        if current_time < 0: # 監視している時間変数が0を下回ると追加時間を加算
            if hold_flag:
                hold_time = current_time
                hold_flag = not hold_flag
            current_time += add_time
            add_word = values['second_title']
            if current_time > 0: # 追加時間があれば時間説明の書き換え
                if current_time - add_time == hold_time:
                    add_word = values['second_title']
                    current_time = 0
                    # alarm_flag = not alarm_flag
            else: # 追加時間がなければ0とする
                event = '-RUN-PAUSE-'
                current_time = 0
                # alarm_flag = not alarm_flag

    else:   # 待機（pause）状態 起動時の諸々を処理
        event, values = window.read(timeout=20)
        if first_flag:
            add_word = values['first_title']
            current_time = values['min'] * 100 * 60 + values['sec'] * 100
            window['text'].update('{:02d}:{:02d}'.format((current_time // 100) // 60, (current_time // 100) % 60))

    window['text'].update('{:02d}:{:02d}'.format((current_time // 100) // 60, (current_time // 100) % 60))
    window['add_text'].update(add_word)

    if event in (sg.WIN_CLOSED, '-QUIT1-'):    # QUIT押下
        break
    if event in (sg.WIN_CLOSED, '-QUIT2-'):
        break

    if event == '-RESET-':  # RESET押下
        paused_time = start_time = time_as_int()
        current_time = values['min'] * 100 * 60 + values['sec'] * 100
        add_word = values['first_title']
        # alarm_flag = False

    elif event == '-RUN-PAUSE-':    # 0秒 or Run/Pause押下
        if first_flag: # 起動時の諸々を処理
            paused_time = time_as_int()
            window['-RUN-PAUSE-'].update('Pause')
            paused = not paused
            first_flag = not first_flag
            start_time = time_as_int()
            continue

        paused = not paused
        if paused:
            paused_time = time_as_int()
        else:
            start_time += time_as_int() - paused_time
        window['-RUN-PAUSE-'].update('Run' if paused else 'Pause')

window.close()
```

タイマーってすごいんだなあ

#結果
##イメージ
![tab1.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1107922/f24a6bb9-8700-52c7-bd2a-931a2fac3958.jpeg)
![tab2.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1107922/920f143b-3c6c-15e7-5369-cd8270de1eac.jpeg)
##動作
![timer.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1107922/6da80b25-ca45-f16f-2903-1b11bf47287e.gif)
という感じに作ってみました。
もう少し簡単になるんじゃないですかね(他人事)

###アラーム音を加える場合の注意
現状音が鳴っている間は00:00表示なので、追加時間がある場合はその時間だけ遅れて表示されます。
(e.g. 追加設定時間1分 and 音源長さ2秒 => 0:58から表示)
この辺りのアレンジはお任せします。

#参考文献
[PySimpleGUI公式のカウントアップタイマーデモ(Github)](https://github.com/PySimpleGUI/PySimpleGUI/blob/master/DemoPrograms/Demo_Desktop_Widget_Timer.py)
実装部のベースは上記のカウントアップタイマーを参照しました。

[PySimpleGUIの基本的な使用方法(Qiita)](https://qiita.com/KatsunoriNakamura/items/376da645e52f7ef7f9ef)
細かいPySimpleGUIの設定は上記を参照しました。

[Pythonでサウンドを扱う(Qiita)](https://qiita.com/hisshi00/items/62c555095b8ff15f9dd2#playsound)
そうなんだ

#Appendix.
次のフリー効果音素材サイトにアラーム音あり
[効果音ラボ](https://soundeffect-lab.info/sound/machine/machine2.html)