初心者向け No.44

ランダム名言ジェネレーター

ボタンを押すたびにランダムな名言を表示するアプリ。リストからの無作為選択と表示更新を学びます。

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

1. アプリ概要

ボタンを押すたびにランダムな名言を表示するアプリ。リストからの無作為選択と表示更新を学びます。

このアプリはツールカテゴリに分類される実践的なGUIアプリです。使用ライブラリは tkinter(標準ライブラリ) で、難易度は ★☆☆ です。

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

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

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

ランダム名言ジェネレーター 実行画面(Windows)
実行画面(Windows)
ランダム名言ジェネレーター 実行画面(Linux Mint)
実行画面(Linux Mint)

2. 機能一覧

  • 「🔀 次の名言」ボタン(show_quote())・「⭐ お気に入り」ボタン(add_favorite())・「📋 コピー」ボタン(copy_quote())で操作
  • tk.Listbox による一覧表示
  • JSON ファイルの保存・読み込み(起動時に自動実行)
  • messagebox.showerror()messagebox.showwarning() によるダイアログ通知
  • ウィンドウはタイトル「ランダム名言ジェネレーター」・サイズ 520x416 で起動
  • お気に入りは favorites.json に保存され、次に起動したときに自動で読み込まれる(保存先は app44.py と同じフォルダ)

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

💡
コードのコピー方法

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

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

# お気に入りの保存先。このスクリプトと同じフォルダに favorites.json が作られる
FAV_FILE = os.path.join(os.path.dirname(os.path.abspath(__file__)), "favorites.json")


