初心者向け No.37

フォントビューアー

インストール済みフォントを一覧表示してプレビューできるアプリ。tkinter.fontモジュールを学びます。

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

1. アプリ概要

インストール済みフォントを一覧表示してプレビューできるアプリ。tkinter.fontモジュールを学びます。

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

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

コードを読む前に実行することをおすすめします。動いている挙動を先に把握しておくと、解説で出てくる関数や処理がどこに対応するかが頭に入りやすくなります。

応用のヒントは、機能を 1 つ増やす・見た目を整える・例外を一つでも丁寧に扱う、のいずれかから始めるのがおすすめです。

フォントビューアー 実行画面(Windows)
実行画面(Windows)
フォントビューアー 実行画面(Linux Mint)
実行画面(Linux Mint)

2. 機能一覧

  • イベント <<ListboxSelect>> から _on_select() を実行
  • tk.Listbox による一覧表示(スクロールバー付き)
  • ウィンドウはタイトル「フォントビューアー」・サイズ 560x480 で起動

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

💡
コードのコピー方法

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

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


class App37:
    """フォントビューアー"""

    def __init__(self, root):
        self.root = root
        self.root.title("フォントビューアー")
        self.root.geometry("560x480")
        self.root.minsize(560, 480)
        self.root.configure(bg="#f8f9fc")
        self._build_ui()

    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=16, pady=12)
        main_frame.pack(fill=tk.BOTH, expand=True)

        paned = tk.PanedWindow(main_frame, orient=tk.HORIZONTAL,
                               bg="#ddd", sashwidth=4)
        paned.pack(fill=tk.BOTH, expand=True)

        # 左: フォント一覧
        left_frame = tk.Frame(paned, bg="#f8f9fc")
        paned.add(left_frame, width=200)

        tk.Label(left_frame, text="フォント一覧", bg="#f8f9fc",
                 font=("Noto Sans JP", 11, "bold")).pack(anchor="w", pady=(0, 4))

        self.search_var = tk.StringVar()
        search_entry = ttk.Entry(left_frame, textvariable=self.search_var, font=("Arial", 11))
        search_entry.pack(fill=tk.X, pady=(0, 4))
        self.search_var.trace("w", self._filter_fonts)

        list_f = tk.Frame(left_frame, bg="#f8f9fc")
        list_f.pack(fill=tk.BOTH, expand=True)
        self.font_listbox = tk.Listbox(list_f, font=("Arial", 10),
                                       bg="white", relief=tk.FLAT, exportselection=False)
        self.font_listbox.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)
        sb = ttk.Scrollbar(list_f, command=self.font_listbox.yview)
        self.font_listbox.configure(yscrollcommand=sb.set)
        sb.pack(side=tk.RIGHT, fill=tk.Y)
        self.font_listbox.bind("<<ListboxSelect>>", self._on_select)

        # 右: プレビュー
        right_frame = tk.Frame(paned, bg="#f8f9fc")
        paned.add(right_frame)

        ctrl_f = tk.Frame(right_frame, bg="#f8f9fc")
        ctrl_f.pack(fill=tk.X, pady=(0, 8))

        tk.Label(ctrl_f, text="サイズ:", bg="#f8f9fc",
                 font=("Noto Sans JP", 10)).pack(side=tk.LEFT)
        self.size_var = tk.IntVar(value=18)
        ttk.Spinbox(ctrl_f, from_=8, to=72, textvariable=self.size_var,
                    width=5, font=("Arial", 11),
                    command=self._update_preview).pack(side=tk.LEFT, padx=4)

        self.bold_var = tk.BooleanVar()
        self.italic_var = tk.BooleanVar()
        ttk.Checkbutton(ctrl_f, text="Bold", variable=self.bold_var,
                        command=self._update_preview).pack(side=tk.LEFT, padx=4)
        ttk.Checkbutton(ctrl_f, text="Italic", variable=self.italic_var,
                        command=self._update_preview).pack(side=tk.LEFT)

        self.selected_label = tk.Label(right_frame, text="フォントを選択してください",
                                       bg="#f8f9fc", font=("Noto Sans JP", 10), fg="#888")
        self.selected_label.pack(anchor="w")

        preview_f = tk.Frame(right_frame, bg="white", relief=tk.SOLID, bd=1)
        preview_f.pack(fill=tk.BOTH, expand=True, pady=(4, 0))
        self.preview_text = tk.Text(preview_f, bg="white", relief=tk.FLAT,
                                    padx=10, pady=10, wrap=tk.WORD)
        self.preview_text.pack(fill=tk.BOTH, expand=True)
        self.preview_text.insert("1.0",
            "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz\n"
            "0123456789 !@#$%^&*()\n"
            "あいうえおかきくけこさしすせそ\n"
            "The quick brown fox jumps over the lazy dog.")

        # フォント読み込み
        self.all_fonts = sorted(tkfont.families())
        self._populate_fonts(self.all_fonts)

    def _populate_fonts(self, fonts):
        self.font_listbox.delete(0, tk.END)
        for f in fonts:
            self.font_listbox.insert(tk.END, f)

    def _filter_fonts(self, *args):
        query = self.search_var.get().lower()
        filtered = [f for f in self.all_fonts if query in f.lower()]
        self._populate_fonts(filtered)

    def _on_select(self, event=None):
        sel = self.font_listbox.curselection()
        if not sel:
            return
        self.current_font = self.font_listbox.get(sel[0])
        self.selected_label.config(text=f"選択中: {self.current_font}")
        self._update_preview()

    def _update_preview(self):
        if not hasattr(self, "current_font"):
            return
        size = self.size_var.get()
        weight = "bold" if self.bold_var.get() else "normal"
        slant = "italic" if self.italic_var.get() else "roman"
        try:
            f = tkfont.Font(family=self.current_font, size=size, weight=weight, slant=slant)
            self.preview_text.configure(font=f)
        except Exception:
            pass


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

