序論
男もすなるPySimpleGUIといふものを女もしてみむとてするなり(大嘘)
初投稿です。
背景・目的
研究室の卒論修論発表会でzoomに表示するためのプレゼンタイマーが必要でした。
指定の発表時間が終わるとリセットされ、質問時間として継続するものです。
既存のものにはjavascriptによるブラウザ表示のもの(こちらのサイト)があったため、使わせていただきました。
本稿では覚えたての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)にも上げています。
# ! 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 = [
    # 発表時間設定・質問時間設定。後者が要らなければそのままでおk
    # 発表部。デフォルトはカップ麺の待ち時間
    [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()
タイマーってすごいんだなあ
結果
イメージ
動作

という感じに作ってみました。
もう少し簡単になるんじゃないですかね(他人事)
アラーム音を加える場合の注意
現状音が鳴っている間は00:00表示なので、追加時間がある場合はその時間だけ遅れて表示されます。
(e.g. 追加設定時間1分 and 音源長さ2秒 => 0:58から表示)
この辺りのアレンジはお任せします。
参考文献
PySimpleGUI公式のカウントアップタイマーデモ(Github)
実装部のベースは上記のカウントアップタイマーを参照しました。
PySimpleGUIの基本的な使用方法(Qiita)
細かいPySimpleGUIの設定は上記を参照しました。
Pythonでサウンドを扱う(Qiita)
そうなんだ
Appendix.
次のフリー効果音素材サイトにアラーム音あり
効果音ラボ