class App44:
    """ランダム名言ジェネレーター"""

    QUOTES = [
        ("シンプルさは究極の洗練である。", "レオナルド・ダ・ヴィンチ"),
        ("プログラミングはアートであり、科学であり、工学でもある。", "ドナルド・クヌース"),
        ("コードの読みやすさは、書きやすさと同じくらい重要だ。", "グイド・ヴァンロッサム"),
        ("最初に動くものを作れ。それから美しくせよ。", "ケント・ベック"),
        ("バグを見つけるよりも、バグを作らないことの方が難しい。", "エドガー・ダイクストラ"),
        ("学ぶことをやめたら、教えることもやめなければならない。", "アルベルト・アインシュタイン"),
        ("失敗は成功のもと。大切なのは諦めないこと。", "トーマス・エジソン"),
        ("千里の道も一歩から。", "老子"),
        ("コードは書く回数より読む回数の方がはるかに多い。", "不明"),
        ("動くコードは最良のドキュメントだ。", "不明"),
        ("シンプルにすることは複雑にするよりも難しい。", "スティーブ・ジョブズ"),
        ("最良のエラーメッセージとは、エラーが起きないことだ。", "不明"),
        ("まず動かせ。正しくせよ。速くせよ。", "肯特・ベック"),
        ("コードを書く前に考え、考える前に仕様を理解せよ。", "不明"),
        ("チームワークは夢を現実にする。", "不明"),
    ]

    def __init__(self, root):
        self.root = root
        self.root.title("ランダム名言ジェネレーター")
        self.root.geometry("520x416")
        self.root.minsize(520, 416)
        self.root.configure(bg="#f8f9fc")
        self.favorites = []
        self._build_ui()
        self._load_favorites()
        self.show_quote()

    def _build_ui(self):
        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_frame = tk.Frame(self.root, bg="#f8f9fc", padx=24, pady=16)
        main_frame.pack(fill=tk.BOTH, expand=True)

        # 名言カード
        card = tk.Frame(main_frame, bg="white", relief=tk.RAISED, bd=1)
        card.pack(fill=tk.X, pady=(0, 14), ipady=16)

        tk.Label(card, text="❝", font=("Noto Sans JP", 24), bg="white",
                 fg="#3776ab").pack(anchor="w", padx=16, pady=(8, 0))

        self.quote_label = tk.Label(card, text="", bg="white",
                                    font=("Noto Sans JP", 13),
                                    wraplength=440, justify="center",
                                    fg="#222")
        self.quote_label.pack(padx=16, pady=(0, 8))

        self.author_label = tk.Label(card, text="", bg="white",
                                     font=("Noto Sans JP", 11, "italic"),
                                     fg="#888")
        self.author_label.pack(anchor="e", padx=16, pady=(0, 8))

        # ボタン
        btn_frame = tk.Frame(main_frame, bg="#f8f9fc")
        btn_frame.pack()
        tk.Button(btn_frame, text="🔀 次の名言",
                  font=("Noto Sans JP", 13, "bold"),
                  bg="#3776ab", fg="white", activebackground="#2a5a8a",
                  relief=tk.FLAT, padx=16, pady=8,
                  command=self.show_quote).pack(side=tk.LEFT, padx=6)
        ttk.Button(btn_frame, text="⭐ お気に入り", command=self.add_favorite).pack(side=tk.LEFT, padx=6)
        ttk.Button(btn_frame, text="📋 コピー", command=self.copy_quote).pack(side=tk.LEFT, padx=6)

        # お気に入り
        fav_frame = ttk.LabelFrame(main_frame, text="お気に入り", padding=6)
        fav_frame.pack(fill=tk.BOTH, expand=True, pady=(14, 0))
        self.fav_listbox = tk.Listbox(fav_frame, font=("Noto Sans JP", 9),
                                      bg="white", relief=tk.FLAT, height=4)
        self.fav_listbox.pack(fill=tk.BOTH, expand=True)

        self.current_quote = None

    def show_quote(self):
        quote, author = random.choice(self.QUOTES)
        self.current_quote = (quote, author)
        self.quote_label.config(text=quote)
        self.author_label.config(text=f"— {author}")

    def add_favorite(self):
        if self.current_quote:
            q, a = self.current_quote
            entry = f"{q}({a})"
            if entry not in self.favorites:
                self.favorites.append(entry)
                self.fav_listbox.insert(tk.END, entry)
                self._save_favorites()

    def copy_quote(self):
        if self.current_quote:
            q, a = self.current_quote
            self.root.clipboard_clear()
            self.root.clipboard_append(f"「{q}」— {a}")

    def _load_favorites(self):
        """保存済みのお気に入りを読み込む(無い・壊れている場合は空のまま続行)"""
        try:
            with open(FAV_FILE, "r", encoding="utf-8") as f:
                saved = json.load(f)
        except FileNotFoundError:
            return
        except (OSError, json.JSONDecodeError):
            messagebox.showwarning(
                "お気に入り",
                f"{FAV_FILE} を読み込めませんでした。お気に入りは空の状態で始めます。")
            return
        if not isinstance(saved, list):
            return
        for entry in saved:
            if isinstance(entry, str) and entry not in self.favorites:
                self.favorites.append(entry)
                self.fav_listbox.insert(tk.END, entry)

    def _save_favorites(self):
        """お気に入りを JSON ファイルへ保存する"""
        try:
            with open(FAV_FILE, "w", encoding="utf-8") as f:
                json.dump(self.favorites, f, ensure_ascii=False, indent=2)
        except OSError as e:
            messagebox.showerror("保存エラー", f"お気に入りを保存できませんでした: {e}")


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

5. コード解説

ランダム名言ジェネレーターのコードを、実際に書かれている実装に沿って解説します。

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

App44 クラスにアプリの全機能をまとめています(メソッド7個・全143行)。__init__ ではタイトル「ランダム名言ジェネレーター」とウィンドウサイズ 520x416 を設定します。そのあと _build_ui()_load_favorites()show_quote() の順に呼び出して初期状態を作ります。

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

ウィジェット構成

画面は tk.Frame×4・tk.Label×4・tk.Buttonttk.Button×2・ttk.LabelFrametk.Listbox で構成しています。見出し付きの枠(LabelFrame)は「お気に入り」のラベルでエリアを区切っています。

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

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

