初心者向け No.052

割り勘計算アプリ

飲食代・人数・チップ率を入力して一人当たりの支払い金額を計算するアプリ。Scaleウィジェットとリアルタイム計算を学ぶ。

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

1. アプリ概要

飲食代・人数・チップ率を入力して一人当たりの支払い金額を計算するアプリ。Scaleウィジェットとリアルタイム計算を学ぶ。

このアプリはtoolsカテゴリの実践的なPythonアプリです。使用ライブラリは tkinter、難易度は ★☆☆ 易しい です。

このアプリは「ツール」カテゴリです。日々の作業を自動化する実用ツールは Python が最も得意とする領域です。短いコードで実用性のある成果物が作れる点が魅力です。tkinter を活かして実装するこの構造は、他のアプリにも応用が効きます。

動かしながら読むことが理解の最短経路です。まずはコードをコピーして実行し、想定どおりに動くことを確認したうえで解説と照らし合わせてください。

カスタマイズでは「機能追加」「UI 改善」「エラー耐性」の三方向で考えると視野が広がります。練習問題にもそれぞれの方向の具体例を用意しています。

割り勘計算アプリ 実行画面(Windows)
実行画面(Windows)
割り勘計算アプリ 実行画面(Linux Mint)
実行画面(Linux Mint)

2. 機能一覧

  • ウィンドウはタイトル「割り勘計算アプリ」・サイズ 600x500 で起動
  • 飲食代の合計を ttk.Entry に入力(初期値 10000 円)。StringVartrace_add("write", ...) により、1文字入力するたびに計算結果が更新される
  • 人数を ttk.Scale で 1〜20 人(初期値 4 人)、チップ率を ttk.Scale で 0〜30 %(初期値 0 %)から指定
  • チップ率のプリセットボタン 0 % / 5 % / 10 % / 15 % / 20 %TIP_PRESETS からループで生成。押すとスライダーの位置と計算結果が同時に切り替わる
  • 「合計(チップ込み)」「うちチップ」「一人あたり」の3つを常に表示(表示は1円単位に丸め、内部の計算は小数のまま)
  • 金額が数値として読めないときは「入力エラー」、マイナスのときは「金額が不正」と表示して計算を止める

3. 事前準備・環境

ℹ️
動作確認環境

Python 3.10 以上 / Windows 11(実機)で起動・基本操作を確認 / Linux Mint 22.3(仮想マシン)で起動・画面表示を確認(macOS は未検証)

Windows 11(実機)で起動と基本操作を確認しています(全機能の網羅テストではありません)。Linux Mint 22.3(仮想マシン)では起動と画面表示を確認しました。macOS は標準ライブラリの範囲で動作する想定ですが、未検証です。

  • Python 3.10 以上
  • OS: Windows 11(実機で起動・基本操作を確認)・Linux Mint 22.3(仮想マシンで起動・画面表示を確認)

4. 完全なソースコード

💡
コードのコピー方法

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

追加インストール不要(標準ライブラリのみ使用)
app052.py
import tkinter as tk
from tkinter import ttk, messagebox

# チップ率のプリセット(%)。ボタンをループで作るときの元データ
TIP_PRESETS = (0, 5, 10, 15, 20)


