中級者向け No.11

パスワードマネージャー

暗号化してパスワードを安全に保存するパスワードマネージャー。cryptographyライブラリでAES暗号化を学びます。

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

1. アプリ概要

暗号化してパスワードを安全に保存するパスワードマネージャー。cryptographyライブラリでAES暗号化を学びます。

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

Pythonでは tkinter を使うことで、クロスプラットフォームなGUIアプリを簡単に作成できます。このアプリを通じて、ウィジェットの配置・イベント処理・データ管理など、GUI開発の実践的なスキルを習得できます。

ソースコードは完全な動作状態で提供しており、コピーしてそのまま実行できます。まずは実行して動作を確認し、その後コードを読んで仕組みを理解していきましょう。カスタマイズセクションでは機能拡張のアイデアも紹介しています。

GUIアプリ開発は、プログラミングの楽しさを実感できる最も効果的な学習方法のひとつです。アプリを作ることで、変数・関数・クラス・イベント処理など、プログラミングの重要な概念が自然と身についていきます。このアプリをきっかけに、オリジナルアプリの開発にも挑戦してみてください。

2. 機能一覧

  • パスワードマネージャーのメイン機能
  • 直感的なGUIインターフェース
  • 入力値のバリデーション
  • エラーハンドリング
  • 結果の見やすい表示
  • キーボードショートカット対応

3. 事前準備・環境

ℹ️
動作確認環境

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

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

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

4. 完全なソースコード

💡
コードのコピー方法

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

app11.py
import tkinter as tk
from tkinter import ttk, messagebox
import json
import os
import hashlib
import base64
import secrets
import string

try:
    from cryptography.fernet import Fernet
    CRYPTO_AVAILABLE = True
except ImportError:
    CRYPTO_AVAILABLE = False


class App11:
    """パスワードマネージャー"""

    DATA_FILE = os.path.join(os.path.dirname(__file__), "passwords.enc")
    KEY_FILE = os.path.join(os.path.dirname(__file__), "passwords.key")

    def __init__(self, root):
        self.root = root
        self.root.title("パスワードマネージャー")
        self.root.geometry("780x540")
        self.root.configure(bg="#f8f9fc")
        self.fernet = None
        self.entries = []
        self.master_hash = None
        self._build_ui()
        self._check_crypto()

    def _check_crypto(self):
        if not CRYPTO_AVAILABLE:
            messagebox.showwarning(
                "ライブラリ未インストール",
                "cryptography が必要です。\n"
                "pip install cryptography でインストールしてください。\n\n"
                "デモモード(暗号化なし)で動作します。")
        self._show_login()

    def _build_ui(self):
        title_frame = tk.Frame(self.root, bg="#c62828", pady=10)
        title_frame.pack(fill=tk.X)
        tk.Label(title_frame, text="🔒 パスワードマネージャー",
                 font=("Noto Sans JP", 15, "bold"),
                 bg="#c62828", fg="white").pack(side=tk.LEFT, padx=12)
        self.lock_btn = ttk.Button(title_frame, text="🔒 ロック",
                                   command=self._lock)
        self.lock_btn.pack(side=tk.RIGHT, padx=12)

        # メインコンテンツ
        self.main_frame = tk.Frame(self.root, bg="#f8f9fc")
        self.main_frame.pack(fill=tk.BOTH, expand=True)

        # ログイン/セットアップフレーム
        self.login_frame = tk.Frame(self.main_frame, bg="#f8f9fc")

        # パスワード一覧フレーム
        self.list_frame = tk.Frame(self.main_frame, bg="#f8f9fc")

        self.status_var = tk.StringVar(value="")
        tk.Label(self.root, textvariable=self.status_var,
                 bg="#dde", font=("Arial", 9), anchor="w", padx=8
                 ).pack(fill=tk.X, side=tk.BOTTOM)

    def _show_login(self):
        self.list_frame.pack_forget()
        self.login_frame.pack(fill=tk.BOTH, expand=True)
        for w in self.login_frame.winfo_children():
            w.destroy()

        c = tk.Frame(self.login_frame, bg="#f8f9fc")
        c.place(relx=0.5, rely=0.4, anchor="center")

        has_data = os.path.exists(self.KEY_FILE)
        title = "マスターパスワードを入力" if has_data else "マスターパスワードを設定"
        tk.Label(c, text=title, font=("Noto Sans JP", 14, "bold"),
                 bg="#f8f9fc").pack(pady=8)

        frame = tk.Frame(c, bg="#f8f9fc")
        frame.pack()
        tk.Label(frame, text="マスターパスワード:",
                 bg="#f8f9fc").grid(row=0, column=0, sticky="w", pady=4)
        self.master_entry = ttk.Entry(frame, show="●", width=24,
                                       font=("Arial", 12))
        self.master_entry.grid(row=0, column=1, padx=8)

        if not has_data:
            tk.Label(frame, text="確認用:",
                     bg="#f8f9fc").grid(row=1, column=0, sticky="w", pady=4)
            self.master_confirm = ttk.Entry(frame, show="●", width=24,
                                             font=("Arial", 12))
            self.master_confirm.grid(row=1, column=1, padx=8)
        else:
            self.master_confirm = None

        btn_text = "ログイン" if has_data else "作成"
        ttk.Button(c, text=btn_text,
                   command=self._authenticate).pack(pady=12)
        self.master_entry.bind("<Return>", lambda e: self._authenticate())
        self.master_entry.focus_set()

    def _authenticate(self):
        pw = self.master_entry.get()
        if not pw:
            messagebox.showwarning("警告", "パスワードを入力してください")
            return

        has_data = os.path.exists(self.KEY_FILE)
        if has_data:
            # 認証
            with open(self.KEY_FILE, "r") as f:
                stored = json.load(f)
            hashed = hashlib.sha256((pw + stored["salt"]).encode()).hexdigest()
            if hashed != stored["hash"]:
                messagebox.showerror("エラー", "パスワードが正しくありません")
                return
            if CRYPTO_AVAILABLE:
                key = base64.urlsafe_b64encode(
                    hashlib.sha256(pw.encode()).digest())
                self.fernet = Fernet(key)
        else:
            # 新規作成
            if self.master_confirm and pw != self.master_confirm.get():
                messagebox.showerror("エラー", "パスワードが一致しません")
                return
            salt = secrets.token_hex(16)
            hashed = hashlib.sha256((pw + salt).encode()).hexdigest()
            with open(self.KEY_FILE, "w") as f:
                json.dump({"hash": hashed, "salt": salt}, f)
            if CRYPTO_AVAILABLE:
                key = base64.urlsafe_b64encode(
                    hashlib.sha256(pw.encode()).digest())
                self.fernet = Fernet(key)

        self._load_passwords()
        self._show_manager()

    def _show_manager(self):
        self.login_frame.pack_forget()
        self.list_frame.pack(fill=tk.BOTH, expand=True)
        for w in self.list_frame.winfo_children():
            w.destroy()

        paned = ttk.PanedWindow(self.list_frame, orient=tk.HORIZONTAL)
        paned.pack(fill=tk.BOTH, expand=True, padx=4, pady=4)

        # 左: 一覧
        left = tk.Frame(paned, bg="#f8f9fc")
        tk.Label(left, text="🔍 検索:").pack(anchor="w", padx=8)
        self.search_var = tk.StringVar()
        search_entry = ttk.Entry(left, textvariable=self.search_var, width=26)
        search_entry.pack(fill=tk.X, padx=8, pady=2)
        self.search_var.trace_add("write", lambda *a: self._refresh_list())

        self.lb = tk.Listbox(left, font=("Arial", 11),
                              selectbackground="#c62828",
                              selectforeground="white")
        sb = ttk.Scrollbar(left, command=self.lb.yview)
        self.lb.configure(yscrollcommand=sb.set)
        sb.pack(side=tk.RIGHT, fill=tk.Y)
        self.lb.pack(fill=tk.BOTH, expand=True, padx=8, pady=4)
        self.lb.bind("<<ListboxSelect>>", self._on_select)
        paned.add(left, weight=1)

        # 右: 詳細・編集
        right = ttk.LabelFrame(paned, text="詳細・編集", padding=12)
        for lbl, attr in [("サービス名", "svc"), ("ユーザー名", "usr"),
                           ("パスワード", "pwd"), ("URL", "url"),
                           ("メモ", "memo")]:
            tk.Label(right, text=f"{lbl}:").pack(anchor="w")
            if lbl == "メモ":
                var = tk.Text(right, height=3, width=28, font=("Arial", 11))
                var.pack(fill=tk.X, pady=2)
                setattr(self, f"{attr}_text", var)
            else:
                var = tk.StringVar()
                entry = ttk.Entry(right, textvariable=var, width=28,
                                  show="●" if lbl == "パスワード" else "")
                entry.pack(fill=tk.X, pady=2)
                setattr(self, f"{attr}_var", var)
                if lbl == "パスワード":
                    self.pwd_entry = entry
                    btn_f = tk.Frame(right, bg=right.cget("background"))
                    btn_f.pack(fill=tk.X)
                    ttk.Button(btn_f, text="👁 表示",
                               command=self._toggle_pwd).pack(side=tk.LEFT, padx=2)
                    ttk.Button(btn_f, text="🎲 生成",
                               command=self._gen_password).pack(side=tk.LEFT, padx=2)
                    ttk.Button(btn_f, text="📋 コピー",
                               command=self._copy_pwd).pack(side=tk.LEFT, padx=2)

        btn_frame = tk.Frame(right, bg=right.cget("background"))
        btn_frame.pack(fill=tk.X, pady=8)
        for text, cmd in [("➕ 追加", self._add_entry),
                           ("✏️ 更新", self._update_entry),
                           ("🗑️ 削除", self._delete_entry)]:
            ttk.Button(btn_frame, text=text, command=cmd).pack(
                side=tk.LEFT, padx=4)
        paned.add(right, weight=1)

        self._refresh_list()
        self.status_var.set(f"{len(self.entries)} 件")

    def _load_passwords(self):
        self.entries = []
        if not os.path.exists(self.DATA_FILE):
            return
        try:
            with open(self.DATA_FILE, "rb") as f:
                raw = f.read()
            if CRYPTO_AVAILABLE and self.fernet:
                raw = self.fernet.decrypt(raw)
            self.entries = json.loads(raw)
        except Exception:
            pass

    def _save_passwords(self):
        raw = json.dumps(self.entries, ensure_ascii=False).encode()
        if CRYPTO_AVAILABLE and self.fernet:
            raw = self.fernet.encrypt(raw)
        with open(self.DATA_FILE, "wb") as f:
            f.write(raw)

    def _refresh_list(self):
        q = self.search_var.get().lower()
        self.lb.delete(0, tk.END)
        self._filtered = [e for e in self.entries
                          if q in e.get("svc", "").lower()
                          or q in e.get("usr", "").lower()]
        for e in self._filtered:
            self.lb.insert(tk.END, f"  {e.get('svc', '')}  ({e.get('usr', '')})")

    def _on_select(self, event):
        sel = self.lb.curselection()
        if not sel:
            return
        idx = sel[0]
        if idx < len(self._filtered):
            e = self._filtered[idx]
            self.svc_var.set(e.get("svc", ""))
            self.usr_var.set(e.get("usr", ""))
            self.pwd_var.set(e.get("pwd", ""))
            self.url_var.set(e.get("url", ""))
            self.memo_text.delete("1.0", tk.END)
            self.memo_text.insert("1.0", e.get("memo", ""))
            self._selected_idx = self.entries.index(e)

    def _add_entry(self):
        svc = self.svc_var.get().strip()
        if not svc:
            messagebox.showwarning("警告", "サービス名を入力してください")
            return
        entry = {
            "svc": svc, "usr": self.usr_var.get(),
            "pwd": self.pwd_var.get(), "url": self.url_var.get(),
            "memo": self.memo_text.get("1.0", tk.END).strip()
        }
        self.entries.append(entry)
        self._save_passwords()
        self._refresh_list()
        self.status_var.set(f"{len(self.entries)} 件")

    def _update_entry(self):
        if not hasattr(self, "_selected_idx"):
            return
        self.entries[self._selected_idx] = {
            "svc": self.svc_var.get(), "usr": self.usr_var.get(),
            "pwd": self.pwd_var.get(), "url": self.url_var.get(),
            "memo": self.memo_text.get("1.0", tk.END).strip()
        }
        self._save_passwords()
        self._refresh_list()

    def _delete_entry(self):
        if not hasattr(self, "_selected_idx"):
            return
        if messagebox.askyesno("確認", "このエントリを削除しますか?"):
            self.entries.pop(self._selected_idx)
            self._save_passwords()
            self._refresh_list()
            self.status_var.set(f"{len(self.entries)} 件")

    def _toggle_pwd(self):
        show = self.pwd_entry.cget("show")
        self.pwd_entry.config(show="" if show else "●")

    def _gen_password(self):
        chars = string.ascii_letters + string.digits + "!@#$%&"
        pwd = "".join(secrets.choice(chars) for _ in range(16))
        self.pwd_var.set(pwd)

    def _copy_pwd(self):
        self.root.clipboard_clear()
        self.root.clipboard_append(self.pwd_var.get())
        self.status_var.set("パスワードをクリップボードにコピーしました")

    def _lock(self):
        self.fernet = None
        self.entries = []
        self._show_login()


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

