Python で「へぇ〜ボタン」を作る(tkinter)

みなさん、へぇ〜ボタンをご存知でしょうか。15年ほど前にテレビで放映されていた「トリビアの泉」という雑学紹介番組で出てきたアイテムで、ゲストが「へぇ〜」と思った度合いに応じて押すボタンです。この番組は当時かなり人気で、へぇ〜ボタンのガチャガチャがあったりしたほどです。トリビアの泉をご存知無い方は以下の wikipedia 記事でも読んでみてください。
ja.wikipedia.org

今回はこの「へぇ〜ボタン」を Python で実装してみたいと思います。せっかくなので、最近勉強している GUI 作成用の標準モジュールである tkinter を利用したいと思います。

仕様

google で「へぇ〜ボタン」で画像検索するといっぱい出てきます。例えば以下のサイトにはトイザらスの商品の画像が載っています。
www.joho.st

へぇ〜ボタンには押した回数を表示する窓がついています。何も押さない状態で「0へぇ〜」、ボタンを押すごとに「1へぇ〜」「2へぇ〜」...と増えていき、「20へぇ〜」になるとそれ以上増えなくなります。またリセットボタンがついており、それを押すとまた「0へぇ〜」に戻ります。

これを Python で実装して、以下のようなウィンドウを表示して、クリックで操作できるようにしたいと思います。
f:id:mat_der_D:20210225235435p:plain
具体的には以下のような挙動にしたいと思います。

  1. python インタープリタを使って起動するとウィンドウが表示される
  2. 「へぇ〜」と書かれたボタンをクリックすると窓に書かれた数字が増える
  3. 「リセット」と書かれたボタンを押すと0へぇ〜に戻る
  4. 右上の「×」を押すと終了する

設計

実装としては「へぇ〜ボタンの機能を持ったオブジェクト(クラス)」と「画面のクリックなどの情報をへぇ〜ボタンに伝え、変わった状態を画面に表示するオブジェクト(GUI)」を用意します。前者は Python の標準の機能で、後者は tkinter を使って実装していきます。

前者の「へぇ〜ボタンクラス」が持つべき変数は

  • 現在の「へぇ〜」の回数

持つべきメソッド(関数)は

  • press 関数(へぇ〜の回数を1増やす, ただし20のときは増やさない)
  • reset 関数(へぇ〜の回数を0にする)

あたりでしょうか。

後者の「へぇ〜ボタンのGUI」は

  • 「へぇ〜」の回数を表示する窓
  • 「へぇ〜」という文字
  • 「へぇ〜」ボタン(「へぇ〜」と書かれており、クリックすると「へぇ〜ボタンクラス」の push 関数が呼び出される)
  • リセットボタン(「リセット」と書かれており、クリックすると「へぇ〜ボタンクラス」の reset 関数が呼び出される)

あたりを持っていると良いでしょう。へぇ〜ボタンクラスを操作するので、へぇ〜ボタンオブジェクト自体も内部に保持するようにします。

へぇ〜ボタンクラスの実装

まずへぇ〜ボタンクラスから実装していきましょう。シンプルな設計なので一気に書いてしまいます。

class HeeButton:
    def __init__(self):
        self.num = 0 # 「へぇ〜」の回数

    def press(self):
        if self.num < 20:
            self.num += 1

    def reset(self):
        self.num = 0

以下のように動作確認できます。

hee_btn = HeeButton() # へぇ〜ボタンオブジェクトを生成
print(hee_btn.num, "へぇ〜") # 0 へぇ〜
for _ in range(15):
    hee_btn.press() # 15 回押す
print(hee_btn.num, "へぇ〜") # 15 へぇ〜
for _ in range(100):
    hee_btn.press() # 100 回押す
print(hee_btn.num, "へぇ〜") # 20 へぇ〜
hee_btn.reset() # リセット
print(hee_btn.num, "へぇ〜") # 0 へぇ〜

これで完成です。

へぇ〜ボタン GUI の実装

GUI を作るのに、今回は tkinter というモジュールを使います。標準で入っているので pip install などを行う必要はありません。
私は最近↓の書籍を使って入門したんですが、とてもシンプルにど基礎を教えてくれるのでオススメです。
www.amazon.co.jp
ここからは上記の書籍の内容をざっくり読んだぐらいの知識を仮定します。
へぇ〜ボタン本体と比べてGUIの実装はコードの分量が多めになるので、「大枠を作る→細部を埋める」という順序で作っていきます。

まず全体の形を作ります。

import tkinter as tk

# ~ ここで class HeeButton を定義(省略) ~

class HeeButtonGUI(tk.Frame):
    def __init__(self, master):
        super().__init__(master) # 大枠を初期化
        self.pack(fill=tk.BOTH, expand=True) # 大枠を配置
        self.config_master() # 全体の設定
        self.create_variable() # 変数を定義
        self.create_widget() # 個々のウィジェット(部品)を作る

        # 内部に「へぇ〜ボタン」オブジェクトを持っておく
        self.hee_btn = HeeButton()
    
    def config_master(self):
        pass # ここで全体の大きさなどを調整する

    def create_variable(self):
        pass # ウィジェット用の変数をここで定義する

    def create_widget(self):
        pass # ここでボタンなどを配置する