「🔀 次の名言」ボタン(show_quote())・「⭐ お気に入り」ボタン(add_favorite())・「📋 コピー」ボタン(copy_quote())を command= で接続しています。

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

JSON への保存と読み込み

データは json.dump() でファイル(favorites.json)へ保存します。起動時は json.load() で読み戻します。

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

例外処理

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

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

お気に入りが次の起動でも残る仕組み

保存の流れは3ステップです。open() でファイルを開く、json.dump() で書き出す(読むときは json.load())、with ブロックを抜けたところで自動的に閉じる。書き出すのは「⭐ お気に入り」を押したとき(add_favorite() の末尾)だけです。

読み込み側は失敗しても止まらないように書いています。ファイルがまだ無い初回起動は FileNotFoundError をそのまま無視して空の一覧で始め、中身が壊れて JSON として読めないときは json.JSONDecodeError、権限などで開けないときは OSError を捕まえ、警告ダイアログを出したうえで空の一覧に戻します。except Exception でまとめて握りつぶさず、想定した3種類だけを捕まえるのが安全な書き方です。

保存先は os.path.dirname(os.path.abspath(__file__)) でスクリプト自身のフォルダを求めて組み立てています。ターミナルを開いている場所(カレントディレクトリ)ではないため、どこから python app44.py を実行しても同じ favorites.json を読み書きします。

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

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

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

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

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

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

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

    title("ランダム名言ジェネレーター")geometry("520x416")configure(bg="#f8f9fc")minsize(520, 416) をコンストラクタで設定します。

  4. 4
    画面部品を並べる

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

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

    「2. 機能一覧」で挙げた各ボタンを command= で対応するメソッド(show_quote()add_favorite()copy_quote())につなぎます。

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

    アプリの本体である add_favorite()(8行)・show_quote()(5行)・copy_quote()(5行) を実装します。

  7. 7
    動作確認する

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

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

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

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

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

💡 クラス定数を書き換えて動きを変える

データはクラス定数(QUOTES)にまとまっています。中身を書き換えて動きが変わることを確かめましょう。複数の定数が対応関係にある場合は、セットで整合させる必要がある点に注意してください。

💡 配色をまとめて管理する

背景色 #f8f9fc をはじめ、色コードが各所に直書きされています。色を1つの辞書にまとめると、テーマの切り替えが1か所の変更で済むようになります。

💡 お気に入りを選んで削除できるようにする

いまの一覧は追加専用で、間違えて登録した名言を消せません。Listboxcurselection() で選択行の位置を取り、self.favorites から del し、Listbox.delete() で画面からも消して、最後に _save_favorites() を呼びます。この順番で書くと、画面の表示と favorites.json の中身がずれません。

💡 背景色とフォントをランダムに変える

名言を切り替えるたびに見た目も変わると、めくる楽しさが出ます。背景色の候補とフォント名の候補をそれぞれリストで持ち、show_quote() の中で random.choice() で選んで、名言カードと quote_labelconfig(bg=...)config(font=...) で反映します。文字が読めない組み合わせにならないよう、候補は明るい背景色だけに絞るのがコツです。

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

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

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

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

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

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

解決法:geometry()minsize() の両方の数値を書き換えて起動サイズを調整してください。なお、このコードにはウィンドウサイズを固定する設定(resizable の指定)が無いため、ウィンドウの端をドラッグしたサイズ変更は既定どおり可能です。ただし minsize(520, 416) を指定しているため、起動時の大きさより小さくは縮められません(縮めると画面部品が表示されなくなるのを防ぐためです)。

❌ お気に入りが次に開くと消えている

原因:お気に入りをリスト変数(self.favorites)にだけ持っていると、ウィンドウを閉じた時点でメモリから消えます。ファイルへ書き出していなければ、次の起動で復元する手段がありません。

