naritoブログ

このブログはDjangoとBootstrap4で作成されました
ソースコード

Tkinterで、簡易電卓を作るシリーズ④計算機能を作る

プログラミング関連 Python Tkinter 約110日前
2017年8月23日16:06
前回、見た目を完成させました。
今回はボタンを押すことで、ちゃんと計算ができるようにしていきます。

まず、現在のコードです。
from tkinter import *
from tkinter import ttk
 
 
# 2次元配列のとおりに、gridでレイアウトを作成する
LAYOUT = [
    ['7', '8', '9', '/'],
    ['4', '5', '6', '*'],
    ['1', '2', '3', '-'],
    ['0', 'C', '=', '+'],
]
 
 
class CalcApp(ttk.Frame):
    """電卓アプリ."""
 
    def __init__(self, master=None):
        super().__init__(master)
        self.create_style()
        self.create_widgets()

    def create_style(self):
        """ボタン、ラベルのスタイルを変更."""
        style = ttk.Style()

        # ラベルのスタイルを上書き
        style.configure(
            'TLabel', font=('Helvetica', 20),
            background='black', foreground='white',
        )
        # ボタンのスタイルを上書き
        style.configure('TButton', font=('Helvetica', 20))

    def create_widgets(self):
        """ウィジェットの作成."""
        self.display_var = StringVar()
        self.display_var.set('0')  # 初期値を0にする
 
        dispay_label = ttk.Label(self, textvariable=self.display_var)
        dispay_label.grid(column=0, row=0, columnspan=4, sticky=(N, S, E, W))
 
        # レイアウトの作成
        for y, row in enumerate(LAYOUT, 1):
            for x, char in enumerate(row):
                button = ttk.Button(self, text=char)
                button.grid(column=x, row=y, sticky=(N, S, E, W))
                button.bind('<Button-1>', self.calc)
        self.grid(column=0, row=0, sticky=(N, S, E, W))
 
        # 横の引き伸ばし設定
        self.columnconfigure(0, weight=1)
        self.columnconfigure(1, weight=1)
        self.columnconfigure(2, weight=1)
        self.columnconfigure(3, weight=1)
 
        # 縦の引き伸ばし設定。0番目の結果表示欄だけ、元の大きさのまま
        self.rowconfigure(0, weight=0)
        self.rowconfigure(1, weight=1)
        self.rowconfigure(2, weight=1)
        self.rowconfigure(3, weight=1)
        self.rowconfigure(4, weight=1)
 
        # ウィンドウ自体の引き伸ばし設定
        self.master.columnconfigure(0, weight=1)
        self.master.rowconfigure(0, weight=1)
 
    def calc(self, event):
        char = event.widget['text']
        self.display_var.set(char)
 
 
def main():
    root = Tk()
    root.title('簡単電卓')
    CalcApp(root)
    root.mainloop()
 
 
if __name__ == '__main__':
    main()
 


現状では、どのボタンを押してもcalcメソッドが呼ばれるようになっています。
ここを改良する必要がありますが、その前に__init__内にインスタンスの変数を一つ追加します。
    def __init__(self, master=None):
        super().__init__(master)
        self.exp_list = ['0']
        self.create_style()
        self.create_widgets()


足したのは、self.exp_listです。
self.exp_list = ['0']


実装方法はいくつかあると思いますが、今回はできるだけシンプルにしました。
各数字、記号のボタンが押されると、self.exp_listに['1', '+', '10', '*', '5'] のように格納されていきます。
=ボタンが押されれば、この'1 + 10 * 5' の文字列をeval()を使って評価し、その結果をリストに格納します。['51']のようにします。
Cボタンならば、リストの中身を['0']にします。
そして、表示欄には、このリストを' '.join()した形..
つまり、'1 + 10 * 5'や、'0'、'51' のように表示させます。
*を押した後に+を押された、等の少し特殊な操作を考える必要がありますが、そこまで難しくはありません。


流れを簡単に書いてみると、以下のようになります。
    def calc(self, event):
        # 押されたボタンのテキストを取得
        char = event.widget['text']

        # 最後に押したボタンの内容
        last = self.exp_list[-1]

        if char == '=':
            print('=が押された')
        elif char == 'C':
            print('Cが押された')
        elif char in ('+', '-', '*', '/'):
            print('記号が押された')
        else:
            print('数字が押された')

        # リストに格納している式を文字列にし、画面に反映
        self.display_var.set(
            ' '.join(self.exp_list)
        )


まずは=を押された場合を考えます。
=を押された際に考えることは、最後の入力が記号だった場合です。
つまり、['1', '+'] のような式だった場合、evalに渡すと怒られます。
なので、最後の入力が記号だったらそれを除くようにします。
        # =ボタン(現在の式の評価)の場合
        if char == '=':
            if last in ('+', '-', '*', '/', '**', '//'): 
                self.exp_list.pop()
            exp = eval(''.join(self.exp_list))
            self.exp_list = [str(exp)]


以下の部分が、最後の入力が記号だったら、という意味になります。
if last in ('+', '-', '*', '/', '**', '//'): 


この判断は以降の処理でも使いそうなので、モジュールの定数にしておきましょう。
# 記号をまとめた定数. if char in CALC_SYMBOLS:... のように使うために定義
CALC_SYMBOLS = ('+', '-', '*', '/', '**', '//')


コードにも反映しておきましょう。
        # =ボタン(現在の式の評価)の場合
        if char == '=':
            if last in CALC_SYMBOLS:  # ここ
                self.exp_list.pop()
            exp = eval(''.join(self.exp_list))
            self.exp_list = [str(exp)]


次にCを押した場合ですが、これは簡単です。リストの式を消去して0を入れるだけ!
        # Cボタン、内容クリア
        elif char == 'C':
            self.exp_list = ['0']