5. コード解説

パスワードマネージャーのコードを詳しく解説します。クラスベースの設計で各機能を整理して実装しています。

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

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

import tkinter as tk
from tkinter import ttk, messagebox
import json
import os
import hashlib
import base64
import secrets
import string

try:
    from cryptography.fernet import Fernet
    CRYPTO_AVAILABLE = True
except ImportError:
    CRYPTO_AVAILABLE = False


class App11:
    """パスワードマネージャー"""

    DATA_FILE = os.path.join(os.path.dirname(__file__), "passwords.enc")
    KEY_FILE = os.path.join(os.path.dirname(__file__), "passwords.key")

    def __init__(self, root):
        self.root = root
        self.root.title("パスワードマネージャー")
        self.root.geometry("780x540")
        self.root.configure(bg="#f8f9fc")
        self.fernet = None
        self.entries = []
        self.master_hash = None
        self._build_ui()
        self._check_crypto()

    def _check_crypto(self):
        if not CRYPTO_AVAILABLE:
            messagebox.showwarning(
                "ライブラリ未インストール",
                "cryptography が必要です。\n"
                "pip install cryptography でインストールしてください。\n\n"
                "デモモード(暗号化なし)で動作します。")
        self._show_login()

    def _build_ui(self):
        title_frame = tk.Frame(self.root, bg="#c62828", pady=10)
        title_frame.pack(fill=tk.X)
        tk.Label(title_frame, text="🔒 パスワードマネージャー",
                 font=("Noto Sans JP", 15, "bold"),
                 bg="#c62828", fg="white").pack(side=tk.LEFT, padx=12)
        self.lock_btn = ttk.Button(title_frame, text="🔒 ロック",
                                   command=self._lock)
        self.lock_btn.pack(side=tk.RIGHT, padx=12)

        # メインコンテンツ
        self.main_frame = tk.Frame(self.root, bg="#f8f9fc")
        self.main_frame.pack(fill=tk.BOTH, expand=True)

        # ログイン/セットアップフレーム
        self.login_frame = tk.Frame(self.main_frame, bg="#f8f9fc")

        # パスワード一覧フレーム
        self.list_frame = tk.Frame(self.main_frame, bg="#f8f9fc")

        self.status_var = tk.StringVar(value="")
        tk.Label(self.root, textvariable=self.status_var,
                 bg="#dde", font=("Arial", 9), anchor="w", padx=8
                 ).pack(fill=tk.X, side=tk.BOTTOM)

    def _show_login(self):
        self.list_frame.pack_forget()
        self.login_frame.pack(fill=tk.BOTH, expand=True)
        for w in self.login_frame.winfo_children():
            w.destroy()

        c = tk.Frame(self.login_frame, bg="#f8f9fc")
        c.place(relx=0.5, rely=0.4, anchor="center")

        has_data = os.path.exists(self.KEY_FILE)
        title = "マスターパスワードを入力" if has_data else "マスターパスワードを設定"
        tk.Label(c, text=title, font=("Noto Sans JP", 14, "bold"),
                 bg="#f8f9fc").pack(pady=8)

        frame = tk.Frame(c, bg="#f8f9fc")
        frame.pack()
        tk.Label(frame, text="マスターパスワード:",
                 bg="#f8f9fc").grid(row=0, column=0, sticky="w", pady=4)
        self.master_entry = ttk.Entry(frame, show="●", width=24,
                                       font=("Arial", 12))
        self.master_entry.grid(row=0, column=1, padx=8)

        if not has_data:
            tk.Label(frame, text="確認用:",
                     bg="#f8f9fc").grid(row=1, column=0, sticky="w", pady=4)
            self.master_confirm = ttk.Entry(frame, show="●", width=24,
                                             font=("Arial", 12))
            self.master_confirm.grid(row=1, column=1, padx=8)
        else:
            self.master_confirm = None

        btn_text = "ログイン" if has_data else "作成"
        ttk.Button(c, text=btn_text,
                   command=self._authenticate).pack(pady=12)
        self.master_entry.bind("<Return>", lambda e: self._authenticate())
        self.master_entry.focus_set()

    def _authenticate(self):
        pw = self.master_entry.get()
        if not pw:
            messagebox.showwarning("警告", "パスワードを入力してください")
            return

        has_data = os.path.exists(self.KEY_FILE)
        if has_data:
            # 認証
            with open(self.KEY_FILE, "r") as f:
                stored = json.load(f)
            hashed = hashlib.sha256((pw + stored["salt"]).encode()).hexdigest()
            if hashed != stored["hash"]:
                messagebox.showerror("エラー", "パスワードが正しくありません")
                return
            if CRYPTO_AVAILABLE:
                key = base64.urlsafe_b64encode(
                    hashlib.sha256(pw.encode()).digest())
                self.fernet = Fernet(key)
        else:
            # 新規作成
            if self.master_confirm and pw != self.master_confirm.get():
                messagebox.showerror("エラー", "パスワードが一致しません")
                return
            salt = secrets.token_hex(16)
            hashed = hashlib.sha256((pw + salt).encode()).hexdigest()
            with open(self.KEY_FILE, "w") as f:
                json.dump({"hash": hashed, "salt": salt}, f)
            if CRYPTO_AVAILABLE:
                key = base64.urlsafe_b64encode(
                    hashlib.sha256(pw.encode()).digest())
                self.fernet = Fernet(key)

        self._load_passwords()
        self._show_manager()

    def _show_manager(self):
        self.login_frame.pack_forget()
        self.list_frame.pack(fill=tk.BOTH, expand=True)
        for w in self.list_frame.winfo_children():
            w.destroy()

        paned = ttk.PanedWindow(self.list_frame, orient=tk.HORIZONTAL)
        paned.pack(fill=tk.BOTH, expand=True, padx=4, pady=4)

        # 左: 一覧
        left = tk.Frame(paned, bg="#f8f9fc")
        tk.Label(left, text="🔍 検索:").pack(anchor="w", padx=8)
        self.search_var = tk.StringVar()
        search_entry = ttk.Entry(left, textvariable=self.search_var, width=26)
        search_entry.pack(fill=tk.X, padx=8, pady=2)
        self.search_var.trace_add("write", lambda *a: self._refresh_list())

        self.lb = tk.Listbox(left, font=("Arial", 11),
                              selectbackground="#c62828",
                              selectforeground="white")
        sb = ttk.Scrollbar(left, command=self.lb.yview)
        self.lb.configure(yscrollcommand=sb.set)
        sb.pack(side=tk.RIGHT, fill=tk.Y)
        self.lb.pack(fill=tk.BOTH, expand=True, padx=8, pady=4)
        self.lb.bind("<<ListboxSelect>>", self._on_select)
        paned.add(left, weight=1)

        # 右: 詳細・編集
        right = ttk.LabelFrame(paned, text="詳細・編集", padding=12)
        for lbl, attr in [("サービス名", "svc"), ("ユーザー名", "usr"),
                           ("パスワード", "pwd"), ("URL", "url"),
                           ("メモ", "memo")]:
            tk.Label(right, text=f"{lbl}:").pack(anchor="w")
            if lbl == "メモ":
                var = tk.Text(right, height=3, width=28, font=("Arial", 11))
                var.pack(fill=tk.X, pady=2)
                setattr(self, f"{attr}_text", var)
            else:
                var = tk.StringVar()
                entry = ttk.Entry(right, textvariable=var, width=28,
                                  show="●" if lbl == "パスワード" else "")
                entry.pack(fill=tk.X, pady=2)
                setattr(self, f"{attr}_var", var)
                if lbl == "パスワード":
                    self.pwd_entry = entry
                    btn_f = tk.Frame(right, bg=right.cget("background"))
                    btn_f.pack(fill=tk.X)
                    ttk.Button(btn_f, text="👁 表示",
                               command=self._toggle_pwd).pack(side=tk.LEFT, padx=2)
                    ttk.Button(btn_f, text="🎲 生成",
                               command=self._gen_password).pack(side=tk.LEFT, padx=2)
                    ttk.Button(btn_f, text="📋 コピー",
                               command=self._copy_pwd).pack(side=tk.LEFT, padx=2)

        btn_frame = tk.Frame(right, bg=right.cget("background"))
        btn_frame.pack(fill=tk.X, pady=8)
        for text, cmd in [("➕ 追加", self._add_entry),
                           ("✏️ 更新", self._update_entry),
                           ("🗑️ 削除", self._delete_entry)]:
            ttk.Button(btn_frame, text=text, command=cmd).pack(
                side=tk.LEFT, padx=4)
        paned.add(right, weight=1)

        self._refresh_list()
        self.status_var.set(f"{len(self.entries)} 件")

    def _load_passwords(self):
        self.entries = []
        if not os.path.exists(self.DATA_FILE):
            return
        try:
            with open(self.DATA_FILE, "rb") as f:
                raw = f.read()
            if CRYPTO_AVAILABLE and self.fernet:
                raw = self.fernet.decrypt(raw)
            self.entries = json.loads(raw)
        except Exception:
            pass

    def _save_passwords(self):
        raw = json.dumps(self.entries, ensure_ascii=False).encode()
        if CRYPTO_AVAILABLE and self.fernet:
            raw = self.fernet.encrypt(raw)
        with open(self.DATA_FILE, "wb") as f:
            f.write(raw)

    def _refresh_list(self):
        q = self.search_var.get().lower()
        self.lb.delete(0, tk.END)
        self._filtered = [e for e in self.entries
                          if q in e.get("svc", "").lower()
                          or q in e.get("usr", "").lower()]
        for e in self._filtered:
            self.lb.insert(tk.END, f"  {e.get('svc', '')}  ({e.get('usr', '')})")

    def _on_select(self, event):
        sel = self.lb.curselection()
        if not sel:
            return
        idx = sel[0]
        if idx < len(self._filtered):
            e = self._filtered[idx]
            self.svc_var.set(e.get("svc", ""))
            self.usr_var.set(e.get("usr", ""))
            self.pwd_var.set(e.get("pwd", ""))
            self.url_var.set(e.get("url", ""))
            self.memo_text.delete("1.0", tk.END)
            self.memo_text.insert("1.0", e.get("memo", ""))
            self._selected_idx = self.entries.index(e)

    def _add_entry(self):
        svc = self.svc_var.get().strip()
        if not svc:
            messagebox.showwarning("警告", "サービス名を入力してください")
            return
        entry = {
            "svc": svc, "usr": self.usr_var.get(),
            "pwd": self.pwd_var.get(), "url": self.url_var.get(),
            "memo": self.memo_text.get("1.0", tk.END).strip()
        }
        self.entries.append(entry)
        self._save_passwords()
        self._refresh_list()
        self.status_var.set(f"{len(self.entries)} 件")

    def _update_entry(self):
        if not hasattr(self, "_selected_idx"):
            return
        self.entries[self._selected_idx] = {
            "svc": self.svc_var.get(), "usr": self.usr_var.get(),
            "pwd": self.pwd_var.get(), "url": self.url_var.get(),
            "memo": self.memo_text.get("1.0", tk.END).strip()
        }
        self._save_passwords()
        self._refresh_list()

    def _delete_entry(self):
        if not hasattr(self, "_selected_idx"):
            return
        if messagebox.askyesno("確認", "このエントリを削除しますか?"):
            self.entries.pop(self._selected_idx)
            self._save_passwords()
            self._refresh_list()
            self.status_var.set(f"{len(self.entries)} 件")

    def _toggle_pwd(self):
        show = self.pwd_entry.cget("show")
        self.pwd_entry.config(show="" if show else "●")

    def _gen_password(self):
        chars = string.ascii_letters + string.digits + "!@#$%&"
        pwd = "".join(secrets.choice(chars) for _ in range(16))
        self.pwd_var.set(pwd)

    def _copy_pwd(self):
        self.root.clipboard_clear()
        self.root.clipboard_append(self.pwd_var.get())
        self.status_var.set("パスワードをクリップボードにコピーしました")

    def _lock(self):
        self.fernet = None
        self.entries = []
        self._show_login()


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

