简易 python 打字计数器

前置 python 库

在终端中输入以下指令安装库:

pip install pynput

作用

监视总打字量,一分钟打字量和 CPM 并用多样的形式呈现。

使用说明

可以直接拖动窗口调整位置。

可以重置计数器。

可以隐藏窗口,按 Ctrl+Shift+T 显示窗口。

如果在本地使用,在终端输入以下指令以启动:

python example.py

其中 example.py 是您保存的文件名。

效果展示

代码

import tkinter as tk
from pynput import keyboard
from collections import deque
import time
from threading import Lock
import math

class TypingCounter:
    def __init__(self):
        self.char_count = 0
        self.is_counting = True
        self.window_hidden = False
        self.key_timestamps = deque(maxlen=1000)
        self.timestamps_lock = Lock()
        self.cpm_history = deque(maxlen=60)
        self.history_lock = Lock()
        self.root = tk.Tk()
        self.root.title("打字计数器 - 按Ctrl+Shift+T显示窗口")
        self.root.overrideredirect(True)
        self.root.attributes('-topmost', True)
        self.root.geometry('+20+70')
        self.root.configure(bg='#f0f0f0')
        self.main_container = tk.Frame(self.root, bg='#f0f0f0')
        self.main_container.pack(padx=10, pady=10)
        self.stats_frame = tk.Frame(self.main_container, bg='#f0f0f0')
        self.stats_frame.pack(side='left', padx=(0, 15))
        self.total_label = tk.Label(
            self.stats_frame,
            text="总字符数: 0",
            font=("Microsoft YaHei", 12, "bold"),
            bg='#f0f0f0',
            fg='#333333'
        )
        self.total_label.pack(anchor='w', pady=(0, 5))
        self.current_min_label = tk.Label(
            self.stats_frame,
            text="当前分钟: 0",
            font=("Microsoft YaHei", 10),
            bg='#f0f0f0',
            fg='#666666'
        )
        self.current_min_label.pack(anchor='w', pady=(0, 5))
        self.avg_label = tk.Label(
            self.stats_frame,
            text="过去5秒平均: 0 CPM",
            font=("Microsoft YaHei", 10),
            bg='#f0f0f0',
            fg='#228B22'
        )
        self.avg_label.pack(anchor='w', pady=(0, 5))
        self.gauge_frame = tk.Frame(self.main_container, bg='#f0f0f0')
        self.gauge_frame.pack(side='right')
        self.canvas_width = 140
        self.canvas_height = 160
        self.canvas = tk.Canvas(
            self.gauge_frame,
            width=self.canvas_width,
            height=self.canvas_height,
            bg='#f0f0f0',
            highlightthickness=0
        )
        self.canvas.pack()
        self.gauge_center_x = self.canvas_width // 2
        self.gauge_center_y = 70
        self.gauge_radius = 45
        self.gauge_width = 12
        self.chart_x = 5
        self.chart_y = 120
        self.chart_width = 130
        self.chart_height = 20
        self.gauge_arc = None
        self.gauge_text = None
        self.bar_chart_items = []
        self.chart_title_text = None
        self.chart_peak_text = None
        self.chart_grid_lines = []
        self.chart_labels = []
        self.color_gradient = [
            (0, 25, '#CC0000'),      # 暗红色
            (25, 50, '#E64A19'),     # 深橙红色
            (50, 75, '#FF5722'),     # 橙红色
            (75, 100, '#FF7043'),    # 亮橙红色
            (100, 125, '#FF8A65'),   # 浅橙红色
            (125, 150, '#FFAB40'),   # 橙色
            (150, 175, '#FFC107'),   # 琥珀色
            (175, 200, '#FFD740'),   # 浅琥珀色
            (200, 225, '#C0CA33'),   # 黄绿色
            (225, 250, '#9CCC65'),   # 浅黄绿色
            (250, 275, '#7CB342'),   # 浅绿色
            (275, 300, '#4CAF50'),   # 绿色
            (300, 325, '#43A047'),   # 深绿色
            (325, 350, '#388E3C'),   # 更深绿色
            (350, 375, '#2E7D32'),   # 深绿色
            (375, 405, '#1B5E20')    # 最深绿色
        ]
        self.draw_gauge_background()
        self.draw_chart_background()
        self.button_frame = tk.Frame(self.root, bg='#f0f0f0')
        self.button_frame.pack(fill='x', padx=10, pady=(0, 10))
        self.reset_btn = tk.Button(
            self.button_frame,
            text="重置计数器",
            command=self.reset_counters,
            font=("Microsoft YaHei", 9),
            bg='#ff6b6b',
            fg='black',
            relief='flat',
            padx=10,
            pady=3
        )
        self.reset_btn.pack(side='left')
        self.hide_btn = tk.Button(
            self.button_frame,
            text="隐藏窗口",
            command=self.toggle_window_visibility,
            font=("Microsoft YaHei", 9),
            bg='#6b6bff',
            fg='black',
            relief='flat',
            padx=10,
            pady=3
        )
        self.hide_btn.pack(side='left', padx=(5, 0))
        self.close_btn = tk.Button(
            self.button_frame,
            text="关闭",
            command=self.on_close,
            font=("Microsoft YaHei", 9),
            bg='#cccccc',
            fg='black',
            relief='flat',
            padx=10,
            pady=3
        )
        self.close_btn.pack(side='right')
        self.main_container.bind('<ButtonPress-1>', self.start_move)
        self.main_container.bind('<ButtonRelease-1>', self.stop_move)
        self.main_container.bind('<B1-Motion>', self.on_move)
        self.last_second_update = time.time()
        self.keyboard_listener = None
        self.start_global_hotkey_listener()
        self.update_display()

    def start_global_hotkey_listener(self):
        def on_activate():
            if self.window_hidden:
                self.show_window()

        def for_canonical(f):
            return lambda k: f(self.canonical(k))
        hotkey = keyboard.HotKey(
            keyboard.HotKey.parse('<ctrl>+<shift>+t'),
            on_activate
        )
        self.keyboard_listener = keyboard.Listener(
            on_press=for_canonical(hotkey.press),
            on_release=for_canonical(hotkey.release)
        )
        self.keyboard_listener.daemon = True
        self.keyboard_listener.start()

    def canonical(self, key):
        if key is None:
            return None
        return key.canonical if hasattr(key, 'canonical') else key

    def draw_gauge_background(self):
        self.canvas.create_arc(
            self.gauge_center_x - self.gauge_radius,
            self.gauge_center_y - self.gauge_radius,
            self.gauge_center_x + self.gauge_radius,
            self.gauge_center_y + self.gauge_radius,
            start=0,
            extent=180,
            style='arc',
            width=self.gauge_width,
            outline='#e0e0e0'
        )
        self.canvas.create_text(
            self.gauge_center_x - self.gauge_radius + 10,
            self.gauge_center_y,
            text="400",
            font=("Microsoft YaHei", 8),
            fill='#666666'
        )
        self.canvas.create_text(
            self.gauge_center_x + self.gauge_radius - 10,
            self.gauge_center_y,
            text="0",
            font=("Microsoft YaHei", 8),
            fill='#666666'
        )
        self.canvas.create_text(
            self.gauge_center_x,
            self.gauge_center_y - self.gauge_radius - 10,
            text="实时速度",
            font=("Microsoft YaHei", 9, "bold"),
            fill='#333333'
        )

    def draw_chart_background(self):
        self.canvas.create_rectangle(
            self.chart_x,
            self.chart_y,
            self.chart_x + self.chart_width,
            self.chart_y + self.chart_height,
            outline='#cccccc',
            width=1
        )
        self.chart_title_text = self.canvas.create_text(
            self.chart_x + self.chart_width // 2,
            self.chart_y - 10,
            text="过去一分钟趋势",
            font=("Microsoft YaHei", 8),
            fill='#666666'
        )

    def toggle_window_visibility(self):
        if self.window_hidden:
            self.show_window()
        else:
            self.hide_window()

    def hide_window(self):
        self.root.attributes('-alpha', 0.0)
        self.root.attributes('-topmost', False)
        self.window_hidden = True
        self.hide_btn.config(text="窗口已隐藏 (Ctrl+Shift+T显示)")
        self.root.title("打字计数器 - 窗口已隐藏,按Ctrl+Shift+T显示")

    def show_window(self):
        self.root.attributes('-alpha', 1.0)
        self.root.attributes('-topmost', True)
        self.window_hidden = False
        self.hide_btn.config(text="隐藏窗口")
        self.root.title("打字计数器 - 按Ctrl+Shift+T显示窗口")
        self.root.focus_force()

    def update_chart_background(self, max_cpm):
        for item in self.chart_grid_lines + self.chart_labels:
            self.canvas.delete(item)
        self.chart_grid_lines = []
        self.chart_labels = []
        if max_cpm < 10:
            max_cpm = 10
        interval = 50
        num_intervals = int(math.ceil(max_cpm / interval))
        for i in range(num_intervals + 1):
            value = i * interval
            y = self.chart_y + self.chart_height - (value / max_cpm) * self.chart_height
            if y >= self.chart_y:
                line_id = self.canvas.create_line(
                    self.chart_x, y,
                    self.chart_x + self.chart_width, y,
                    fill='#f0f0f0' if value > 0 else '#cccccc',
                    width=1
                )
                self.chart_grid_lines.append(line_id)
                label_id = self.canvas.create_text(
                    self.chart_x - 5,
                    y,
                    text=str(value),
                    font=("Microsoft YaHei", 6),
                    fill='#999999',
                    anchor='e'
                )
                self.chart_labels.append(label_id)
        if self.chart_peak_text:
            self.canvas.delete(self.chart_peak_text)
        self.chart_peak_text = self.canvas.create_text(
            self.chart_x + self.chart_width // 2,
            self.chart_y + self.chart_height + 10,
            text=f"峰值: {int(max_cpm)}",
            font=("Microsoft YaHei", 7),
            fill='#666666'
        )

    def get_color_for_cpm(self, cpm):
        clamped_cpm = max(0, min(400, cpm))
        for min_val, max_val, color in self.color_gradient:
            if min_val <= clamped_cpm < max_val:
                return color
        return '#00FF00'

    def update_gauge(self, cpm_value):
        cpm_clamped = max(0, min(400, cpm_value))
        angle = (cpm_clamped / 400) * 180
        color = self.get_color_for_cpm(cpm_clamped)
        if self.gauge_arc:
            self.canvas.delete(self.gauge_arc)
        self.gauge_arc = self.canvas.create_arc(
            self.gauge_center_x - self.gauge_radius,
            self.gauge_center_y - self.gauge_radius,
            self.gauge_center_x + self.gauge_radius,
            self.gauge_center_y + self.gauge_radius,
            start=0,
            extent=angle,
            style='arc',
            width=self.gauge_width,
            outline=color
        )
        if self.gauge_text:
            self.canvas.delete(self.gauge_text)
        self.gauge_text = self.canvas.create_text(
            self.gauge_center_x,
            self.gauge_center_y + 5,
            text=f"{int(cpm_clamped)}",
            font=("Microsoft YaHei", 14, "bold"),
            fill=color
        )
    def update_chart(self):
        for item in self.bar_chart_items:
            self.canvas.delete(item)
        self.bar_chart_items = []
        with self.history_lock:
            history = list(self.cpm_history)
        if not history:
            if self.chart_peak_text:
                self.canvas.itemconfig(self.chart_peak_text, text="峰值: 0")
            return
        if len(history) > 30:
            history = history[-30:]

        bar_count = len(history)
        if bar_count == 0:
            return
        peak_cpm = max(history)
        display_max = max(peak_cpm, 10)
        self.update_chart_background(display_max)
        bar_width = max(1, (self.chart_width - bar_count + 1) // bar_count)
        spacing = 1
        for i, cpm in enumerate(history):
            if display_max > 0:
                bar_height = (cpm / display_max) * self.chart_height
                bar_height = max(1, bar_height)
            else:
                bar_height = 1

            bar_height = min(bar_height, self.chart_height)
            x1 = self.chart_x + i * (bar_width + spacing)
            y1 = self.chart_y + self.chart_height - bar_height
            x2 = x1 + bar_width
            y2 = self.chart_y + self.chart_height

            color = self.get_color_for_cpm(cpm)
            bar_id = self.canvas.create_rectangle(
                x1, y1, x2, y2,
                fill=color,
                outline=color,
                width=1
            )
            self.bar_chart_items.append(bar_id)

    def record_cpm_history(self, cpm_value):
        current_time = time.time()
        if current_time - self.last_second_update >= 1.0:
            with self.history_lock:
                self.cpm_history.append(cpm_value)
            self.last_second_update = current_time
    def calculate_cpm(self):
        current_time = time.time()
        five_seconds_ago = current_time - 5

        with self.timestamps_lock:
            recent_keys = [ts for ts in self.key_timestamps if ts >= five_seconds_ago]
            count = len(recent_keys)
            if count >= 1:
                time_window = 5
            else:
                return 0.0

            if time_window > 0:
                cpm = (count / time_window) * 60
            else:
                cpm = 0.0

            return round(cpm, 1)
    def calculate_current_minute(self):
        current_time = time.time()
        one_minute_ago = current_time - 60
        with self.timestamps_lock:
            count = sum(1 for ts in self.key_timestamps if ts >= one_minute_ago)
        return count
    def on_press(self, key):
        if not self.is_counting:
            return

        try:
            if hasattr(key, 'char') and key.char:
                self.char_count += 1
                # 记录按键时间戳
                with self.timestamps_lock:
                    self.key_timestamps.append(time.time())
        except AttributeError:
            pass
    def update_display(self):
        cpm = self.calculate_cpm()
        current_min_count = self.calculate_current_minute()
        self.record_cpm_history(cpm)
        self.total_label.config(text=f"总字符数: {self.char_count}")
        self.current_min_label.config(text=f"当前分钟: {current_min_count}")
        self.avg_label.config(text=f"过去5秒平均: {cpm} CPM")
        self.update_gauge(cpm)
        self.update_chart()
        color = self.get_color_for_cpm(cpm)
        self.avg_label.config(fg=color)
        self.root.after(100, self.update_display)
    def reset_counters(self):
        self.char_count = 0
        with self.timestamps_lock:
            self.key_timestamps.clear()
        with self.history_lock:
            self.cpm_history.clear()
    def start_move(self, event):
        self.x = event.x
        self.y = event.y

    def stop_move(self, event):
        self.x = None
        self.y = None

    def on_move(self, event):
        deltax = event.x - self.x
        deltay = event.y - self.y
        x = self.root.winfo_x() + deltax
        y = self.root.winfo_y() + deltay
        self.root.geometry(f"+{x}+{y}")
    def on_close(self):
        self.is_counting = False
        if self.keyboard_listener:
            self.keyboard_listener.stop()
        self.root.destroy()

    def run(self):
        listener = keyboard.Listener(on_press=self.on_press)
        listener.daemon = True
        listener.start()
        self.root.mainloop()

if __name__ == "__main__":
    counter = TypingCounter()
    counter.run()
posted @ 2025-12-25 18:38  wangtairan114  阅读(2)  评论(1)    收藏  举报