if __name__ == "__main__":
    root = tk.Tk() # 親玉
    app = HeeButtonGUI(root) # アプリケーションのオブジェクト
    app.mainloop() # アプリケーションを起動, 実行    

この状態でも起動しますが、なにもない窓が表示されるだけです。
ここにボタンと窓を配置していきましょう。create_widget 関数を作っていきます。
tkinter では基本的な配置の仕方として以下の3つがあります:

  1. pack ... 単に並べていくスタイル。シンプルな配置では便利。
  2. grid ... セルの形に領域を分けて配置するスタイル。複雑な配置ではこれを使う。
  3. place ... より静的に座標で場所を指定して配置するスタイル。あまり使わない。

今回は grid を使ってみましょう。表を使って書いてみると↓こんな感じでしょうか。
f:id:mat_der_D:20210226005646p:plain
コードで書くとこんな感じです。

    def create_widget(self):
        # 窓
        num_lbl = tk.Label(self, text="0")
        num_lbl.grid(column=0, row=0)
        # 窓の右の文字
        hee_lbl = tk.Label(self, text="へぇ〜")
        hee_lbl.grid(column=1, row=0)
        # へぇ〜ボタン
        hee_btn = tk.Button(self, text="へぇ〜")
        hee_btn.grid(column=0, row=1, columnspan=2) # ぶち抜く
        # リセットボタン
        reset_btn = tk.Button(self, text="リセット")
        reset_btn.grid(column=1, row=2)

とりあえずこの状態で実行するとこんな感じで表示されました。
f:id:mat_der_D:20210226221303p:plain
なんだかかなり不格好です。あとクリックしても数字が増えません。

クリックすると数字が増えるようにする

今の状態では num_lbl で text="0" と定義されているため、増えようがありません。増えるようにするためには、"変数"を定義する必要があります。
ここでは、string に対応する変数である tk.StringVar() を使います。 create_variable で定義します。

    def create_variable(self):
        self.hee_var = tk.StringVar() # へぇ〜の回数
        self.hee_var.set("0") # 初期化

これを窓の変数に設定してみましょう。create_widget 関数を編集します。

    def create_widget(self):
        # 窓
        num_lbl = tk.Label(self, textvar=self.hee_var)
        num_lbl.grid(column=0, row=0)
        # 窓の右の文字
        hee_lbl = tk.Label(self, text="へぇ〜")
        hee_lbl.grid(column=1, row=0)
        # へぇ〜ボタン
        hee_btn = tk.Button(self, text="へぇ〜")
        hee_btn.grid(column=0, row=1, columnspan=2)
        # リセットボタン
        reset_btn = tk.Button(self, text="リセット")
        reset_btn.grid(column=1, row=2)

まだ増えません。なぜなら変数を定義しただけで、変数を増やすルールを定めていないからです。ということで「ボタンを押すと self.hee_var が増える」を実装します。

    def press_hee(self):
        self.hee_btn.press() # ボタンを押す
        self.hee_var.set(str(self.hee_btn.num)) # 回数を代入

ボタンを押すとこの関数が実行されるようにします。

    def create_widget(self):
        # 窓
        num_lbl = tk.Label(self, textvar=self.hee_var)
        num_lbl.grid(column=0, row=0)
        # 窓の右の文字
        hee_lbl = tk.Label(self, text="へぇ〜")
        hee_lbl.grid(column=1, row=0)
        # へぇ〜ボタン
        hee_btn = tk.Button(
            self, text="へぇ〜", command=self.press_hee, # 設定!
        )
        hee_btn.grid(column=0, row=1, columnspan=2)
        # リセットボタン
        reset_btn = tk.Button(self, text="リセット")
        reset_btn.grid(column=1, row=2)

これで実行すると、クリックで数字が増えるようになります。

リセットできるようにする

同じノリでリセットボタンが動くようにしましょう。今回は「リセットボタンを押す」関数を定義して、リセットボタンに登録するだけでOKです。

    def create_widget(self):
        # 窓
        num_lbl = tk.Label(self, textvar=self.hee_var)
        num_lbl.grid(column=0, row=0)
        # 窓の右の文字
        hee_lbl = tk.Label(self, text="へぇ〜")
        hee_lbl.grid(column=1, row=0)
        # へぇ〜ボタン
        hee_btn = tk.Button(
            self, text="へぇ〜", command=self.press_hee,
        )
        hee_btn.grid(column=0, row=1, columnspan=2)
        # リセットボタン
        reset_btn = tk.Button(
            self, text="リセット", command=self.press_reset, # 設定!
        )
        reset_btn.grid(column=1, row=2)

    def press_reset(self):
        self.hee_btn.reset() # リセットする
        self.hee_var.set(str(self.hee_btn.num)) # 回数を代入