LabelFrameによるセクション分け

ttk.LabelFrame を使うことで、入力エリアと結果エリアを視覚的に分けられます。padding引数でフレーム内の余白を設定し、見やすいレイアウトを実現しています。

import tkinter as tk
from tkinter import ttk, messagebox
import json
import os
import hashlib
import base64
import secrets
import string

try:
    from cryptography.fernet import Fernet
    CRYPTO_AVAILABLE = True
except ImportError:
    CRYPTO_AVAILABLE = False


class App11:
    """パスワードマネージャー"""

    DATA_FILE = os.path.join(os.path.dirname(__file__), "passwords.enc")
    KEY_FILE = os.path.join(os.path.dirname(__file__), "passwords.key")

    def __init__(self, root):
        self.root = root
        self.root.title("パスワードマネージャー")
        self.root.geometry("780x540")
        self.root.configure(bg="#f8f9fc")
        self.fernet = None
        self.entries = []
        self.master_hash = None
        self._build_ui()
        self._check_crypto()

    def _check_crypto(self):
        if not CRYPTO_AVAILABLE:
            messagebox.showwarning(
                "ライブラリ未インストール",
                "cryptography が必要です。\n"
                "pip install cryptography でインストールしてください。\n\n"
                "デモモード(暗号化なし)で動作します。")
        self._show_login()

    def _build_ui(self):
        title_frame = tk.Frame(self.root, bg="#c62828", pady=10)
        title_frame.pack(fill=tk.X)
        tk.Label(title_frame, text="🔒 パスワードマネージャー",
                 font=("Noto Sans JP", 15, "bold"),
                 bg="#c62828", fg="white").pack(side=tk.LEFT, padx=12)
        self.lock_btn = ttk.Button(title_frame, text="🔒 ロック",
                                   command=self._lock)
        self.lock_btn.pack(side=tk.RIGHT, padx=12)

        # メインコンテンツ
        self.main_frame = tk.Frame(self.root, bg="#f8f9fc")
        self.main_frame.pack(fill=tk.BOTH, expand=True)

        # ログイン/セットアップフレーム
        self.login_frame = tk.Frame(self.main_frame, bg="#f8f9fc")

        # パスワード一覧フレーム
        self.list_frame = tk.Frame(self.main_frame, bg="#f8f9fc")

        self.status_var = tk.StringVar(value="")
        tk.Label(self.root, textvariable=self.status_var,
                 bg="#dde", font=("Arial", 9), anchor="w", padx=8
                 ).pack(fill=tk.X, side=tk.BOTTOM)

    def _show_login(self):
        self.list_frame.pack_forget()
        self.login_frame.pack(fill=tk.BOTH, expand=True)
        for w in self.login_frame.winfo_children():
            w.destroy()

        c = tk.Frame(self.login_frame, bg="#f8f9fc")
        c.place(relx=0.5, rely=0.4, anchor="center")

        has_data = os.path.exists(self.KEY_FILE)
        title = "マスターパスワードを入力" if has_data else "マスターパスワードを設定"
        tk.Label(c, text=title, font=("Noto Sans JP", 14, "bold"),
                 bg="#f8f9fc").pack(pady=8)

        frame = tk.Frame(c, bg="#f8f9fc")
        frame.pack()
        tk.Label(frame, text="マスターパスワード:",
                 bg="#f8f9fc").grid(row=0, column=0, sticky="w", pady=4)
        self.master_entry = ttk.Entry(frame, show="●", width=24,
                                       font=("Arial", 12))
        self.master_entry.grid(row=0, column=1, padx=8)

        if not has_data:
            tk.Label(frame, text="確認用:",
                     bg="#f8f9fc").grid(row=1, column=0, sticky="w", pady=4)
            self.master_confirm = ttk.Entry(frame, show="●", width=24,
                                             font=("Arial", 12))
            self.master_confirm.grid(row=1, column=1, padx=8)
        else:
            self.master_confirm = None

        btn_text = "ログイン" if has_data else "作成"
        ttk.Button(c, text=btn_text,
                   command=self._authenticate).pack(pady=12)
        self.master_entry.bind("<Return>", lambda e: self._authenticate())
        self.master_entry.focus_set()

    def _authenticate(self):
        pw = self.master_entry.get()
        if not pw:
            messagebox.showwarning("警告", "パスワードを入力してください")
            return

        has_data = os.path.exists(self.KEY_FILE)
        if has_data:
            # 認証
            with open(self.KEY_FILE, "r") as f:
                stored = json.load(f)
            hashed = hashlib.sha256((pw + stored["salt"]).encode()).hexdigest()
            if hashed != stored["hash"]:
                messagebox.showerror("エラー", "パスワードが正しくありません")
                return
            if CRYPTO_AVAILABLE:
                key = base64.urlsafe_b64encode(
                    hashlib.sha256(pw.encode()).digest())
                self.fernet = Fernet(key)
        else:
            # 新規作成
            if self.master_confirm and pw != self.master_confirm.get():
                messagebox.showerror("エラー", "パスワードが一致しません")
                return
            salt = secrets.token_hex(16)
            hashed = hashlib.sha256((pw + salt).encode()).hexdigest()
            with open(self.KEY_FILE, "w") as f:
                json.dump({"hash": hashed, "salt": salt}, f)
            if CRYPTO_AVAILABLE:
                key = base64.urlsafe_b64encode(
                    hashlib.sha256(pw.encode()).digest())
                self.fernet = Fernet(key)

        self._load_passwords()
        self._show_manager()

    def _show_manager(self):
        self.login_frame.pack_forget()
        self.list_frame.pack(fill=tk.BOTH, expand=True)
        for w in self.list_frame.winfo_children():
            w.destroy()

        paned = ttk.PanedWindow(self.list_frame, orient=tk.HORIZONTAL)
        paned.pack(fill=tk.BOTH, expand=True, padx=4, pady=4)

        # 左: 一覧
        left = tk.Frame(paned, bg="#f8f9fc")
        tk.Label(left, text="🔍 検索:").pack(anchor="w", padx=8)
        self.search_var = tk.StringVar()
        search_entry = ttk.Entry(left, textvariable=self.search_var, width=26)
        search_entry.pack(fill=tk.X, padx=8, pady=2)
        self.search_var.trace_add("write", lambda *a: self._refresh_list())

        self.lb = tk.Listbox(left, font=("Arial", 11),
                              selectbackground="#c62828",
                              selectforeground="white")
        sb = ttk.Scrollbar(left, command=self.lb.yview)
        self.lb.configure(yscrollcommand=sb.set)
        sb.pack(side=tk.RIGHT, fill=tk.Y)
        self.lb.pack(fill=tk.BOTH, expand=True, padx=8, pady=4)
        self.lb.bind("<<ListboxSelect>>", self._on_select)
        paned.add(left, weight=1)

        # 右: 詳細・編集
        right = ttk.LabelFrame(paned, text="詳細・編集", padding=12)
        for lbl, attr in [("サービス名", "svc"), ("ユーザー名", "usr"),
                           ("パスワード", "pwd"), ("URL", "url"),
                           ("メモ", "memo")]:
            tk.Label(right, text=f"{lbl}:").pack(anchor="w")
            if lbl == "メモ":
                var = tk.Text(right, height=3, width=28, font=("Arial", 11))
                var.pack(fill=tk.X, pady=2)
                setattr(self, f"{attr}_text", var)
            else:
                var = tk.StringVar()
                entry = ttk.Entry(right, textvariable=var, width=28,
                                  show="●" if lbl == "パスワード" else "")
                entry.pack(fill=tk.X, pady=2)
                setattr(self, f"{attr}_var", var)
                if lbl == "パスワード":
                    self.pwd_entry = entry
                    btn_f = tk.Frame(right, bg=right.cget("background"))
                    btn_f.pack(fill=tk.X)
                    ttk.Button(btn_f, text="👁 表示",
                               command=self._toggle_pwd).pack(side=tk.LEFT, padx=2)
                    ttk.Button(btn_f, text="🎲 生成",
                               command=self._gen_password).pack(side=tk.LEFT, padx=2)
                    ttk.Button(btn_f, text="📋 コピー",
                               command=self._copy_pwd).pack(side=tk.LEFT, padx=2)

        btn_frame = tk.Frame(right, bg=right.cget("background"))
        btn_frame.pack(fill=tk.X, pady=8)
        for text, cmd in [("➕ 追加", self._add_entry),
                           ("✏️ 更新", self._update_entry),
                           ("🗑️ 削除", self._delete_entry)]:
            ttk.Button(btn_frame, text=text, command=cmd).pack(
                side=tk.LEFT, padx=4)
        paned.add(right, weight=1)

        self._refresh_list()
        self.status_var.set(f"{len(self.entries)} 件")

    def _load_passwords(self):
        self.entries = []
        if not os.path.exists(self.DATA_FILE):
            return
        try:
            with open(self.DATA_FILE, "rb") as f:
                raw = f.read()
            if CRYPTO_AVAILABLE and self.fernet:
                raw = self.fernet.decrypt(raw)
            self.entries = json.loads(raw)
        except Exception:
            pass

    def _save_passwords(self):
        raw = json.dumps(self.entries, ensure_ascii=False).encode()
        if CRYPTO_AVAILABLE and self.fernet:
            raw = self.fernet.encrypt(raw)
        with open(self.DATA_FILE, "wb") as f:
            f.write(raw)

    def _refresh_list(self):
        q = self.search_var.get().lower()
        self.lb.delete(0, tk.END)
        self._filtered = [e for e in self.entries
                          if q in e.get("svc", "").lower()
                          or q in e.get("usr", "").lower()]
        for e in self._filtered:
            self.lb.insert(tk.END, f"  {e.get('svc', '')}  ({e.get('usr', '')})")

    def _on_select(self, event):
        sel = self.lb.curselection()
        if not sel:
            return
        idx = sel[0]
        if idx < len(self._filtered):
            e = self._filtered[idx]
            self.svc_var.set(e.get("svc", ""))
            self.usr_var.set(e.get("usr", ""))
            self.pwd_var.set(e.get("pwd", ""))
            self.url_var.set(e.get("url", ""))
            self.memo_text.delete("1.0", tk.END)
            self.memo_text.insert("1.0", e.get("memo", ""))
            self._selected_idx = self.entries.index(e)

    def _add_entry(self):
        svc = self.svc_var.get().strip()
        if not svc:
            messagebox.showwarning("警告", "サービス名を入力してください")
            return
        entry = {
            "svc": svc, "usr": self.usr_var.get(),
            "pwd": self.pwd_var.get(), "url": self.url_var.get(),
            "memo": self.memo_text.get("1.0", tk.END).strip()
        }
        self.entries.append(entry)
        self._save_passwords()
        self._refresh_list()
        self.status_var.set(f"{len(self.entries)} 件")

    def _update_entry(self):
        if not hasattr(self, "_selected_idx"):
            return
        self.entries[self._selected_idx] = {
            "svc": self.svc_var.get(), "usr": self.usr_var.get(),
            "pwd": self.pwd_var.get(), "url": self.url_var.get(),
            "memo": self.memo_text.get("1.0", tk.END).strip()
        }
        self._save_passwords()
        self._refresh_list()

    def _delete_entry(self):
        if not hasattr(self, "_selected_idx"):
            return
        if messagebox.askyesno("確認", "このエントリを削除しますか?"):
            self.entries.pop(self._selected_idx)
            self._save_passwords()
            self._refresh_list()
            self.status_var.set(f"{len(self.entries)} 件")

    def _toggle_pwd(self):
        show = self.pwd_entry.cget("show")
        self.pwd_entry.config(show="" if show else "●")

    def _gen_password(self):
        chars = string.ascii_letters + string.digits + "!@#$%&"
        pwd = "".join(secrets.choice(chars) for _ in range(16))
        self.pwd_var.set(pwd)

    def _copy_pwd(self):
        self.root.clipboard_clear()
        self.root.clipboard_append(self.pwd_var.get())
        self.status_var.set("パスワードをクリップボードにコピーしました")

    def _lock(self):
        self.fernet = None
        self.entries = []
        self._show_login()


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

