初心者向け No.070

星座占い

生年月日を入力して星座を判定し今日の運勢をランダム表示するアプリ。日付計算とdictの使い方を学べる。

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

1. アプリ概要

生年月日を入力して星座を判定し今日の運勢をランダム表示するアプリ。日付計算とdictの使い方を学べる。

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

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

完成形を見てから細部の解説に進む流れが効果的です。実行→気になる箇所を解説で確認→自分の手で改造、というサイクルで定着が早まります。

カスタマイズ章では具体的な拡張アイデアを示しています。一つ実装するごとに動作確認することで、変更が予期せぬ副作用を起こさないかも体感できます。

2. 機能一覧

  • 星座占いのメイン機能
  • 直感的なGUIインターフェース
  • 入力値のバリデーション
  • エラーハンドリング
  • 結果の見やすい表示
  • クリア機能付き

3. 事前準備・環境

ℹ️
動作確認環境

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

以下の環境で動作確認しています。

  • Python 3.10 以上
  • OS: Windows 10/11・macOS 12+・Ubuntu 20.04+

4. 完全なソースコード

💡
コードのコピー方法

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

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


class App070:
    """星座占い: 生年月日から星座を判定し、今日の運勢を表示"""

    # 星座データ: (名前, シンボル, 開始月日, 終了月日)
    ZODIACS = [
        ("やぎ座",   "♑", (12, 22), (1, 19)),
        ("みずがめ座", "♒", (1, 20), (2, 18)),
        ("うお座",   "♓", (2, 19), (3, 20)),
        ("おひつじ座", "♈", (3, 21), (4, 19)),
        ("おうし座",  "♉", (4, 20), (5, 20)),
        ("ふたご座",  "♊", (5, 21), (6, 21)),
        ("かに座",   "♋", (6, 22), (7, 22)),
        ("しし座",   "♌", (7, 23), (8, 22)),
        ("おとめ座",  "♍", (8, 23), (9, 22)),
        ("てんびん座", "♎", (9, 23), (10, 23)),
        ("さそり座",  "♏", (10, 24), (11, 22)),
        ("いて座",   "♐", (11, 23), (12, 21)),
    ]

    LUCK_LEVELS = ["大吉", "中吉", "小吉", "吉", "末吉", "凶"]
    LUCKY_COLORS = ["赤", "青", "緑", "黄", "ピンク", "紫", "白", "金", "オレンジ"]
    LUCKY_ITEMS = ["腕時計", "ハンカチ", "コーヒー", "本", "鉛筆", "イヤホン", "観葉植物", "アロマ", "メガネ"]
    MESSAGES = [
        "新しい挑戦が幸運を呼びそうです。",
        "周囲の人への感謝が運気アップの鍵。",
        "落ち着いた行動が吉と出るでしょう。",
        "アイデアが冴える1日。メモを忘れずに。",
        "予想外の出来事から良い縁が生まれそう。",
        "健康管理に気を配ると好調をキープできます。",
        "古い友人との再会が幸運を運びます。",
        "金銭は慎重に、衝動買いは避けて。",
    ]

    def __init__(self, root):
        self.root = root
        self.root.title("星座占い")
        self.root.geometry("600x550")
        self.root.configure(bg="#f8f9fc")
        self._build_ui()

    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=10)
        form.pack(fill=tk.X)
        today = date.today()
        ttk.Label(form, text="年").grid(row=0, column=0, sticky="w")
        self.year_var = tk.StringVar(value=str(today.year - 25))
        ttk.Combobox(form, textvariable=self.year_var, width=6,
                     values=[str(y) for y in range(1900, today.year + 1)]
                     ).grid(row=0, column=1, padx=4)
        ttk.Label(form, text="月").grid(row=0, column=2, sticky="w")
        self.month_var = tk.StringVar(value="1")
        ttk.Combobox(form, textvariable=self.month_var, width=4,
                     values=[str(m) for m in range(1, 13)]
                     ).grid(row=0, column=3, padx=4)
        ttk.Label(form, text="日").grid(row=0, column=4, sticky="w")
        self.day_var = tk.StringVar(value="1")
        ttk.Combobox(form, textvariable=self.day_var, width=4,
                     values=[str(d) for d in range(1, 32)]
                     ).grid(row=0, column=5, padx=4)
        ttk.Button(form, text="占う", command=self.fortune).grid(row=0, column=6, padx=10)

        # 結果表示
        result_frame = ttk.LabelFrame(main, text="占い結果", padding=12)
        result_frame.pack(fill=tk.BOTH, expand=True, pady=10)

        self.zodiac_var = tk.StringVar(value="生年月日を入力して占ってください")
        tk.Label(result_frame, textvariable=self.zodiac_var, bg="#f8f9fc",
                 font=("Noto Sans JP", 22, "bold"), fg="#3776ab").pack(pady=10)

        self.detail_text = tk.Text(result_frame, height=10, font=("Noto Sans JP", 11),
                                   bg="white", relief=tk.FLAT, wrap=tk.WORD, state=tk.DISABLED)
        self.detail_text.pack(fill=tk.BOTH, expand=True)

    def _detect_zodiac(self, month, day):
        """月日から星座を判定する"""
        for name, sym, start, end in self.ZODIACS:
            sm, sd = start
            em, ed = end
            # 年をまたぐ (やぎ座) の場合
            if sm > em:
                if (month == sm and day >= sd) or (month == em and day <= ed):
                    return name, sym
            else:
                if (month == sm and day >= sd) or (month == em and day <= ed) or (sm < month < em):
                    return name, sym
        return None, None

    def fortune(self):
        """生年月日から星座と運勢を表示する"""
        try:
            y = int(self.year_var.get())
            m = int(self.month_var.get())
            d = int(self.day_var.get())
            birth = date(y, m, d)
        except ValueError:
            messagebox.showwarning("入力エラー", "正しい生年月日を入力してください")
            return
        if birth > date.today():
            messagebox.showwarning("入力エラー", "未来の日付は入力できません")
            return
        name, sym = self._detect_zodiac(m, d)
        if not name:
            messagebox.showerror("エラー", "星座を判定できませんでした")
            return
        # 同じ日付なら同じ運勢になるよう seed を固定 (今日の日付 + 生年月日)
        today = date.today()
        seed_str = f"{name}-{today.isoformat()}"
        rng = random.Random(seed_str)
        luck = rng.choice(self.LUCK_LEVELS)
        color = rng.choice(self.LUCKY_COLORS)
        item = rng.choice(self.LUCKY_ITEMS)
        msg = rng.choice(self.MESSAGES)
        score = rng.randint(40, 100)
        # 年齢
        age = today.year - birth.year - ((today.month, today.day) < (birth.month, birth.day))
        # 表示
        self.zodiac_var.set(f"{sym} {name}")
        self.detail_text.config(state=tk.NORMAL)
        self.detail_text.delete("1.0", tk.END)
        lines = [
            f"生年月日: {birth.isoformat()} (満 {age} 歳)",
            f"今日の日付: {today.isoformat()}",
            "",
            f"今日の運勢: {luck}  (運気スコア: {score}/100)",
            f"ラッキーカラー: {color}",
            f"ラッキーアイテム: {item}",
            "",
            f"💬 {msg}",
        ]
        self.detail_text.insert("1.0", "\n".join(lines))
        self.detail_text.config(state=tk.DISABLED)


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

