ساخت اپ تست سرعت تایپ با Tkinter در Python

پروژه‌های رابط گرافیکی پایتون

آموزش ۱۳ دقیقه مطالعه
فهرست مطالب
این مطلب از سایت tool.hamidvalad.ir گرفته شده است

معرفی

در این آموزش یک اپلیکیشن تست سرعت تایپ با رابط گرافیکی مدرن می‌سازیم که سرعت (WPM)، دقت و زمان را محاسبه می‌کند. برنامه شامل صفحه خوش‌آمد، دستورالعمل، فضای تایپ با رنگ‌بندی کاراکترهای صحیح/غلط، نوار پیشرفت، ذخیره رکوردها و صفحه نتایج با رتبه‌بندی است. هدف ایجاد یک ابزار تمرینی با تجربه کاربری خوب و پایه‌ای مناسب برای گسترش است.

نصب و پیش‌نیازها

پیش‌نیازها و نکات نصب:

  • Python 3.8+ (توصیه‌شده 3.10/3.11)
  • tkinter (به صورت پیش‌فرض در پایتون موجود است) - کتابخانه اصلی برای ایجاد رابط گرافیکی و ویجت‌ها
  • time - برای مدیریت زمان، محاسبه مدت تست و ایجاد تاخیر در انیمیشن‌ها
  • random - برای انتخاب تصادفی متن‌های نمونه از لیست SAMPLE_TEXTS
  • math - برای محاسبات ریاضی مورد نیاز در رسم شکل‌های هندسی و انیمیشن‌ها
  • json - برای ذخیره و بازیابی امتیازات برتر در فایل high_scores.json
  • os - برای بررسی وجود فایل امتیازات و مدیریت مسیرهای فایل

این پروژه تنها به کتابخانه‌های استاندارد پایتون نیاز دارد:

کدنویسی پروژه

Python
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()

چکیدهٔ ساختار کد و توضیح قطعات مهم:

  1. SAMPLE_TEXTS: آرایه‌ای از رشته‌ها برای تمرین — متن‌های تصادفی هنگام شروع تست انتخاب می‌شوند.
  2. ModernButton: یک کلاس کانواس سفارشی برای دکمه‌های مدرن با لبه‌های گرد، افکت hover و انیمیشن کلیک. این کلاس با استفاده از متدهای canvas رسم و مقیاس‌دهی می‌کند و برای زیباسازی رابط به‌کار رفته (اگر خواستید می‌توان آن را به دکمهٔ استاندارد بازگرداند).
  3. 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: نمایش صفحهٔ نتایج با رتبه‌بندی و دکمه‌های مجدد.
  4. نکات پیاده‌سازی کلیدی:
    • برای رنگی کردن حروف از Text.tag_configure استفاده شده تا هر کاراکتر با تگ مناسب درج شود.
    • ورودی به‌صورت پیش‌فرض اجازهٔ تایپ دارد؛ ما فقط متن واردشده را در یک لیست نگهداری می‌کنیم (self.current_text) تا اعمال رنگ و منطق مقایسه ساده باشد.
    • محاسبهٔ WPM بر پایهٔ قاعدهٔ معمول است: هر 5 کاراکتر = یک کلمه.
    • ذخیرهٔ رکوردها با محدود کردن به ۱۰ رکورد برتر انجام می‌شود.
  5. تعمیرات رایج:
    • اگر متن کامل نمایش داده نشد، از مقدار مناسب wraplength و اندازهٔ پنجره استفاده کنید.
    • اگر در بعضی توزیع‌ها tkinter نبود، بستهٔ سیستمی آن را نصب کنید.
    • برای ویجت کانواس سفارشی (ModernButton) اگر باگ دیدید، موقتا از tk.Button استفاده کنید تا منطق تست را جدا کنید.

نحوه اجرا

مراحل اجرای برنامه:

  1. مطمئن شوید Python و tkinter نصب هستند.
  2. فایل برنامه (مثلاً main.py) را با دستور زیر اجرا کنید:
    Terminal
    python main.py
  3. در صفحهٔ خوش‌آمد نام خود را وارد کنید و Enter یا دکمهٔ شروع را بزنید.
  4. در صفحهٔ تست متن نمایش داده می‌شود؛ با اولین کاراکتر تایمر شروع می‌شود. با تکمیل متن تست، نتیجه ذخیره و صفحهٔ نتایج نمایش داده می‌شود.
  5. برای خروج سریع می‌توانید کلید Escape را فشار دهید تا پنجرهٔ تأیید نمایش یابد.

توجه: فایل رکوردها به‌صورت high_scores.json در همان پوشهٔ برنامه ذخیره می‌شود. اگر می‌خواهید آن را ریست کنید، فایل را حذف کنید یا محتویات آن را پاک کنید.

منابع و مستندات

منابع مفید برای توسعهٔ بیشتر و یادگیری:

  • مستندات رسمی Tkinter (برای ویجت‌ها، تگ‌های متن و مدیریت رویداد).
  • مستندات json در پایتون برای ذخیره و بارگذاری رکوردها.
  • مقالات و آموزش‌های مربوط به محاسبهٔ WPM و روش‌های متداول اندازه‌گیری سرعت تایپ.
  • برای توسعه: اضافه کردن حالت‌های زمانی (مثلاً تست ۳۰ ثانیه‌ای)، آمار تاریخی، یا استفاده از پایگاه دادهٔ سبک مثل SQLite برای ذخیرهٔ پیشرفته.

پیشنهاد توسعه‌های عملی: افزودن صفحهٔ تنظیمات (تغییر فونت، طول تست)، پشتیبانی از چند زبان، و افزودن نمودار پیشرفت با کتابخانه‌هایی مانند matplotlib (در صورت نیاز به خروجی گرافیکی). همچنین بهینه‌سازی انیمیشن پس‌زمینه برای دستگاه‌های ضعیف با کاهش تعداد ذرات یا فریم‌ها.