解決法:このコードでは add_favorite() のたびに _save_favorites()favorites.json へ書き出し、起動時に _load_favorites() で読み戻しています。消えている場合は、app44.py と同じフォルダに favorites.json が作られているかを確認してください。無い場合は書き込み権限のないフォルダにスクリプトを置いている可能性があり、そのときは「保存エラー」のダイアログが出ます。

9. 練習問題

アプリの理解を深めるための練習問題です。気になるものから挑戦してみてください(課題3は定型の発展課題です)。QUOTES(14〜30行)は学習用に短くまとめた文で、作者名まで含めて出典を保証するものではありません。本や歌詞、記事の文章を書き足すときは著作権に注意し、自分の手元で試す範囲にとどめてください。「⭐ お気に入り」を押すと、app44.py と同じフォルダに favorites.json が作られます(8行・131〜137行)。

  1. 課題1:直前と同じ名言が続けて出ないようにする

    「🔀 次の名言」を押すと、show_quote()random.choice() で15件から1つ選びます(91〜95行)。前に出した名言を避ける処理はありません。毎回15件から選び直すので、同じ名言が2回続く確率は約6.7%(15回に1回)です。

    期待結果:「🔀 次の名言」を何度押しても、直前と同じ名言が2回続けて出ない

    合格条件

    • 押すたびに表示される名言が変わる
    • 選ばれた名言は self.current_quote(93行)に入り、「⭐ お気に入り」と「📋 コピー」がこれまでどおり動く
    • 起動直後の1回目(41行)でもエラーにならない

    ヒント①(どこを触るか): 触るのは1か所です。show_quote() の92行、random.choice() で選んでいるところです。
    ヒント②(使うもの): while を使い、選んだ組が self.current_quote と同じあいだは選び直します。self.current_quote は89行で None から始まるため、1回目は1度で抜けられます。
    つまずきやすい点: 名言を1件だけの構成に書き換えると、この while は永久に終わりません。len(self.QUOTES) > 1 を先に確かめると安全です。random.sample(self.QUOTES, 2) で2件取り出す書き方もありますが、これも1件だと ValueError になります。

  2. 課題2:「⭐ お気に入り」を押した結果を画面で知らせる

    「⭐ お気に入り」を押しても、画面には何も出ません(97〜104行)。すでに登録済みの名言なら一覧も増えないので、ボタンが効いたのか壊れているのかを区別できません。登録できたかどうかは、下のお気に入り一覧(85〜87行)が増えたかどうかで判断するしかありません。

    期待結果:同じ名言で「⭐ お気に入り」を2回押すと、1回目は「お気に入りに追加しました」、2回目は「すでにお気に入りにあります」と出る

    合格条件

    • 1回目のあと、お気に入り一覧(85〜87行)が1行だけ増える
    • 2回目は一覧が増えない(101行の重複チェックはそのまま残す)
    • favorites.json にも1件ぶんだけ増える

    ヒント①(どこを触るか): 触るのは2か所です。追加できたとき=104行の _save_favorites() の下と、すでにあったとき=101行の if に対する else です。
    ヒント②(使うもの): どちらも messagebox.showinfo("お気に入り", "…") の1行で足ります。messagebox は2行目で読み込み済みで、120〜122行に同じ書き方があります。
    つまずきやすい点: 知らせを101行より前へ置くと、追加できていないのに「追加しました」と出ます。104行の _save_favorites() は保存に失敗すると自分でエラーを出すので(136〜137行)、その前に「追加しました」と出すと知らせが2つ並びます。順番は「一覧に足す → 保存する → 知らせる」です。

  3. 課題3:キー操作に対応させる

    ボタンから呼んでいる show_quote() を、キーボードのキーからも呼べるようにしてみましょう。tkinter では bind を使ってキーイベントとメソッドを結び付けられます。

🚀
次に挑戦するアプリ

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

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

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

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

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