子供の YouTube 見すぎ問題を解決するために Python で嫌がらせをする

きっかけ
--------

以前は休日の度に子供を連れ出してあちこちに遊びに出かけていたのですが、コロナ禍のせいで気軽に外に出られなくなってしまいました。YouTube のおかげで家にこもっていても退屈はしないようですが、YouTube 見すぎが気になりはじめました。

手段
----

以下の機能のスクリーンセーバー的なプログラムを作ることにしました。

- 全画面を隠す
- 小学生に解ける問題をいくつか出す
- 問題に全問正解したら画面を隠すのを止めて普通に見られるようにする

これを 5 分に一回繰り返すことにします。

高圧的に「YouTube 禁止！」と言わずに「YouTube を見るならその分ちょっとは勉強もしてね」という感じになることをイメージしています。

問題の内容ですが、ターゲットは小学一年生なので、最大 20 までの２つの数の足し算にしました。
いい問題を思いついたらその都度問題を追加・入れ替えしていこうと思います。

実装
----

子供には Ubuntu を入れたパソコンを使わせています。

子供相手なので、当面ガチガチに抜け道を塞ぐ必要はないかと考え、お手軽に Python と tkinter で実装しました。

全画面ウィンドウを表示して他のウィンドウ操作をできなくして、全画面ウィンドウ上に問題と回答入力欄を表示します。
回答入力があるとその内容をチェックして、不正解がなくなったら終了します。

Gnome 起動時に、このような Python プログラムを 5 分おきに動かすシェルスクリプトを設定しました。


効果
----

5 分に一回だと割と高頻度なので相当嫌がられるかと思ったのですが、意外と抵抗なく受け入れているようです。
出題中も YouTube の音声が BGM 的に流れ続けるのも抵抗感を薄れさせているのかもしれません。

「パパ、見てみて！足し算できたよ！」と喜んでやってくれています（今のところは）。

子供がウィンドウ一覧ショートカットキーを発見したらそのときはショートカットキー登録を削除するなり考えなければならないでしょう。

~~~python
# -*- mode:python;coding:utf-8 -*-
import random
import sys
import tkinter
import time
from typing import Any, List, Type

QA_INTERVAL = 300
DEBUG = False

def abort_process(event: tkinter.Event) -> None:
    u'''強制終了
    '''
    sys.exit(1)

class QAFrame(tkinter.Frame):
    u'''問題表示クラス'''
    def __init__(self, window: tkinter.Tk,
                 *args: Any, **kwargs: Any) -> None:
        tkinter.Frame.__init__(self, window, *args, **kwargs)
    def result(self) -> bool:
        return False
class AnsEntry(tkinter.Entry):
    u'''回答入力
    '''
    def __init__(self, frame: tkinter.Frame,
                 status: tkinter.StringVar, trueAns: str) -> None:
        tkinter.Entry.__init__(self, frame)
        self.status = status
        self.trueAns = trueAns
        self.stringVar = tkinter.StringVar()
        self.stringVar.trace('w', self._updated)
        self.configure(textvariable=self.stringVar)
    def _updated(self, *args: Any) -> None:
        self.status.set("updated: {0}".format(args[0]))
        if args[0] != self.stringVar._name:
            return
        s = self.stringVar.get()
        if s == self.trueAns:
            self._success()
        else:
            self._ng()
    def _success(self) -> None:
        self.status.set("OK")
    def _ng(self) -> None:
        self.status.set("NG")
    def result(self) -> bool:
        return str(self.status.get()) == "OK"
class QACalcAddFrame(QAFrame):
    u'''簡単な足し算の問題を出す
    '''
    def __init__(self, window: tkinter.Tk,
                 *args: Any, **kwargs: Any) -> None:
        QAFrame.__init__(self, window, *args, **kwargs)
        self.pack()
        a = random.randrange(0, 20)
        b = random.randrange(0, 20)
        qtext =  '{0} + {1} ='.format(a, b)
        status = tkinter.StringVar()
        statusLabel = tkinter.Label(self, textvariable=status)
        questionLabel = tkinter.Label(self, text=qtext)
        self.ansEntry = AnsEntry(self, status, str(a + b))
        statusLabel.grid(column=0, row=0,)
        questionLabel.grid(column=1, row=0)
        self.ansEntry.grid(column=2, row=0)
    def result(self) -> bool:
        u'''回答内容が正しいなら文字列 'OK' を返す。
        '''
        return self.ansEntry.result()
class SummaryFrame(tkinter.Frame):
    u'''結果表示
    '''
    def __init__(self, window: tkinter.Tk,
                 qainstances: List[QAFrame], *args: Any, **kwargs: Any) -> None:
        tkinter.Frame.__init__(self, window, *args, **kwargs)
        self.window = window
        self.qainstances = qainstances
        self.after(1000, self._check)
        self.status = tkinter.StringVar()
        statusLabel = tkinter.Label(self, textvariable=self.status)
        statusLabel.pack()
    def _check(self) -> None:
        ngcount = sum([(0 if qainstance.result() else 1)
                       for qainstance in self.qainstances])
        if ngcount == 0:
            self.status.set('おわり！！！'.format(ngcount))
            self.after(1000, self.window.destroy)
        else:
            self.status.set('のこり {0} もん'.format(ngcount))
            self.after(1000, self._check)

# 問題を3つ出す
QACLASSES: List[Type[QAFrame]] = [QACalcAddFrame,QACalcAddFrame,QACalcAddFrame]

def display_qa() -> None:
    # メイン関数
    window = tkinter.Tk()
    time.sleep(1)
    window.attributes('-fullscreen', True)
    window.title("test window")
    qainstances = []
    for qaclass in QACLASSES:
        qa = qaclass(window, pady=10, padx=10)
        qainstances.append(qa)
    summary = SummaryFrame(window, qainstances)
    summary.pack()
    # デバッグ用
    if DEBUG:
        button = tkinter.Button(window, text="exit")
        button.pack()
        button.bind("<ButtonPress>", abort_process)
    window.mainloop()

if __name__ == '__main__':
    display_qa()
~~~