Entryウィジェットとイベントバインド

ttk.Entryで入力フィールドを作成します。bind('', ...)でEnterキー押下時に処理を実行できます。これにより、マウスを使わずキーボードだけで操作できるUXが実現できます。

import tkinter as tk
from tkinter import ttk, messagebox
import json
import os
import hashlib
import base64
import secrets
import string

try:
    from cryptography.fernet import Fernet
    CRYPTO_AVAILABLE = True
except ImportError:
    CRYPTO_AVAILABLE = False


class App11:
    """パスワードマネージャー"""

    DATA_FILE = os.path.join(os.path.dirname(__file__), "passwords.enc")
    KEY_FILE = os.path.join(os.path.dirname(__file__), "passwords.key")

    def __init__(self, root):
        self.root = root
        self.root.title("パスワードマネージャー")
        self.root.geometry("780x540")
        self.root.configure(bg="#f8f9fc")
        self.fernet = None
        self.entries = []
        self.master_hash = None
        self._build_ui()
        self._check_crypto()

    def _check_crypto(self):
        if not CRYPTO_AVAILABLE:
            messagebox.showwarning(
                "ライブラリ未インストール",
                "cryptography が必要です。\n"
                "pip install cryptography でインストールしてください。\n\n"
                "デモモード(暗号化なし)で動作します。")
        self._show_login()

    def _build_ui(self):
        title_frame = tk.Frame(self.root, bg="#c62828", pady=10)
        title_frame.pack(fill=tk.X)
        tk.Label(title_frame, text="🔒 パスワードマネージャー",
                 font=("Noto Sans JP", 15, "bold"),
                 bg="#c62828", fg="white").pack(side=tk.LEFT, padx=12)
        self.lock_btn = ttk.Button(title_frame, text="🔒 ロック",
                                   command=self._lock)
        self.lock_btn.pack(side=tk.RIGHT, padx=12)

        # メインコンテンツ
        self.main_frame = tk.Frame(self.root, bg="#f8f9fc")
        self.main_frame.pack(fill=tk.BOTH, expand=True)

        # ログイン/セットアップフレーム
        self.login_frame = tk.Frame(self.main_frame, bg="#f8f9fc")

        # パスワード一覧フレーム
        self.list_frame = tk.Frame(self.main_frame, bg="#f8f9fc")

        self.status_var = tk.StringVar(value="")
        tk.Label(self.root, textvariable=self.status_var,
                 bg="#dde", font=("Arial", 9), anchor="w", padx=8
                 ).pack(fill=tk.X, side=tk.BOTTOM)

    def _show_login(self):
        self.list_frame.pack_forget()
        self.login_frame.pack(fill=tk.BOTH, expand=True)
        for w in self.login_frame.winfo_children():
            w.destroy()

        c = tk.Frame(self.login_frame, bg="#f8f9fc")
        c.place(relx=0.5, rely=0.4, anchor="center")

        has_data = os.path.exists(self.KEY_FILE)
        title = "マスターパスワードを入力" if has_data else "マスターパスワードを設定"
        tk.Label(c, text=title, font=("Noto Sans JP", 14, "bold"),
                 bg="#f8f9fc").pack(pady=8)

        frame = tk.Frame(c, bg="#f8f9fc")
        frame.pack()
        tk.Label(frame, text="マスターパスワード:",
                 bg="#f8f9fc").grid(row=0, column=0, sticky="w", pady=4)
        self.master_entry = ttk.Entry(frame, show="●", width=24,
                                       font=("Arial", 12))
        self.master_entry.grid(row=0, column=1, padx=8)

        if not has_data:
            tk.Label(frame, text="確認用:",
                     bg="#f8f9fc").grid(row=1, column=0, sticky="w", pady=4)
            self.master_confirm = ttk.Entry(frame, show="●", width=24,
                                             font=("Arial", 12))
            self.master_confirm.grid(row=1, column=1, padx=8)
        else:
            self.master_confirm = None

        btn_text = "ログイン" if has_data else "作成"
        ttk.Button(c, text=btn_text,
                   command=self._authenticate).pack(pady=12)
        self.master_entry.bind("<Return>", lambda e: self._authenticate())
        self.master_entry.focus_set()

    def _authenticate(self):
        pw = self.master_entry.get()
        if not pw:
            messagebox.showwarning("警告", "パスワードを入力してください")
            return

        has_data = os.path.exists(self.KEY_FILE)
        if has_data:
            # 認証
            with open(self.KEY_FILE, "r") as f:
                stored = json.load(f)
            hashed = hashlib.sha256((pw + stored["salt"]).encode()).hexdigest()
            if hashed != stored["hash"]:
                messagebox.showerror("エラー", "パスワードが正しくありません")
                return
            if CRYPTO_AVAILABLE:
                key = base64.urlsafe_b64encode(
                    hashlib.sha256(pw.encode()).digest())
                self.fernet = Fernet(key)
        else:
            # 新規作成
            if self.master_confirm and pw != self.master_confirm.get():
                messagebox.showerror("エラー", "パスワードが一致しません")
                return
            salt = secrets.token_hex(16)
            hashed = hashlib.sha256((pw + salt).encode()).hexdigest()
            with open(self.KEY_FILE, "w") as f:
                json.dump({"hash": hashed, "salt": salt}, f)
            if CRYPTO_AVAILABLE:
                key = base64.urlsafe_b64encode(
                    hashlib.sha256(pw.encode()).digest())
                self.fernet = Fernet(key)

        self._load_passwords()
        self._show_manager()

    def _show_manager(self):
        self.login_frame.pack_forget()
        self.list_frame.pack(fill=tk.BOTH, expand=True)
        for w in self.list_frame.winfo_children():
            w.destroy()

        paned = ttk.PanedWindow(self.list_frame, orient=tk.HORIZONTAL)
        paned.pack(fill=tk.BOTH, expand=True, padx=4, pady=4)

        # 左: 一覧
        left = tk.Frame(paned, bg="#f8f9fc")
        tk.Label(left, text="🔍 検索:").pack(anchor="w", padx=8)
        self.search_var = tk.StringVar()
        search_entry = ttk.Entry(left, textvariable=self.search_var, width=26)
        search_entry.pack(fill=tk.X, padx=8, pady=2)
        self.search_var.trace_add("write", lambda *a: self._refresh_list())

        self.lb = tk.Listbox(left, font=("Arial", 11),
                              selectbackground="#c62828",
                              selectforeground="white")
        sb = ttk.Scrollbar(left, command=self.lb.yview)
        self.lb.configure(yscrollcommand=sb.set)
        sb.pack(side=tk.RIGHT, fill=tk.Y)
        self.lb.pack(fill=tk.BOTH, expand=True, padx=8, pady=4)
        self.lb.bind("<<ListboxSelect>>", self._on_select)
        paned.add(left, weight=1)

        # 右: 詳細・編集
        right = ttk.LabelFrame(paned, text="詳細・編集", padding=12)
        for lbl, attr in [("サービス名", "svc"), ("ユーザー名", "usr"),
                           ("パスワード", "pwd"), ("URL", "url"),
                           ("メモ", "memo")]:
            tk.Label(right, text=f"{lbl}:").pack(anchor="w")
            if lbl == "メモ":
                var = tk.Text(right, height=3, width=28, font=("Arial", 11))
                var.pack(fill=tk.X, pady=2)
                setattr(self, f"{attr}_text", var)
            else:
                var = tk.StringVar()
                entry = ttk.Entry(right, textvariable=var, width=28,
                                  show="●" if lbl == "パスワード" else "")
                entry.pack(fill=tk.X, pady=2)
                setattr(self, f"{attr}_var", var)
                if lbl == "パスワード":
                    self.pwd_entry = entry
                    btn_f = tk.Frame(right, bg=right.cget("background"))
                    btn_f.pack(fill=tk.X)
                    ttk.Button(btn_f, text="👁 表示",
                               command=self._toggle_pwd).pack(side=tk.LEFT, padx=2)
                    ttk.Button(btn_f, text="🎲 生成",
                               command=self._gen_password).pack(side=tk.LEFT, padx=2)
                    ttk.Button(btn_f, text="📋 コピー",
                               command=self._copy_pwd).pack(side=tk.LEFT, padx=2)

        btn_frame = tk.Frame(right, bg=right.cget("background"))
        btn_frame.pack(fill=tk.X, pady=8)
        for text, cmd in [("➕ 追加", self._add_entry),
                           ("✏️ 更新", self._update_entry),
                           ("🗑️ 削除", self._delete_entry)]:
            ttk.Button(btn_frame, text=text, command=cmd).pack(
                side=tk.LEFT, padx=4)
        paned.add(right, weight=1)

        self._refresh_list()
        self.status_var.set(f"{len(self.entries)} 件")

    def _load_passwords(self):
        self.entries = []
        if not os.path.exists(self.DATA_FILE):
            return
        try:
            with open(self.DATA_FILE, "rb") as f:
                raw = f.read()
            if CRYPTO_AVAILABLE and self.fernet:
                raw = self.fernet.decrypt(raw)
            self.entries = json.loads(raw)
        except Exception:
            pass

    def _save_passwords(self):
        raw = json.dumps(self.entries, ensure_ascii=False).encode()
        if CRYPTO_AVAILABLE and self.fernet:
            raw = self.fernet.encrypt(raw)
        with open(self.DATA_FILE, "wb") as f:
            f.write(raw)

    def _refresh_list(self):
        q = self.search_var.get().lower()
        self.lb.delete(0, tk.END)
        self._filtered = [e for e in self.entries
                          if q in e.get("svc", "").lower()
                          or q in e.get("usr", "").lower()]
        for e in self._filtered:
            self.lb.insert(tk.END, f"  {e.get('svc', '')}  ({e.get('usr', '')})")

    def _on_select(self, event):
        sel = self.lb.curselection()
        if not sel:
            return
        idx = sel[0]
        if idx < len(self._filtered):
            e = self._filtered[idx]
            self.svc_var.set(e.get("svc", ""))
            self.usr_var.set(e.get("usr", ""))
            self.pwd_var.set(e.get("pwd", ""))
            self.url_var.set(e.get("url", ""))
            self.memo_text.delete("1.0", tk.END)
            self.memo_text.insert("1.0", e.get("memo", ""))
            self._selected_idx = self.entries.index(e)

    def _add_entry(self):
        svc = self.svc_var.get().strip()
        if not svc:
            messagebox.showwarning("警告", "サービス名を入力してください")
            return
        entry = {
            "svc": svc, "usr": self.usr_var.get(),
            "pwd": self.pwd_var.get(), "url": self.url_var.get(),
            "memo": self.memo_text.get("1.0", tk.END).strip()
        }
        self.entries.append(entry)
        self._save_passwords()
        self._refresh_list()
        self.status_var.set(f"{len(self.entries)} 件")

    def _update_entry(self):
        if not hasattr(self, "_selected_idx"):
            return
        self.entries[self._selected_idx] = {
            "svc": self.svc_var.get(), "usr": self.usr_var.get(),
            "pwd": self.pwd_var.get(), "url": self.url_var.get(),
            "memo": self.memo_text.get("1.0", tk.END).strip()
        }
        self._save_passwords()
        self._refresh_list()

    def _delete_entry(self):
        if not hasattr(self, "_selected_idx"):
            return
        if messagebox.askyesno("確認", "このエントリを削除しますか?"):
            self.entries.pop(self._selected_idx)
            self._save_passwords()
            self._refresh_list()
            self.status_var.set(f"{len(self.entries)} 件")

    def _toggle_pwd(self):
        show = self.pwd_entry.cget("show")
        self.pwd_entry.config(show="" if show else "●")

    def _gen_password(self):
        chars = string.ascii_letters + string.digits + "!@#$%&"
        pwd = "".join(secrets.choice(chars) for _ in range(16))
        self.pwd_var.set(pwd)

    def _copy_pwd(self):
        self.root.clipboard_clear()
        self.root.clipboard_append(self.pwd_var.get())
        self.status_var.set("パスワードをクリップボードにコピーしました")

    def _lock(self):
        self.fernet = None
        self.entries = []
        self._show_login()


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

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

