【开源解析】基于Python+Qt打造智能应用时长统计工具 - 你的数字生活分析师
📊 【开源解析】基于Python+Qt打造智能应用时长统计工具 - 你的数字生活分析师
🌈 个人主页:创客白泽 - CSDN博客
🔥 系列专栏:🐍《Python开源项目实战》
💡 热爱不止于代码,热情源自每一个灵感闪现的夜晚。愿以开源之火,点亮前行之路。
🐋 希望大家多多支持,我们一起进步!
👍 🎉如果文章对你有帮助的话,欢迎 点赞 👍🏻 评论 💬 收藏 ⭐️ 加关注+💗分享给更多人哦
🌟 概述:数字时代的时间管理者
在数字化生活日益普及的今天,我们每天与各种应用程序的交互时间越来越长。但你是否真正了解自己的数字生活习惯?哪些应用占用了你最多的时间?时间都花在了哪里?
今天我将介绍一款自主研发的应用时长统计工具,它能够:
- 实时监控应用使用情况
- 可视化展示时间分配
- 分析每周使用趋势
- 最小化到系统托盘后台运行
这款工具采用Python+Qt开发,结合matplotlib实现专业级数据可视化,是程序员和普通用户都能轻松使用的效率工具。
🛠️ 功能全景图
核心功能矩阵
功能模块 | 技术实现 | 数据维度 |
---|---|---|
实时监控 | psutil+win32api跨平台采集 | 秒级精度 |
数据持久化 | JSON序列化+按日期存储 | 历史数据可追溯 |
可视化分析 | Matplotlib+Qt5嵌入式图表 | 多维数据呈现 |
系统托盘 | QSystemTrayIcon | 无感后台运行 |
周趋势分析 | 时间序列聚合+堆叠柱状图 | 七日对比 |
技术栈亮点
- 应用识别:Windows/MacOS/Linux三平台兼容
- 性能优化:定时器精准控制1秒采集间隔
- 数据安全:双重备份机制(实时+每日)
- 交互设计:标签页自动刷新+表格高亮交互
🖥️ 效果展示
1. 实时数据看板
特点说明:
- 当前运行应用蓝色高亮
- 使用时长TOP3金色标识
- 实时刷新排名变化
2. 时长占比分析
智能处理:
- 自动合并<5%的小项为"其他"
- 中心显示总时长
- 响应式标签防重叠
3. 周趋势图谱
分析维度:
- Top5应用每日对比
- 小时为单位直观显示
- 颜色编码区分应用
🛠️ 部署与使用指南
环境准备
# 基础依赖
pip install pyqt5 psutil matplotlib# 平台特定依赖
# Windows
pip install pywin32
使用教程
python app_usage_tracker.py
-
最小化到托盘:关闭窗口自动后台运行
-
数据查看:
- 实时数据页:查看当前会话统计
- 图表页:点击标签自动刷新
-
数据存储:
app_usage_data/
目录下查看历史数据- 每日数据自动归档
自定义配置
# 修改监控频率(毫秒)
self.timer.start(1000) # 默认1秒# 修改数据存储路径
self.data_dir = "custom_data_path"
🔍 核心代码解析
1. 应用进程监控引擎
def get_current_app(self):"""跨平台应用识别核心方法"""try:if platform.system() == "Windows":import win32guiwindow = win32gui.GetForegroundWindow()_, pid = win32gui.GetWindowThreadProcessId(window)return psutil.Process(pid).name()# 其他平台处理...except Exception as e:print(f"应用识别异常: {e}")return "Unknown"
技术要点:
- Windows使用win32gui获取前台窗口
- Linux依赖xdotool工具链
- MacOS通过AppKit接口
2. 数据可视化引擎
def update_weekly_chart(self):"""周趋势图生成逻辑"""# 1. 数据准备dates = [datetime.now().date() - timedelta(days=i) for i in range(6, -1, -1)]# 2. Top5应用筛选app_total_time = defaultdict(float)for date in dates:daily_data = self.load_daily_data(date.strftime("%Y-%m-%d"))for app, secs in (daily_data or {}).items():app_total_time[app] += secs# 3. 柱状图绘制fig, ax = plt.subplots(figsize=(12,7))for i, (app, _) in enumerate(sorted(app_total_time.items(), key=lambda x: x[1], reverse=True)[:5]):daily_usage = [self.load_daily_data(d.strftime("%Y-%m-%d")).get(app, 0)/3600 for d in dates]ax.bar(x + i*width, daily_usage, width, label=app)# 4. 图表美化...
设计亮点:
- 动态数据聚合
- 自动响应式布局
- 视觉层次分明
3. 系统托盘集成
def init_system_tray(self):"""托盘图标管理系统"""self.tray_icon = QSystemTrayIcon(self)menu = QMenu()menu.addAction("显示主界面", self.show)menu.addAction("退出", self.close_app)self.tray_icon.setContextMenu(menu)self.tray_icon.show()
交互设计:
- 右键菜单快速操作
- 双击恢复窗口
- 气泡消息提示
📥 源码下载
import sys
import time
from datetime import datetime, timedelta
import json
import matplotlib.pyplot as plt
from matplotlib.backends.backend_qt5agg import FigureCanvasQTAgg as FigureCanvas
from PyQt5.QtWidgets import (QApplication, QMainWindow, QVBoxLayout, QWidget, QTableWidget, QTableWidgetItem, QSystemTrayIcon, QMenu, QAction, QMessageBox, QTabWidget, QHeaderView,QLabel)
from PyQt5.QtCore import QTimer, Qt
from PyQt5.QtGui import QIcon, QFont, QColor
import psutil
import platform
import os
import numpy as np
from matplotlib import cm
from matplotlib.font_manager import FontProperties# 设置matplotlib支持中文显示
plt.rcParams['font.sans-serif'] = ['Microsoft YaHei'] # 使用微软雅黑
plt.rcParams['axes.unicode_minus'] = False # 用来正常显示负号class AppUsageTracker(QMainWindow):def __init__(self):super().__init__()# 初始化数据self.app_data = {}self.current_app = Noneself.last_update_time = time.time()self.data_dir = "app_usage_data"self.data_file = os.path.join(self.data_dir, "current_usage.json")# 创建数据目录os.makedirs(self.data_dir, exist_ok=True)# 加载历史数据self.load_data()# 初始化UIself.init_ui()# 设置系统托盘self.init_system_tray()# 设置定时器self.timer = QTimer()self.timer.timeout.connect(self.update_app_usage)self.timer.start(1000) # 每秒更新一次# 每周数据定时保存self.weekly_save_timer = QTimer()self.weekly_save_timer.timeout.connect(self.save_weekly_data)self.weekly_save_timer.start(3600000) # 每小时检查一次def init_ui(self):"""初始化用户界面"""self.setWindowTitle("📊 应用使用时长统计")self.setWindowIcon(QIcon(self.get_emoji_icon("📊")))self.setGeometry(100, 100, 1200, 800) # 增大窗口尺寸# 主布局main_widget = QWidget()self.setCentralWidget(main_widget)layout = QVBoxLayout(main_widget)# 创建标签页self.tabs = QTabWidget()self.tabs.currentChanged.connect(self.on_tab_changed)layout.addWidget(self.tabs)# 初始化各标签页self.init_realtime_tab()self.init_bar_chart_tab()self.init_pie_chart_tab()self.init_weekly_chart_tab() # 修改为周统计图表页# 添加工具栏self.init_toolbar()def on_tab_changed(self, index):"""标签页切换时自动刷新图表"""if index == 1: # 使用时长排行标签页self.update_bar_chart()elif index == 2: # 使用时长占比标签页self.update_pie_chart()elif index == 3: # 周统计数据标签页self.update_weekly_chart()def init_realtime_tab(self):"""初始化实时数据标签页"""self.realtime_tab = QWidget()self.tabs.addTab(self.realtime_tab, "⏱️ 实时数据")layout = QVBoxLayout(self.realtime_tab)# 添加标题title_label = QLabel("应用使用时长实时统计")title_label.setFont(QFont("Microsoft YaHei", 12, QFont.Bold))title_label.setAlignment(Qt.AlignCenter)title_label.setStyleSheet("padding: 10px;")layout.addWidget(title_label)# 创建表格self.table = QTableWidget()self.table.setColumnCount(3)self.table.setHorizontalHeaderLabels(["排名", "应用名称", "使用时长"])# 设置表格样式self.table.setStyleSheet("QTableWidget { border: 1px solid #e0e0e0; }""QTableWidget::item { padding: 5px; }""QHeaderView::section { background-color: #f0f0f0; padding: 5px; }")self.table.setColumnWidth(0, 60)self.table.setColumnWidth(1, 300)self.table.horizontalHeader().setSectionResizeMode(2, QHeaderView.Stretch)self.table.setEditTriggers(QTableWidget.NoEditTriggers)self.table.setSortingEnabled(False)self.table.setAlternatingRowColors(True)layout.addWidget(self.table)def init_bar_chart_tab(self):"""初始化条形图标签页"""self.bar_chart_tab = QWidget()self.tabs.addTab(self.bar_chart_tab, "📈 使用时长排行")layout = QVBoxLayout(self.bar_chart_tab)# 添加标题title_label = QLabel("应用使用时长排行榜")title_label.setFont(QFont("Microsoft YaHei", 12, QFont.Bold))title_label.setAlignment(Qt.AlignCenter)title_label.setStyleSheet("padding: 10px;")layout.addWidget(title_label)# 创建图表self.bar_figure = plt.figure(figsize=(10, 6), dpi=100, facecolor='white')self.bar_canvas = FigureCanvas(self.bar_figure)layout.addWidget(self.bar_canvas)def init_pie_chart_tab(self):"""初始化饼图标签页"""self.pie_chart_tab = QWidget()self.tabs.addTab(self.pie_chart_tab, "🍰 使用时长占比")layout = QVBoxLayout(self.pie_chart_tab)# 添加标题title_label = QLabel("应用使用时长占比")title_label.setFont(QFont("Microsoft YaHei", 12, QFont.Bold))title_label.setAlignment(Qt.AlignCenter)title_label.setStyleSheet("padding: 10px;")layout.addWidget(title_label)# 创建图表self.pie_figure = plt.figure(figsize=(10, 6), dpi=100, facecolor='white')self.pie_canvas = FigureCanvas(self.pie_figure)layout.addWidget(self.pie_canvas)def init_weekly_chart_tab(self):"""初始化周统计图表标签页"""self.weekly_chart_tab = QWidget()self.tabs.addTab(self.weekly_chart_tab, "📅 周使用趋势")layout = QVBoxLayout(self.weekly_chart_tab)# 添加标题title_label = QLabel("近七日应用使用时长趋势")title_label.setFont(QFont("Microsoft YaHei", 12, QFont.Bold))title_label.setAlignment(Qt.AlignCenter)title_label.setStyleSheet("padding: 10px;")layout.addWidget(title_label)# 创建图表self.weekly_figure = plt.figure(figsize=(12, 7), dpi=100, facecolor='white')self.weekly_canvas = FigureCanvas(self.weekly_figure)layout.addWidget(self.weekly_canvas)def init_toolbar(self):"""初始化工具栏"""self.refresh_button = QAction("🔄 刷新图表", self)self.refresh_button.triggered.connect(self.update_all_charts)self.toolbar = self.addToolBar("工具栏")self.toolbar.addAction(self.refresh_button)def init_system_tray(self):"""初始化系统托盘"""self.tray_icon = QSystemTrayIcon(self)self.tray_icon.setIcon(QIcon(self.get_emoji_icon("⏱️")))tray_menu = QMenu()show_action = QAction("👀 显示窗口", self)show_action.triggered.connect(self.show)tray_menu.addAction(show_action)exit_action = QAction("🚪 退出", self)exit_action.triggered.connect(self.close_app)tray_menu.addAction(exit_action)self.tray_icon.setContextMenu(tray_menu)self.tray_icon.show()self.tray_icon.activated.connect(self.tray_icon_clicked)def tray_icon_clicked(self, reason):"""托盘图标点击事件处理"""if reason == QSystemTrayIcon.DoubleClick:self.show()self.activateWindow()def close_app(self):"""退出应用程序"""self.save_data()self.tray_icon.hide()QApplication.quit()def closeEvent(self, event):"""重写关闭事件,最小化到托盘"""event.ignore()self.hide()self.tray_icon.showMessage("应用使用时长统计","程序已最小化到系统托盘",QSystemTrayIcon.Information,2000)def get_emoji_icon(self, emoji):"""将emoji转换为图标"""return emojidef get_current_app(self):"""获取当前活动窗口的应用"""try:current_app = "Unknown"if platform.system() == "Windows":import win32guiimport win32processwindow = win32gui.GetForegroundWindow()_, pid = win32process.GetWindowThreadProcessId(window)try:process = psutil.Process(pid)current_app = process.name()current_app = os.path.splitext(current_app)[0]except (psutil.NoSuchProcess, psutil.AccessDenied):current_app = "Unknown"elif platform.system() == "Darwin":from AppKit import NSWorkspacecurrent_app = NSWorkspace.sharedWorkspace().frontmostApplication().localizedName()else: # Linuximport subprocesstry:window_id = subprocess.check_output(["xdotool", "getactivewindow"]).decode().strip()pid = subprocess.check_output(["xdotool", "getwindowpid", window_id]).decode().strip()process = psutil.Process(int(pid))current_app = process.name()current_app = os.path.splitext(current_app)[0]except:current_app = "Unknown"# 清理应用名称for suffix in ['.exe', '.bin', '.app']:if current_app.endswith(suffix):current_app = current_app[:-len(suffix)]return current_appexcept Exception as e:print(f"获取应用名称出错: {e}")return "Unknown"def update_app_usage(self):"""更新应用使用时长"""current_app = self.get_current_app()current_time = time.time()time_elapsed = current_time - self.last_update_timeif self.current_app is not None:if self.current_app in self.app_data:self.app_data[self.current_app] += time_elapsedelse:self.app_data[self.current_app] = time_elapsedself.current_app = current_appself.last_update_time = current_timeself.update_table()if current_time % 3600 < 1: # 大约每小时保存一次self.save_data()def update_table(self):"""更新表格数据"""sorted_data = sorted(self.app_data.items(), key=lambda x: x[1], reverse=True)self.table.setRowCount(len(sorted_data))for row, (app, seconds) in enumerate(sorted_data):# 排名rank_item = QTableWidgetItem(str(row + 1))rank_item.setTextAlignment(Qt.AlignCenter)# 应用名称app_item = QTableWidgetItem(app)# 使用时长time_item = QTableWidgetItem(self.format_time(seconds))time_item.setTextAlignment(Qt.AlignRight)# 设置数据for item in [rank_item, app_item, time_item]:item.setData(Qt.UserRole, seconds)self.table.setItem(row, 0, rank_item)self.table.setItem(row, 1, app_item)self.table.setItem(row, 2, time_item)# 高亮显示当前运行应用if app == self.current_app:for col in range(3):item = self.table.item(row, col)item.setBackground(QColor(220, 240, 255))item.setFont(QFont("Microsoft YaHei", 9, QFont.Bold))# 设置前三名样式if row < 3:for col in range(3):item = self.table.item(row, col)item.setBackground(QColor(255, 240, 200))item.setFont(QFont("Microsoft YaHei", 9, QFont.Bold))def update_all_charts(self):"""更新所有图表"""self.update_bar_chart()self.update_pie_chart()self.update_weekly_chart()def update_bar_chart(self):"""更新条形图"""self.bar_figure.clear()ax = self.bar_figure.add_subplot(111)if not self.app_data:ax.text(0.5, 0.5, "暂无数据", ha='center', va='center', fontsize=12)self.bar_canvas.draw()return# 准备数据sorted_data = sorted(self.app_data.items(), key=lambda x: x[1], reverse=True)apps = [app[:15] + '...' if len(app) > 15 else app for app, _ in sorted_data]times = [secs / 3600 for _, secs in sorted_data]# 创建条形图colors = cm.viridis(np.linspace(0.2, 0.8, len(apps)))bars = ax.barh(np.arange(len(apps)), times, color=colors, edgecolor='none')# 设置标签ax.set_yticks(np.arange(len(apps)))ax.set_yticklabels(apps)# 添加数据标签for bar in bars:width = bar.get_width()ax.text(width, bar.get_y() + bar.get_height()/2,f' {width:.1f}h', va='center', ha='left', fontsize=9)# 美化图表ax.set_xlabel('使用时长 (小时)', fontsize=11)ax.set_title('应用使用时长统计', fontsize=13, pad=15)ax.spines['top'].set_visible(False)ax.spines['right'].set_visible(False)ax.grid(True, axis='x', color='#e0e0e0', linestyle='--', alpha=0.7)ax.invert_yaxis()self.bar_figure.tight_layout()self.bar_canvas.draw()def update_pie_chart(self):"""更新饼图"""self.pie_figure.clear()ax = self.pie_figure.add_subplot(111)if not self.app_data:ax.text(0.5, 0.5, "暂无数据", ha='center', va='center', fontsize=12)self.pie_canvas.draw()return# 准备数据sorted_data = sorted(self.app_data.items(), key=lambda x: x[1], reverse=True)total_time = sum(secs for _, secs in sorted_data)app_percentages = [(app, (secs/total_time)*100) for app, secs in sorted_data]# 分离主要应用和其他应用main_apps = [item for item in app_percentages if item[1] >= 5]other_apps = [item for item in app_percentages if item[1] < 5]other_time = sum(secs for _, secs in sorted_data if (secs/total_time)*100 < 5)if other_apps:labels = [app for app, _ in main_apps] + ["其他"]sizes = [secs for _, secs in sorted_data if (secs/total_time)*100 >= 5] + [other_time]else:labels = [app for app, _ in main_apps]sizes = [secs for _, secs in sorted_data]# 限制标签长度labels = [label[:12] + '...' if len(label) > 12 else label for label in labels]percentages = [(size/total_time)*100 for size in sizes]# 创建饼图colors = cm.plasma(np.linspace(0.2, 0.8, len(labels)))font = FontProperties(size=9)wedges, texts, autotexts = ax.pie(sizes,labels=labels,colors=colors,autopct=lambda p: f'{p:.1f}%' if p >= 5 else '',startangle=90,wedgeprops={'linewidth': 1, 'edgecolor': 'white'},textprops={'fontproperties': font},pctdistance=0.85,labeldistance=1.05)# 添加中心总时长ax.text(0, 0, f"总时长\n{self.format_time(total_time)}", ha='center', va='center', fontsize=11)# 添加图例和标题ax.set_title('应用使用时长占比 (≥5%显示)', fontsize=13, pad=15)legend_labels = [f"{label} ({p:.1f}%)" for label, p in zip(labels, percentages)]ax.legend(wedges, legend_labels,title="应用列表",loc="center left",bbox_to_anchor=(1, 0, 0.5, 1),frameon=False,prop=font)self.pie_figure.tight_layout()self.pie_canvas.draw()def update_weekly_chart(self):"""更新周统计柱状图"""self.weekly_figure.clear()ax = self.weekly_figure.add_subplot(111)# 获取最近7天的日期today = datetime.now().date()dates = [(today - timedelta(days=i)).strftime("%m-%d") for i in range(6, -1, -1)]full_dates = [(today - timedelta(days=i)).strftime("%Y-%m-%d") for i in range(6, -1, -1)]# 收集所有应用名称all_apps = set()weekly_data = {}for date in full_dates:daily_data = self.load_daily_data(date)if daily_data:weekly_data[date] = daily_dataall_apps.update(daily_data.keys())if not all_apps:ax.text(0.5, 0.5, "暂无周数据", ha='center', va='center', fontsize=12)self.weekly_canvas.draw()return# 选择前5个最常用的应用app_total_time = {app: 0 for app in all_apps}for date in full_dates:if date in weekly_data:for app, secs in weekly_data[date].items():app_total_time[app] += secstop_apps = sorted(app_total_time.items(), key=lambda x: x[1], reverse=True)[:5]top_apps = [app for app, _ in top_apps]# 准备柱状图数据bar_width = 0.15x = np.arange(len(dates))# 创建颜色映射colors = cm.viridis(np.linspace(0.2, 0.8, len(top_apps)))# 绘制每个应用的柱状图for i, app in enumerate(top_apps):app_times = []for date in full_dates:if date in weekly_data and app in weekly_data[date]:app_times.append(weekly_data[date][app] / 3600) # 转换为小时else:app_times.append(0)ax.bar(x + i*bar_width, app_times, bar_width, label=app[:12] + '...' if len(app) > 12 else app,color=colors[i])# 设置x轴标签ax.set_xticks(x + bar_width * (len(top_apps)-1)/2)ax.set_xticklabels(dates)# 美化图表ax.set_xlabel('日期', fontsize=11)ax.set_ylabel('使用时长 (小时)', fontsize=11)ax.set_title('近七日应用使用时长趋势 (Top 5应用)', fontsize=13, pad=15)ax.legend(bbox_to_anchor=(1.05, 1), loc='upper left')ax.grid(True, axis='y', color='#e0e0e0', linestyle='--', alpha=0.7)self.weekly_figure.tight_layout()self.weekly_canvas.draw()def format_time(self, seconds):"""将秒数格式化为 HH:MM:SS"""hours = int(seconds // 3600)minutes = int((seconds % 3600) // 60)seconds = int(seconds % 60)return f"{hours:02d}:{minutes:02d}:{seconds:02d}"def load_data(self):"""加载保存的数据"""try:if os.path.exists(self.data_file):with open(self.data_file, "r", encoding='utf-8') as f:self.app_data = {k: float(v) for k, v in json.load(f).items()}except (FileNotFoundError, json.JSONDecodeError, ValueError) as e:print(f"加载数据出错: {e}")self.app_data = {}def save_data(self):"""保存当前数据"""try:with open(self.data_file, "w", encoding='utf-8') as f:json.dump(self.app_data, f, ensure_ascii=False, indent=2)except Exception as e:print(f"保存数据出错: {e}")def load_daily_data(self, date):"""加载指定日期的数据"""filename = os.path.join(self.data_dir, f"app_usage_{date}.json")try:if os.path.exists(filename):with open(filename, "r", encoding='utf-8') as f:return json.load(f)except (FileNotFoundError, json.JSONDecodeError) as e:print(f"加载每日数据出错: {e}")return Nonedef save_weekly_data(self):"""每周数据保存"""today = datetime.now().date().strftime("%Y-%m-%d")filename = os.path.join(self.data_dir, f"app_usage_{today}.json")try:with open(filename, "w", encoding='utf-8') as f:json.dump(self.app_data, f, ensure_ascii=False, indent=2)except Exception as e:print(f"保存每周数据出错: {e}")self.app_data = {}self.current_app = self.get_current_app()self.last_update_time = time.time()if __name__ == "__main__":app = QApplication(sys.argv)app.setApplicationName("应用使用时长统计")app.setApplicationDisplayName("📊 应用使用时长统计")tracker = AppUsageTracker()tracker.show()sys.exit(app.exec_())
## 🎯 深度优化建议### 性能提升方向1. **数据压缩**:对长期存储的JSON数据进行gzip压缩
2. **增量更新**:改用SQLite替代JSON文件存储
3. **采样优化**:动态调整监控频率(活跃时高频,闲置时低频)### 功能扩展### 用户体验改进- 添加主题切换功能
- 支持导出CSV/PDF报告
- 增加数据筛选功能## 💡 总结与展望这款应用时长统计工具通过:1. **精准监控**:秒级采集精度
2. **直观可视化**:专业级图表呈现
3. **无感运行**:完善的托盘管理
4. **数据持久化**:双重备份机制实现了对数字生活习惯的全方位分析。特别适合:- 自由职业者时间管理
- 家长监控儿童设备使用
- 程序员分析开发效率**未来演进路线**:- 移动端配套应用
- 浏览器插件集成
- AI使用建议功能------> 📌 **版权声明**:本文代码采用MIT开源协议,转载请注明出处。如需商业使用请联系作者授权。
>
> **互动区**:你的数字生活时间分配合理吗?欢迎在评论区分享你的使用体验和改进建议!