初心者向け No.059

四則演算ドリル

ランダムな計算問題を出題して正誤判定・スコア表示するドリルアプリ。難易度設定と制限時間モード付き。

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

1. アプリ概要

ランダムな計算問題を出題して正誤判定・スコア表示するドリルアプリ。難易度設定と制限時間モード付き。

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

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

コピー&実行で動作確認 → コード解説で構造理解 → カスタマイズで応用、の 3 ステップで進めると、短時間で本質を押さえられます。

アプリを完成させた後は、自分の使い方に合わせて改造するのが学びを定着させる近道です。カスタマイズ章のアイデアを足がかりに、独自機能を一つ追加してみてください。

四則演算ドリル 実行画面(Windows)
実行画面(Windows)
四則演算ドリル 実行画面(Linux Mint)
実行画面(Linux Mint)

2. 機能一覧

  • 「スタート」ボタン(start())・「回答」ボタン(_answer())・「次の問題」ボタン(next_question())で操作
  • キー <Return> から _answer() を実行
  • after(500, ...) による500ミリ秒間隔の _tick() 呼び出し(画面の自動更新)
  • messagebox.showinfo() によるダイアログ通知
  • ウィンドウはタイトル「四則演算ドリル」・サイズ 560x460 で起動

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. 完全なソースコード

💡
コードのコピー方法

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

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


OPS = {
    "+": (operator.add, "+"),
    "-": (operator.sub, "-"),
    "×": (operator.mul, "×"),
    "÷": (operator.floordiv, "÷"),
}


class App059:
    """四則演算ドリル"""

    def __init__(self, root):
        self.root = root
        self.root.title("四則演算ドリル")
        self.root.geometry("560x460")
        self.root.configure(bg="#f8f9fc")
        self.score = 0
        self.total = 0
        self.start_ts = None
        self.time_limit = 60
        self.timed = False
        self.running = False
        self.current = None  # (a, op, b, ans)
        self._build_ui()

    def _build_ui(self):
        """UI を構築する"""
        title = tk.Frame(self.root, bg="#3776ab", pady=12)
        title.pack(fill=tk.X)
        tk.Label(title, text="四則演算ドリル", font=("Noto Sans JP", 16, "bold"),
                 bg="#3776ab", fg="white").pack()

        opt = ttk.LabelFrame(self.root, text="設定", padding=8)
        opt.pack(fill=tk.X, padx=15, pady=8)
        ttk.Label(opt, text="難易度:").grid(row=0, column=0, padx=4)
        self.level_var = tk.StringVar(value="ふつう")
        ttk.Combobox(opt, textvariable=self.level_var, state="readonly", width=10,
                     values=["かんたん", "ふつう", "むずかしい"]).grid(row=0, column=1, padx=4)
        ttk.Label(opt, text="演算:").grid(row=0, column=2, padx=4)
        self.op_vars = {}
        for i, sym in enumerate("+-×÷"):
            v = tk.IntVar(value=1)
            self.op_vars[sym] = v
            ttk.Checkbutton(opt, text=sym, variable=v).grid(row=0, column=3 + i, padx=2)
        self.timed_var = tk.IntVar(value=0)
        ttk.Checkbutton(opt, text="制限時間モード(60秒)",
                        variable=self.timed_var).grid(row=0, column=8, padx=10)

        score = tk.Frame(self.root, bg="#f8f9fc")
        score.pack(fill=tk.X, pady=4)
        self.score_label = tk.Label(score, text="正解 0 / 0",
                                    font=("Noto Sans JP", 12, "bold"),
                                    bg="#f8f9fc", fg="#3776ab")
        self.score_label.pack(side=tk.LEFT, padx=20)
        self.time_label = tk.Label(score, text="", bg="#f8f9fc",
                                   font=("Noto Sans JP", 12))
        self.time_label.pack(side=tk.RIGHT, padx=20)

        self.q_label = tk.Label(self.root, text="スタートを押してね",
                                font=("Noto Sans JP", 28, "bold"),
                                bg="white", relief=tk.GROOVE, pady=20)
        self.q_label.pack(fill=tk.X, padx=20, pady=10)

        self.entry = ttk.Entry(self.root, font=("Arial", 18),
                               justify="center", width=10)
        self.entry.pack()
        self.entry.bind("<Return>", lambda e: self._answer())

        btns = tk.Frame(self.root, bg="#f8f9fc")
        btns.pack(pady=10)
        ttk.Button(btns, text="スタート", command=self.start).pack(side=tk.LEFT, padx=4)
        ttk.Button(btns, text="回答", command=self._answer).pack(side=tk.LEFT, padx=4)
        ttk.Button(btns, text="次の問題", command=self.next_question).pack(side=tk.LEFT, padx=4)

        self.fb = tk.Label(self.root, text="", bg="#f8f9fc",
                          font=("Noto Sans JP", 12))
        self.fb.pack()

    def _range(self):
        """難易度に応じた数値範囲を返す"""
        level = self.level_var.get()
        if level == "かんたん":
            return 1, 10
        if level == "むずかしい":
            return 10, 99
        return 1, 30

    def _allowed_ops(self):
        """選択中の演算子記号一覧を返す"""
        ops = [s for s, v in self.op_vars.items() if v.get()]
        return ops or ["+"]

    def start(self):
        """ドリルを開始する"""
        self.score = 0
        self.total = 0
        self.score_label.config(text="正解 0 / 0")
        self.timed = bool(self.timed_var.get())
        self.start_ts = time.time()
        self.running = True
        self.next_question()
        if self.timed:
            self._tick()

    def next_question(self):
        """新しい問題を作成する"""
        if not self.running:
            self.start()
            return
        lo, hi = self._range()
        sym = random.choice(self._allowed_ops())
        op_func, _ = OPS[sym]
        a = random.randint(lo, hi)
        b = random.randint(lo, hi)
        if sym == "-" and a < b:
            a, b = b, a
        if sym == "÷":
            # 整数除算でも余りが出ないように調整
            b = max(1, b)
            ans = random.randint(lo, hi)
            a = b * ans
        ans = op_func(a, b)
        self.current = (a, sym, b, ans)
        self.q_label.config(text=f"{a} {sym} {b} = ?")
        self.entry.delete(0, tk.END)
        self.entry.focus_set()
        self.fb.config(text="")

    def _answer(self):
        """回答を判定する"""
        if not self.running or self.current is None:
            return
        try:
            user = int(self.entry.get().strip())
        except ValueError:
            self.fb.config(text="数値で入力してください", fg="red")
            return
        a, sym, b, ans = self.current
        self.total += 1
        if user == ans:
            self.score += 1
            self.fb.config(text="正解!", fg="green")
        else:
            self.fb.config(text=f"不正解。正解: {ans}", fg="red")
        self.score_label.config(text=f"正解 {self.score} / {self.total}")
        self.root.after(700, self.next_question)

    def _tick(self):
        """残り時間を更新する"""
        if not (self.running and self.timed):
            return
        remain = self.time_limit - int(time.time() - self.start_ts)
        if remain <= 0:
            self.running = False
            self.time_label.config(text="残り 0 秒")
            messagebox.showinfo("結果",
                                f"終了!\n正解: {self.score} / {self.total}")
            return
        self.time_label.config(text=f"残り {remain} 秒")
        self.root.after(500, self._tick)


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