結果表示にはtk.Textウィジェットを使います。state=tk.DISABLEDでユーザーが直接編集できないようにし、表示前にNORMALに切り替えてからinsert()で内容を更新します。

import tkinter as tk
from tkinter import ttk, messagebox
import json
import os
import hashlib
import base64
import secrets
import string

try:
    from cryptography.fernet import Fernet
    CRYPTO_AVAILABLE = True
except ImportError:
    CRYPTO_AVAILABLE = False


class App11:
    """パスワードマネージャー"""

    DATA_FILE = os.path.join(os.path.dirname(__file__), "passwords.enc")
    KEY_FILE = os.path.join(os.path.dirname(__file__), "passwords.key")

    def __init__(self, root):
        self.root = root
        self.root.title("パスワードマネージャー")
        self.root.geometry("780x540")
        self.root.configure(bg="#f8f9fc")
        self.fernet = None
        self.entries = []
        self.master_hash = None
        self._build_ui()
        self._check_crypto()

    def _check_crypto(self):
        if not CRYPTO_AVAILABLE:
            messagebox.showwarning(
                "ライブラリ未インストール",
                "cryptography が必要です。\n"
                "pip install cryptography でインストールしてください。\n\n"
                "デモモード(暗号化なし)で動作します。")
        self._show_login()

    def _build_ui(self):
        title_frame = tk.Frame(self.root, bg="#c62828", pady=10)
        title_frame.pack(fill=tk.X)
        tk.Label(title_frame, text="🔒 パスワードマネージャー",
                 font=("Noto Sans JP", 15, "bold"),
                 bg="#c62828", fg="white").pack(side=tk.LEFT, padx=12)
        self.lock_btn = ttk.Button(title_frame, text="🔒 ロック",
                                   command=self._lock)
        self.lock_btn.pack(side=tk.RIGHT, padx=12)

        # メインコンテンツ
        self.main_frame = tk.Frame(self.root, bg="#f8f9fc")
        self.main_frame.pack(fill=tk.BOTH, expand=True)

        # ログイン/セットアップフレーム
        self.login_frame = tk.Frame(self.main_frame, bg="#f8f9fc")

        # パスワード一覧フレーム
        self.list_frame = tk.Frame(self.main_frame, bg="#f8f9fc")

        self.status_var = tk.StringVar(value="")
        tk.Label(self.root, textvariable=self.status_var,
                 bg="#dde", font=("Arial", 9), anchor="w", padx=8
                 ).pack(fill=tk.X, side=tk.BOTTOM)

    def _show_login(self):
        self.list_frame.pack_forget()
        self.login_frame.pack(fill=tk.BOTH, expand=True)
        for w in self.login_frame.winfo_children():
            w.destroy()

        c = tk.Frame(self.login_frame, bg="#f8f9fc")
        c.place(relx=0.5, rely=0.4, anchor="center")

        has_data = os.path.exists(self.KEY_FILE)
        title = "マスターパスワードを入力" if has_data else "マスターパスワードを設定"
        tk.Label(c, text=title, font=("Noto Sans JP", 14, "bold"),
                 bg="#f8f9fc").pack(pady=8)

        frame = tk.Frame(c, bg="#f8f9fc")
        frame.pack()
        tk.Label(frame, text="マスターパスワード:",
                 bg="#f8f9fc").grid(row=0, column=0, sticky="w", pady=4)
        self.master_entry = ttk.Entry(frame, show="●", width=24,
                                       font=("Arial", 12))
        self.master_entry.grid(row=0, column=1, padx=8)

        if not has_data:
            tk.Label(frame, text="確認用:",
                     bg="#f8f9fc").grid(row=1, column=0, sticky="w", pady=4)
            self.master_confirm = ttk.Entry(frame, show="●", width=24,
                                             font=("Arial", 12))
            self.master_confirm.grid(row=1, column=1, padx=8)
        else:
            self.master_confirm = None

        btn_text = "ログイン" if has_data else "作成"
        ttk.Button(c, text=btn_text,
                   command=self._authenticate).pack(pady=12)
        self.master_entry.bind("<Return>", lambda e: self._authenticate())
        self.master_entry.focus_set()

    def _authenticate(self):
        pw = self.master_entry.get()
        if not pw:
            messagebox.showwarning("警告", "パスワードを入力してください")
            return

        has_data = os.path.exists(self.KEY_FILE)
        if has_data:
            # 認証
            with open(self.KEY_FILE, "r") as f:
                stored = json.load(f)
            hashed = hashlib.sha256((pw + stored["salt"]).encode()).hexdigest()
            if hashed != stored["hash"]:
                messagebox.showerror("エラー", "パスワードが正しくありません")
                return
            if CRYPTO_AVAILABLE:
                key = base64.urlsafe_b64encode(
                    hashlib.sha256(pw.encode()).digest())
                self.fernet = Fernet(key)
        else:
            # 新規作成
            if self.master_confirm and pw != self.master_confirm.get():
                messagebox.showerror("エラー", "パスワードが一致しません")
                return
            salt = secrets.token_hex(16)
            hashed = hashlib.sha256((pw + salt).encode()).hexdigest()
            with open(self.KEY_FILE, "w") as f:
                json.dump({"hash": hashed, "salt": salt}, f)
            if CRYPTO_AVAILABLE:
                key = base64.urlsafe_b64encode(
                    hashlib.sha256(pw.encode()).digest())
                self.fernet = Fernet(key)

        self._load_passwords()
        self._show_manager()

    def _show_manager(self):
        self.login_frame.pack_forget()
        self.list_frame.pack(fill=tk.BOTH, expand=True)
        for w in self.list_frame.winfo_children():
            w.destroy()

        paned = ttk.PanedWindow(self.list_frame, orient=tk.HORIZONTAL)
        paned.pack(fill=tk.BOTH, expand=True, padx=4, pady=4)

        # 左: 一覧
        left = tk.Frame(paned, bg="#f8f9fc")
        tk.Label(left, text="🔍 検索:").pack(anchor="w", padx=8)
        self.search_var = tk.StringVar()
        search_entry = ttk.Entry(left, textvariable=self.search_var, width=26)
        search_entry.pack(fill=tk.X, padx=8, pady=2)
        self.search_var.trace_add("write", lambda *a: self._refresh_list())

        self.lb = tk.Listbox(left, font=("Arial", 11),
                              selectbackground="#c62828",
                              selectforeground="white")
        sb = ttk.Scrollbar(left, command=self.lb.yview)
        self.lb.configure(yscrollcommand=sb.set)
        sb.pack(side=tk.RIGHT, fill=tk.Y)
        self.lb.pack(fill=tk.BOTH, expand=True, padx=8, pady=4)
        self.lb.bind("<<ListboxSelect>>", self._on_select)
        paned.add(left, weight=1)

        # 右: 詳細・編集
        right = ttk.LabelFrame(paned, text="詳細・編集", padding=12)
        for lbl, attr in [("サービス名", "svc"), ("ユーザー名", "usr"),
                           ("パスワード", "pwd"), ("URL", "url"),
                           ("メモ", "memo")]:
            tk.Label(right, text=f"{lbl}:").pack(anchor="w")
            if lbl == "メモ":
                var = tk.Text(right, height=3, width=28, font=("Arial", 11))
                var.pack(fill=tk.X, pady=2)
                setattr(self, f"{attr}_text", var)
            else:
                var = tk.StringVar()
                entry = ttk.Entry(right, textvariable=var, width=28,
                                  show="●" if lbl == "パスワード" else "")
                entry.pack(fill=tk.X, pady=2)
                setattr(self, f"{attr}_var", var)
                if lbl == "パスワード":
                    self.pwd_entry = entry
                    btn_f = tk.Frame(right, bg=right.cget("background"))
                    btn_f.pack(fill=tk.X)
                    ttk.Button(btn_f, text="👁 表示",
                               command=self._toggle_pwd).pack(side=tk.LEFT, padx=2)
                    ttk.Button(btn_f, text="🎲 生成",
                               command=self._gen_password).pack(side=tk.LEFT, padx=2)
                    ttk.Button(btn_f, text="📋 コピー",
                               command=self._copy_pwd).pack(side=tk.LEFT, padx=2)

        btn_frame = tk.Frame(right, bg=right.cget("background"))
        btn_frame.pack(fill=tk.X, pady=8)
        for text, cmd in [("➕ 追加", self._add_entry),
                           ("✏️ 更新", self._update_entry),
                           ("🗑️ 削除", self._delete_entry)]:
            ttk.Button(btn_frame, text=text, command=cmd).pack(
                side=tk.LEFT, padx=4)
        paned.add(right, weight=1)

        self._refresh_list()
        self.status_var.set(f"{len(self.entries)} 件")

    def _load_passwords(self):
        self.entries = []
        if not os.path.exists(self.DATA_FILE):
            return
        try:
            with open(self.DATA_FILE, "rb") as f:
                raw = f.read()
            if CRYPTO_AVAILABLE and self.fernet:
                raw = self.fernet.decrypt(raw)
            self.entries = json.loads(raw)
        except Exception:
            pass

    def _save_passwords(self):
        raw = json.dumps(self.entries, ensure_ascii=False).encode()
        if CRYPTO_AVAILABLE and self.fernet:
            raw = self.fernet.encrypt(raw)
        with open(self.DATA_FILE, "wb") as f:
            f.write(raw)

    def _refresh_list(self):
        q = self.search_var.get().lower()
        self.lb.delete(0, tk.END)
        self._filtered = [e for e in self.entries
                          if q in e.get("svc", "").lower()
                          or q in e.get("usr", "").lower()]
        for e in self._filtered:
            self.lb.insert(tk.END, f"  {e.get('svc', '')}  ({e.get('usr', '')})")

    def _on_select(self, event):
        sel = self.lb.curselection()
        if not sel:
            return
        idx = sel[0]
        if idx < len(self._filtered):
            e = self._filtered[idx]
            self.svc_var.set(e.get("svc", ""))
            self.usr_var.set(e.get("usr", ""))
            self.pwd_var.set(e.get("pwd", ""))
            self.url_var.set(e.get("url", ""))
            self.memo_text.delete("1.0", tk.END)
            self.memo_text.insert("1.0", e.get("memo", ""))
            self._selected_idx = self.entries.index(e)

    def _add_entry(self):
        svc = self.svc_var.get().strip()
        if not svc:
            messagebox.showwarning("警告", "サービス名を入力してください")
            return
        entry = {
            "svc": svc, "usr": self.usr_var.get(),
            "pwd": self.pwd_var.get(), "url": self.url_var.get(),
            "memo": self.memo_text.get("1.0", tk.END).strip()
        }
        self.entries.append(entry)
        self._save_passwords()
        self._refresh_list()
        self.status_var.set(f"{len(self.entries)} 件")

    def _update_entry(self):
        if not hasattr(self, "_selected_idx"):
            return
        self.entries[self._selected_idx] = {
            "svc": self.svc_var.get(), "usr": self.usr_var.get(),
            "pwd": self.pwd_var.get(), "url": self.url_var.get(),
            "memo": self.memo_text.get("1.0", tk.END).strip()
        }
        self._save_passwords()
        self._refresh_list()

    def _delete_entry(self):
        if not hasattr(self, "_selected_idx"):
            return
        if messagebox.askyesno("確認", "このエントリを削除しますか?"):
            self.entries.pop(self._selected_idx)
            self._save_passwords()
            self._refresh_list()
            self.status_var.set(f"{len(self.entries)} 件")

    def _toggle_pwd(self):
        show = self.pwd_entry.cget("show")
        self.pwd_entry.config(show="" if show else "●")

    def _gen_password(self):
        chars = string.ascii_letters + string.digits + "!@#$%&"
        pwd = "".join(secrets.choice(chars) for _ in range(16))
        self.pwd_var.set(pwd)

    def _copy_pwd(self):
        self.root.clipboard_clear()
        self.root.clipboard_append(self.pwd_var.get())
        self.status_var.set("パスワードをクリップボードにコピーしました")

    def _lock(self):
        self.fernet = None
        self.entries = []
        self._show_login()


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

