タイピング速度テスト
例文をタイプして速度(WPM)と正確率を計測するアプリ。時間計測と文字比較の実装を学びます。
1. アプリ概要
例文をタイプして速度(WPM)と正確率を計測するアプリ。時間計測と文字比較の実装を学びます。
このアプリはゲームカテゴリに分類される実践的なGUIアプリです。使用ライブラリは tkinter(標準ライブラリ) で、難易度は ★★☆ です。
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. 完全なソースコード
右上の「コピー」ボタンをクリックするとコードをクリップボードにコピーできます。
import tkinter as tk
from tkinter import ttk, messagebox
import time
class App41:
"""タイピング速度テスト"""
TEXTS = [
"Python is a versatile programming language that is easy to learn.",
"The quick brown fox jumps over the lazy dog.",
"Programming is the art of telling another human what one wants the computer to do.",
"In Python, indentation is used to define code blocks.",
"Functions allow you to organize code into reusable pieces.",
]
def __init__(self, root):
self.root = root
self.root.title("タイピング速度テスト")
self.root.geometry("560x440")
self.root.configure(bg="#f8f9fc")
self.start_time = None
self.current_text = ""
self._build_ui()
self.new_test()
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=20, pady=14)
main_frame.pack(fill=tk.BOTH, expand=True)
# お題テキスト
text_frame = ttk.LabelFrame(main_frame, text="入力するテキスト", padding=10)
text_frame.pack(fill=tk.X, pady=(0, 10))
self.target_label = tk.Label(text_frame, text="", bg=text_frame.cget("bg"),
font=("Courier New", 12), wraplength=500,
justify="left", anchor="w")
self.target_label.pack(fill=tk.X)
# 入力エリア
input_frame = ttk.LabelFrame(main_frame, text="ここに入力してください", padding=10)
input_frame.pack(fill=tk.X, pady=(0, 10))
self.input_text = tk.Text(input_frame, font=("Courier New", 12), height=3,
bg="white", fg="#333", wrap=tk.WORD,
insertbackground="#3776ab")
self.input_text.pack(fill=tk.X)
self.input_text.bind("<KeyRelease>", self.on_type)
# 統計
stats_frame = tk.Frame(main_frame, bg="#f8f9fc")
stats_frame.pack(fill=tk.X, pady=(0, 8))
self.stat_labels = {}
for col, (label, key) in enumerate([
("WPM", "wpm"), ("正確率", "accuracy"), ("経過時間", "elapsed"), ("進捗", "progress")
]):
box = tk.Frame(stats_frame, bg="white", relief=tk.SOLID, bd=1, padx=12, pady=8)
box.pack(side=tk.LEFT, fill=tk.X, expand=True, padx=4)
tk.Label(box, text=label, bg="white", fg="#888",
font=("Noto Sans JP", 9)).pack()
lbl = tk.Label(box, text="--", bg="white", fg="#3776ab",
font=("Noto Sans JP", 16, "bold"))
lbl.pack()
self.stat_labels[key] = lbl
# ボタン
btn_frame = tk.Frame(main_frame, bg="#f8f9fc")
btn_frame.pack()
ttk.Button(btn_frame, text="新しいテスト", command=self.new_test).pack(side=tk.LEFT, padx=6)
self.result_label = tk.Label(main_frame, text="", bg="#f8f9fc",
font=("Noto Sans JP", 12, "bold"))
self.result_label.pack(pady=(6, 0))
def new_test(self):
import random
self.current_text = random.choice(self.TEXTS)
self.target_label.config(text=self.current_text)
self.input_text.delete("1.0", tk.END)
self.input_text.config(state=tk.NORMAL, bg="white")
self.input_text.focus()
self.start_time = None
self.result_label.config(text="")
for lbl in self.stat_labels.values():
lbl.config(text="--", fg="#3776ab")
def on_type(self, event=None):
typed = self.input_text.get("1.0", tk.END).rstrip("\n")
if not typed:
self.start_time = None
return
if self.start_time is None:
self.start_time = time.time()
elapsed = time.time() - self.start_time
target = self.current_text
# 正確率
correct = sum(1 for a, b in zip(typed, target) if a == b)
accuracy = correct / max(len(typed), 1) * 100
# WPM
words = len(typed.split())
wpm = words / (elapsed / 60) if elapsed > 0 else 0
# 進捗
progress = len(typed) / len(target) * 100
self.stat_labels["wpm"].config(text=f"{wpm:.0f}")
self.stat_labels["accuracy"].config(text=f"{accuracy:.1f}%")
self.stat_labels["elapsed"].config(text=f"{elapsed:.1f}秒")
self.stat_labels["progress"].config(text=f"{min(progress, 100):.0f}%")
# 完了判定
if typed == target:
self.input_text.config(bg="#d4edda")
self.result_label.config(
text=f"🎉 完了! WPM: {wpm:.0f} 正確率: {accuracy:.1f}% 時間: {elapsed:.1f}秒",
fg="#27ae60")
elif not target.startswith(typed):
self.input_text.config(bg="#f8d7da")
else:
self.input_text.config(bg="white")
if __name__ == "__main__":
root = tk.Tk()
app = App41(root)
root.mainloop()
5. コード解説
タイピング速度テストのコードを詳しく解説します。クラスベースの設計で各機能を整理して実装しています。
クラス設計とコンストラクタ
App41クラスにアプリの全機能をまとめています。__init__メソッドでウィンドウの基本設定を行い、_build_ui()でUI構築、process()でメイン処理を担当します。この分離により、各メソッドの責任が明確になりコードが読みやすくなります。
import tkinter as tk
from tkinter import ttk, messagebox
import time
class App41:
"""タイピング速度テスト"""
TEXTS = [
"Python is a versatile programming language that is easy to learn.",
"The quick brown fox jumps over the lazy dog.",
"Programming is the art of telling another human what one wants the computer to do.",
"In Python, indentation is used to define code blocks.",
"Functions allow you to organize code into reusable pieces.",
]
def __init__(self, root):
self.root = root
self.root.title("タイピング速度テスト")
self.root.geometry("560x440")
self.root.configure(bg="#f8f9fc")
self.start_time = None
self.current_text = ""
self._build_ui()
self.new_test()
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=20, pady=14)
main_frame.pack(fill=tk.BOTH, expand=True)
# お題テキスト
text_frame = ttk.LabelFrame(main_frame, text="入力するテキスト", padding=10)
text_frame.pack(fill=tk.X, pady=(0, 10))
self.target_label = tk.Label(text_frame, text="", bg=text_frame.cget("bg"),
font=("Courier New", 12), wraplength=500,
justify="left", anchor="w")
self.target_label.pack(fill=tk.X)
# 入力エリア
input_frame = ttk.LabelFrame(main_frame, text="ここに入力してください", padding=10)
input_frame.pack(fill=tk.X, pady=(0, 10))
self.input_text = tk.Text(input_frame, font=("Courier New", 12), height=3,
bg="white", fg="#333", wrap=tk.WORD,
insertbackground="#3776ab")
self.input_text.pack(fill=tk.X)
self.input_text.bind("<KeyRelease>", self.on_type)
# 統計
stats_frame = tk.Frame(main_frame, bg="#f8f9fc")
stats_frame.pack(fill=tk.X, pady=(0, 8))
self.stat_labels = {}
for col, (label, key) in enumerate([
("WPM", "wpm"), ("正確率", "accuracy"), ("経過時間", "elapsed"), ("進捗", "progress")
]):
box = tk.Frame(stats_frame, bg="white", relief=tk.SOLID, bd=1, padx=12, pady=8)
box.pack(side=tk.LEFT, fill=tk.X, expand=True, padx=4)
tk.Label(box, text=label, bg="white", fg="#888",
font=("Noto Sans JP", 9)).pack()
lbl = tk.Label(box, text="--", bg="white", fg="#3776ab",
font=("Noto Sans JP", 16, "bold"))
lbl.pack()
self.stat_labels[key] = lbl
# ボタン
btn_frame = tk.Frame(main_frame, bg="#f8f9fc")
btn_frame.pack()
ttk.Button(btn_frame, text="新しいテスト", command=self.new_test).pack(side=tk.LEFT, padx=6)
self.result_label = tk.Label(main_frame, text="", bg="#f8f9fc",
font=("Noto Sans JP", 12, "bold"))
self.result_label.pack(pady=(6, 0))
def new_test(self):
import random
self.current_text = random.choice(self.TEXTS)
self.target_label.config(text=self.current_text)
self.input_text.delete("1.0", tk.END)
self.input_text.config(state=tk.NORMAL, bg="white")
self.input_text.focus()
self.start_time = None
self.result_label.config(text="")
for lbl in self.stat_labels.values():
lbl.config(text="--", fg="#3776ab")
def on_type(self, event=None):
typed = self.input_text.get("1.0", tk.END).rstrip("\n")
if not typed:
self.start_time = None
return
if self.start_time is None:
self.start_time = time.time()
elapsed = time.time() - self.start_time
target = self.current_text
# 正確率
correct = sum(1 for a, b in zip(typed, target) if a == b)
accuracy = correct / max(len(typed), 1) * 100
# WPM
words = len(typed.split())
wpm = words / (elapsed / 60) if elapsed > 0 else 0
# 進捗
progress = len(typed) / len(target) * 100
self.stat_labels["wpm"].config(text=f"{wpm:.0f}")
self.stat_labels["accuracy"].config(text=f"{accuracy:.1f}%")
self.stat_labels["elapsed"].config(text=f"{elapsed:.1f}秒")
self.stat_labels["progress"].config(text=f"{min(progress, 100):.0f}%")
# 完了判定
if typed == target:
self.input_text.config(bg="#d4edda")
self.result_label.config(
text=f"🎉 完了! WPM: {wpm:.0f} 正確率: {accuracy:.1f}% 時間: {elapsed:.1f}秒",
fg="#27ae60")
elif not target.startswith(typed):
self.input_text.config(bg="#f8d7da")
else:
self.input_text.config(bg="white")
if __name__ == "__main__":
root = tk.Tk()
app = App41(root)
root.mainloop()
LabelFrameによるセクション分け
ttk.LabelFrame を使うことで、入力エリアと結果エリアを視覚的に分けられます。padding引数でフレーム内の余白を設定し、見やすいレイアウトを実現しています。
import tkinter as tk
from tkinter import ttk, messagebox
import time
class App41:
"""タイピング速度テスト"""
TEXTS = [
"Python is a versatile programming language that is easy to learn.",
"The quick brown fox jumps over the lazy dog.",
"Programming is the art of telling another human what one wants the computer to do.",
"In Python, indentation is used to define code blocks.",
"Functions allow you to organize code into reusable pieces.",
]
def __init__(self, root):
self.root = root
self.root.title("タイピング速度テスト")
self.root.geometry("560x440")
self.root.configure(bg="#f8f9fc")
self.start_time = None
self.current_text = ""
self._build_ui()
self.new_test()
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=20, pady=14)
main_frame.pack(fill=tk.BOTH, expand=True)
# お題テキスト
text_frame = ttk.LabelFrame(main_frame, text="入力するテキスト", padding=10)
text_frame.pack(fill=tk.X, pady=(0, 10))
self.target_label = tk.Label(text_frame, text="", bg=text_frame.cget("bg"),
font=("Courier New", 12), wraplength=500,
justify="left", anchor="w")
self.target_label.pack(fill=tk.X)
# 入力エリア
input_frame = ttk.LabelFrame(main_frame, text="ここに入力してください", padding=10)
input_frame.pack(fill=tk.X, pady=(0, 10))
self.input_text = tk.Text(input_frame, font=("Courier New", 12), height=3,
bg="white", fg="#333", wrap=tk.WORD,
insertbackground="#3776ab")
self.input_text.pack(fill=tk.X)
self.input_text.bind("<KeyRelease>", self.on_type)
# 統計
stats_frame = tk.Frame(main_frame, bg="#f8f9fc")
stats_frame.pack(fill=tk.X, pady=(0, 8))
self.stat_labels = {}
for col, (label, key) in enumerate([
("WPM", "wpm"), ("正確率", "accuracy"), ("経過時間", "elapsed"), ("進捗", "progress")
]):
box = tk.Frame(stats_frame, bg="white", relief=tk.SOLID, bd=1, padx=12, pady=8)
box.pack(side=tk.LEFT, fill=tk.X, expand=True, padx=4)
tk.Label(box, text=label, bg="white", fg="#888",
font=("Noto Sans JP", 9)).pack()
lbl = tk.Label(box, text="--", bg="white", fg="#3776ab",
font=("Noto Sans JP", 16, "bold"))
lbl.pack()
self.stat_labels[key] = lbl
# ボタン
btn_frame = tk.Frame(main_frame, bg="#f8f9fc")
btn_frame.pack()
ttk.Button(btn_frame, text="新しいテスト", command=self.new_test).pack(side=tk.LEFT, padx=6)
self.result_label = tk.Label(main_frame, text="", bg="#f8f9fc",
font=("Noto Sans JP", 12, "bold"))
self.result_label.pack(pady=(6, 0))
def new_test(self):
import random
self.current_text = random.choice(self.TEXTS)
self.target_label.config(text=self.current_text)
self.input_text.delete("1.0", tk.END)
self.input_text.config(state=tk.NORMAL, bg="white")
self.input_text.focus()
self.start_time = None
self.result_label.config(text="")
for lbl in self.stat_labels.values():
lbl.config(text="--", fg="#3776ab")
def on_type(self, event=None):
typed = self.input_text.get("1.0", tk.END).rstrip("\n")
if not typed:
self.start_time = None
return
if self.start_time is None:
self.start_time = time.time()
elapsed = time.time() - self.start_time
target = self.current_text
# 正確率
correct = sum(1 for a, b in zip(typed, target) if a == b)
accuracy = correct / max(len(typed), 1) * 100
# WPM
words = len(typed.split())
wpm = words / (elapsed / 60) if elapsed > 0 else 0
# 進捗
progress = len(typed) / len(target) * 100
self.stat_labels["wpm"].config(text=f"{wpm:.0f}")
self.stat_labels["accuracy"].config(text=f"{accuracy:.1f}%")
self.stat_labels["elapsed"].config(text=f"{elapsed:.1f}秒")
self.stat_labels["progress"].config(text=f"{min(progress, 100):.0f}%")
# 完了判定
if typed == target:
self.input_text.config(bg="#d4edda")
self.result_label.config(
text=f"🎉 完了! WPM: {wpm:.0f} 正確率: {accuracy:.1f}% 時間: {elapsed:.1f}秒",
fg="#27ae60")
elif not target.startswith(typed):
self.input_text.config(bg="#f8d7da")
else:
self.input_text.config(bg="white")
if __name__ == "__main__":
root = tk.Tk()
app = App41(root)
root.mainloop()
Entryウィジェットとイベントバインド
ttk.Entryで入力フィールドを作成します。bind('
import tkinter as tk
from tkinter import ttk, messagebox
import time
class App41:
"""タイピング速度テスト"""
TEXTS = [
"Python is a versatile programming language that is easy to learn.",
"The quick brown fox jumps over the lazy dog.",
"Programming is the art of telling another human what one wants the computer to do.",
"In Python, indentation is used to define code blocks.",
"Functions allow you to organize code into reusable pieces.",
]
def __init__(self, root):
self.root = root
self.root.title("タイピング速度テスト")
self.root.geometry("560x440")
self.root.configure(bg="#f8f9fc")
self.start_time = None
self.current_text = ""
self._build_ui()
self.new_test()
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=20, pady=14)
main_frame.pack(fill=tk.BOTH, expand=True)
# お題テキスト
text_frame = ttk.LabelFrame(main_frame, text="入力するテキスト", padding=10)
text_frame.pack(fill=tk.X, pady=(0, 10))
self.target_label = tk.Label(text_frame, text="", bg=text_frame.cget("bg"),
font=("Courier New", 12), wraplength=500,
justify="left", anchor="w")
self.target_label.pack(fill=tk.X)
# 入力エリア
input_frame = ttk.LabelFrame(main_frame, text="ここに入力してください", padding=10)
input_frame.pack(fill=tk.X, pady=(0, 10))
self.input_text = tk.Text(input_frame, font=("Courier New", 12), height=3,
bg="white", fg="#333", wrap=tk.WORD,
insertbackground="#3776ab")
self.input_text.pack(fill=tk.X)
self.input_text.bind("<KeyRelease>", self.on_type)
# 統計
stats_frame = tk.Frame(main_frame, bg="#f8f9fc")
stats_frame.pack(fill=tk.X, pady=(0, 8))
self.stat_labels = {}
for col, (label, key) in enumerate([
("WPM", "wpm"), ("正確率", "accuracy"), ("経過時間", "elapsed"), ("進捗", "progress")
]):
box = tk.Frame(stats_frame, bg="white", relief=tk.SOLID, bd=1, padx=12, pady=8)
box.pack(side=tk.LEFT, fill=tk.X, expand=True, padx=4)
tk.Label(box, text=label, bg="white", fg="#888",
font=("Noto Sans JP", 9)).pack()
lbl = tk.Label(box, text="--", bg="white", fg="#3776ab",
font=("Noto Sans JP", 16, "bold"))
lbl.pack()
self.stat_labels[key] = lbl
# ボタン
btn_frame = tk.Frame(main_frame, bg="#f8f9fc")
btn_frame.pack()
ttk.Button(btn_frame, text="新しいテスト", command=self.new_test).pack(side=tk.LEFT, padx=6)
self.result_label = tk.Label(main_frame, text="", bg="#f8f9fc",
font=("Noto Sans JP", 12, "bold"))
self.result_label.pack(pady=(6, 0))
def new_test(self):
import random
self.current_text = random.choice(self.TEXTS)
self.target_label.config(text=self.current_text)
self.input_text.delete("1.0", tk.END)
self.input_text.config(state=tk.NORMAL, bg="white")
self.input_text.focus()
self.start_time = None
self.result_label.config(text="")
for lbl in self.stat_labels.values():
lbl.config(text="--", fg="#3776ab")
def on_type(self, event=None):
typed = self.input_text.get("1.0", tk.END).rstrip("\n")
if not typed:
self.start_time = None
return
if self.start_time is None:
self.start_time = time.time()
elapsed = time.time() - self.start_time
target = self.current_text
# 正確率
correct = sum(1 for a, b in zip(typed, target) if a == b)
accuracy = correct / max(len(typed), 1) * 100
# WPM
words = len(typed.split())
wpm = words / (elapsed / 60) if elapsed > 0 else 0
# 進捗
progress = len(typed) / len(target) * 100
self.stat_labels["wpm"].config(text=f"{wpm:.0f}")
self.stat_labels["accuracy"].config(text=f"{accuracy:.1f}%")
self.stat_labels["elapsed"].config(text=f"{elapsed:.1f}秒")
self.stat_labels["progress"].config(text=f"{min(progress, 100):.0f}%")
# 完了判定
if typed == target:
self.input_text.config(bg="#d4edda")
self.result_label.config(
text=f"🎉 完了! WPM: {wpm:.0f} 正確率: {accuracy:.1f}% 時間: {elapsed:.1f}秒",
fg="#27ae60")
elif not target.startswith(typed):
self.input_text.config(bg="#f8d7da")
else:
self.input_text.config(bg="white")
if __name__ == "__main__":
root = tk.Tk()
app = App41(root)
root.mainloop()
Textウィジェットでの結果表示
結果表示にはtk.Textウィジェットを使います。state=tk.DISABLEDでユーザーが直接編集できないようにし、表示前にNORMALに切り替えてからinsert()で内容を更新します。
import tkinter as tk
from tkinter import ttk, messagebox
import time
class App41:
"""タイピング速度テスト"""
TEXTS = [
"Python is a versatile programming language that is easy to learn.",
"The quick brown fox jumps over the lazy dog.",
"Programming is the art of telling another human what one wants the computer to do.",
"In Python, indentation is used to define code blocks.",
"Functions allow you to organize code into reusable pieces.",
]
def __init__(self, root):
self.root = root
self.root.title("タイピング速度テスト")
self.root.geometry("560x440")
self.root.configure(bg="#f8f9fc")
self.start_time = None
self.current_text = ""
self._build_ui()
self.new_test()
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=20, pady=14)
main_frame.pack(fill=tk.BOTH, expand=True)
# お題テキスト
text_frame = ttk.LabelFrame(main_frame, text="入力するテキスト", padding=10)
text_frame.pack(fill=tk.X, pady=(0, 10))
self.target_label = tk.Label(text_frame, text="", bg=text_frame.cget("bg"),
font=("Courier New", 12), wraplength=500,
justify="left", anchor="w")
self.target_label.pack(fill=tk.X)
# 入力エリア
input_frame = ttk.LabelFrame(main_frame, text="ここに入力してください", padding=10)
input_frame.pack(fill=tk.X, pady=(0, 10))
self.input_text = tk.Text(input_frame, font=("Courier New", 12), height=3,
bg="white", fg="#333", wrap=tk.WORD,
insertbackground="#3776ab")
self.input_text.pack(fill=tk.X)
self.input_text.bind("<KeyRelease>", self.on_type)
# 統計
stats_frame = tk.Frame(main_frame, bg="#f8f9fc")
stats_frame.pack(fill=tk.X, pady=(0, 8))
self.stat_labels = {}
for col, (label, key) in enumerate([
("WPM", "wpm"), ("正確率", "accuracy"), ("経過時間", "elapsed"), ("進捗", "progress")
]):
box = tk.Frame(stats_frame, bg="white", relief=tk.SOLID, bd=1, padx=12, pady=8)
box.pack(side=tk.LEFT, fill=tk.X, expand=True, padx=4)
tk.Label(box, text=label, bg="white", fg="#888",
font=("Noto Sans JP", 9)).pack()
lbl = tk.Label(box, text="--", bg="white", fg="#3776ab",
font=("Noto Sans JP", 16, "bold"))
lbl.pack()
self.stat_labels[key] = lbl
# ボタン
btn_frame = tk.Frame(main_frame, bg="#f8f9fc")
btn_frame.pack()
ttk.Button(btn_frame, text="新しいテスト", command=self.new_test).pack(side=tk.LEFT, padx=6)
self.result_label = tk.Label(main_frame, text="", bg="#f8f9fc",
font=("Noto Sans JP", 12, "bold"))
self.result_label.pack(pady=(6, 0))
def new_test(self):
import random
self.current_text = random.choice(self.TEXTS)
self.target_label.config(text=self.current_text)
self.input_text.delete("1.0", tk.END)
self.input_text.config(state=tk.NORMAL, bg="white")
self.input_text.focus()
self.start_time = None
self.result_label.config(text="")
for lbl in self.stat_labels.values():
lbl.config(text="--", fg="#3776ab")
def on_type(self, event=None):
typed = self.input_text.get("1.0", tk.END).rstrip("\n")
if not typed:
self.start_time = None
return
if self.start_time is None:
self.start_time = time.time()
elapsed = time.time() - self.start_time
target = self.current_text
# 正確率
correct = sum(1 for a, b in zip(typed, target) if a == b)
accuracy = correct / max(len(typed), 1) * 100
# WPM
words = len(typed.split())
wpm = words / (elapsed / 60) if elapsed > 0 else 0
# 進捗
progress = len(typed) / len(target) * 100
self.stat_labels["wpm"].config(text=f"{wpm:.0f}")
self.stat_labels["accuracy"].config(text=f"{accuracy:.1f}%")
self.stat_labels["elapsed"].config(text=f"{elapsed:.1f}秒")
self.stat_labels["progress"].config(text=f"{min(progress, 100):.0f}%")
# 完了判定
if typed == target:
self.input_text.config(bg="#d4edda")
self.result_label.config(
text=f"🎉 完了! WPM: {wpm:.0f} 正確率: {accuracy:.1f}% 時間: {elapsed:.1f}秒",
fg="#27ae60")
elif not target.startswith(typed):
self.input_text.config(bg="#f8d7da")
else:
self.input_text.config(bg="white")
if __name__ == "__main__":
root = tk.Tk()
app = App41(root)
root.mainloop()
例外処理とmessagebox
try-except で ValueError と Exception を捕捉し、messagebox.showerror() でユーザーにわかりやすいエラーメッセージを表示します。入力バリデーションは必ず実装しましょう。
import tkinter as tk
from tkinter import ttk, messagebox
import time
class App41:
"""タイピング速度テスト"""
TEXTS = [
"Python is a versatile programming language that is easy to learn.",
"The quick brown fox jumps over the lazy dog.",
"Programming is the art of telling another human what one wants the computer to do.",
"In Python, indentation is used to define code blocks.",
"Functions allow you to organize code into reusable pieces.",
]
def __init__(self, root):
self.root = root
self.root.title("タイピング速度テスト")
self.root.geometry("560x440")
self.root.configure(bg="#f8f9fc")
self.start_time = None
self.current_text = ""
self._build_ui()
self.new_test()
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=20, pady=14)
main_frame.pack(fill=tk.BOTH, expand=True)
# お題テキスト
text_frame = ttk.LabelFrame(main_frame, text="入力するテキスト", padding=10)
text_frame.pack(fill=tk.X, pady=(0, 10))
self.target_label = tk.Label(text_frame, text="", bg=text_frame.cget("bg"),
font=("Courier New", 12), wraplength=500,
justify="left", anchor="w")
self.target_label.pack(fill=tk.X)
# 入力エリア
input_frame = ttk.LabelFrame(main_frame, text="ここに入力してください", padding=10)
input_frame.pack(fill=tk.X, pady=(0, 10))
self.input_text = tk.Text(input_frame, font=("Courier New", 12), height=3,
bg="white", fg="#333", wrap=tk.WORD,
insertbackground="#3776ab")
self.input_text.pack(fill=tk.X)
self.input_text.bind("<KeyRelease>", self.on_type)
# 統計
stats_frame = tk.Frame(main_frame, bg="#f8f9fc")
stats_frame.pack(fill=tk.X, pady=(0, 8))
self.stat_labels = {}
for col, (label, key) in enumerate([
("WPM", "wpm"), ("正確率", "accuracy"), ("経過時間", "elapsed"), ("進捗", "progress")
]):
box = tk.Frame(stats_frame, bg="white", relief=tk.SOLID, bd=1, padx=12, pady=8)
box.pack(side=tk.LEFT, fill=tk.X, expand=True, padx=4)
tk.Label(box, text=label, bg="white", fg="#888",
font=("Noto Sans JP", 9)).pack()
lbl = tk.Label(box, text="--", bg="white", fg="#3776ab",
font=("Noto Sans JP", 16, "bold"))
lbl.pack()
self.stat_labels[key] = lbl
# ボタン
btn_frame = tk.Frame(main_frame, bg="#f8f9fc")
btn_frame.pack()
ttk.Button(btn_frame, text="新しいテスト", command=self.new_test).pack(side=tk.LEFT, padx=6)
self.result_label = tk.Label(main_frame, text="", bg="#f8f9fc",
font=("Noto Sans JP", 12, "bold"))
self.result_label.pack(pady=(6, 0))
def new_test(self):
import random
self.current_text = random.choice(self.TEXTS)
self.target_label.config(text=self.current_text)
self.input_text.delete("1.0", tk.END)
self.input_text.config(state=tk.NORMAL, bg="white")
self.input_text.focus()
self.start_time = None
self.result_label.config(text="")
for lbl in self.stat_labels.values():
lbl.config(text="--", fg="#3776ab")
def on_type(self, event=None):
typed = self.input_text.get("1.0", tk.END).rstrip("\n")
if not typed:
self.start_time = None
return
if self.start_time is None:
self.start_time = time.time()
elapsed = time.time() - self.start_time
target = self.current_text
# 正確率
correct = sum(1 for a, b in zip(typed, target) if a == b)
accuracy = correct / max(len(typed), 1) * 100
# WPM
words = len(typed.split())
wpm = words / (elapsed / 60) if elapsed > 0 else 0
# 進捗
progress = len(typed) / len(target) * 100
self.stat_labels["wpm"].config(text=f"{wpm:.0f}")
self.stat_labels["accuracy"].config(text=f"{accuracy:.1f}%")
self.stat_labels["elapsed"].config(text=f"{elapsed:.1f}秒")
self.stat_labels["progress"].config(text=f"{min(progress, 100):.0f}%")
# 完了判定
if typed == target:
self.input_text.config(bg="#d4edda")
self.result_label.config(
text=f"🎉 完了! WPM: {wpm:.0f} 正確率: {accuracy:.1f}% 時間: {elapsed:.1f}秒",
fg="#27ae60")
elif not target.startswith(typed):
self.input_text.config(bg="#f8d7da")
else:
self.input_text.config(bg="white")
if __name__ == "__main__":
root = tk.Tk()
app = App41(root)
root.mainloop()
6. ステップバイステップガイド
このアプリをゼロから自分で作る手順を解説します。コードをコピーするだけでなく、実際に手順を追って自分で書いてみましょう。
-
1ファイルを作成する
新しいファイルを作成して app41.py と保存します。
-
2クラスの骨格を作る
App41クラスを定義し、__init__とmainloop()の最小構成を作ります。
-
3タイトルバーを作る
Frameを使ってカラーバー付きのタイトルエリアを作ります。
-
4入力フォームを実装する
LabelFrameとEntryウィジェットで入力エリアを作ります。
-
5処理ロジックを実装する
_calculate()メソッドに計算・処理ロジックを実装します。
-
6結果表示を実装する
TextウィジェットかLabelに結果を表示する_show_result()を実装します。
-
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つ追加してみましょう。どんな機能があると便利か考えてから実装してください。
-
課題2:UIの改善
色・フォント・レイアウトを変更して、より使いやすいUIにカスタマイズしてみましょう。
-
課題3:保存機能の追加
入力値や計算結果をファイルに保存する機能を追加しましょう。jsonやcsvモジュールを使います。