初心者向け No.02

シンプル電卓

四則演算ができる電卓アプリです。Gridレイアウトとボタン生成の効率的な実装を学びます。

🎯 難易度: ★☆☆ 易しい 📦 ライブラリ: tkinter(標準ライブラリ) ⏱️ 制作時間: 30〜60分

1. アプリ概要

電卓はGUI学習の定番プロジェクトです。多数のボタンを整列させるGridレイアウト、計算式の文字列処理、エラーハンドリングなど、実践的なGUIプログラミングの要素が凝縮されています。

このアプリでは、数字ボタン(0〜9)・演算子ボタン(+−×÷)・機能ボタン(C、=、.)を組み合わせた本格的な電卓を実装します。ボタンを一つひとつ手動で作る代わりに、リストと繰り返し処理を使って効率的に生成する方法も学べます。

計算処理には Python の組み込み関数 eval() を使います。eval()は文字列をPythonの式として評価する強力な機能ですが、ユーザー入力をそのままeval()に渡すのはセキュリティリスクがあります。このアプリでは安全な数式のみを受け付ける検証も実装します。

また、ZeroDivisionError(ゼロ除算)や不正な式の入力など、現実のアプリで必ず考慮すべきエラーハンドリングについても詳しく解説します。

シンプル電卓 起動時の画面
起動時の画面
シンプル電卓 数式入力中の画面
数式入力中
シンプル電卓 計算結果表示の画面
計算結果表示

2. 機能一覧

  • 数字と小数点の入力
  • 四則演算(加算・減算・乗算・除算)
  • パーセント計算
  • バックスペース(1文字削除)
  • クリア(全消し)
  • キーボード入力対応
  • エラーハンドリング(ゼロ除算・不正式)

3. 事前準備・環境

ℹ️
動作確認環境

Python 3.10 以上 / Windows・Mac・Linux すべて対応

tkinterのみ使用します。Pythonがインストールされていれば追加作業は不要です。

4. 完全なソースコード

💡
コードのコピー方法

右上の「コピー」ボタンをクリックするとコードをクリップボードにコピーできます。

app002.py
import tkinter as tk