これでリセットできるようになりました。

不格好さを改善する

ここまでの編集で数字を増やしたりリセットしたりできるようになりました。でも見た目が微妙です。細かいサイズ調整をして、いい感じにしていきましょう。まずはキャンパス全体の設定を変えます。

    def config_master(self):
        self.master.geometry("180x120") # サイズ指定
        self.master.resizable(width=False, height=False) # サイズ変更を禁止
        self.grid_columnconfigure(0, weight=1) # カラムの長さ比指定
        self.grid_columnconfigure(1, weight=1) # カラムの長さ比指定

全体サイズを指定し、内部に含まれるカラムの長さの比を指定します。この時点で以下のようになります。
f:id:mat_der_D:20210226233304p:plain
横のサイズに関してはいい感じになりました。縦方向にもいい感じに伸ばしてみましょう。(ついでに横方向にもいい感じに伸ばしてみましょう。)ウィジェットの設定を細々といじります。

    def create_widget(self):
        # 窓
        num_lbl = tk.Label(
            self, textvar=self.hee_var,
            font=("", 20), # フォントサイズを指定
            anchor="e", # 右寄せ
        )
        num_lbl.grid(
            column=0, row=0, 
            sticky="EW",# 領域を横に伸ばす
        )
        # 窓の右の文字
        hee_lbl = tk.Label(
            self, text="へぇ〜",
            anchor="sw", # 左下寄せ
        )
        hee_lbl.grid(
            column=1, row=0, 
            sticky="NEWS", # 領域を上下左右に伸ばす
        )
        # へぇ〜ボタン
        hee_btn = tk.Button(
            self, text="へぇ〜", command=self.press_hee,
            height=4, # 縦方向に長さ指定
        )
        hee_btn.grid(
            column=0, row=1, columnspan=2,
            sticky="EW", # 領域を左右に伸ばす
            padx=5, # 横方向の余白を作る
        )
        # リセットボタン
        reset_btn = tk.Button(
            self, text="リセット", command=self.press_reset,
        )
        reset_btn.grid(column=1, row=2)

ここまで編集して実行すると、以下のウィンドウが表示されます。
f:id:mat_der_D:20210226234503p:plain
最後におまけでウィンドウのタイトルを設定します。

    def config_master(self):
        self.master.geometry("180x120")
        self.master.resizable(width=False, height=False)
        self.grid_columnconfigure(0, weight=1)
        self.grid_columnconfigure(1, weight=1)
        self.master.title("へぇボタン") # タイトルを設定

これで以下のようになります。
f:id:mat_der_D:20210226234714p:plain
とりあえずいい感じになったので、これで完成としたいと思います。

まとめ

完成したコード全体は以下のようになります。

import tkinter as tk


class HeeButton:
    def __init__(self):
        self.num = 0

    def press(self):
        if self.num < 20:
            self.num += 1

    def reset(self):
        self.num = 0


class HeeButtonGUI(tk.Frame):
    def __init__(self, master):
        super().__init__(master)
        self.pack(fill=tk.BOTH, expand=True)
        self.config_master()
        self.create_variable()
        self.create_widget()

        self.hee_btn = HeeButton()
    
    def config_master(self):
        self.master.geometry("180x120")
        self.master.resizable(width=False, height=False)
        self.grid_columnconfigure(0, weight=1)
        self.grid_columnconfigure(1, weight=1)
        self.master.title("へぇボタン")

    def create_variable(self):
        self.hee_var = tk.StringVar()
        self.hee_var.set("0")
        
    def create_widget(self):
        # 窓
        num_lbl = tk.Label(
            self, textvar=self.hee_var,
            font=("", 20),
            anchor="e",
        )
        num_lbl.grid(
            column=0, row=0, 
            sticky="EW",
        )
        # 窓の右の文字
        hee_lbl = tk.Label(
            self, text="へぇ〜",
            anchor="sw",
        )
        hee_lbl.grid(
            column=1, row=0, 
            sticky="NEWS",
        )
        # へぇ〜ボタン
        hee_btn = tk.Button(
            self, text="へぇ〜", command=self.press_hee,
            height=4,
        )
        hee_btn.grid(
            column=0, row=1, columnspan=2,
            sticky="EW",
            padx=5,
        )
        # リセットボタン
        reset_btn = tk.Button(
            self, text="リセット", command=self.press_reset,
        )
        reset_btn.grid(column=1, row=2)

    def press_hee(self):
        self.hee_btn.press()
        self.hee_var.set(str(self.hee_btn.num))

    def press_reset(self):
        self.hee_btn.reset()
        self.hee_var.set(str(self.hee_btn.num))
        
        
if __name__ == "__main__":
    root = tk.Tk()
    app = HeeButtonGUI(root)
    app.mainloop()

これを使って、へぇ〜ボタンライフをお楽しみください!