class App052:
    """割り勘計算アプリ: 金額・人数・チップ率から1人あたりの金額を算出"""

    def __init__(self, root):
        self.root = root
        self.root.title("割り勘計算アプリ")
        self.root.geometry("600x500")
        self.root.configure(bg="#f8f9fc")
        self._build_ui()
        self._calculate()  # 初期表示

    def _build_ui(self):
        """UI を構築する"""
        # タイトル
        title_frame = tk.Frame(self.root, bg="#3776ab", pady=12)
        title_frame.pack(fill=tk.X)
        tk.Label(
            title_frame, text="割り勘計算アプリ",
            font=("Noto Sans JP", 16, "bold"),
            bg="#3776ab", fg="white"
        ).pack()

        main = tk.Frame(self.root, bg="#f8f9fc", padx=20, pady=20)
        main.pack(fill=tk.BOTH, expand=True)

        # 入力フォーム
        form = ttk.LabelFrame(main, text="入力", padding=12)
        form.pack(fill=tk.X)

        ttk.Label(form, text="飲食代の合計 (円)").grid(row=0, column=0, sticky="w", pady=4)
        self.amount_var = tk.StringVar(value="10000")
        self.amount_entry = ttk.Entry(form, textvariable=self.amount_var, width=15)
        self.amount_entry.grid(row=0, column=1, sticky="w", padx=8)
        self.amount_var.trace_add("write", lambda *a: self._calculate())

        ttk.Label(form, text="人数").grid(row=1, column=0, sticky="w", pady=4)
        self.people_var = tk.IntVar(value=4)
        self.people_scale = ttk.Scale(
            form, from_=1, to=20, orient=tk.HORIZONTAL,
            variable=self.people_var, command=lambda v: self._on_scale()
        )
        self.people_scale.grid(row=1, column=1, sticky="ew", padx=8)
        self.people_label = ttk.Label(form, text="4 人")
        self.people_label.grid(row=1, column=2, sticky="w")

        ttk.Label(form, text="チップ率 (%)").grid(row=2, column=0, sticky="w", pady=4)
        self.tip_var = tk.DoubleVar(value=0)
        self.tip_scale = ttk.Scale(
            form, from_=0, to=30, orient=tk.HORIZONTAL,
            variable=self.tip_var, command=lambda v: self._on_scale()
        )
        self.tip_scale.grid(row=2, column=1, sticky="ew", padx=8)
        self.tip_label = ttk.Label(form, text="0 %")
        self.tip_label.grid(row=2, column=2, sticky="w")

        # チップ率のプリセットボタン(ループで生成)
        ttk.Label(form, text="よく使うチップ率").grid(row=3, column=0, sticky="w", pady=4)
        preset = ttk.Frame(form)
        preset.grid(row=3, column=1, sticky="w", padx=8)
        for rate in TIP_PRESETS:
            # 既定引数 r=rate でその場の値を束縛する(ここを lambda: だけにすると全ボタンが 20% になる)
            ttk.Button(
                preset, text=f"{rate}%", width=5,
                command=lambda r=rate: self._set_tip(r)
            ).pack(side=tk.LEFT, padx=(0, 4))

        form.columnconfigure(1, weight=1)

        # 結果表示
        result = ttk.LabelFrame(main, text="計算結果", padding=12)
        result.pack(fill=tk.BOTH, expand=True, pady=(12, 0))
        self.total_var = tk.StringVar(value="-")
        self.per_var = tk.StringVar(value="-")
        self.tip_amt_var = tk.StringVar(value="-")
        ttk.Label(result, text="合計 (チップ込み):").grid(row=0, column=0, sticky="w", pady=4)
        ttk.Label(result, textvariable=self.total_var, font=("Noto Sans JP", 12, "bold")).grid(row=0, column=1, sticky="w")
        ttk.Label(result, text="うちチップ:").grid(row=1, column=0, sticky="w", pady=4)
        ttk.Label(result, textvariable=self.tip_amt_var).grid(row=1, column=1, sticky="w")
        ttk.Label(result, text="一人あたり:").grid(row=2, column=0, sticky="w", pady=4)
        ttk.Label(result, textvariable=self.per_var, font=("Noto Sans JP", 14, "bold"), foreground="#3776ab").grid(row=2, column=1, sticky="w")

    def _set_tip(self, rate):
        """プリセットボタンで押された率をチップ率に反映する"""
        self.tip_var.set(rate)
        self._on_scale()

    def _on_scale(self):
        """Scale 変更ハンドラ: ラベル更新と再計算"""
        self.people_label.config(text=f"{int(self.people_var.get())} 人")
        self.tip_label.config(text=f"{int(self.tip_var.get())} %")
        self._calculate()

    def _calculate(self):
        """金額を計算して結果を更新する"""
        try:
            amount = float(self.amount_var.get())
        except ValueError:
            self.total_var.set("入力エラー")
            self.per_var.set("-")
            self.tip_amt_var.set("-")
            return
        if amount < 0:
            self.total_var.set("金額が不正")
            self.per_var.set("-")
            self.tip_amt_var.set("-")
            return
        people = max(1, int(self.people_var.get()))
        tip_pct = float(self.tip_var.get())
        tip_amt = amount * tip_pct / 100
        total = amount + tip_amt
        per = total / people
        self.total_var.set(f"{total:,.0f} 円")
        self.tip_amt_var.set(f"{tip_amt:,.0f} 円 ({tip_pct:.0f}%)")
        self.per_var.set(f"{per:,.0f} 円 / 人")


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

5. コード解説

割り勘計算アプリ: 金額・人数・チップ率から1人あたりの金額を算出のコードを、実際に書かれている実装に沿って解説します。

クラス設計とコンストラクタ