class Calculator:
    """電卓アプリのメインクラス"""

    # ボタンの定義: (テキスト, 行, 列, colspan, bg色)
    BUTTONS = [
        ("C",   1, 0, 1, "#e74c3c"), ("←",  1, 1, 1, "#e67e22"),
        ("%",   1, 2, 1, "#95a5a6"), ("÷",  1, 3, 1, "#3498db"),
        ("7",   2, 0, 1, "#ecf0f1"), ("8",   2, 1, 1, "#ecf0f1"),
        ("9",   2, 2, 1, "#ecf0f1"), ("×",  2, 3, 1, "#3498db"),
        ("4",   3, 0, 1, "#ecf0f1"), ("5",   3, 1, 1, "#ecf0f1"),
        ("6",   3, 2, 1, "#ecf0f1"), ("−",  3, 3, 1, "#3498db"),
        ("1",   4, 0, 1, "#ecf0f1"), ("2",   4, 1, 1, "#ecf0f1"),
        ("3",   4, 2, 1, "#ecf0f1"), ("+",   4, 3, 1, "#3498db"),
        ("0",   5, 0, 2, "#ecf0f1"), (".",   5, 2, 1, "#ecf0f1"),
        ("=",   5, 3, 1, "#2ecc71"),
    ]

    def __init__(self, root):
        self.root = root
        self.root.title("電卓")
        self.root.geometry("280x380")
        self.root.resizable(False, False)
        self.root.configure(bg="#2c3e50")
        self.expression = ""
        self._build_ui()
        self._bind_keyboard()

    def _build_ui(self):
        """UIを構築する"""
        # 表示エリア
        self.display_var = tk.StringVar(value="0")
        display = tk.Label(
            self.root,
            textvariable=self.display_var,
            font=("Arial", 28, "bold"),
            bg="#34495e", fg="white",
            anchor="e", padx=12,
            height=2
        )
        display.grid(row=0, column=0, columnspan=4,
                     sticky="nsew", padx=4, pady=(4, 2))

        # ボタンを生成
        for (text, row, col, span, color) in self.BUTTONS:
            fg = "white" if color != "#ecf0f1" else "#2c3e50"
            btn = tk.Button(
                self.root,
                text=text,
                font=("Arial", 16, "bold"),
                bg=color, fg=fg,
                activebackground=self._darken(color),
                relief=tk.FLAT,
                cursor="hand2",
                command=lambda t=text: self.on_press(t)
            )
            btn.grid(
                row=row, column=col, columnspan=span,
                sticky="nsew", padx=2, pady=2
            )

        # グリッドの重み設定(均等に拡張)
        for i in range(4):
            self.root.columnconfigure(i, weight=1)
        for i in range(6):
            self.root.rowconfigure(i, weight=1)

    def _darken(self, hex_color):
        """色を少し暗くする"""
        mapping = {
            "#ecf0f1": "#bdc3c7", "#3498db": "#2980b9",
            "#e74c3c": "#c0392b", "#2ecc71": "#27ae60",
            "#e67e22": "#d35400", "#95a5a6": "#7f8c8d",
        }
        return mapping.get(hex_color, hex_color)

    def _bind_keyboard(self):
        """キーボード入力をバインドする"""
        # Tkinter の bind には記号そのものではなく keysym 名を使う必要がある
        key_map = {
            "Key-0": "0", "Key-1": "1", "Key-2": "2", "Key-3": "3", "Key-4": "4",
            "Key-5": "5", "Key-6": "6", "Key-7": "7", "Key-8": "8", "Key-9": "9",
            "plus": "+", "minus": "−", "asterisk": "×", "slash": "÷",
            "period": ".", "Return": "=", "BackSpace": "←", "Escape": "C",
        }
        for key, action in key_map.items():
            self.root.bind(f"<{key}>", lambda e, a=action: self.on_press(a))

    def on_press(self, char):
        """ボタン押下時の処理"""
        if char == "C":
            self.expression = ""
            self.display_var.set("0")
        elif char == "←":
            self.expression = self.expression[:-1]
            self.display_var.set(self.expression or "0")
        elif char == "=":
            self._calculate()
        elif char == "%":
            try:
                self.expression = str(eval(self.expression) / 100)
                self.display_var.set(self.expression)
            except Exception:
                self.display_var.set("Error")
                self.expression = ""
        else:
            # 記号を実際の演算子に変換
            op_map = {"×": "*", "÷": "/", "−": "-"}
            self.expression += op_map.get(char, char)
            self.display_var.set(self.expression)

    def _calculate(self):
        """計算実行"""
        try:
            result = eval(self.expression)
            # 整数の場合は.0を除去
            if isinstance(result, float) and result.is_integer():
                result = int(result)
            self.expression = str(result)
            self.display_var.set(self.expression)
        except ZeroDivisionError:
            self.display_var.set("0除算エラー")
            self.expression = ""
        except Exception:
            self.display_var.set("エラー")
            self.expression = ""


if __name__ == "__main__":
    root = tk.Tk()
    app = Calculator(root)
    root.mainloop()

5. コード解説

電卓アプリはクラスを使って実装しています。クラスを使うことでデータ(expression等)とUI操作をまとめて管理できます。

クラス設計の考え方

Calculatorクラスに電卓の全機能をまとめています。__init__でウィンドウ設定・UI構築・キーバインドを初期化します。クラスを使うことでグローバル変数を避け、コードが整理されます。

class Calculator:
    def __init__(self, root):
        self.root = root
        self.expression = ""  # 入力式を保持

BTUTONSリスト:データドリブンなボタン生成

ボタンの情報をリストで定義し、ループで一括生成しています。これにより各ボタンを個別にコーディングする手間が省けます。タプルの中身は(テキスト、行、列、colspan、背景色)です。

for (text, row, col, span, color) in self.BUTTONS:
    btn = tk.Button(root, text=text, ...)
    btn.grid(row=row, column=col, columnspan=span)

lambdaによるコマンドの束縛

forループ内でcommandを設定する際、lambda t=text: self.on_press(t) のように書きます。t=text はデフォルト引数で現在のtextの値をキャプチャします。lambda: self.on_press(text) ではすべてのボタンが最後のtextを参照してしまうので注意が必要です。

# 正しい書き方(デフォルト引数でキャプチャ)
command=lambda t=text: self.on_press(t)

# 間違い(すべて最後の値になる)
command=lambda: self.on_press(text)

Gridレイアウトの使い方

