版本更新啦!
周五么 就是要把事办黄,把尿喝白!
现在情况作出修改
1、锁屏后不再计时,只对不锁屏状态进线统计时间
2、需要增加一个自动开始计时开机状态下9:00到19:00(时间可修改)
3、历史查询的日期做一个日期控件可点击选择
4、可以支持周或月统计,然后可以导出为Excel
先放上新的效果图



增加日期控件需要安装openpyxl
PS C:\Users\xxx.xxx> pip install openpyxl Collecting openpyxl Downloading openpyxl-3.1.5-py2.py3-none-any.whl.metadata (2.5 kB) Collecting et-xmlfile (from openpyxl) Downloading et_xmlfile-2.0.0-py3-none-any.whl.metadata (2.7 kB) Downloading openpyxl-3.1.5-py2.py3-none-any.whl (250 kB) Downloading et_xmlfile-2.0.0-py3-none-any.whl (18 kB) Installing collected packages: et-xmlfile, openpyxl Successfully installed et-xmlfile-2.0.0 openpyxl-3.1.5
完整代码:
# -*- coding: utf-8 -*- # idle_timerv4.py # 空闲计时器应用 V5 (Python 3.x) # 导出Excel功能需要安装 openpyxl: pip install openpyxl import sqlite3 import time import threading import datetime import calendar import ctypes from ctypes import wintypes import tkinter as tk from tkinter import ttk, filedialog, messagebox # --- 1. 数据库管理模块 --- class DatabaseManager: def __init__(self, db_path='idle_tracker.db'): self.db_path = db_path 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() start_str = start_time.strftime('%Y-%m-%d %H:%M:%S') end_str = end_time.strftime('%Y-%m-%d %H:%M:%S') date_str = start_time.strftime('%Y-%m-%d') cursor.execute(''' INSERT INTO idle_sessions (start_time, end_time, duration_seconds, date) VALUES (?, ?, ?, ?) ''', (start_str, end_str, 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 return 0 except Exception: return 0 @staticmethod def is_workstation_locked(): try: user32 = ctypes.windll.user32 h_desktop = user32.OpenInputDesktop(0, False, 0x0100) if h_desktop == 0: return True user32.CloseDesktop(h_desktop) return False except Exception: return False # --- 3. 智能日期与维度选择控件 --- class DimensionDatePicker(ttk.Frame): def __init__(self, master, callback=None, **kwargs): super().__init__(master, **kwargs) self.callback = callback self.dim_var = tk.StringVar(value="day") self.date_var = tk.StringVar() ttk.Label(self, text="维度:").pack(side='left', padx=5) for text, val in [("日", "day"), ("周", "week"), ("月", "month")]: rb = ttk.Radiobutton(self, text=text, variable=self.dim_var, value=val, command=self.on_dim_change) rb.pack(side='left', padx=2) self.entry = ttk.Entry(self, textvariable=self.date_var, width=16, state='readonly') self.entry.pack(side='left', padx=5) self.btn = ttk.Button(self, text="📅", width=3, command=self.show_picker) self.btn.pack(side='left', padx=2) self.on_dim_change() def on_dim_change(self): today = datetime.datetime.now() dim = self.dim_var.get() if dim == 'day': self.date_var.set(today.strftime('%Y-%m-%d')) elif dim == 'week': monday = today - datetime.timedelta(days=today.weekday()) self.date_var.set(monday.strftime('%Y-%m-%d') + ' (周)') elif dim == 'month': self.date_var.set(today.strftime('%Y-%m') + ' (月)') if self.callback: self.callback(self) def get_query_range(self): dim = self.dim_var.get() date_str = self.date_var.get() if dim == 'day': target = datetime.datetime.strptime(date_str, '%Y-%m-%d').date() return target, target elif dim == 'week': base_str = date_str.split(' ')[0] target = datetime.datetime.strptime(base_str, '%Y-%m-%d').date() end = target + datetime.timedelta(days=6) return target, end elif dim == 'month': base_str = date_str.split(' ')[0] target = datetime.datetime.strptime(base_str, '%Y-%m').date() _, last_day = calendar.monthrange(target.year, target.month) end = target.replace(day=last_day) return target, end def show_picker(self): x = self.winfo_pointerx() y = self.winfo_pointery() picker_win = tk.Toplevel(self) picker_win.title("选择日期") picker_win.geometry(f"280x280+{x}+{y}") picker_win.transient(self.winfo_toplevel()) picker_win.grab_set() dim = self.dim_var.get() date_str = self.date_var.get() if dim == 'day': current = datetime.datetime.strptime(date_str, '%Y-%m-%d') elif dim == 'week': current = datetime.datetime.strptime(date_str.split(' ')[0], '%Y-%m-%d') elif dim == 'month': current = datetime.datetime.strptime(date_str.split(' ')[0], '%Y-%m') year = current.year month = current.month header_frame = ttk.Frame(picker_win) header_frame.pack(fill='x', pady=5) def prev_month(): nonlocal year, month month -= 1 if month < 1: month = 12; year -= 1 update_calendar() def next_month(): nonlocal year, month month += 1 if month > 12: month = 1; year += 1 update_calendar() ttk.Button(header_frame, text="<", command=prev_month).pack(side='left', padx=10) lbl_month = ttk.Label(header_frame, text=f"{year}年{month}月", font=('Arial', 12, 'bold')) lbl_month.pack(side='left', expand=True) ttk.Button(header_frame, text=">", command=next_month).pack(side='right', padx=10) cal_frame = ttk.Frame(picker_win) cal_frame.pack(fill='both', expand=True, padx=10, pady=5) days = ["一", "二", "三", "四", "五", "六", "日"] for i, day in enumerate(days): ttk.Label(cal_frame, text=day, width=4).grid(row=0, column=i, padx=2, pady=2) def update_calendar(): for widget in cal_frame.winfo_children(): if isinstance(widget, ttk.Button): widget.destroy() lbl_month.config(text=f"{year}年{month}月") cal = calendar.Calendar(firstweekday=0) month_days = cal.monthdayscalendar(year, month) for r, week in enumerate(month_days): for c, day in enumerate(week): if day != 0: btn = ttk.Button(cal_frame, text=str(day), width=3, command=lambda d=day: select_date(d)) btn.grid(row=r+1, column=c, padx=2, pady=2) def select_date(day): selected_date = datetime.date(year, month, day) if dim == 'day': self.date_var.set(selected_date.strftime('%Y-%m-%d')) elif dim == 'week': monday = selected_date - datetime.timedelta(days=selected_date.weekday()) self.date_var.set(monday.strftime('%Y-%m-%d') + ' (周)') elif dim == 'month': self.date_var.set(selected_date.strftime('%Y-%m') + ' (月)') picker_win.destroy() if self.callback: self.callback(self) update_calendar() # --- 4. 辅助函数 --- def format_duration(seconds): h = int(seconds // 3600) m = int((seconds % 3600) // 60) s = int(seconds % 60) return f"{h:02d}:{m:02d}:{s:02d}" # --- 5. 空闲监控模块 --- class IdleMonitor: def __init__(self, threshold_seconds, db_manager, work_start_str, work_end_str, 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 try: self.work_start = datetime.datetime.strptime(work_start_str, "%H:%M").time() self.work_end = datetime.datetime.strptime(work_end_str, "%H:%M").time() except ValueError: self.work_start = datetime.time(9, 0) self.work_end = datetime.time(19, 0) self.log("[警告] 工作时间格式错误,已重置为 09:00 - 19:00") 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 last_input_ms = WindowsAPIHelper.get_last_input_info() current_ms = ctypes.windll.kernel32.GetTickCount() locked = WindowsAPIHelper.is_workstation_locked() current_time = datetime.datetime.now().time() idle_seconds = (current_ms - last_input_ms) / 1000.0 in_work_hours = self.work_start <= current_time <= self.work_end if locked: if self.is_timing: self._end_session("检测到锁屏,结束空闲会话。") else: if in_work_hours and idle_seconds > self.threshold: if not self.is_timing: self._start_session() else: if self.is_timing: if not in_work_hours: self._end_session("超出工作时间,结束空闲会话。") else: self._end_session("检测到用户活动,结束空闲会话。") if int(idle_seconds) != last_logged_idle_seconds: self.log(f"空闲: {idle_seconds:.1f}s | 工作时间: {'是' if in_work_hours else '否'} | 锁屏: {'是' if locked else '否'}") 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} | 持续: {format_duration(duration)}") 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}s。模式: {'手动' 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("手动结束计时") # --- 6. GUI 主应用 --- class App: def __init__(self, root): self.root = root self.root.title("空闲计时器 (增强版 V5)") self.root.geometry("750x520") 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=8) self.threshold_entry.grid(row=0, column=1, padx=5, pady=5) self.threshold_entry.insert(0, "180") ttk.Label(config_frame, text="工作开始:").grid(row=0, column=2, padx=5, pady=5) self.work_start_entry = ttk.Entry(config_frame, width=8) self.work_start_entry.grid(row=0, column=3, padx=5, pady=5) self.work_start_entry.insert(0, "09:00") ttk.Label(config_frame, text="工作结束:").grid(row=0, column=4, padx=5, pady=5) self.work_end_entry = ttk.Entry(config_frame, width=8) self.work_end_entry.grid(row=0, column=5, padx=5, pady=5) self.work_end_entry.insert(0, "19:00") self.mode_var = tk.StringVar(value="auto") ttk.Radiobutton(config_frame, text="自动", variable=self.mode_var, value="auto").grid(row=0, column=6, padx=5) ttk.Radiobutton(config_frame, text="手动", variable=self.mode_var, value="manual").grid(row=0, column=7, padx=5) self.start_stop_button = ttk.Button(config_frame, text="启动监控", command=self.toggle_monitoring) self.start_stop_button.grid(row=0, column=8, 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="00:00:00") ttk.Label(stats_frame, textvariable=self.total_duration_var, font=('Arial', 10, 'bold')).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=8) 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()) work_start = self.work_start_entry.get() work_end = self.work_end_entry.get() is_manual = self.mode_var.get() == "manual" self.idle_monitor = IdleMonitor(threshold, self.db_manager, work_start, work_end, 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) self.total_duration_var.set(format_duration(total_duration)) 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("900x550") top_frame = ttk.Frame(history_win) top_frame.pack(fill='x', padx=10, pady=5) # ===== 1. 先创建表格和底部统计控件 ===== 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', 'date'), show='headings', yscrollcommand=tree_scroll.set) tree.heading('start', text='开始时间') tree.heading('end', text='结束时间') tree.heading('duration', text='持续时间') tree.heading('date', text='日期') tree.column('start', width=180) tree.column('end', width=180) tree.column('duration', width=100) tree.column('date', width=100) 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="总计: 00:00:00") ttk.Label(bottom_frame, textvariable=total_var, font=('Arial', 10, 'bold')).pack(side='left', padx=10) # ===== 2. 定义依赖 tree 和 total_var 的函数 ===== def load_records(picker): start_date, end_date = picker.get_query_range() with self.db_manager.lock: cursor = self.db_manager.conn.cursor() cursor.execute(''' SELECT start_time, end_time, duration_seconds, date FROM idle_sessions WHERE date BETWEEN ? AND ? ORDER BY start_time DESC ''', (start_date.isoformat(), end_date.isoformat())) records = cursor.fetchall() for i in tree.get_children(): tree.delete(i) total_duration = 0 for record in records: start, end, duration, date = record total_duration += duration duration_str = format_duration(duration) start = start.replace('T', ' ') if 'T' in start else start end = end.replace('T', ' ') if 'T' in end else end tree.insert('', 'end', values=(start, end, duration_str, date)) total_var.set(f"总计: {format_duration(total_duration)}") def export_excel(picker): start_date, end_date = picker.get_query_range() with self.db_manager.lock: cursor = self.db_manager.conn.cursor() cursor.execute(''' SELECT start_time, end_time, duration_seconds, date FROM idle_sessions WHERE date BETWEEN ? AND ? ORDER BY start_time DESC ''', (start_date.isoformat(), end_date.isoformat())) records = cursor.fetchall() if not records: messagebox.showinfo("提示", "当前维度下没有可导出的记录。") return try: from openpyxl import Workbook from openpyxl.styles import Font wb = Workbook() ws = wb.active ws.title = "空闲统计" headers = ['开始时间', '结束时间', '持续时间', '日期'] ws.append(headers) for cell in ws[1]: cell.font = Font(bold=True) for row in records: start, end, duration, date = row start_fmt = start.replace('T', ' ') if 'T' in start else start end_fmt = end.replace('T', ' ') if 'T' in end else end dur_fmt = format_duration(duration) ws.append([start_fmt, end_fmt, dur_fmt, date]) filename = f"空闲统计_{start_date}_至_{end_date}.xlsx" wb.save(filename) messagebox.showinfo("成功", f"导出成功!\n文件保存在当前目录: {filename}") self.log(f"导出成功: {filename}") except ImportError: messagebox.showerror("错误", "导出失败:请先安装 openpyxl 库。\n请在命令行执行: pip install openpyxl") # ===== 3. 最后创建日期选择器(此时函数和UI组件都已就绪) ===== date_picker = DimensionDatePicker(top_frame, callback=lambda p: load_records(p)) date_picker.pack(side='left', padx=5, pady=5) ttk.Button(top_frame, text="导出Excel", command=lambda: export_excel(date_picker)).pack(side='left', padx=10, pady=5) ttk.Button(top_frame, text="关闭", command=history_win.destroy).pack(side='right', padx=10, pady=5) # 初始加载数据 load_records(date_picker) # --- 7. 主程序入口 --- if __name__ == "__main__": root = tk.Tk() app = App(root) root.mainloop()
本文来自博客园,作者:綦霖,转载请注明原文链接:https://www.cnblogs.com/yc-weblog/p/22312994
浙公网安备 33010602011771号