App052 クラスにアプリの全機能をまとめています(メソッド5個・全127行)。__init__ ではタイトル「割り勘計算アプリ」とウィンドウサイズ 600x500 を設定します。そのあと _build_ui()_calculate() の順に呼び出して初期状態を作ります。

※ 該当部分のコード本体は 「4. 完全なソースコード」 をご参照ください(重複表示を避けるため再掲を省略しています)。

ウィジェット構成

画面は tk.Frame×2・tk.Labelttk.LabelFrame×2・ttk.Label×12・ttk.Entryttk.Scale×2・ttk.Framettk.Button(ループで生成) で構成しています。見出し付きの枠(LabelFrame)は「入力」「計算結果」のラベルでエリアを区切っています。

※ 該当部分のコード本体は 「4. 完全なソースコード」 をご参照ください(重複表示を避けるため再掲を省略しています)。

イベント処理とボタンの接続

ボタンはループ内で command=lambda r=rate: self._set_tip(r) の形で生成しています。既定引数でループ変数を固定するのがポイントで、これを書かないと全ボタンが最後の値で動いてしまいます。

※ 該当部分のコード本体は 「4. 完全なソースコード」 をご参照ください(重複表示を避けるため再掲を省略しています)。

ループで作るボタンに lambda を渡すときの落とし穴

for rate in TIP_PRESETS: の中で command=lambda: self._set_tip(rate) と書くと、5個のボタンはすべて 20 % として動きます。lambda の中の rate が読まれるのはボタンを作った瞬間ではなく、ボタンが押された瞬間だからです。押された時点でループはとっくに終わっていて、rate には最後の値 20 が入ったまま残っています(遅延束縛=late binding)。

これを避けるのが lambda r=rate: という書き方です。既定引数は関数を作るときに評価されるため、その回の rate がボタンごとに固定されます。rate をそのまま参照する形に書き換えた app052.py を用意して5個のボタンを順に押し、そのつど tip_var の値を記録したところ、結果は [20, 20, 20, 20, 20]。掲載コードのまま(lambda r=rate:)で同じ手順を踏むと [0, 5, 10, 15, 20] でした(Python 3.12.10・Windows 11 で確認)。

例外処理

try-except で ValueError を捕捉しています。

※ 該当部分のコード本体は 「4. 完全なソースコード」 をご参照ください(重複表示を避けるため再掲を省略しています)。

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

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

  1. 1
    ファイルを作成する

    新しいファイルを作成して app052.py と保存します。使うのは tkinter だけなので、追加インストールは不要です。

  2. 2
    クラスの骨格を作る

    App052 クラスを定義し、__init__ と、末尾の root = tk.Tk()mainloop() の最小構成を書いて、まず空のウィンドウが出ることを確認します。

  3. 3
    ウィンドウを設定する

    title("割り勘計算アプリ")geometry("600x500")configure(bg="#f8f9fc") をコンストラクタで設定します。

  4. 4
    画面部品を並べる

    _build_ui() の中で、tk.Frame×2・tk.Labelttk.LabelFrame×2・ttk.Label×12・ttk.Entryttk.Scale×2・ttk.Framettk.Button(ループで生成) を作って配置します(掲載コードと同じ並び順で書くとレイアウトが一致します)。まず表示だけ確認しましょう。

  5. 5
    イベントを接続する

    ループ生成のボタンは command=lambda r=rate: self._set_tip(r) の形で接続します。

  6. 6
    中心になるメソッドを実装する

    アプリの本体である _set_tip()(4行)・_calculate()(22行)・_on_scale()(5行) を実装します。

  7. 7
    動作確認する

    python app052.py で起動し、各ボタンが反応することを確認します。

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

基本機能を習得したら、以下のカスタマイズに挑戦してみましょう。

💡 ダークモードを追加する

bg色・fg色を辞書で管理し、ボタン1つでダークモード・ライトモードを切り替えられるようにしましょう。

💡 結果をファイルに保存する

このコードに保存処理はないため、表示中の数値や結果はアプリを閉じると消えます。テキストファイルへ書き出して、次回起動時に読み込む機能を追加してみましょう。

💡 設定ダイアログ

フォントサイズや色などの設定をユーザーが変更できるオプションダイアログを追加しましょう。

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

❌ 文字のフォントが崩れる・見た目が違う

原因:このコードは font=("Noto Sans JP", ...) のようにフォント名を直接指定しています。環境にこのフォントが入っていないと、OS が代わりのフォントで表示するため、見本と見た目が変わることがあります。

解決法:動作には支障ありません。気になる場合は font 引数の名前を自分の OS に入っているフォントへ書き換えるか、font 引数を省略してください。

