本稿では、Pythonの基礎だけでWindows向けの簡易電卓アプリを作ります。GUIは標準ライブラリのtkinter、四則演算の計算はast(抽象構文木)で式を解析して安全寄りに評価します。外部ライブラリは使いません。
「ウィンドウを開く」「ボタンを並べる」「イベント(クリックやキー入力)で関数を呼ぶ」といったGUIアプリの基礎を、できるだけ噛み砕いて説明します。最後にPyInstallerでEXE化して単体配布する手順も紹介します。
1. なぜtkinter?なぜ“基礎だけ”?
tkinterはPythonに同梱の標準GUIツールキットで、追加インストールなしに使えます。Windowsでも動作し(macOS/Linuxでも同様)、小規模なユーティリティや学習には最適です。tkinterはTcl/TkへのPythonインターフェイスで、各ウィジェット(ボタン・ラベル・入力欄など)はPythonのクラスとして提供されます。
GUIの配置には「geometry manager」を使い、本稿ではグリッド(grid)で電卓のボタンを盤面のように配置します。gridは行×列の考え方で、電卓のUIと相性が良い幾何管理手法です。(参考:tkinter公式、Tkの概念とgrid概要)。※詳細は後掲の出典を参照
- 標準装備:追加の依存がなく学習に集中できる
- シンプル:少ないコード量で動く=読みやすい
- 移植性:Windows/macOS/Linuxで同様に動く
「Pythonの基礎だけで」という条件は、逆に言えば「言語本体と標準機能を使って設計・分解・実装する練習」に最適です。電卓は、入力・表示・状態管理・演算というGUIアプリのコアをすべて含み、学習題材としてちょうど良いサイズ感です。
2. アプリ設計(最小にして実用)
今回の電卓は以下の要件を満たします。
- 表示:現在の式と結果を1行表示(右寄せ、等号で確定)
- 入力:数字・小数点・四則演算(+−×÷)、括弧、%(百分率)、±(符号反転)
- 編集:C(クリア)、⌫(バックスペース)
- 評価:式を安全寄りに評価(
ast
で許可ノードだけを再帰評価) - 操作:ボタン+キーボード(Enter==、Backspace=⌫、Esc=C など)
設計のポイントは「表示用の文字列」と「内部評価用の式」を分けることです。ボタンには見やすい記号(×、÷、−)を出しつつ、内部ではPython演算子(*
、/
、-
)に正規化して保存・評価します。UIの見やすさと実装の簡潔さを両立できます。
3. tkinter超入門(必要最小の基礎)
- ウィンドウ生成:
root = tkinter.Tk()
でルートウィンドウを作る。root.title
やroot.geometry
で見た目を調整。 - ウィジェット:
Label
(表示)、Button
(ボタン)、Entry
(入力欄)など。今回の表示はEntry
かLabel
でOK。右寄せやフォント設定はオプション引数で。 - 配置:
grid(row=i, column=j)
で盤面に配置。行列のweight
を設定するとリサイズ時に伸縮する。 - イベント:ボタンの
command=関数
でクリック時に処理。root.bind('<Return>', func)
でキー入力も拾える。
tkinterはPython標準のGUIで、TkのウィジェットをPythonクラスでラップしたものです。詳細は公式ドキュメント(tkinter・tk)やTkDocsのgridチュートリアルがまとまっています。(出典は末尾)
4. 数式評価を“安全寄り”にする考え方
ユーザー入力をそのままeval()
するのは危険です。本稿ではast
で式をパースし、許可したノード(演算子・数値・括弧・単項符号)だけを再帰的に評価します。こうすることで、関数呼び出しや属性アクセスなどを遮断し、四則演算+数値の範囲に制限できます。必要十分な範囲で「安全寄り」と言える実装です(完全なサンドボックスではない点は念のため)。
5. まずは完成コード(コメント付き)
以下を calculator_tk.py
として保存してください。WordPressでは<pre><code class="language-python">
で貼れば見やすく表示されます。
# Day55: 標準ライブラリだけで作るWindows電卓(tkinter + ast)
# ---------------------------------------------------------------
# 依存: Python 3.x(tkinterは標準同梱)
# ・GUI: tkinter
# ・式評価: ast(許可ノードのみ再帰評価)
# ・対応: +, -, *, /, //, %, **, 括弧, 単項+-, 小数点、C, ⌫, %, +/-
# ・操作: ボタン + キーボード(Enter, Backspace, Esc)
# ---------------------------------------------------------------
import tkinter as tk # GUI本体(標準)
from tkinter import ttk # 見た目の良いウィジェット群(標準)
import ast # 抽象構文木:evalの代わりに安全寄り評価
import operator as op
# 許可する二項演算子を辞書化(astノード -> Pythonの演算関数)
BIN_OPS = {
ast.Add: op.add,
ast.Sub: op.sub,
ast.Mult: op.mul,
ast.Div: op.truediv,
ast.FloorDiv: op.floordiv,
ast.Mod: op.mod,
ast.Pow: op.pow,
}
# 許可する単項演算子
UNARY_OPS = {
ast.UAdd: lambda x: +x,
ast.USub: lambda x: -x,
}
def safe_eval(expr: str) -> float:
"""astで式を解析し、許可ノードだけを再帰評価(四則演算+一部拡張)。"""
# 空や不正末尾は弾く(例: "1+")
if not expr or expr.strip() == "":
return 0.0
node = ast.parse(expr, mode="eval") # 式モードでパース
return _eval_ast(node.body)
def _eval_ast(node):
"""許可ノードのみを再帰評価。その他は例外。"""
if isinstance(node, ast.BinOp) and type(node.op) in BIN_OPS:
left = _eval_ast(node.left)
right = _eval_ast(node.right)
return BIN_OPS[type(node.op)](left, right)
if isinstance(node, ast.UnaryOp) and type(node.op) in UNARY_OPS:
operand = _eval_ast(node.operand)
return UNARY_OPS[type(node.op)](operand)
# Python 3.8+ 数値リテラルは Constant、古い版だと Num
if isinstance(node, ast.Constant) and isinstance(node.value, (int, float)):
return node.value
if hasattr(ast, "Num") and isinstance(node, ast.Num): # 互換
return node.n
if isinstance(node, ast.Expression):
return _eval_ast(node.body)
if isinstance(node, ast.Expr):
return _eval_ast(node.value)
# 許可しないもの(関数呼び出し、名前参照など)は弾く
raise ValueError("許可されていない式、または構文です。")
class CalculatorApp:
"""シンプル電卓本体。表示は人に優しく、内部はPython演算子で保持。"""
def __init__(self, root: tk.Tk):
self.root = root
self.root.title("電卓(tkinter版)")
self.root.geometry("320x420")
self.root.minsize(280, 380)
# 表示用と内部用の式
self.display_var = tk.StringVar(value="0")
self.internal_expr = "" # 内部評価用(*, /, - などに正規化)
# スタイル(ttk)
style = ttk.Style(self.root)
style.theme_use(style.theme_use()) # 既定テーマ
style.configure("Display.TEntry", font=("Segoe UI", 20))
style.configure("Calc.TButton", font=("Segoe UI", 14), padding=6)
self._build_ui()
self._bind_keys()
def _build_ui(self):
# 上部:表示(Entryを右寄せ・読み取り専用風)
entry = ttk.Entry(self.root, textvariable=self.display_var,
justify="right", style="Display.TEntry")
entry.state(["readonly"]) # 入力はボタン/キーからのみ
entry.grid(row=0, column=0, columnspan=4, sticky="nsew", padx=8, pady=(12, 8))
# ボタン定義(表示ラベル, 内部トークン, ハンドラ)
buttons = [
[("C", "C", self.clear), ("⌫", "BACK", self.backspace), ("%", "%", self.percent), ("÷", "/", self.input_op)],
[("7", "7", self.input_char), ("8", "8", self.input_char), ("9", "9", self.input_char), ("×", "*", self.input_op)],
[("4", "4", self.input_char), ("5", "5", self.input_char), ("6", "6", self.input_char), ("−", "-", self.input_op)],
[("1", "1", self.input_char), ("2", "2", self.input_char), ("3", "3", self.input_char), ("+", "+", self.input_op)],
[("+/−", "NEG", self.negate), ("0", "0", self.input_char), (".", ".", self.input_dot), ("=", "=", self.equals)],
]
# 配置
for r, row in enumerate(buttons, start=1):
for c, (label, token, handler) in enumerate(row):
btn = ttk.Button(self.root, text=label, style="Calc.TButton",
command=lambda t=token, h=handler: h(t))
btn.grid(row=r, column=c, sticky="nsew", padx=4, pady=4)
# 伸縮設定:表示・ボタンがリサイズに追従
for i in range(4):
self.root.columnconfigure(i, weight=1, uniform="col")
for i in range(6): # 0行は表示、1〜5行はボタン
self.root.rowconfigure(i, weight=1)
def _bind_keys(self):
# 数字・演算子・括弧・ドット
for ch in "0123456789()+-*/.%":
self.root.bind(ch, self._on_key)
# Enter=計算、BackSpace=⌫、Escape=クリア
self.root.bind("<Return>", lambda e: self.equals("="))
self.root.bind("<KP_Enter>", lambda e: self.equals("="))
self.root.bind("<BackSpace>", lambda e: self.backspace("BACK"))
self.root.bind("<Escape>", lambda e: self.clear("C"))
# ----------------- 入力系ハンドラ -----------------
def input_char(self, ch: str):
"""数字入力など:内部式と表示を更新"""
self._append(ch)
def input_op(self, opch: str):
"""演算子入力:直前が演算子なら置換、数字なら追加"""
if not self.internal_expr:
# 先頭に演算子は原則不可(ただし単項-はNEGで対応)
if opch in "+*/%":
return
if self.internal_expr and self.internal_expr[-1] in "+-*/%.":
self.internal_expr = self.internal_expr[:-1] + opch
else:
self.internal_expr += opch
self._sync_display()
def input_dot(self, _):
"""小数点:直前が演算子or空なら '0.' を挿入、数値中の2重ドットは抑止"""
if not self.internal_expr or self.internal_expr[-1] in "+-*/%(":
self._append("0.")
return
# 直近の数値に既に '.' が含まれていないかチェック
last = self._last_number()
if "." in last:
return
self._append(".")
def negate(self, _):
"""±:直近の数値の符号を反転"""
if not self.internal_expr:
return
idx, num = self._last_number(with_index=True)
if not num:
return
if num.startswith("-"):
new = num[1:] # マイナスを外す
else:
new = "-" + num
self.internal_expr = self.internal_expr[:idx] + new + self.internal_expr[idx + len(num):]
self._sync_display()
def percent(self, _):
"""%:直近の数値を 1/100 に(例:12 → 0.12)"""
if not self.internal_expr:
return
idx, num = self._last_number(with_index=True)
if not num:
return
try:
val = float(num) / 100.0
except Exception:
return
new = str(val)
self.internal_expr = self.internal_expr[:idx] + new + self.internal_expr[idx + len(num):]
self._sync_display()
def backspace(self, _):
"""⌫:最後の1文字を削除"""
if not self.internal_expr:
return
self.internal_expr = self.internal_expr[:-1]
self._sync_display(default_zero=True)
def clear(self, _):
"""C:全クリア"""
self.internal_expr = ""
self.display_var.set("0")
def equals(self, _):
"""=:安全寄り評価で計算して表示に反映"""
if not self.internal_expr:
return
try:
# 括弧のバランスや末尾演算子チェックは ast が構文エラーで検出
result = safe_eval(self.internal_expr)
# 表示は整数っぽければ小数点を省く
if float(result).is_integer():
self.internal_expr = str(int(result))
else:
self.internal_expr = str(result)
self._sync_display()
except ZeroDivisionError:
self.display_var.set("エラー(0除算)")
self.internal_expr = ""
except Exception:
self.display_var.set("エラー(式が不正)")
self.internal_expr = ""
def _on_key(self, event):
ch = event.char
if ch in "0123456789()":
self._append(ch)
elif ch in "+-*/%":
self.input_op(ch)
elif ch == ".":
self.input_dot(".")
# それ以外は無視
# ----------------- 内部ユーティリティ -----------------
def _append(self, token: str):
self.internal_expr += token
self._sync_display()
def _sync_display(self, default_zero=False):
"""内部式 → 表示。見やすさのため ×÷− の置換はここでも可能。"""
disp = self.internal_expr
# 表示だけ ×, ÷ に置き換えたい場合は以下を有効化:
disp = disp.replace("*", "×").replace("/", "÷")
if default_zero and not disp:
disp = "0"
self.display_var.set(disp if disp else "0")
def _last_number(self, with_index=False):
"""内部式の末尾から連続する数値部分を抽出。index付き返却も可。"""
s = self.internal_expr
if not s:
return ("", "") if with_index else ""
i = len(s) - 1
# 数値(0-9, ., 先頭の-)を逆走査
while i >= 0 and (s[i].isdigit() or s[i] == "."):
i -= 1
# 単項マイナスに対応(演算子や括弧の直後の '-' は数値に含める)
if i >= 0 and s[i] == "-" and (i == 0 or s[i-1] in "+-*/%("):
i -= 1
num = s[i+1:]
return (i+1, num) if with_index else num
if __name__ == "__main__":
root = tk.Tk()
app = CalculatorApp(root)
root.mainloop()
試し方:保存後、コマンドラインで python calculator_tk.py
を実行。ウィンドウが開けば成功です。数字や演算子ボタンのほか、キーボードの数字・+
-
*
/
%
.
、Enter(=)、Backspace(⌫)、Esc(C)が使えます。
6. コードの読み方(要点を一気に掴む)
- 安全寄り評価:
safe_eval()
はast.parse(..., mode="eval")
の結果について、二項演算(+
-
*
/
//
%
**
)と単項演算(+/-
)と数値リテラル(int/float
)だけを再帰評価。関数呼び出しや名前参照は例外にします。 - UIとロジックの分離:表示テキストは
StringVar
で束縛、内部の式文字列は別に保持。見た目(×、÷など)と内部ロジック(*
、/
)の役割を分けて実装を簡潔に。 - grid配置:行列でボタンを並べ、
columnconfigure/rowconfigure
のweight
を設定してリサイズ対応。 - イベント:ボタンの
command
と、root.bind()
でキー入力を拾う二系統。 - ±・%:末尾の数値だけを取り出して加工。全体の式構造は崩さないのがコツ。
7. よくあるつまずき(トラブル対処)
- 「式が不正」エラーが出る:末尾が演算子、括弧の開閉不一致、小数点の重複など。構文エラーは
ast.parse
が検出します。 - 0除算:例外をキャッチして「エラー(0除算)」を表示。アプリが落ちないよう防御。
- 小数点が二度入る:直近の数値を検査し、既に
.
があれば無視。 - 演算子が連続する:直前が演算子なら置換にするなどの「簡易矯正」を実装。
8. EXE化して配布(任意)
Python未インストールのPCでも動かしたい場合、PyInstallerで単一実行ファイル化できます(Windowsでビルド)。
pip install pyinstaller
pyinstaller --noconsole --onefile calculator_tk.py
完了すると dist/calculator_tk.exe
が生成されます。必要に応じて --icon=icon.ico
を追加し、アイコンを設定しましょう。PyInstallerはクロスコンパイラではないため、Windows版EXEはWindows上で作る必要がある点に注意してください。
9. ここまでで学べるPython基礎
- 関数・クラス:役割ごとに関数化、UIはクラスで構造化
- 標準ライブラリ:tkinterとastを組み合わせて「安全寄りの電卓」
- イベント駆動:GUIは「押されたら動く」コールバック設計
- 例外処理:ユーザー入力は必ず失敗し得る=落ちないコードが重要
10. 発展アイデア
- 履歴(Tape)パネルを追加して、計算過程をスクロール表示
- 関数キー:
sqrt
、sin
など数学関数(許可関数だけ追加し安全を担保) - テーマ切替(Light/Dark)、フォント拡大、アクセシビリティ配慮
- 単位変換(長さ・重さ・通貨)モードの追加
参考・出典
- tkinter 公式ドキュメント:Python標準のTkインターフェースの解説。レイアウトやウィジェットの基本がまとまっています。 :contentReference[oaicite:0]{index=0}
- TkDocs:gridの実践的チュートリアル(GUIの行×列レイアウトの基礎)。 :contentReference[oaicite:1]{index=1}
- ast モジュール 公式ドキュメント:抽象構文木を扱う標準モジュール。安全寄り評価のための基礎知識。 :contentReference[oaicite:2]{index=2}
- PyInstaller 公式:単体実行ファイル化の手順と注意点。 :contentReference[oaicite:3]{index=3}
以上で、標準ライブラリだけで作る電卓アプリの完成です。tkinterは“古典的”な手触りながら、学習と小規模ユーティリティには今も強力な選択肢です。まずはこの電卓で、PythonとGUIの「手応え」を掴んでください。慣れてきたら機能拡張やデザイン改善にも挑戦してみましょう。
コメント