今天突发奇想想看看自己一天能空闲(moyu)多久,所以就开发了个空闲计时小工具
基础逻辑就是实时监控自己电脑的鼠标和键盘,当启动小工具时就开始实时监控,当符合你设置的空闲市场后开始计时然后可以累计,还可以把每天的空闲时间都记录下来

本工具是基于Python进行开发,为了可以查询累计记录还用了轻量化数据库SQLite进行储存,如下为详细代码:
# -*- coding: utf-8 -*- # idle_timerv4.py # 完整的空闲计时器应用 (Python 3.x) import sqlite3 import time import threading import datetime import ctypes from ctypes import wintypes import tkinter as tk from tkinter import ttk # --- 1. 数据库管理模块 --- class DatabaseManager: def __init__(self, db_path='idle_tracker.db'): self.db_path = db_path # 使用 check_same_thread=False 允许跨线程使用,配合锁保证安全 self.conn = sqlite3.connect(db_path, check_same_thread=False) self.lock = threading.Lock() self.create_table() def create_table(self): with self.lock: cursor = self.conn.cursor() cursor.execute(''' CREATE TABLE IF NOT EXISTS idle_sessions ( id INTEGER PRIMARY KEY AUTOINCREMENT, start_time TEXT NOT NULL, end_time TEXT NOT NULL, duration_seconds REAL NOT NULL, date TEXT NOT NULL ) ''') self.conn.commit() def insert_record(self, start_time, end_time, duration): with self.lock: cursor = self.conn.cursor() date_str = start_time.strftime('%Y-%m-%d') cursor.execute(''' INSERT INTO idle_sessions (start_time, end_time, duration_seconds, date) VALUES (?, ?, ?, ?) ''', (start_time.isoformat(), end_time.isoformat(), duration, date_str)) self.conn.commit() def get_sessions_by_date(self, date_str): with self.lock: cursor = self.conn.cursor() cursor.execute(''' SELECT start_time, end_time, duration_seconds FROM idle_sessions WHERE date = ? ''', (date_str,)) return cursor.fetchall() # --- 2. Windows API 辅助类 --- class WindowsAPIHelper: @staticmethod def get_last_input_info(): """获取系统自启动或上次用户输入以来经过的毫秒数。""" try: class LASTINPUTINFO(ctypes.Structure): _fields_ = [("cbSize", wintypes.UINT), ("dwTime", wintypes.DWORD)] lii = LASTINPUTINFO() lii.cbSize = ctypes.sizeof(LASTINPUTINFO) if ctypes.windll.user32.GetLastInputInfo(ctypes.byref(lii)): return lii.dwTime else: return 0 except Exception as e: print(f"获取最后输入信息失败: {e}") return 0 @staticmethod def is_workstation_locked(): """检查工作站是否被锁定""" try: user32 = ctypes.windll.user32 h_desktop = user32.OpenInputDesktop(0, False, 0x0100) # DESKTOP_READOBJECTS if h_desktop == 0: return True # 无法打开输入桌面,视为锁定 user32.CloseDesktop(h_desktop) return False except Exception: return False # --- 3. 空闲监控模块 --- class IdleMonitor: def __init__(self, threshold_seconds, db_manager, log_callback=None): self.threshold = threshold_seconds self.db = db_manager self.log = log_callback if log_callback else print self.thread = None self._stop_event = threading.Event() self.is_timing = False self.session_start_time = None self.manual_mode = False def _monitor_loop(self): last_logged_idle_seconds = -1 while not self._stop_event.is_set(): try: # 如果处于手动模式,跳过自动检测逻辑 if self.manual_mode: self._stop_event.wait(1.0) continue # 1. 获取系统信息 last_input_ms = WindowsAPIHelper.get_last_input_info() current_ms = ctypes.windll.kernel32.GetTickCount() locked = WindowsAPIHelper.is_workstation_locked() # 2. 计算空闲时间 idle_seconds = (current_ms - last_input_ms) / 1000.0 # 3. 核心状态机逻辑 if locked: if not self.is_timing: self._start_session() else: if idle_seconds > self.threshold: if not self.is_timing: self._start_session() else: if self.is_timing: self._end_session("检测到用户活动,结束空闲会话。") # 4. 日志记录 if int(idle_seconds) != last_logged_idle_seconds: self.log(f"系统空闲时间: {idle_seconds:.1f} 秒") last_logged_idle_seconds = int(idle_seconds) except Exception as e: self.log(f"[错误] 监控循环发生异常: {e}") self._stop_event.wait(1.0) def _start_session(self): self.is_timing = True self.session_start_time = datetime.datetime.now() self.log(f"⏱️ 开始计时: {self.session_start_time.strftime('%H:%M:%S')}") def _end_session(self, reason=""): if not self.is_timing: return self.is_timing = False # 先重置状态,防止重复触发 end_time = datetime.datetime.now() duration = (end_time - self.session_start_time).total_seconds() self.log(f"⏱️ 结束计时: {end_time.strftime('%H:%M:%S')} | 原因: {reason} | 持续时间: {duration:.1f} 秒") try: self.db.insert_record(self.session_start_time, end_time, duration) except Exception as e: self.log(f"[错误] 写入数据库失败: {e}") self.session_start_time = None def start_monitoring(self, manual=False): self.manual_mode = manual self._stop_event.clear() self.log(f"监控已启动,空闲阈值: {self.threshold} 秒。模式: {'手动' if manual else '自动'}") self.thread = threading.Thread(target=self._monitor_loop, daemon=True) self.thread.start() def stop_monitoring(self): self._stop_event.set() if self.thread: self.thread.join(timeout=2.0) self.log("监控已停止。") def manual_start(self): if not self.is_timing: self._start_session() def manual_stop(self): if self.is_timing: self._end_session("手动结束计时") # --- 4. GUI 主应用 --- class App: def __init__(self, root): self.root = root self.root.title("空闲计时器") self.root.geometry("700x500") self.db_manager = DatabaseManager() self.idle_monitor = None # --- 配置区 --- config_frame = ttk.LabelFrame(root, text="设置") config_frame.pack(fill="x", padx=10, pady=5) ttk.Label(config_frame, text="空闲阈值 (秒):").grid(row=0, column=0, padx=5, pady=5) self.threshold_entry = ttk.Entry(config_frame, width=10) self.threshold_entry.grid(row=0, column=1, padx=5, pady=5) self.threshold_entry.insert(0, "30") # 模式选择 self.mode_var = tk.StringVar(value="auto") ttk.Radiobutton(config_frame, text="自动模式", variable=self.mode_var, value="auto").grid(row=0, column=2, padx=5) ttk.Radiobutton(config_frame, text="手动模式", variable=self.mode_var, value="manual").grid(row=0, column=3, padx=5) self.start_stop_button = ttk.Button(config_frame, text="启动监控", command=self.toggle_monitoring) self.start_stop_button.grid(row=0, column=4, padx=10, pady=5) # 手动控制按钮 self.manual_frame = ttk.Frame(root) self.manual_frame.pack(fill="x", padx=10, pady=5) self.manual_start_btn = ttk.Button(self.manual_frame, text="手动开始计时", command=self.manual_start_timer, state='disabled') self.manual_start_btn.pack(side='left', padx=5) self.manual_stop_btn = ttk.Button(self.manual_frame, text="手动结束计时", command=self.manual_stop_timer, state='disabled') self.manual_stop_btn.pack(side='left', padx=5) # --- 统计信息区 --- stats_frame = ttk.LabelFrame(root, text="今日统计") stats_frame.pack(fill="x", padx=10, pady=5) ttk.Label(stats_frame, text="总空闲时间:").grid(row=0, column=0, padx=5, pady=5) self.total_duration_var = tk.StringVar(value="0 秒") ttk.Label(stats_frame, textvariable=self.total_duration_var).grid(row=0, column=1, padx=5, pady=5) ttk.Button(stats_frame, text="刷新统计", command=self.refresh_daily_stats).grid(row=0, column=2, padx=10, pady=5) ttk.Button(stats_frame, text="查看历史记录", command=self.show_history).grid(row=0, column=3, padx=10, pady=5) # --- 日志区 --- log_frame = ttk.LabelFrame(root, text="日志") log_frame.pack(fill="both", expand=True, padx=10, pady=5) self.log_text = tk.Text(log_frame, state='disabled', height=12) self.log_text.pack(side='left', fill="both", expand=True, padx=5, pady=5) scrollbar = ttk.Scrollbar(log_frame, orient='vertical', command=self.log_text.yview) scrollbar.pack(side='right', fill='y') self.log_text.configure(yscrollcommand=scrollbar.set) self.log("应用已启动。请设置阈值并点击'启动监控'。") self.update_stats_periodically() def log(self, message): def update_log(): self.log_text.config(state='normal') self.log_text.insert('end', f"{message}\n") self.log_text.see('end') self.log_text.config(state='disabled') self.root.after(0, update_log) def toggle_monitoring(self): if self.idle_monitor is None or not self.idle_monitor.thread.is_alive(): threshold = int(self.threshold_entry.get()) is_manual = self.mode_var.get() == "manual" self.idle_monitor = IdleMonitor(threshold, self.db_manager, self.log) self.idle_monitor.start_monitoring(manual=is_manual) self.start_stop_button.config(text="停止监控") if is_manual: self.manual_start_btn.config(state='normal') self.manual_stop_btn.config(state='normal') else: self.manual_start_btn.config(state='disabled') self.manual_stop_btn.config(state='disabled') else: self.idle_monitor.stop_monitoring() self.idle_monitor = None self.start_stop_button.config(text="启动监控") self.manual_start_btn.config(state='disabled') self.manual_stop_btn.config(state='disabled') self.refresh_daily_stats() def manual_start_timer(self): if self.idle_monitor and self.idle_monitor.manual_mode: self.idle_monitor.manual_start() def manual_stop_timer(self): if self.idle_monitor and self.idle_monitor.manual_mode: self.idle_monitor.manual_stop() self.refresh_daily_stats() def refresh_daily_stats(self): today_str = datetime.datetime.now().strftime('%Y-%m-%d') records = self.db_manager.get_sessions_by_date(today_str) total_duration = sum(record[2] for record in records) hours = int(total_duration // 3600) minutes = int((total_duration % 3600) // 60) seconds = int(total_duration % 60) if hours > 0: time_str = f"{hours}小时 {minutes}分钟 {seconds}秒" elif minutes > 0: time_str = f"{minutes}分钟 {seconds}秒" else: time_str = f"{seconds}秒" self.total_duration_var.set(time_str) def update_stats_periodically(self): self.refresh_daily_stats() self.root.after(10000, self.update_stats_periodically) def show_history(self): history_win = tk.Toplevel(self.root) history_win.title("历史记录") history_win.geometry("800x500") top_frame = ttk.Frame(history_win) top_frame.pack(fill='x', padx=10, pady=5) ttk.Label(top_frame, text="选择日期:").grid(row=0, column=0, padx=5, pady=5) date_var = tk.StringVar(value=datetime.datetime.now().strftime('%Y-%m-%d')) date_entry = ttk.Entry(top_frame, textvariable=date_var, width=15) date_entry.grid(row=0, column=1, padx=5, pady=5) ttk.Button(top_frame, text="加载记录", command=lambda: load_records()).grid(row=0, column=2, padx=10, pady=5) ttk.Button(top_frame, text="关闭", command=history_win.destroy).grid(row=0, column=3, padx=10, pady=5) table_frame = ttk.Frame(history_win) table_frame.pack(fill='both', expand=True, padx=10, pady=10) tree_scroll = ttk.Scrollbar(table_frame) tree_scroll.pack(side='right', fill='y') tree = ttk.Treeview(table_frame, columns=('start', 'end', 'duration'), show='headings', yscrollcommand=tree_scroll.set) tree.heading('start', text='开始时间') tree.heading('end', text='结束时间') tree.heading('duration', text='持续时间') tree.column('start', width=200) tree.column('end', width=200) tree.column('duration', width=150) tree.pack(side='left', fill='both', expand=True) tree_scroll.config(command=tree.yview) bottom_frame = ttk.Frame(history_win) bottom_frame.pack(fill='x', padx=10, pady=5) total_var = tk.StringVar(value="总计: 0 秒") ttk.Label(bottom_frame, textvariable=total_var).pack(side='left', padx=10) def load_records(): date = date_var.get() records = self.db_manager.get_sessions_by_date(date) for i in tree.get_children(): tree.delete(i) total_duration = 0 for record in records: start, end, duration = record total_duration += duration hours = int(duration // 3600) minutes = int((duration % 3600) // 60) seconds = int(duration % 60) duration_str = f"{hours:02d}:{minutes:02d}:{seconds:02d}" tree.insert('', 'end', values=(start, end, duration_str)) hours = int(total_duration // 3600) minutes = int((total_duration % 3600) // 60) seconds = int(total_duration % 60) if hours > 0: total_str = f"总计: {hours}小时 {minutes}分钟 {seconds}秒" elif minutes > 0: total_str = f"总计: {minutes}分钟 {seconds}秒" else: total_str = f"总计: {seconds}秒" total_var.set(total_str) load_records() # --- 5. 主程序入口 (关键启动代码) --- if __name__ == "__main__": root = tk.Tk() app = App(root) root.mainloop()
本文来自博客园,作者:綦霖,转载请注明原文链接:https://www.cnblogs.com/yc-weblog/p/22279522
浙公网安备 33010602011771号