معرفی
در این آموزش یک اپلیکیشن تست سرعت تایپ با رابط گرافیکی مدرن میسازیم که سرعت (WPM)، دقت و زمان را محاسبه میکند. برنامه شامل صفحه خوشآمد، دستورالعمل، فضای تایپ با رنگبندی کاراکترهای صحیح/غلط، نوار پیشرفت، ذخیره رکوردها و صفحه نتایج با رتبهبندی است. هدف ایجاد یک ابزار تمرینی با تجربه کاربری خوب و پایهای مناسب برای گسترش است.
نصب و پیشنیازها
پیشنیازها و نکات نصب:
- Python 3.8+ (توصیهشده 3.10/3.11)
tkinter(به صورت پیشفرض در پایتون موجود است) - کتابخانه اصلی برای ایجاد رابط گرافیکی و ویجتهاtime- برای مدیریت زمان، محاسبه مدت تست و ایجاد تاخیر در انیمیشنهاrandom- برای انتخاب تصادفی متنهای نمونه از لیست SAMPLE_TEXTSmath- برای محاسبات ریاضی مورد نیاز در رسم شکلهای هندسی و انیمیشنهاjson- برای ذخیره و بازیابی امتیازات برتر در فایل high_scores.jsonos- برای بررسی وجود فایل امتیازات و مدیریت مسیرهای فایل
این پروژه تنها به کتابخانههای استاندارد پایتون نیاز دارد:
کدنویسی پروژه
import tkinter as tk
from tkinter import ttk, messagebox
import time
import random
import math
import json
import os
# Sample texts for variety
SAMPLE_TEXTS = [
"The quick brown fox jumps over the lazy dog while programming in Python.",
"Speed typing tests help improve your keyboard skills and accuracy over time.",
"Programming is the art of telling another human what one wants the computer to do.",
"Coding is not just about writing lines, it's about solving problems creatively.",
"Practice makes perfect when it comes to typing speed and programming skills.",
"The best way to learn programming is by writing code and making mistakes.",
"Clean code is simple, direct, and reads like well-written prose with purpose.",
"Debugging is like being a detective in a crime movie where you are also the murderer.",
"The only way to learn a new programming language is by writing programs in it.",
"Programming is the process of creating a set of instructions that tell a computer how to perform a task."
]
class ModernButton(tk.Canvas):
def __init__(self, parent, text, command, width=200, height=50, bg_color="#3498DB",
hover_color="#2980B9", text_color="white", font=("Arial", 12, "bold")):
super().__init__(parent, width=width, height=height, highlightthickness=0)
self.command = command
self.bg_color = bg_color
self.hover_color = hover_color
self.text_color = text_color
self.font = font
self.text = text
self.width = width
self.height = height
self.is_hovered = False
self.bind("", self.on_enter)
self.bind("", self.on_leave)
self.bind("", self.on_click)
self.bind("", self.on_release)
self.draw_button()
def draw_button(self, color=None):
self.delete("all")
color = color or self.bg_color
# Draw rounded rectangle
radius = 15
self.create_rounded_rectangle(0, 0, self.width, self.height, radius, fill=color, outline="")
# Draw text
self.create_text(self.width//2, self.height//2, text=self.text,
fill=self.text_color, font=self.font)
def create_rounded_rectangle(self, x1, y1, x2, y2, radius, **kwargs):
points = [x1+radius, y1,
x2-radius, y1,
x2, y1,
x2, y1+radius,
x2, y2-radius,
x2, y2,
x2-radius, y2,
x1+radius, y2,
x1, y2,
x1, y2-radius,
x1, y1+radius,
x1, y1]
return self.create_polygon(points, **kwargs, smooth=True)
def on_enter(self, event):
self.is_hovered = True
self.animate_color(self.hover_color)
def on_leave(self, event):
self.is_hovered = False
self.animate_color(self.bg_color)
def animate_color(self, target_color):
current_color = self.bg_color
steps = 10
def interpolate_color(color1, color2, factor):
r1, g1, b1 = int(color1[1:3], 16), int(color1[3:5], 16), int(color1[5:7], 16)
r2, g2, b2 = int(color2[1:3], 16), int(color2[3:5], 16), int(color2[5:7], 16)
r = int(r1 + (r2 - r1) * factor)
g = int(g1 + (g2 - g1) * factor)
b = int(b1 + (b2 - b1) * factor)
return f"#{r:02x}{g:02x}{b:02x}"
for i in range(steps + 1):
factor = i / steps
color = interpolate_color(current_color, target_color, factor)
self.after(i * 3, lambda c=color: self.draw_button(c))
def on_click(self, event):
# Visual feedback - shrink effect
self.scale("all", self.width/2, self.height/2, 0.95, 0.95)
# Darken color for click feedback
click_color = self.darken_color(self.hover_color if self.is_hovered else self.bg_color)
self.draw_button(click_color)
def on_release(self, event):
# Restore original size and color
self.scale("all", self.width/2, self.height/2, 1/0.95, 1/0.95)
self.draw_button(self.hover_color if self.is_hovered else self.bg_color)
# Execute command after a short delay for better visual feedback
self.after(150, self.command)
def darken_color(self, color, factor=0.8):
r, g, b = int(color[1:3], 16), int(color[3:5], 16), int(color[5:7], 16)
r = int(r * factor)
g = int(g * factor)
b = int(b * factor)
return f"#{r:02x}{g:02x}{b:02x}"
class TypingTestApp:
def __init__(self, root):
self.root = root
self.root.title("🚀 Speed Typing Master Pro")
self.root.geometry("1200x800")
self.root.configure(bg='#1a1a2e')
# Center the window
self.center_window()
# Variables
self.name = ""
self.target_text = ""
self.current_text = []
self.start_time = 0
self.wpm = 0
self.accuracy = 0
self.test_active = False
self.high_scores = self.load_high_scores()
# Animation variables
self.animation_running = False
# Configure styles
self.setup_styles()
# Show animated welcome screen
self.show_animated_welcome()
def center_window(self):
"""Center the window on screen"""
self.root.update_idletasks()
width = 1200
height = 800
x = (self.root.winfo_screenwidth() // 2) - (width // 2)
y = (self.root.winfo_screenheight() // 2) - (height // 2)
self.root.geometry(f'{width}x{height}+{x}+{y}')
def setup_styles(self):
"""Configure custom styles for widgets"""
style = ttk.Style()
style.theme_use('clam')
# Configure custom styles
style.configure('Title.TLabel',
font=('Arial', 28, 'bold'),
background='#1a1a2e',
foreground='#e6e6e6')
style.configure('Subtitle.TLabel',
font=('Arial', 16),
background='#1a1a2e',
foreground='#b8b8b8')
def load_high_scores(self):
"""Load high scores from file"""
try:
if os.path.exists('high_scores.json'):
with open('high_scores.json', 'r') as f:
return json.load(f)
except:
pass
return []
def save_high_score(self):
"""Save current score to high scores"""
score = {
'name': self.name,
'wpm': self.wpm,
'accuracy': self.accuracy,
'date': time.strftime('%Y-%m-%d %H:%M:%S')
}
self.high_scores.append(score)
self.high_scores.sort(key=lambda x: x['wpm'], reverse=True)
self.high_scores = self.high_scores[:10] # Keep top 10
try:
with open('high_scores.json', 'w') as f:
json.dump(self.high_scores, f)
except:
pass
def clear_frame(self):
"""Clear all widgets from the root"""
self.animation_running = False
for widget in self.root.winfo_children():
widget.destroy()
def create_animated_background(self):
"""Create an animated background canvas"""
canvas = tk.Canvas(self.root, bg='#1a1a2e', highlightthickness=0)
canvas.pack(fill='both', expand=True)
# Create floating particles
self.floating_particles = []
for _ in range(20): # Reduced for better performance
x = random.randint(0, 1200)
y = random.randint(0, 800)
size = random.uniform(1, 3)
speed = random.uniform(0.2, 1)
particle = canvas.create_oval(x, y, x+size, y+size, fill='#3498DB', outline='')
self.floating_particles.append({
'id': particle,
'x': x, 'y': y,
'speed': speed,
'size': size
})
def animate_particles():
if not self.animation_running:
return
for particle in self.floating_particles:
# Move particle
particle['y'] += particle['speed']
if particle['y'] > 800:
particle['y'] = 0
particle['x'] = random.randint(0, 1200)
canvas.coords(
particle['id'],
particle['x'], particle['y'],
particle['x'] + particle['size'], particle['y'] + particle['size']
)
canvas.after(50, animate_particles)
self.animation_running = True
animate_particles()
return canvas
def show_animated_welcome(self):
"""Display animated welcome screen"""
self.clear_frame()
canvas = self.create_animated_background()
# Main container
main_frame = tk.Frame(canvas, bg='#1a1a2e')
canvas.create_window(600, 400, window=main_frame)
# Title with typing animation
title_text = "🚀 Speed Typing Master Pro"
title_label = tk.Label(main_frame, text="", font=('Arial', 32, 'bold'),
bg='#1a1a2e', fg='#e6e6e6')
title_label.pack(pady=(80, 20))
# Animate title typing
def type_title(text, index=0):
if index <= len(text):
title_label.config(text=text[:index])
if index < len(text):
main_frame.after(50, lambda: type_title(text, index + 1))
type_title(title_text)
subtitle_label = tk.Label(main_frame, text="Test and Improve Your Typing Speed",
font=('Arial', 18), bg='#1a1a2e', fg='#b8b8b8')
subtitle_label.pack(pady=(0, 60))
# Name entry frame with animation
name_frame = tk.Frame(main_frame, bg='#1a1a2e')
name_frame.pack(pady=30)
name_label = tk.Label(name_frame, text="Enter Your Name:", font=('Arial', 16),
bg='#1a1a2e', fg='#b8b8b8')
name_label.pack(pady=(0, 15))
self.name_entry = tk.Entry(name_frame, font=('Arial', 16), width=25,
bg='#2d3047', fg='white', insertbackground='white',
relief='flat', bd=2, highlightthickness=2,
highlightbackground='#3498DB', highlightcolor='#3498DB')
self.name_entry.pack(pady=10, ipady=12, ipadx=20)
self.name_entry.focus()
self.name_entry.bind('', lambda e: self.start_instructions())
# Modern start button
button_frame = tk.Frame(main_frame, bg='#1a1a2e')
button_frame.pack(pady=40)
# Use a regular button for reliability
start_btn = tk.Button(button_frame,
text="Start Typing Test",
font=('Arial', 16, 'bold'),
bg='#3498DB',
fg='white',
relief='flat',
padx=30,
pady=15,
cursor='hand2',
command=self.start_instructions)
start_btn.pack()
# Add hover effects to the regular button
def on_enter(e):
start_btn.config(bg='#2980B9')
def on_leave(e):
start_btn.config(bg='#3498DB')
start_btn.bind("", on_enter)
start_btn.bind("", on_leave)
# Footer
footer_label = tk.Label(main_frame, text="Press Enter to start",
font=('Arial', 14), bg='#1a1a2e', fg='#7f8c8d')
footer_label.pack(pady=30)
def start_instructions(self):
"""Get name and show instructions"""
self.name = self.name_entry.get().strip()
if not self.name:
messagebox.showwarning("Name Required", "Please enter your name to continue.")
return
self.show_instructions_screen()
def show_instructions_screen(self):
"""Display animated instructions screen"""
self.clear_frame()
canvas = self.create_animated_background()
main_frame = tk.Frame(canvas, bg='#1a1a2e')
canvas.create_window(600, 400, window=main_frame)
# Title
title_label = tk.Label(main_frame, text="📋 Instructions",
font=('Arial', 32, 'bold'), bg='#1a1a2e', fg='#e6e6e6')
title_label.pack(pady=(40, 40))
# Instructions
instructions = [
"🎯 Type the text displayed in the test area as quickly and accurately as possible",
"📊 Your typing speed (WPM) will be calculated in real-time",
"⌨️ Use Backspace to correct mistakes",
"⏱️ The timer starts when you type the first character",
"🏆 Try to achieve the highest WPM with perfect accuracy",
"🔄 Multiple sample texts for varied practice",
"🌟 Compete for high scores and track your progress"
]
for i, instruction in enumerate(instructions):
instr_frame = tk.Frame(main_frame, bg='#1a1a2e')
instr_frame.pack(fill='x', pady=8)
bullet_label = tk.Label(instr_frame, text="•", font=('Arial', 20),
bg='#1a1a2e', fg='#3498DB')
bullet_label.pack(side='left', padx=(50, 15))
instr_label = tk.Label(instr_frame, text=instruction,
font=('Arial', 16), bg='#1a1a2e', fg='#b8b8b8',
justify='left')
instr_label.pack(side='left', fill='x')
# Start test button - Using regular button for reliability
button_frame = tk.Frame(main_frame, bg='#1a1a2e')
button_frame.pack(pady=50)
start_btn = tk.Button(button_frame,
text="Begin Test Journey",
font=('Arial', 16, 'bold'),
bg='#27AE60',
fg='white',
relief='flat',
padx=30,
pady=15,
cursor='hand2',
command=self.start_typing_test)
start_btn.pack()
# Add hover effects
def on_enter(e):
start_btn.config(bg='#219653')
def on_leave(e):
start_btn.config(bg='#27AE60')
start_btn.bind("", on_enter)
start_btn.bind("", on_leave)
def start_typing_test(self):
"""Start the typing test with animations"""
self.clear_frame()
self.target_text = random.choice(SAMPLE_TEXTS)
self.current_text = []
self.test_active = False
self.start_time = 0
# Main container with animated background
canvas = self.create_animated_background()
main_frame = tk.Frame(canvas, bg='#1a1a2e')
canvas.create_window(600, 400, window=main_frame)
# Header with stats
header_frame = tk.Frame(main_frame, bg='#16213e', relief='raised', bd=2)
header_frame.pack(fill='x', pady=(0, 30), padx=20)
stats_data = [
("Player:", self.name, "#b8b8b8"),
("WPM:", "0", "#27AE60"),
("Accuracy:", "0%", "#3498DB"),
("Time:", "0s", "#F39C12")
]
self.stat_labels = {}
for i, (label, value, color) in enumerate(stats_data):
stat_frame = tk.Frame(header_frame, bg='#16213e')
stat_frame.pack(side='left', expand=True, padx=25, pady=20)
label_widget = tk.Label(stat_frame, text=label, font=('Arial', 14),
bg='#16213e', fg='#b8b8b8')
label_widget.pack()
value_widget = tk.Label(stat_frame, text=value, font=('Arial', 20, 'bold'),
bg='#16213e', fg=color)
value_widget.pack()
self.stat_labels[label] = value_widget
# Target text display
target_frame = tk.Frame(main_frame, bg='#0f3460', relief='sunken', bd=3)
target_frame.pack(fill='x', pady=20, padx=20)
target_label = tk.Label(target_frame, text=self.target_text, font=('Arial', 18),
bg='#0f3460', fg='#b8b8b8', wraplength=1000,
justify='left')
target_label.pack(padx=25, pady=25)
# Typing area - FIXED VERSION
typing_frame = tk.Frame(main_frame, bg='#1a1a2e')
typing_frame.pack(fill='x', pady=25, padx=20)
# Create the typing display properly
self.typing_display = tk.Text(typing_frame, height=5, font=('Arial', 18),
bg='#1f4068', fg='#e6e6e6', wrap='word',
insertbackground='white', relief='solid', bd=2,
padx=15, pady=15, selectbackground='#3498DB')
self.typing_display.pack(fill='x')
# Configure text tags for colors
self.typing_display.tag_configure('correct', foreground='#27AE60', font=('Arial', 18, 'bold'))
self.typing_display.tag_configure('incorrect', foreground='#E74C3C', font=('Arial', 18, 'bold'))
# Set focus to the typing area
self.typing_display.focus_set()
# Bind events - FIXED: Don't return "break" for all keys
self.typing_display.bind('', self.on_key_press)
# Progress bar
self.progress_var = tk.DoubleVar()
progress_frame = tk.Frame(main_frame, bg='#1a1a2e')
progress_frame.pack(fill='x', pady=15, padx=20)
progress_label = tk.Label(progress_frame, text="Completion:", font=('Arial', 14),
bg='#1a1a2e', fg='#b8b8b8')
progress_label.pack(anchor='w')
# Custom progress bar
self.progress_canvas = tk.Canvas(progress_frame, height=20, bg='#1a1a2e', highlightthickness=0)
self.progress_canvas.pack(fill='x', pady=8)
self.progress_bar_bg = self.progress_canvas.create_rectangle(0, 0, 1000, 20, fill='#2d3047', outline='')
self.progress_bar_fg = self.progress_canvas.create_rectangle(0, 0, 0, 20, fill='#27AE60', outline='')
# Instructions
instr_label = tk.Label(main_frame, text="Start typing to begin the test | ESC to quit",
font=('Arial', 12), bg='#1a1a2e', fg='#7f8c8d')
instr_label.pack(pady=10)
# Bind escape key
self.root.bind('', self.quit_test)
def on_key_press(self, event):
"""Handle key press events - FIXED VERSION"""
if not self.test_active:
self.test_active = True
self.start_time = time.time()
self.update_stats()
# Handle special keys
if event.keysym == 'BackSpace':
if self.current_text:
self.current_text.pop()
# Allow default backspace behavior
return None
elif event.keysym == 'Escape':
self.quit_test()
return "break"
elif len(event.char) == 1 and ord(event.char) >= 32: # Printable characters
if len(self.current_text) < len(self.target_text):
self.current_text.append(event.char)
# Allow the character to be typed normally
return None
# Update display after processing
self.root.after(10, self.update_display)
# Don't break the event for normal typing
return None
def update_display(self):
"""Update the typing display with colored text"""
# Clear and rebuild the display
self.typing_display.delete('1.0', tk.END)
# Insert all current text with proper coloring
for i, char in enumerate(self.current_text):
if i < len(self.target_text):
correct_char = self.target_text[i]
if char == correct_char:
# Correct character - green
self.typing_display.insert(tk.END, char, 'correct')
else:
# Incorrect character - red
self.typing_display.insert(tk.END, char, 'incorrect')
else:
# Extra characters beyond target
self.typing_display.insert(tk.END, char)
# Update progress
progress = len(self.current_text) / len(self.target_text)
target_width = progress * 1000
self.progress_canvas.coords(self.progress_bar_fg, 0, 0, target_width, 20)
# Check for completion
joined_text = ''.join(self.current_text)
if joined_text == self.target_text and self.test_active:
self.test_active = False
self.save_high_score()
self.root.after(500, self.show_results)
def update_stats(self):
"""Update WPM, accuracy, and time statistics"""
if not self.test_active:
return
current_time = time.time()
time_elapsed = current_time - self.start_time
# Calculate WPM
if time_elapsed > 0:
self.wpm = round((len(self.current_text) / 5) / (time_elapsed / 60))
else:
self.wpm = 0
# Calculate accuracy
if self.current_text:
correct_chars = sum(1 for i, char in enumerate(self.current_text)
if i < len(self.target_text) and char == self.target_text[i])
self.accuracy = (correct_chars / len(self.current_text)) * 100
else:
self.accuracy = 0
# Update labels
self.stat_labels["WPM:"].config(text=str(self.wpm))
self.stat_labels["Accuracy:"].config(text=f"{self.accuracy:.1f}%")
self.stat_labels["Time:"].config(text=f"{time_elapsed:.1f}s")
# Schedule next update
self.root.after(100, self.update_stats)
def show_results(self):
"""Show animated final results screen"""
self.clear_frame()
canvas = self.create_animated_background()
main_frame = tk.Frame(canvas, bg='#1a1a2e')
canvas.create_window(600, 400, window=main_frame)
# Result emoji and rating based on performance
if self.wpm >= 80:
emoji, rating, color = "🏆", "Legendary Typist!", "#F1C40F"
elif self.wpm >= 60:
emoji, rating, color = "⭐", "Expert Typist!", "#27AE60"
elif self.wpm >= 40:
emoji, rating, color = "🚀", "Great Job!", "#3498DB"
elif self.wpm >= 25:
emoji, rating, color = "👍", "Good Progress!", "#F39C12"
else:
emoji, rating, color = "💪", "Keep Practicing!", "#E74C3C"
# Title
title_label = tk.Label(main_frame, text=f"{emoji} Test Completed! {emoji}",
font=('Arial', 36, 'bold'), bg='#1a1a2e', fg=color)
title_label.pack(pady=(50, 30))
# Results card
card_frame = tk.Frame(main_frame, bg='#16213e', relief='raised', bd=3)
card_frame.pack(pady=20, padx=50, fill='both', expand=True)
# Player name
name_label = tk.Label(card_frame, text=f"Player: {self.name}",
font=('Arial', 22), bg='#16213e', fg='#e6e6e6')
name_label.pack(pady=25)
# Stats
stats = [
("Final WPM", f"{self.wpm} WPM", "#27AE60"),
("Accuracy", f"{self.accuracy:.1f}%", "#3498DB"),
("Rating", rating, color)
]
for label, value, stat_color in stats:
stat_frame = tk.Frame(card_frame, bg='#16213e')
stat_frame.pack(fill='x', pady=18, padx=60)
label_widget = tk.Label(stat_frame, text=label, font=('Arial', 18),
bg='#16213e', fg='#b8b8b8')
label_widget.pack(side='left')
value_widget = tk.Label(stat_frame, text=value, font=('Arial', 20, 'bold'),
bg='#16213e', fg=stat_color)
value_widget.pack(side='right')
# High scores section
if self.high_scores:
scores_frame = tk.Frame(card_frame, bg='#16213e')
scores_frame.pack(pady=20)
scores_label = tk.Label(scores_frame, text="🏆 High Scores",
font=('Arial', 16), bg='#16213e', fg='#F1C40F')
scores_label.pack()
top_scores = "\n".join([f"{s['name']}: {s['wpm']} WPM" for s in self.high_scores[:3]])
scores_text = tk.Label(scores_frame, text=top_scores, font=('Arial', 14),
bg='#16213e', fg='#b8b8b8')
scores_text.pack(pady=10)
# Buttons
button_frame = tk.Frame(main_frame, bg='#1a1a2e')
button_frame.pack(pady=40)
retry_btn = tk.Button(button_frame,
text="Try Again",
font=('Arial', 14, 'bold'),
bg='#3498DB',
fg='white',
relief='flat',
padx=25,
pady=12,
cursor='hand2',
command=self.start_typing_test)
retry_btn.pack(side='left', padx=15)
menu_btn = tk.Button(button_frame,
text="Main Menu",
font=('Arial', 14, 'bold'),
bg='#9B59B6',
fg='white',
relief='flat',
padx=25,
pady=12,
cursor='hand2',
command=self.show_animated_welcome)
menu_btn.pack(side='left', padx=15)
quit_btn = tk.Button(button_frame,
text="Quit",
font=('Arial', 14, 'bold'),
bg='#E74C3C',
fg='white',
relief='flat',
padx=25,
pady=12,
cursor='hand2',
command=self.root.quit)
quit_btn.pack(side='left', padx=15)
# Add hover effects to all buttons
for btn in [retry_btn, menu_btn, quit_btn]:
colors = {
retry_btn: ('#3498DB', '#2980B9'),
menu_btn: ('#9B59B6', '#8E44AD'),
quit_btn: ('#E74C3C', '#C0392B')
}
normal_color, hover_color = colors[btn]
def make_enter(normal=normal_color, hover=hover_color):
return lambda e: e.widget.config(bg=hover)
def make_leave(normal=normal_color, hover=hover_color):
return lambda e: e.widget.config(bg=normal)
btn.bind("", make_enter())
btn.bind("", make_leave())
def quit_test(self, event=None):
"""Quit the current test with confirmation"""
if messagebox.askyesno("Quit Test", "Are you sure you want to quit the current test?"):
self.show_results()
def main():
root = tk.Tk()
app = TypingTestApp(root)
root.mainloop()
if __name__ == "__main__":
main()
چکیدهٔ ساختار کد و توضیح قطعات مهم:
- SAMPLE_TEXTS: آرایهای از رشتهها برای تمرین — متنهای تصادفی هنگام شروع تست انتخاب میشوند.
- ModernButton: یک کلاس کانواس سفارشی برای دکمههای مدرن با لبههای گرد، افکت hover و انیمیشن کلیک. این کلاس با استفاده از متدهای canvas رسم و مقیاسدهی میکند و برای زیباسازی رابط بهکار رفته (اگر خواستید میتوان آن را به دکمهٔ استاندارد بازگرداند).
- TypingTestApp: هستهٔ اپ — در سازنده (constructor) تنظیمات اولیه، بارگذاری رکوردها و نمایش صفحهٔ خوشآمد انجام میشود. روشهای مهم:
center_window: مرکز کردن پنجره.create_animated_background: ساخت بوم انیمیشنی پسزمینه با ذرات شناور (برای جلوهٔ بصری).show_animated_welcomeوshow_instructions_screen: صفحات مقدماتی و راهنما.start_typing_test: آمادهسازی متن هدف، ایجاد ویجتهای نمایش متن و ناحیهٔ تایپ، و شروع وضعیت تست.on_key_press: رسیدگی به رویدادهای صفحهکلید — ثبت کاراکترها، حذف با بکاسپیس، شروع تایمر و جلوگیری از بلاک کردن رفتار پیشفرض ورودی.update_display: رنگآمیزی کاراکترها بهصورت صحیح (سبز) یا غلط (قرمز) و بهروزرسانی نوار پیشرفت.update_stats: محاسبهٔ WPM، دقت و زمان و نمایش آنها بهصورت بلادرنگ.save_high_scoreوload_high_scores: ذخیره و بارگذاری رکوردها در فایل JSON.show_results: نمایش صفحهٔ نتایج با رتبهبندی و دکمههای مجدد.- نکات پیادهسازی کلیدی:
- برای رنگی کردن حروف از
Text.tag_configureاستفاده شده تا هر کاراکتر با تگ مناسب درج شود. - ورودی بهصورت پیشفرض اجازهٔ تایپ دارد؛ ما فقط متن واردشده را در یک لیست نگهداری میکنیم (
self.current_text) تا اعمال رنگ و منطق مقایسه ساده باشد. - محاسبهٔ WPM بر پایهٔ قاعدهٔ معمول است: هر 5 کاراکتر = یک کلمه.
- ذخیرهٔ رکوردها با محدود کردن به ۱۰ رکورد برتر انجام میشود.
- برای رنگی کردن حروف از
- تعمیرات رایج:
- اگر متن کامل نمایش داده نشد، از مقدار مناسب
wraplengthو اندازهٔ پنجره استفاده کنید. - اگر در بعضی توزیعها
tkinterنبود، بستهٔ سیستمی آن را نصب کنید. - برای ویجت کانواس سفارشی (ModernButton) اگر باگ دیدید، موقتا از
tk.Buttonاستفاده کنید تا منطق تست را جدا کنید.
- اگر متن کامل نمایش داده نشد، از مقدار مناسب
نحوه اجرا
مراحل اجرای برنامه:
- مطمئن شوید Python و tkinter نصب هستند.
- فایل برنامه (مثلاً
main.py) را با دستور زیر اجرا کنید:python main.py - در صفحهٔ خوشآمد نام خود را وارد کنید و Enter یا دکمهٔ شروع را بزنید.
- در صفحهٔ تست متن نمایش داده میشود؛ با اولین کاراکتر تایمر شروع میشود. با تکمیل متن تست، نتیجه ذخیره و صفحهٔ نتایج نمایش داده میشود.
- برای خروج سریع میتوانید کلید
Escapeرا فشار دهید تا پنجرهٔ تأیید نمایش یابد.
توجه: فایل رکوردها بهصورت high_scores.json در همان پوشهٔ برنامه ذخیره میشود. اگر میخواهید آن را ریست کنید، فایل را حذف کنید یا محتویات آن را پاک کنید.
منابع و مستندات
منابع مفید برای توسعهٔ بیشتر و یادگیری:
- مستندات رسمی Tkinter (برای ویجتها، تگهای متن و مدیریت رویداد).
- مستندات json در پایتون برای ذخیره و بارگذاری رکوردها.
- مقالات و آموزشهای مربوط به محاسبهٔ WPM و روشهای متداول اندازهگیری سرعت تایپ.
- برای توسعه: اضافه کردن حالتهای زمانی (مثلاً تست ۳۰ ثانیهای)، آمار تاریخی، یا استفاده از پایگاه دادهٔ سبک مثل SQLite برای ذخیرهٔ پیشرفته.
پیشنهاد توسعههای عملی: افزودن صفحهٔ تنظیمات (تغییر فونت، طول تست)، پشتیبانی از چند زبان، و افزودن نمودار پیشرفت با کتابخانههایی مانند matplotlib (در صورت نیاز به خروجی گرافیکی). همچنین بهینهسازی انیمیشن پسزمینه برای دستگاههای ضعیف با کاهش تعداد ذرات یا فریمها.