[Python] 纯文本查看 复制代码
# -*- 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()