5. コード解説

四則演算ドリルのコードを、実際に書かれている実装に沿って解説します。

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

App059 クラスにアプリの全機能をまとめています(メソッド8個・全173行)。__init__ ではタイトル「四則演算ドリル」とウィンドウサイズ 560x460 を設定します。あわせて状態を保持する変数(self.scoreself.totalself.start_tsself.time_limit)を初期化し、最後に _build_ui() で画面を組み立てます。

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

ウィジェット構成

画面は tk.Frame×3・tk.Label×5・ttk.LabelFramettk.Label×2・ttk.Comboboxttk.Checkbutton(ループで生成)・ttk.Entryttk.Button×3 で構成しています。見出し付きの枠(LabelFrame)は「設定」のラベルでエリアを区切っています。

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

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

「スタート」ボタン(start())・「回答」ボタン(_answer())・「次の問題」ボタン(next_question())を command= で接続しています。また bind("<Return>", ...) から _answer() を呼べるようにイベントも登録しています。

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

after() による定期処理

_tick() の末尾で after(500, ...) を呼び、500ミリ秒後に自分自身の再実行を予約しています。tkinter では while ループで待つと画面が固まるため、この「予約の連鎖」で定期処理を作るのが定石です。

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

例外処理

try-except で ValueError を捕捉しています。あわせて messagebox.showinfo() のダイアログで、ユーザーへの通知・確認を行います。

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

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

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

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

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

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

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

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

    title("四則演算ドリル")geometry("560x460")configure(bg="#f8f9fc") をコンストラクタで設定します。

  4. 4
    画面部品を並べる

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

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

    「2. 機能一覧」で挙げた各ボタンを command= で対応するメソッド(start()_answer()next_question())につなぎます。bind("<Return>", ...) の登録も忘れずに。

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

    アプリの本体である next_question()(23行)・_tick()(13行)・_answer()(18行)・start()(11行) を実装します。定期処理は after(500, ...) で自分自身を予約し続ける形にします。

  7. 7
    動作確認する

    python app059.py で起動し、各ボタンが反応すること・表示が自動で更新されること・キー操作(<Return>)が効くことを確認します。

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

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

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

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