5. コード解説

フォントビューアーのコードを、実際に書かれている実装に沿って解説します。

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

App37 クラスにアプリの全機能をまとめています(メソッド6個・全126行)。__init__ ではタイトル「フォントビューアー」とウィンドウサイズ 560x480 を設定します。最後に _build_ui() で画面を組み立てます。

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

ウィジェット構成

画面は tk.Frame×7・tk.Label×4・tk.PanedWindowttk.Entrytk.Listboxttk.Scrollbarttk.Spinboxttk.Checkbutton×2・tk.Text で構成しています。PanedWindow を使っているので、ペインの境界をマウスでドラッグして幅を変えられます。

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

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

また bind("<<ListboxSelect>>", ...) から _on_select() を呼べるようにイベントも登録しています。

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

例外処理

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

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

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

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

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

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

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

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

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

    title("フォントビューアー")geometry("560x480")configure(bg="#f8f9fc")minsize(560, 480) をコンストラクタで設定します。

  4. 4
    画面部品を並べる

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

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

    bind("<<ListboxSelect>>", ...) の登録も忘れずに。

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

    アプリの本体である _on_select()(7行)・_update_preview()(11行)・_populate_fonts()(4行) を実装します。

  7. 7
    動作確認する

    python app37.py で起動し、一通りの操作が動くことを確認します。

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

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

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

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

💡 一覧の内容をファイルに書き出す

tk.Listbox に溜めた内容は、アプリを閉じると消えます(このコードに保存処理はありません)。一覧を1行ずつテキストファイルへ書き出す保存ボタンを追加しましょう。保存先の選択には tkinter の filedialog モジュールが使えます(このコードでは未使用なので from tkinter import filedialog の追記が必要です)。

💡 入力履歴機能

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

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

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

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

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

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

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

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

9. 練習問題

アプリの理解を深めるための練習問題です。気になるものから挑戦してみてください(課題3は定型の発展課題です)。一覧に出るのは89行の tkfont.families() が返す、そのパソコンに入っているフォントです。並ぶ名前は環境によって変わるので、下の例と同じ名前が無くても問題ありません。

  1. 課題1:サイズを直接打ち込んでもプレビューを更新する

    サイズ欄(61〜64行)は上下の矢印を押したときだけプレビューが変わります。数字を打ち込んだときも変わるようにしましょう。

    期待結果:フォントを選んでからサイズ欄を 48 に打ち替えると、矢印を押さなくてもプレビューの文字がすぐ大きくなる

    合格条件

    • 直接入力でも矢印でもプレビューのサイズが変わる
    • サイズ欄の数字を全部消してもエラーにならない
    • Bold・Italic のチェックは今までどおり効く

    ヒント①(どこを触るか): 触るのは2か所です。self.size_var を作っている61行のすぐあと(値の変化を見張る部分)と、_update_preview()(110〜120行)の先頭です。
    ヒント②(使うもの): 40行の self.search_var.trace("w", self._filter_fonts) が手本です。ただし trace() は古い書き方で、標準ライブラリの説明にも trace_add() を使うよう書かれています。呼び出しでは引数が3つ渡るため、lambda *args: self._update_preview() で包むか、_filter_fonts(self, *args)(97行)のように *args を足します。
    つまずきやすい点: 数字を全部消すと欄が空になり、113行の self.size_var.get() が値を読めずにエラーになります。try が始まるのは116行なので、113行は守られていません。空のときは何もせず return する処理を _update_preview() の先頭に足します。

  2. 課題2:検索で1件も見つからないときに知らせる

    検索欄(37〜40行)に一致しない文字を入れると、一覧が空になるだけで理由が分かりません。何件見つかったかを表示しましょう。

    期待結果:検索欄に zzzzz と入れると一覧が空になり、追加したラベルが「0 件」に変わる。文字を消すと全体の件数に戻る

    合格条件

    • 一致するフォントが無いとき、見つからなかったと分かる表示になる
    • 検索欄を空にすると、表示が全体の件数に戻る
    • プレビューと「選択中」の表示(107行)は変わらない

    ヒント①(どこを触るか): 触るのは2か所です。左側にラベルを1つ足す部分(34〜35行の「フォント一覧」ラベルのすぐ下)と、_filter_fonts()(97〜100行)の最後です。
    ヒント②(使うもの): _filter_fonts()filtered はリストなので、len(filtered) で件数が分かります。表示の書き換えは self.count_label.config(text=f"{len(filtered)} 件") です。起動した直後は全件が並んでいるので、ラベルの最初の文字は空のままでかまいません。
    つまずきやすい点: 「該当なし」を一覧(Listbox)の中に入れると、それも選べる項目になります。選ぶと _on_select()(102〜108行)が動き、107行に「選択中: 該当なし」と出ます。知らせる文字は一覧の外のラベルに出します。

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

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

🚀
次に挑戦するアプリ

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

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

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

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

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