例外処理とmessagebox

try-except で ValueError と Exception を捕捉し、messagebox.showerror() でユーザーにわかりやすいエラーメッセージを表示します。入力バリデーションは必ず実装しましょう。

import tkinter as tk
from tkinter import ttk, messagebox
import json
import os
import hashlib
import base64
import secrets
import string

try:
    from cryptography.fernet import Fernet
    CRYPTO_AVAILABLE = True
except ImportError:
    CRYPTO_AVAILABLE = False


class App11:
    """パスワードマネージャー"""

    DATA_FILE = os.path.join(os.path.dirname(__file__), "passwords.enc")
    KEY_FILE = os.path.join(os.path.dirname(__file__), "passwords.key")

    def __init__(self, root):
        self.root = root
        self.root.title("パスワードマネージャー")
        self.root.geometry("780x540")
        self.root.configure(bg="#f8f9fc")
        self.fernet = None
        self.entries = []
        self.master_hash = None
        self._build_ui()
        self._check_crypto()

    def _check_crypto(self):
        if not CRYPTO_AVAILABLE:
            messagebox.showwarning(
                "ライブラリ未インストール",
                "cryptography が必要です。\n"
                "pip install cryptography でインストールしてください。\n\n"
                "デモモード(暗号化なし)で動作します。")
        self._show_login()

    def _build_ui(self):
        title_frame = tk.Frame(self.root, bg="#c62828", pady=10)
        title_frame.pack(fill=tk.X)
        tk.Label(title_frame, text="🔒 パスワードマネージャー",
                 font=("Noto Sans JP", 15, "bold"),
                 bg="#c62828", fg="white").pack(side=tk.LEFT, padx=12)
        self.lock_btn = ttk.Button(title_frame, text="🔒 ロック",
                                   command=self._lock)
        self.lock_btn.pack(side=tk.RIGHT, padx=12)

        # メインコンテンツ
        self.main_frame = tk.Frame(self.root, bg="#f8f9fc")
        self.main_frame.pack(fill=tk.BOTH, expand=True)

        # ログイン/セットアップフレーム
        self.login_frame = tk.Frame(self.main_frame, bg="#f8f9fc")

        # パスワード一覧フレーム
        self.list_frame = tk.Frame(self.main_frame, bg="#f8f9fc")

        self.status_var = tk.StringVar(value="")
        tk.Label(self.root, textvariable=self.status_var,
                 bg="#dde", font=("Arial", 9), anchor="w", padx=8
                 ).pack(fill=tk.X, side=tk.BOTTOM)

    def _show_login(self):
        self.list_frame.pack_forget()
        self.login_frame.pack(fill=tk.BOTH, expand=True)
        for w in self.login_frame.winfo_children():
            w.destroy()

        c = tk.Frame(self.login_frame, bg="#f8f9fc")
        c.place(relx=0.5, rely=0.4, anchor="center")

        has_data = os.path.exists(self.KEY_FILE)
        title = "マスターパスワードを入力" if has_data else "マスターパスワードを設定"
        tk.Label(c, text=title, font=("Noto Sans JP", 14, "bold"),
                 bg="#f8f9fc").pack(pady=8)

        frame = tk.Frame(c, bg="#f8f9fc")
        frame.pack()
        tk.Label(frame, text="マスターパスワード:",
                 bg="#f8f9fc").grid(row=0, column=0, sticky="w", pady=4)
        self.master_entry = ttk.Entry(frame, show="●", width=24,
                                       font=("Arial", 12))
        self.master_entry.grid(row=0, column=1, padx=8)

        if not has_data:
            tk.Label(frame, text="確認用:",
                     bg="#f8f9fc").grid(row=1, column=0, sticky="w", pady=4)
            self.master_confirm = ttk.Entry(frame, show="●", width=24,
                                             font=("Arial", 12))
            self.master_confirm.grid(row=1, column=1, padx=8)
        else:
            self.master_confirm = None

        btn_text = "ログイン" if has_data else "作成"
        ttk.Button(c, text=btn_text,
                   command=self._authenticate).pack(pady=12)
        self.master_entry.bind("<Return>", lambda e: self._authenticate())
        self.master_entry.focus_set()

    def _authenticate(self):
        pw = self.master_entry.get()
        if not pw:
            messagebox.showwarning("警告", "パスワードを入力してください")
            return

        has_data = os.path.exists(self.KEY_FILE)
        if has_data:
            # 認証
            with open(self.KEY_FILE, "r") as f:
                stored = json.load(f)
            hashed = hashlib.sha256((pw + stored["salt"]).encode()).hexdigest()
            if hashed != stored["hash"]:
                messagebox.showerror("エラー", "パスワードが正しくありません")
                return
            if CRYPTO_AVAILABLE:
                key = base64.urlsafe_b64encode(
                    hashlib.sha256(pw.encode()).digest())
                self.fernet = Fernet(key)
        else:
            # 新規作成
            if self.master_confirm and pw != self.master_confirm.get():
                messagebox.showerror("エラー", "パスワードが一致しません")
                return
            salt = secrets.token_hex(16)
            hashed = hashlib.sha256((pw + salt).encode()).hexdigest()
            with open(self.KEY_FILE, "w") as f:
                json.dump({"hash": hashed, "salt": salt}, f)
            if CRYPTO_AVAILABLE:
                key = base64.urlsafe_b64encode(
                    hashlib.sha256(pw.encode()).digest())
                self.fernet = Fernet(key)

        self._load_passwords()
        self._show_manager()

    def _show_manager(self):
        self.login_frame.pack_forget()
        self.list_frame.pack(fill=tk.BOTH, expand=True)
        for w in self.list_frame.winfo_children():
            w.destroy()

        paned = ttk.PanedWindow(self.list_frame, orient=tk.HORIZONTAL)
        paned.pack(fill=tk.BOTH, expand=True, padx=4, pady=4)

        # 左: 一覧
        left = tk.Frame(paned, bg="#f8f9fc")
        tk.Label(left, text="🔍 検索:").pack(anchor="w", padx=8)
        self.search_var = tk.StringVar()
        search_entry = ttk.Entry(left, textvariable=self.search_var, width=26)
        search_entry.pack(fill=tk.X, padx=8, pady=2)
        self.search_var.trace_add("write", lambda *a: self._refresh_list())

        self.lb = tk.Listbox(left, font=("Arial", 11),
                              selectbackground="#c62828",
                              selectforeground="white")
        sb = ttk.Scrollbar(left, command=self.lb.yview)
        self.lb.configure(yscrollcommand=sb.set)
        sb.pack(side=tk.RIGHT, fill=tk.Y)
        self.lb.pack(fill=tk.BOTH, expand=True, padx=8, pady=4)
        self.lb.bind("<<ListboxSelect>>", self._on_select)
        paned.add(left, weight=1)

        # 右: 詳細・編集
        right = ttk.LabelFrame(paned, text="詳細・編集", padding=12)
        for lbl, attr in [("サービス名", "svc"), ("ユーザー名", "usr"),
                           ("パスワード", "pwd"), ("URL", "url"),
                           ("メモ", "memo")]:
            tk.Label(right, text=f"{lbl}:").pack(anchor="w")
            if lbl == "メモ":
                var = tk.Text(right, height=3, width=28, font=("Arial", 11))
                var.pack(fill=tk.X, pady=2)
                setattr(self, f"{attr}_text", var)
            else:
                var = tk.StringVar()
                entry = ttk.Entry(right, textvariable=var, width=28,
                                  show="●" if lbl == "パスワード" else "")
                entry.pack(fill=tk.X, pady=2)
                setattr(self, f"{attr}_var", var)
                if lbl == "パスワード":
                    self.pwd_entry = entry
                    btn_f = tk.Frame(right, bg=right.cget("background"))
                    btn_f.pack(fill=tk.X)
                    ttk.Button(btn_f, text="👁 表示",
                               command=self._toggle_pwd).pack(side=tk.LEFT, padx=2)
                    ttk.Button(btn_f, text="🎲 生成",
                               command=self._gen_password).pack(side=tk.LEFT, padx=2)
                    ttk.Button(btn_f, text="📋 コピー",
                               command=self._copy_pwd).pack(side=tk.LEFT, padx=2)

        btn_frame = tk.Frame(right, bg=right.cget("background"))
        btn_frame.pack(fill=tk.X, pady=8)
        for text, cmd in [("➕ 追加", self._add_entry),
                           ("✏️ 更新", self._update_entry),
                           ("🗑️ 削除", self._delete_entry)]:
            ttk.Button(btn_frame, text=text, command=cmd).pack(
                side=tk.LEFT, padx=4)
        paned.add(right, weight=1)

        self._refresh_list()
        self.status_var.set(f"{len(self.entries)} 件")

    def _load_passwords(self):
        self.entries = []
        if not os.path.exists(self.DATA_FILE):
            return
        try:
            with open(self.DATA_FILE, "rb") as f:
                raw = f.read()
            if CRYPTO_AVAILABLE and self.fernet:
                raw = self.fernet.decrypt(raw)
            self.entries = json.loads(raw)
        except Exception:
            pass

    def _save_passwords(self):
        raw = json.dumps(self.entries, ensure_ascii=False).encode()
        if CRYPTO_AVAILABLE and self.fernet:
            raw = self.fernet.encrypt(raw)
        with open(self.DATA_FILE, "wb") as f:
            f.write(raw)

    def _refresh_list(self):
        q = self.search_var.get().lower()
        self.lb.delete(0, tk.END)
        self._filtered = [e for e in self.entries
                          if q in e.get("svc", "").lower()
                          or q in e.get("usr", "").lower()]
        for e in self._filtered:
            self.lb.insert(tk.END, f"  {e.get('svc', '')}  ({e.get('usr', '')})")

    def _on_select(self, event):
        sel = self.lb.curselection()
        if not sel:
            return
        idx = sel[0]
        if idx < len(self._filtered):
            e = self._filtered[idx]
            self.svc_var.set(e.get("svc", ""))
            self.usr_var.set(e.get("usr", ""))
            self.pwd_var.set(e.get("pwd", ""))
            self.url_var.set(e.get("url", ""))
            self.memo_text.delete("1.0", tk.END)
            self.memo_text.insert("1.0", e.get("memo", ""))
            self._selected_idx = self.entries.index(e)

    def _add_entry(self):
        svc = self.svc_var.get().strip()
        if not svc:
            messagebox.showwarning("警告", "サービス名を入力してください")
            return
        entry = {
            "svc": svc, "usr": self.usr_var.get(),
            "pwd": self.pwd_var.get(), "url": self.url_var.get(),
            "memo": self.memo_text.get("1.0", tk.END).strip()
        }
        self.entries.append(entry)
        self._save_passwords()
        self._refresh_list()
        self.status_var.set(f"{len(self.entries)} 件")

    def _update_entry(self):
        if not hasattr(self, "_selected_idx"):
            return
        self.entries[self._selected_idx] = {
            "svc": self.svc_var.get(), "usr": self.usr_var.get(),
            "pwd": self.pwd_var.get(), "url": self.url_var.get(),
            "memo": self.memo_text.get("1.0", tk.END).strip()
        }
        self._save_passwords()
        self._refresh_list()

    def _delete_entry(self):
        if not hasattr(self, "_selected_idx"):
            return
        if messagebox.askyesno("確認", "このエントリを削除しますか?"):
            self.entries.pop(self._selected_idx)
            self._save_passwords()
            self._refresh_list()
            self.status_var.set(f"{len(self.entries)} 件")

    def _toggle_pwd(self):
        show = self.pwd_entry.cget("show")
        self.pwd_entry.config(show="" if show else "●")

    def _gen_password(self):
        chars = string.ascii_letters + string.digits + "!@#$%&"
        pwd = "".join(secrets.choice(chars) for _ in range(16))
        self.pwd_var.set(pwd)

    def _copy_pwd(self):
        self.root.clipboard_clear()
        self.root.clipboard_append(self.pwd_var.get())
        self.status_var.set("パスワードをクリップボードにコピーしました")

    def _lock(self):
        self.fernet = None
        self.entries = []
        self._show_login()


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

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

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

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

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

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

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

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

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

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

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

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

    _calculate()メソッドに計算・処理ロジックを実装します。

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

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

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

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

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

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

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

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

💡 データのエクスポート機能

計算結果をCSV・TXTファイルに保存するエクスポート機能を追加しましょう。filedialog.asksaveasfilename()でファイル保存ダイアログが使えます。

💡 入力履歴機能

以前の入力値を覚えておいてComboboxのドロップダウンで再選択できる履歴機能を追加しましょう。

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

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

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

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

❌ ウィンドウのサイズが変更できない

原因:resizable(False, False)が設定されています。

解決法:resizable(True, True)に変更してください。

9. 練習問題

アプリの理解を深めるための練習問題です。難易度順に挑戦してみてください。

  1. 課題1:機能拡張

    パスワードマネージャーに新しい機能を1つ追加してみましょう。どんな機能があると便利か考えてから実装してください。

  2. 課題2:UIの改善

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

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

    入力値や計算結果をファイルに保存する機能を追加しましょう。jsonやcsvモジュールを使います。

🚀
次に挑戦するアプリ

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