1.优化了退出逻辑,关闭的时候可以选择是否最后化托盘
2.优化了计数功能,加了延时器,保证数字的弹跳显示正常
更新1.3.0版本:
1.优化了获取的窗口句柄
2.更新记录逻辑,新增树状表格记录显示
3.新增导出,清空操作按钮
使用DeepSeek辅助制作的
功能:
1.记录当日总的键盘键入数量
2.表格展示不同软件中的键入数量,占比
3.导出数据

keyrecord.png (23.38 KB, 下载次数: 0)
下载附件
2025-8-11 15:30 上传
源码如下:
[Python] 纯文本查看 复制代码import keyboard
import time
from datetime import datetime
import psutil
import json
import os
import tkinter as tk
from tkinter import ttk, messagebox, filedialog
from threading import Thread, Event, Lock
from collections import defaultdict
import win32gui
import win32process
import win32event
import win32api
import win32con
import winerror
import sys
import pystray
from PIL import Image, ImageDraw
mutex = None
stop_event = Event()
class WindowsKeyLogger:
def __init__(self):
self.total_keystrokes = 0
self.app_keystrokes = defaultdict(int)
self.current_app = self.get_active_app()
self.log_file = "keystroke_log.json"
self.lock = Lock()
self.running = False
self.load_data()
self.valid_keys = {
'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm',
'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z',
'0', '1', '2', '3', '4', '5', '6', '7', '8', '9',
'num 0', 'num 1', 'num 2', 'num 3', 'num 4',
'num 5', 'num 6', 'num 7', 'num 8', 'num 9',
'space', 'enter', 'tab', 'backspace', 'delete',
',', '.', '/', ';', "'", '[', ']', '\\', '-', '=', '`',
'!', '@', '#', '$', '%', '^', '&', '*', '(', ')', '_', '+',
':', '"', '{', '}', '|', '~', '', '?'
}
def get_active_app(self):
try:
window = win32gui.GetForegroundWindow()
_, pid = win32process.GetWindowThreadProcessId(window)
process = psutil.Process(pid)
name = process.name()
if name == "ApplicationFrameHost.exe":
child_window = win32gui.GetWindow(window, win32gui.GW_CHILD)
_, child_pid = win32process.GetWindowThreadProcessId(child_window)
child_process = psutil.Process(child_pid)
name = child_process.name()
name = name.replace(".exe", "").replace(".EXE", "")
return name.split('.')[0] or "Unknown"
except Exception:
return "Unknown"
def load_data(self):
if os.path.exists(self.log_file):
try:
with open(self.log_file, 'r') as f:
data = json.load(f)
today = datetime.now().strftime("%Y-%m-%d")
if today in data:
self.total_keystrokes = data[today]["total"]
self.app_keystrokes = defaultdict(int, data[today]["apps"])
except Exception:
self.total_keystrokes = 0
self.app_keystrokes = defaultdict(int)
def save_data(self):
today = datetime.now().strftime("%Y-%m-%d")
data = {}
if os.path.exists(self.log_file):
with open(self.log_file, 'r') as f:
try:
data = json.load(f)
except:
data = {}
data[today] = {
"total": self.total_keystrokes,
"apps": dict(self.app_keystrokes)
}
with open(self.log_file, 'w') as f:
json.dump(data, f, indent=4)
def export_data(self, filepath):
with self.lock:
export_dict = {
"date": datetime.now().strftime("%Y-%m-%d"),
"total_keystrokes": self.total_keystrokes,
"app_keystrokes": dict(self.app_keystrokes)
}
with open(filepath, 'w', encoding='utf-8') as f:
json.dump(export_dict, f, indent=4, ensure_ascii=False)
def is_valid_key(self, event):
if event.event_type != keyboard.KEY_DOWN:
return False
key_name = event.name.lower()
if key_name in self.valid_keys:
return True
if len(key_name) == 1 and key_name.isprintable():
return True
return False
def on_key_event(self, event):
if not self.running:
return
if not self.is_valid_key(event):
return
new_app = self.get_active_app()
with self.lock:
if new_app != self.current_app:
self.current_app = new_app
self.total_keystrokes += 1
self.app_keystrokes[self.current_app] += 1
def start_logging(self):
if not self.running:
self.running = True
keyboard.hook(self.on_key_event)
def stop_logging(self):
if self.running:
self.running = False
keyboard.unhook_all()
with self.lock:
self.save_data()
class KeyLoggerGUI:
def __init__(self, root):
self.root = root
self.root.title("Windows键盘敲击记录器")
self.root.geometry("600x400")
self.key_logger = WindowsKeyLogger()
self.create_widgets()
self.update_ui()
self.root.protocol("WM_DELETE_WINDOW", self.on_window_close)
self.icon = None
self.create_tray_icon()
self.is_hidden = False
def create_widgets(self):
frame = ttk.Frame(self.root, padding=10)
frame.pack(fill=tk.BOTH, expand=True)
btn_frame = ttk.Frame(frame)
btn_frame.pack(fill=tk.X, pady=5)
self.start_btn = ttk.Button(btn_frame, text="开始记录", command=self.start_logging)
self.start_btn.pack(side=tk.LEFT, padx=5)
self.stop_btn = ttk.Button(btn_frame, text="停止记录", command=self.stop_logging, state=tk.DISABLED)
self.stop_btn.pack(side=tk.LEFT, padx=5)
self.export_btn = ttk.Button(btn_frame, text="导出统计", command=self.export_stats)
self.export_btn.pack(side=tk.LEFT, padx=5)
total_frame = ttk.LabelFrame(frame, text="今日总敲击数")
total_frame.pack(fill=tk.X, pady=5)
self.total_label = ttk.Label(total_frame, text="0", font=("Arial", 24))
self.total_label.pack()
app_frame = ttk.LabelFrame(frame, text="按应用程序统计")
app_frame.pack(fill=tk.BOTH, expand=True)
self.tree = ttk.Treeview(app_frame, columns=("count", "percentage"), show='tree headings')
self.tree.heading("#0", text="应用程序")
self.tree.heading("count", text="敲击数")
self.tree.heading("percentage", text="百分比")
self.tree.column("#0", width=250, anchor=tk.W)
self.tree.column("count", width=100, anchor=tk.CENTER)
self.tree.column("percentage", width=100, anchor=tk.CENTER)
self.tree.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)
scrollbar = ttk.Scrollbar(app_frame, orient=tk.VERTICAL, command=self.tree.yview)
self.tree.configure(yscrollcommand=scrollbar.set)
scrollbar.pack(side=tk.RIGHT, fill=tk.Y)
def update_ui(self):
with self.key_logger.lock:
total = self.key_logger.total_keystrokes
apps = dict(self.key_logger.app_keystrokes)
self.total_label.config(text=str(total))
self.tree.delete(*self.tree.get_children())
sorted_apps = sorted(apps.items(), key=lambda x: x[1], reverse=True)
for app, count in sorted_apps:
percentage = f"{(count / total) * 100:.1f}%" if total > 0 else "0%"
self.tree.insert('', tk.END, iid=app, text=app, values=(count, percentage))
self.root.after(100, self.update_ui)
def start_logging(self):
self.key_logger.start_logging()
self.start_btn.config(state=tk.DISABLED)
self.stop_btn.config(state=tk.NORMAL)
def stop_logging(self):
self.key_logger.stop_logging()
self.start_btn.config(state=tk.NORMAL)
self.stop_btn.config(state=tk.DISABLED)
def export_stats(self):
filepath = filedialog.asksaveasfilename(
defaultextension=".json",
filetypes=[("JSON 文件", "*.json"), ("所有文件", "*.*")],
title="导出统计数据"
)
if filepath:
try:
self.key_logger.export_data(filepath)
messagebox.showinfo("导出成功", f"统计数据已导出至:\n{filepath}")
except Exception as e:
messagebox.showerror("导出失败", f"导出统计数据失败:\n{e}")
def on_window_close(self):
answer = messagebox.askquestion("退出确认", "关闭窗口时要最小化到托盘吗?选择否则退出程序。", icon='question')
if answer == 'yes':
self.hide_window()
else:
self.quit_app()
def hide_window(self):
if not self.is_hidden:
self.root.withdraw()
self.is_hidden = True
def show_window(self):
if self.is_hidden:
self.root.deiconify()
self.is_hidden = False
def quit_app(self, icon=None, item=None):
self.key_logger.stop_logging()
if self.icon:
self.icon.stop()
self.root.destroy()
sys.exit(0)
def create_image(self, width, height, color1, color2):
image = Image.new('RGB', (width, height), color1)
dc = ImageDraw.Draw(image)
dc.rectangle(
(width // 2, 0, width, height // 2),
fill=color2)
dc.rectangle(
(0, height // 2, width // 2, height),
fill=color2)
return image
def create_tray_icon(self):
image = self.create_image(64, 64, 'black', 'white')
menu = pystray.Menu(
pystray.MenuItem('显示/隐藏窗口', self.toggle_window),
pystray.MenuItem('退出', self.quit_app)
)
self.icon = pystray.Icon("keylogger", image, "键盘敲击记录器", menu)
t = Thread(target=self.icon.run, daemon=True)
t.start()
def toggle_window(self, icon, item):
if self.is_hidden:
self.show_window()
else:
self.hide_window()
def create_mutex():
global mutex
mutex = win32event.CreateMutex(None, False, "KeyLoggerUniqueMutexName")
last_error = win32api.GetLastError()
if last_error == winerror.ERROR_ALREADY_EXISTS:
return False
return True
def main():
if not create_mutex():
tk.messagebox.showerror("错误", "程序已在运行!")
sys.exit(0)
root = tk.Tk()
app = KeyLoggerGUI(root)
root.mainloop()
if __name__ == "__main__":
main()
成品:
百度云:https://pan.baidu.com/s/18Z17_fAWlkPvgm-yRxOjIw?pwd=xgmb 提取码: xgmb
蓝奏云:https://wwwt.lanzouw.com/iTwoK34fqedg
密码:fj2e