❌ 写経したら全部のボタンが同じ動きになる

原因:ループでボタンを作るとき、このコードは command=lambda r=rate: self._set_tip(r) のように既定引数でループ変数を固定しています。これを省略してループ変数を直接書くと、ループ終了後の最後の値が全ボタンで共有されてしまいます(Python のクロージャの有名な罠です)。

解決法:掲載コードのとおり、lambda の既定引数でその時点の値を渡してください。

❌ ウィンドウの大きさが画面に合わない

原因:geometry("600x500") の固定サイズで起動するためです。

解決法:この数値を書き換えて起動サイズを調整してください。なお、このコードにはウィンドウサイズを固定する設定(resizable の指定)が無いため、ウィンドウの端をドラッグしたサイズ変更は既定どおり可能です。

9. 練習問題

アプリの理解を深めるための練習問題です。気になるものから挑戦してみてください(課題3は定型の発展課題です)。どちらの課題も、起動時に入っている金額とスライダーだけで確かめられます(入力した数字はどこにも保存されません)。

  1. 課題1:一人あたりの金額を切り上げにする

    いまの一人あたりは四捨五入なので、割り切れないときは全員から集めても合計に届きません。切り上げにして、集めた額が不足しないようにしましょう。

    期待結果:飲食代 10000 円・人数 3 人・チップ率 0 % のとき「一人あたり: 3,334 円 / 人」になる。いまは「3,333 円 / 人」で、3人から集めても 9,999 円にしかならない

    合格条件

    • 割り切れるとき(10000 円・4人)は「2,500 円 / 人」のまま変わらない
    • 「合計 (チップ込み)」と「うちチップ」の表示は変えない
    • スライダーを動かすと今までどおりすぐに計算し直される

    ヒント①(どこを触るか): 触るのは2か所です。ファイル冒頭の import 行と、_calculate()per = total / people の1行です。
    ヒント②(使うもの): 切り上げには math.ceil() を使います。このファイルは math を読み込んでいないので、2行目の from tkinter import ttk, messagebox の下に import math を1行足してから per = math.ceil(total / people) にします。表示の f"{per:,.0f} 円 / 人" はそのままで動きます。
    つまずきやすい点: round() は四捨五入ではなく偶数側へ寄せる丸めなので、round(2.5)3 ではなく 2 になります。表示に使っている {per:,.0f} も同じ丸め方で、書式だけ変えても不足は直りません。なお切り上げると集めた額は合計をわずかに上回ります(10000 円を3人なら 10,002 円)。

  2. 課題2:金額が空のときは「入力エラー」を出さない

    いま飲食代の欄をすべて消すと「入力エラー」と出ます。まだ何も打っていないだけなので、エラー扱いではなく「-」に戻しましょう。

    期待結果:飲食代の欄を空にすると「合計 (チップ込み): -」「うちチップ: -」「一人あたり: -」の3つが並ぶ。いまは合計の欄だけ「入力エラー」と出る

    合格条件

    • abc のように数字でない文字を入れたときは、今までどおり「入力エラー」と出る
    • 数字を打ち直すとすぐに計算結果が戻る
    • 空白キーだけを打った状態でも「-」になる

    ヒント①(どこを触るか): 触るのは1か所です。_calculate() の最初の try: の手前です。
    ヒント②(使うもの): raw = self.amount_var.get().strip() を取り出し、if not raw: のときは3つの表示をすべて "-" にして return します。float() に渡すのはそのあとです。
    つまずきやすい点: 金額欄は StringVartrace_add("write", lambda *a: self._calculate()) につながっていて、1文字消すたびに _calculate() が呼ばれます。.strip() を付けずに if not self.amount_var.get(): とだけ書くと、空白キーだけの状態は空と見なされず float(" ")ValueError になって「入力エラー」が出ます。

  3. 課題3:保存機能の追加

    処理結果をファイルに保存する機能を追加しましょう。

🚀
次に挑戦するアプリ

このアプリをマスターしたら、次のアプリに挑戦しましょう。

🐛
エラーが出て動かないときは

写経中に赤いエラー文が出たら、Pythonエラー一覧&解決法(英語メッセージ逆引き)で原因と直し方をすぐ確認できます。

📖
独学を1冊で体系化するなら

写経しながら「なぜこう書くのか」が気になり始めたら、入門書を1冊通して読むと断片的な知識がつながります。Python入門書のおすすめ2冊(当サイトの参考書ランキング総合1〜2位)で、独学者向けの最初の1冊を比較しています。