5. コード解説

星座占いのコードを詳しく解説します。クラスベースの設計で各機能を整理して実装しています。

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

App070クラスにアプリの全機能をまとめています。__init__でウィンドウ設定、_build_ui()でUI構築、process()でメイン処理を担当します。責任の分離により、コードが読みやすくなります。

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

UIレイアウトの構築

LabelFrameで入力エリアと結果エリアを視覚的に分けています。pack()で縦に並べ、expand=Trueで結果エリアが画面いっぱいに広がるよう設定しています。

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

イベント処理

ボタンのcommand引数でクリックイベントを、bind('')でEnterキーイベントを処理します。どちらの操作でも同じprocess()が呼ばれ、コードの重複を避けられます。

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

Textウィジェットでの結果表示

tk.Textウィジェットをstate=DISABLED(読み取り専用)で作成し、更新時はNORMALに変更してinsert()で内容を書き込み、再びDISABLEDに戻します。

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

例外処理とエラーハンドリング

try-exceptでValueErrorとExceptionを捕捉し、messagebox.showerror()でエラーメッセージを表示します。予期しないエラーも処理することで、アプリの堅牢性が向上します。

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

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

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

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

    新しいファイルを作成して app070.py と保存します。

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

    App070クラスを定義し、__init__とmainloop()の最小構成を作ります。

  3. 3
    タイトルバーを作る

    Frameを使ってカラーバー付きのタイトルエリアを作ります。

  4. 4
    入力フォームを実装する

    LabelFrameとEntryウィジェットで入力エリアを作ります。

  5. 5
    処理ロジックを実装する

    _execute()メソッドにメインロジックを実装します。

  6. 6
    結果表示を実装する

    TextウィジェットかLabelに結果を表示する_show_result()を実装します。

  7. 7
    エラー処理を追加する

    try-exceptとmessageboxでエラーハンドリングを追加します。

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

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

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

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

💡 データの保存機能

処理結果をCSV・TXTファイルに保存する機能を追加しましょう。filedialog.asksaveasfilename()でファイル保存ダイアログが使えます。

💡 設定ダイアログ

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

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

❌ 日本語フォントが表示されない

原因:システムに日本語フォントが見つからない場合があります。

解決法:font引数を省略するかシステムに合ったフォントを指定してください。

❌ ライブラリのインポートエラー

原因:必要なライブラリがインストールされていません。

解決法:pip install コマンドで必要なライブラリをインストールしてください。

❌ ウィンドウサイズが合わない

原因:画面解像度や表示スケールによって異なる場合があります。

解決法:root.geometry()で適切なサイズに調整してください。

9. 練習問題

アプリの理解を深めるための練習問題です。

  1. 課題1:機能拡張

    星座占いに新しい機能を1つ追加してみましょう。

  2. 課題2:UIの改善

    色・フォント・レイアウトを変更して、より使いやすいUIにカスタマイズしましょう。

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

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

🚀
次に挑戦するアプリ

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