💡 更新間隔を変えてみる

after(500, ...) の 500 が更新間隔(ミリ秒)です。大きくすると更新頻度が下がって CPU 負荷が軽くなり、小さくすると更新が細かくなります(表示の刻み幅より細かくしても見た目は変わりません)。数値を変えて違いを確かめてみましょう。

💡 設定ダイアログ

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

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

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

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

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

❌ 改造したら表示の更新が二重になった・速くなった

原因:_tick()after(500, ...) で自分自身を予約し続ける構造です。予約を止めずにもう一度開始経路を通ると、予約の連鎖が2本になり、更新が倍速になります。

解決法:開始処理の前に動作中かどうかをフラグで判定してください。

<Return> キーを押しても反応しない

原因:bind("<Return>", ...) はウィジェット単位で登録されるため、ウィンドウ(またはバインド先)にキーボードフォーカスが無いとイベントが届きません。

解決法:一度ウィンドウ内をクリックしてフォーカスを与えてから、キーを押してください。

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

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

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

9. 練習問題

アプリの理解を深めるための練習問題です。気になるものから挑戦してみてください(課題3は定型の発展課題です)。どちらの課題も新しいライブラリは要りません(設定の「制限時間モード(60秒)」にチェックを入れて確かめます)。

  1. 課題1:残り10秒を切ったら時間表示を赤くする

    制限時間モードの残り時間は、いつも同じ色で表示されます。終わりが近いことが目で分かるように、残り10秒から色を変えましょう。

    期待結果:「制限時間モード(60秒)」にチェックを入れてスタートする。「残り 11 秒」までは今までの色のままで、「残り 10 秒」に変わった時点から文字が赤くなる。

    合格条件

    • 残り11秒以上のあいだは赤くならない
    • 残り10秒から0秒までは赤いままになる
    • もう一度スタートすると、最初の「残り 60 秒」は赤くない

    ヒント①(どこを触るか): 触るのは1か所です。_tick()self.time_label.config(text=f"残り {remain} 秒") です。remain は同じ _tick() の中で先に計算されています。
    ヒント②(使うもの): color = "red" if remain <= 10 else "black" を作り、self.time_label.config(text=f"残り {remain} 秒", fg=color) のように色も一緒に渡します。self.time_labeltk.Label なので fg の指定が可能です。
    つまずきやすい点: 赤いときだけ fg="red" を渡す書き方だと、いちど赤くした文字は次のゲームでも赤いままです。config() を呼ぶたびに、黒と赤のどちらかを必ず渡す形にしてください。なお start()time_label を空にしないので、制限時間モードのチェックを外して始めると、前回の「残り 0 秒」が消えずに残ります。

  2. 課題2:時間切れのあとに勝手に再スタートしないようにする

    制限時間モードで時間切れになったあと「次の問題」を押すと、断りなく新しいゲームが始まってスコアが消えます。案内を出すだけにしましょう。

    期待結果:時間切れで「正解 3 / 5」と出ている状態で「次の問題」を押すと、「先に『スタート』を押してください」と出る。スコア欄は「正解 3 / 5」のまま変わらない(いまは「正解 0 / 0」に戻って新しい問題が出る)。

    合格条件

    • 時間切れのあとに「次の問題」を押しても、スコア欄の数字が変わらない
    • 起動直後に「次の問題」を押すと、案内だけが出て問題は出ない
    • 「スタート」を押せば今までどおり最初から始まる

    ヒント①(どこを触るか): 触るのは1か所です。next_question() の先頭にある if not self.running: の中の self.start() です。
    ヒント②(使うもの): self.start()messagebox.showinfo("情報", "先に『スタート』を押してください") に置き換え、その下の return はそのまま残します。messagebox は冒頭の from tkinter import messagebox, ttk で読み込み済みです。
    つまずきやすい点: 時間切れの直前に回答していると、_answer() が入れた self.root.after(700, self.next_question) の予約が時間切れのあとに動きます。直す前はこの予約だけで新しいゲームが始まるので、ボタンを押していないのにスコアが 0 に戻ります。直したあとは、結果ダイアログの後ろに案内が1回出ることがありますが、スコアは消えません。

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

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

🚀
次に挑戦するアプリ

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

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

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

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

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