grid() は行・列でウィジェットを格子状に配置します。columnspan=2 で複数列にまたがる配置ができます(0ボタンは2列分)。sticky="nsew" でセルいっぱいに広げます。columnconfigure(weight=1) で列を均等に拡張します。

btn.grid(row=5, column=0, columnspan=2, sticky="nsew")
self.root.columnconfigure(0, weight=1)  # 均等拡張

eval()による式の評価とセキュリティ

eval()は文字列をPythonの式として評価します。"3+4*2" → 11 のように計算できます。ただし、eval()は任意のコードを実行できるため、外部からの入力を直接渡すのは危険です。このアプリでは数字と演算子のみを受け付けてeval()に渡しているため安全です。

# 安全な使用例(数値と演算子のみ)
result = eval("3+4*2")  # → 11

# 危険な使用例(絶対にやってはいけない)
# user_input = input()  # 悪意ある入力の可能性
# eval(user_input)

6. ステップバイステップガイド

このアプリをゼロから自分で作る手順を解説します。コードをコピーするだけでなく、実際に手順を追って自分で書いてみましょう。

  1. 1
    クラスの骨組みを作る

    Calculatorクラスを定義し、__init__メソッドにウィンドウ設定を記述します。

  2. 2
    表示エリア(ディスプレイ)を作る

    StringVarと紐付けたLabelで計算式・結果を表示するエリアを作ります。

  3. 3
    ボタンデータを定義する

    BTUTONSリストにボタン情報(テキスト・位置・色)を定義します。

  4. 4
    ボタンを一括生成する

    forループでBTUTONSを繰り返し、各ボタンウィジェットを生成・配置します。

  5. 5
    on_press()メソッドを実装する

    各ボタンが押されたときの処理(数字追加・クリア・計算)を実装します。

  6. 6
    キーボード入力を追加する

    bind()でキーボードの数字・演算子キーを電卓操作に対応させます。

  7. 7
    エラーハンドリングを追加する

    try-exceptでゼロ除算・不正入力を捕捉してエラーメッセージを表示します。

7. カスタマイズアイデア

基本機能を習得したら、以下のカスタマイズに挑戦してみましょう。少しずつ機能を追加することで、Pythonのスキルが飛躍的に向上します。

💡 科学計算機能を追加する

mathモジュールを使ってsin・cos・sqrt・log などの科学計算ボタンを追加できます。

import math
if char == "sin":
    result = math.sin(math.radians(float(self.expression)))

💡 計算履歴を記録する

計算結果をリストに保存し、ScrolledTextウィジェットで履歴一覧を表示する機能を追加しましょう。

💡 テーマを切り替えられるようにする

LIGHT_THEMEとDARK_THEMEの辞書を定義し、トグルボタンでテーマを切り替えられるようにしましょう。

8. よくある問題と解決法

❌ 計算結果が「3.0」のように表示される

原因:Python除算は常にfloatを返します。

解決法:is_integer()で整数かチェックしてint()に変換します。

if isinstance(result, float) and result.is_integer():
    result = int(result)

❌ キーボード入力が効かない

原因:tkinterのbind()では、"+""*"などの記号をそのまま使うことができず、keysym名を指定する必要があります。

解決法:記号の代わりにkeysym名("plus""minus""asterisk""slash")を使い、数字は"Key-0""Key-9"形式で指定してください。

# NG: 記号をそのまま使う
self.root.bind("<+>", ...)   # 動作しない

# OK: keysym名を使う
self.root.bind("<plus>", ...)    # +
self.root.bind("<asterisk>", ...)  # *
self.root.bind("<Key-1>", ...)    # 数字1

9. 練習問題

アプリの理解を深めるための練習問題です。難易度順に挑戦してみてください。

  1. 課題1:バックスペース機能

    表示中の数式の末尾1文字を削除するバックスペースボタンを実装してください。

  2. 課題2:メモリ機能

    M+(メモリに加算)・M-(メモリから減算)・MR(メモリ呼び出し)・MC(メモリクリア)の4つのメモリ機能ボタンを追加してください。

  3. 課題3:連続計算

    「3 + 4 = 7」の後にさらに「+ 2 =」と打つと「9」になる連続計算機能を実装してください。

🚀
次に挑戦するアプリ

電卓の次は、複数の入力フォームを使った温度変換アプリに挑戦しましょう。