今回はTkinterを使ってGUIの電卓アプリを作成してみましょう。シンプルでイメージしやすいため初心者の練習用にはうってつけで、Frameウィジェット、Grid配置、bindメソッドによるトリガーイベントと処理の紐づけなどを実践的に学ぶことができます。
目次
フレームウィジェットを作成・配置
今回作成する電卓アプリはフレームを2枚使います。
というのも計算式や結果を表示するディスプレイと、数字などのボタンを押す部分は見た目も機能も全然違うため構造的に分けておくべきというのが一つ。
2つ目は1つ目の問題に起因するのですが、ディスプレイ画面はpackメソッドで1次元的に、ボタンはgridメソッドで2次元的に配置することになります。
tkinterの仕様上、同じコンテナ内にpackメソッドとgridメソッドは混在できないため、それぞれフレームウィジェットで囲ってあげます。
import tkinter as tk # tk.Frameを継承したApplicationクラスを作成 class Application(tk.Frame): def __init__(self, master=None): super().__init__(master) # ウィンドウの設定 root.title("電卓アプリ") root.geometry("310x440") self.calc_var = tk.StringVar() # 計算式用の動的変数 self.ans_var = tk.StringVar() # 結果用の動的変数 # 実行内容 self.pack() self.create_widget() # create_widgetメソッドを定義 def create_widget(self): # ディスプレイ用のフレーム self.calc_frame = tk.Frame(self.master, width=300, height=60, bg="lightgreen") self.calc_frame.propagate(False) # (この設定がTrueだと内側のwidgetに合わせたフレームサイズになる) self.calc_frame.pack(side=tk.TOP, padx=10, pady=20) # ボタン用のフレーム self.button_frame = tk.Frame(self.master, width=300, height=360, bg="gray") self.button_frame.propagate(False) # (この設定がTrueだと内側のwidgetに合わせたフレームサイズになる) self.button_frame.pack(side=tk.TOP) # メインの処理 if __name__ == "__main__": root = tk.Tk() app = Application(master=root) app.mainloop()
サイズ感やバランスを見るためにフレームに背景色を付けて見やすくしています。
ラベルとボタンを作成・配置
次に各フレーム内にラベルやボタンといったウィジェットを配置していきましょう。
import tkinter as tk # tk.Frameを継承したApplicationクラスを作成 class Application(tk.Frame): def __init__(self, master=None): super().__init__(master) # ウィンドウの設定 root.title("電卓アプリ") root.geometry("310x440") # 変数定義 self.button_number = [ ["B", "", "C", "/"], ["7", "8", "9", "*"], ["4", "5", "6", "-"], ["1", "2", "3", "+"], ["00", "0", ".", "="] ] # 実行内容 self.pack() self.create_widget() self.create_button() # create_widgetメソッドを定義 def create_widget(self): # フレーム self.calc_frame = tk.Frame(self.master, width=300, height=60, bg="lightgreen") self.calc_frame.propagate(False) self.calc_frame.pack(side=tk.TOP, padx=10, pady=20) self.button_frame = tk.Frame(self.master, width=300, height=360, bg="GREEN") self.button_frame.propagate(False) self.button_frame.pack(side=tk.TOP) #ディスプレイ用ウィジェット変数の定義 self.calc_var = tk.StringVar() # 計算式用の動的変数 self.ans_var = tk.StringVar() # 結果用の動的変数 # 計算式用ディスプレイ self.calc_label = tk.Label(self.calc_frame, textvariable=self.calc_var, font=("游ゴシック体", "15", "bold")) # 計算式用のLabel self.calc_label.pack(anchor=tk.E) # 右揃えで配置 # 結果用ディスプレイ self.ans_label = tk.Label(self.calc_frame, textvariable=self.ans_var, font=("游ゴシック体", "20", "bold")) # 結果用のLabel self.ans_label.pack(anchor=tk.E) # 右揃えで配置 # create_buttonメソッドを定義 def create_button(self): for y, row in enumerate(self.button_number): # button_numberリストの各要素を取得 for x, num in enumerate(row): # button_numberリストの各要素内の要素を取得 button = tk.Button(self.button_frame, text=num, font=("游ゴシック体", "15", "bold"), width=5, height=2) button.grid(row=y, column=x) # 列や行を指定して配置 # メインの処理 if __name__ == "__main__": root = tk.Tk() app = Application(master=root) app.mainloop()
12~19行目、ボタンは規則的に2次元配置するため表示するテキストを二次元リストとして定義。
49~54行目のcreate_buttonメソッドの中でfor文によるループ処理で各ボタンにテキストを埋め込んで配置しています。
37~47行目ではディスプレイ用のラベルを作成・配置しています。
ラベルに表示するテキストはユーザーの入力次第の変数となるため、ウィジェット変数StringVarを使用しています。
これで電卓アプリの見た目はできました。
あとはボタンが押された時の処理を記述していきます。
各ボタンの処理を記述
# 前略 self.symbol = ["+", "-", "*", "/"] # 記号のリスト self.calc_str = "" # 計算式用の変数 # 中略 # create_buttonメソッドを定義 def create_button(self): for y, row in enumerate(self.button_number): # button_numberリストの各要素を取得 for x, num in enumerate(row): # button_numberリストの各要素内の要素を取得 button = tk.Button(self.button_frame, text=num, font=("游ゴシック体", "15", "bold"), width=5, height=2) button.grid(row=y, column=x) # 列と行を指定して配置 button.bind('<Button-1>', self.button_clicked) # Buttonが左クリックされた場合 # ボタンが押された時の処理 def button_clicked(self, event): check = event.widget["text"] # 押されたボタンのテキストを取得 # クリアの場合OK if check == "C": self.calc_str = "" self.calc_var.set(self.calc_str) # 計算用ディスプレイの表示を変更 self.ans_var.set("") # 結果用ディスプレイの表示を変更 # バックの場合OK elif check == "B": self.calc_str = self.calc_str[:-1] # 最後の1文字以外を代入 self.calc_var.set(self.calc_str) # 計算用ディスプレイの表示を変更 # イコールの場合 elif check == "=": if self.calc_str != "": # calc_strが空でない場合 if self.calc_str[-1:] in self.symbol: # 最期の1文字が記号の場合 self.calc_str = self.calc_str[:-1] # 最後の1文字以外を代入 # 共通処理 res = "= " + str(eval(self.calc_str)) # calc_strの中身を式として計算 self.calc_var.set(self.calc_str) # 計算用ディスプレイの表示を変更 self.ans_var.set(res) # 結果をans_var変数にセット self.calc_str = str(eval(self.calc_str)) # calc_strにも計算結果を代入 # 記号の場合 elif check in self.symbol: if self.calc_str[-1:] not in self.symbol and self.calc_str != "": # 最後の1文字が記号も空でもない場合 self.calc_str += check # calc_strに最後の1文字を連結 self.calc_var.set(self.calc_str) # 計算用ディスプレイの表示を変更 elif self.calc_str[-1:] in self.symbol: # 最後の1文字が記号の場合 self.calc_str = self.calc_str[:-1] + check # 最後の1文字を今押した記号に入れ替える self.calc_var.set(self.calc_str) # 計算用ディスプレイの表示を変更 # その他、数字などの場合 else: self.calc_str += check # 末尾に最後の文字を連結 self.calc_var.set(self.calc_str) # 計算用ディスプレイの表示を変更
bindメソッドによる紐づけ
14行目でbindメソッドを使ってイベント情報を取得し、button_clickedメソッドに渡します。
17行目でbutton_clickedメソッドを作成し、18行目でイベント情報から押されたボタンのテキストを取得するよう記述しています。
ボタンが押された時の処理はcommandオプションで関数と紐づけることもできますが、その場合ボタン1つ1つに対し処理を書くことになるため手間ですし、コードが冗長になってしまいます。
このようにbindメソッドを使ってイベント情報を取得し、関数と紐づけることで、同様の処理を簡潔にわかりやすく記述することができます。
【Tkinter】bindメソッドによるトリガーイベントとコールバック関数の紐づけ
C(クリア)、B(バック)ボタン
さて、肝心のボタンが押された時の処理を記述しましょう。
3,4行目で記号のリストと計算式を文字列として一時的に保存するための変数を用意しています。これらを使って各ボタンの処理を作っていきます。
C(クリア)の場合とB(バック)の場合の処理は簡単です。
C(クリア)は計算式と両ディスプレイの表示を全て空に、B(バックは)calc_str の中身をスライスして最後の1文字以外を代入、それを計算用ディスプレイに表示するだけです。
イコール、記号、数字ボタン
本格的に処理を書く必要があるのはこの3種類のボタンです。
これらはボタンを押すタイミング(その時計算式用の変数に何が代入されているのか、直前に押されたボタンは何か)によって挙動が変わります。
例えば数字ボタンは
- 数字 → 数字と押した場合は文字列を連結
- 数字 → イコールと押した場合は現在の式を計算して結果を出力
- 数字 → 記号と押した場合は文字列を連結して計算式用のディスプレイに反映するが、計算結果用のディスプレイには反映しない
といった具合です。
これらを完璧に作ろうとすると電卓は意外と処理のパターンが多く、また、ボタンを押す順番によっては処理がメーカーによって異なるパターンもあります。
上記を参考に自分が気に入った電卓を作ってみて下さい。
押されたボタンは一度文字列型として連結して計算式用の変数(calc_str)に保存し、式を計算するタイミングでeval関数を使うと便利です。
完成
import tkinter as tk # tk.Frameを継承したApplicationクラスを作成 class Application(tk.Frame): def __init__(self, master=None): super().__init__(master) # ウィンドウの設定 root.title("電卓アプリ") root.geometry("310x440") # 変数定義 self.button_number = [ ["B", "", "C", "/"], ["7", "8", "9", "*"], ["4", "5", "6", "-"], ["1", "2", "3", "+"], ["00", "0", ".", "="] ] self.symbol = ["+", "-", "*", "/"] self.calc_str = "" # 実行内容 self.pack() self.create_widget() self.create_button() # create_widgetメソッドを定義 def create_widget(self): # フレーム self.calc_frame = tk.Frame(self.master, width=300, height=60) self.calc_frame.propagate(False) # (この設定がTrueだと内側のwidgetに合わせたフレームサイズになる) self.calc_frame.pack(side=tk.TOP, padx=10, pady=20) self.button_frame = tk.Frame(self.master, width=300, height=360) self.button_frame.propagate(False) # (この設定がTrueだと内側のwidgetに合わせたフレームサイズになる) self.button_frame.pack(side=tk.TOP) # ディスプレイ用ウィジェット変数の定義 self.calc_var = tk.StringVar() # 計算用ディスプレイの表示 self.ans_var = tk.StringVar() # 結果用ディスプレイの表示 # 計算式用ディスプレイ self.calc_label = tk.Label(self.calc_frame, textvariable=self.calc_var, font=("游ゴシック体", "15", "bold")) # 計算式用のLabel self.calc_label.pack(anchor=tk.E) # 右揃えで配置 # 結果用ディスプレイ self.ans_label = tk.Label(self.calc_frame, textvariable=self.ans_var, font=("游ゴシック体", "20", "bold")) # 結果用のLabel self.ans_label.pack(anchor=tk.E) # 右揃えで配置 # create_buttonメソッドを定義 def create_button(self): for y, row in enumerate(self.button_number): # button_numberリストの各要素を取得 for x, num in enumerate(row): # button_numberリストの各要素内の要素を取得 button = tk.Button(self.button_frame, text=num, font=("游ゴシック体", "15", "bold"), width=5, height=2) button.grid(row=y, column=x) # 列と行を指定して配置 button.bind('<Button-1>', self.button_clicked) # Buttonが左クリックされた場合 # ボタンが押された時の処理 def button_clicked(self, event): check = event.widget["text"] # 押されたボタンのテキストを取得 # クリアの場合OK if check == "C": self.calc_str = "" self.calc_var.set(self.calc_str) # 計算用ディスプレイの表示を変更 self.ans_var.set("") # 結果用ディスプレイの表示を変更 # バックの場合OK elif check == "B": self.calc_str = self.calc_str[:-1] # 最後の1文字以外を代入 self.calc_var.set(self.calc_str) # 計算用ディスプレイの表示を変更 # イコールの場合 elif check == "=": if self.calc_str != "": # calc_strが空でない場合 if self.calc_str[-1:] in self.symbol: # 最期の1文字が記号の場合 self.calc_str = self.calc_str[:-1] # 最後の1文字以外を代入 # 計算&出力 res = "= " + str(eval(self.calc_str)) # calc_strの中身を式として計算 self.calc_var.set(self.calc_str) # 計算用ディスプレイの表示を変更 self.ans_var.set(res) # 結果をans_var変数にセット self.calc_str = str(eval(self.calc_str)) # calc_strにも計算結果を代入 # 記号の場合 elif check in self.symbol: if self.calc_str[-1:] not in self.symbol and self.calc_str != "": # 最後の1文字が記号も空でもない場合 self.calc_str += check # calc_strに最後の1文字を連結 self.calc_var.set(self.calc_str) # 計算用ディスプレイの表示を変更 elif self.calc_str[-1:] in self.symbol: # 最後の1文字が記号の場合 self.calc_str = self.calc_str[:-1] + check # 最後の1文字を今押した記号に入れ替える self.calc_var.set(self.calc_str) # 計算用ディスプレイの表示を変更 # その他、数字などの場合 else: self.calc_str += check # 末尾に最後の文字を連結 self.calc_var.set(self.calc_str) # 計算用ディスプレイの表示を変更 # メインの処理 if __name__ == "__main__": root = tk.Tk() app = Application(master=root) app.mainloop()