各演算記号を押された際の処理は、少し複雑です。
考えておくケースは、以下の4つです。
・最後の入力が「/」で、今の入力も「/」の場合→最後の入力(exp_list[-1])を「//」に変える
・最後の入力が「*」で、今の入力も「*」の場合→最後の入力を「**」に変える
・最後の入力が演算記号で、今の入力も演算記号の場合(「/」を押された後に「+」などの場合)→最後の入力を、今入力された記号に変える
・それ以外(最後の入力が数値)で、今の入力も演算記号→今の入力をリストの末尾に追加

//や**は、専用のボタンでも作っておけばもっと楽に作れましたが、仕方ありません。

これをコードにしてみると、以下のようになります。
        # +,-,*,/,等の記号を押した場合
        elif char in CALC_SYMBOLS:
            if last == char == '/':
                self.exp_list[-1] += '/'
            elif last == char == '*':
                self.exp_list[-1] += '*'
            elif last in CALC_SYMBOLS:
                self.exp_list[-1] = char
            else:
                self.exp_list.append(char)



そして、数値のボタンを押した際です。
・最後の入力が0だった場合→最後の入力(exp_list[-1])を、今入力された数値にする。['0']→['1'], ['1', '+', '0']→['1', '+', '1']
・最後の入力が演算記号だった場合→リストの末尾に、入力された数値を追加する。['1', '*', '5'] のようにする
・それ以外、最後の入力が数値で0じゃない場合→['1']を['16']のように、最後の入力にそのまま足す

以下のようになります。
        # それ以外、数字を押した場合
        else:
            if last == '0':
                self.exp_list[-1] = char
            elif last in CALC_SYMBOLS:
                self.exp_list.append(char)
            else:
                self.exp_list[-1] += char



最後に、全コードをもう一度貼っておきます。
from tkinter import *
from tkinter import ttk
 
 
# 2次元配列のとおりに、gridでレイアウトを作成する
LAYOUT = [
    ['7', '8', '9', '/'],
    ['4', '5', '6', '*'],
    ['1', '2', '3', '-'],
    ['0', 'C', '=', '+'],
]

# 記号をまとめた定数. if char in CALC_SYMBOLS:... のように使うために定義
CALC_SYMBOLS = ('+', '-', '*', '/', '**', '//')


class CalcApp(ttk.Frame):
    """電卓アプリ."""
 
    def __init__(self, master=None):
        super().__init__(master)
        self.exp_list = ['0']
        self.create_style()
        self.create_widgets()

    def create_style(self):
        """ボタン、ラベルのスタイルを変更."""
        style = ttk.Style()

        # ラベルのスタイルを上書き
        style.configure(
            'TLabel', font=('Helvetica', 20),
            background='black', foreground='white',
        )
        # ボタンのスタイルを上書き
        style.configure('TButton', font=('Helvetica', 20))

    def create_widgets(self):
        """ウィジェットの作成."""
        self.display_var = StringVar()
        self.display_var.set('0')  # 初期値を0にする
 
        dispay_label = ttk.Label(self, textvariable=self.display_var)
        dispay_label.grid(column=0, row=0, columnspan=4, sticky=(N, S, E, W))
 
        # レイアウトの作成
        for y, row in enumerate(LAYOUT, 1):
            for x, char in enumerate(row):
                button = ttk.Button(self, text=char)
                button.grid(column=x, row=y, sticky=(N, S, E, W))
                button.bind('<Button-1>', self.calc)
        self.grid(column=0, row=0, sticky=(N, S, E, W))
 
        # 横の引き伸ばし設定
        self.columnconfigure(0, weight=1)
        self.columnconfigure(1, weight=1)
        self.columnconfigure(2, weight=1)
        self.columnconfigure(3, weight=1)
 
        # 縦の引き伸ばし設定。0番目の結果表示欄だけ、元の大きさのまま
        self.rowconfigure(0, weight=0)
        self.rowconfigure(1, weight=1)
        self.rowconfigure(2, weight=1)
        self.rowconfigure(3, weight=1)
        self.rowconfigure(4, weight=1)
 
        # ウィンドウ自体の引き伸ばし設定
        self.master.columnconfigure(0, weight=1)
        self.master.rowconfigure(0, weight=1)
 
    def calc(self, event):
        # 押されたボタンのテキストを取得
        char = event.widget['text']

        # 最後に押したボタンの内容
        last = self.exp_list[-1]

        # =ボタン(現在の式の評価)の場合
        if char == '=':
            if last in CALC_SYMBOLS:
                self.exp_list.pop()
            exp = eval(''.join(self.exp_list))
            self.exp_list = [str(exp)]

        # Cボタン、内容クリア
        elif char == 'C':
            self.exp_list = ['0']

        # +,-,*,/,等の記号を押した場合
        elif char in CALC_SYMBOLS:
            if last == char == '/':
                self.exp_list[-1] += '/'
            elif last == char == '*':
                self.exp_list[-1] += '*'
            elif last in CALC_SYMBOLS:
                self.exp_list[-1] = char
            else:
                self.exp_list.append(char)

        # それ以外、数字を押した場合
        else:
            if last == '0':
                self.exp_list[-1] = char
            elif last in CALC_SYMBOLS:
                self.exp_list.append(char)
            else:
                self.exp_list[-1] += char

        # リストに格納している式を文字列にし、画面に反映
        self.display_var.set(
            ' '.join(self.exp_list)
        )
 
 
def main():
    root = Tk()
    root.title('簡単電卓')
    CalcApp(root)
    root.mainloop()
 
 
if __name__ == '__main__':
    main()
 



Githubにも上げており、こちらはpipでインストールすると「simplecalc」というコマンドで動かせるようにしています。
https://github.com/naritotakizawa/simplecalc