├── LICENSE
├── README.md
├── doc
├── appsetting.png
├── icons.ico
├── mainwin1.png
├── mainwin2.png
├── mainwin3.PNG
├── mainwin4.PNG
├── mainwin5.png
├── mainwin6.png
└── time.png
└── showtime.py
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) ShowTime, liaanj
4 |
5 | All rights reserved.
6 |
7 | Permission is hereby granted, free of charge, to any person obtaining a copy
8 | of this software and associated documentation files (the "Software"), to deal
9 | in the Software without restriction, including without limitation the rights
10 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
11 | copies of the Software, and to permit persons to whom the Software is
12 | furnished to do so, subject to the following conditions:
13 |
14 | The above copyright notice and this permission notice shall be included in all
15 | copies or substantial portions of the Software.
16 |
17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
18 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
19 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
20 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
21 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
22 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
23 | SOFTWARE.
24 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 | # ShowTime - 时光追踪器
3 |
4 |
5 |

6 |
7 |
8 |
9 | **ShowTime** 是一款基于 PySide6 开发的 Windows 的应用,旨在帮助用户追踪系统解锁时间和当前应用的使用时长。
10 |
11 | 在快节奏的现代生活中,我们常常会发现自己在不知不觉中浪费了大量时间。例如,您可能会在 B 站刷视频,一个小时的娱乐时间很快就过去了,却什么事都没干。
12 |
13 | **ShowTime** 正是为了解决这些问题而设计的。它通过任务栏透明窗口的形式,实时显示您自上次解锁系统以来的时间和当前应用程序的使用时长,帮助您时刻关注自己的时间分配,避免无意识的时间浪费。无论是工作中的高效利用,还是娱乐时的自律管理,ShowTime 都能成为您的得力助手。
14 |
15 |
16 | ### 为什么选择 ShowTime?
17 |
18 | - **实时监控**:清晰展示解锁时间和应用使用时长,让您时刻了解自己的时间分配情况。
19 | - **智能隐藏**:在全屏应用启动时可以手动拖动窗口到边缘来隐藏或自定义窗口位置,避免干扰您的工作或娱乐体验。
20 | - **高度可定制**:多种外观设置,包括字体大小、颜色、窗口尺寸、进度条颜色与位置,满足您的个性化需求。
21 | - **提醒功能**:根据设定的时间提醒您,帮助您及时调整使用习惯,提升时间管理能力。
22 | - **开机自启**:可选择设置程序开机自动启动,随时随地掌控您的时间。
23 |
24 | 通过 **ShowTime**,您可以有效减少在无意义应用上的时间投入,提升工作和生活的效率与质量。立即下载并体验 ShowTime,让时间管理变得更加轻松高效!
25 |
26 | ### 主界面
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 | *主界面显示解锁时间和应用使用时间*
38 |
39 | ### 设置界面
40 |
41 |
42 |
43 | *设置界面允许用户自定义外观和功能*
44 |
45 | ### 提醒设置
46 |
47 |
48 |
49 | *提醒设置界面,帮助用户配置提醒通知。*
50 |
51 | ## 使用指南
52 |
53 | 1. **启动程序**:双击运行 `showtime.exe` 或在命令行中执行 `python showtime.py`。
54 | 2. **主界面**:程序启动后会在屏幕上显示一个透明窗口,展示解锁时间和当前应用的使用时间,拖动将其放到合适的地方。
55 | 3. **右键菜单**:右键点击窗口,打开上下文菜单,提供以下选项:
56 | - 清除时间
57 | - 设置提醒
58 | - 外观设置
59 | - 暂停/继续计时
60 | - 开机自启
61 | - 退出应用
62 | - 关于软件
63 | 4. **设置提醒**:通过右键菜单中的“设置提醒”选项,您可以根据解锁时间或应用使用时间设置提醒。
64 | 5. **外观设置**:在“外观设置”中,您可以自定义字体大小、颜色、窗口尺寸、进度条颜色等。
65 | 6. **全屏隐藏**:在全屏状态下,程序会自动记录您所拖动到的位置,或者可以将其拖动到屏幕边缘以进行隐藏。在位置出问题的时候可以打开配置文件调整位置并重启软件。
66 |
67 | ## 配置说明
68 |
69 | 程序会在用户主目录下创建一个配置文件 `.my_app_config.json`,保存用户的个性化设置。您可以通过以下方式进行配置:
70 |
71 | - **通过程序界面**:使用外观设置界面进行修改,所有设置会自动保存到配置文件中。
72 | - **手动编辑**:直接编辑配置文件 `.my_app_config.json`,调整窗口位置、尺寸、颜色等参数。请确保输入的数值和颜色代码格式正确。
73 |
74 | ## 贡献指南
75 |
76 | 欢迎任何形式的贡献!您可以通过以下方式参与:
77 |
78 | - **提交问题**:在 GitHub 问题区报告 Bug 或提出改进建议。
79 | - **提交拉取请求**:修复 Bug 或添加新功能,请确保代码质量和文档完整。
80 | - **提出建议**:在讨论区分享您的想法和建议,帮助我们改进 ShowTime。
81 |
82 |
83 | ## 许可证
84 |
85 | 本项目采用 MIT 许可证。详情请参阅 [LICENSE](https://github.com/liaanj/ShowTime/blob/main/LICENSE) 文件。
86 |
87 |
--------------------------------------------------------------------------------
/doc/appsetting.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/liaanj/ShowTime/2143fddbf4655edbf582bd2c35e8abb7b28a5169/doc/appsetting.png
--------------------------------------------------------------------------------
/doc/icons.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/liaanj/ShowTime/2143fddbf4655edbf582bd2c35e8abb7b28a5169/doc/icons.ico
--------------------------------------------------------------------------------
/doc/mainwin1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/liaanj/ShowTime/2143fddbf4655edbf582bd2c35e8abb7b28a5169/doc/mainwin1.png
--------------------------------------------------------------------------------
/doc/mainwin2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/liaanj/ShowTime/2143fddbf4655edbf582bd2c35e8abb7b28a5169/doc/mainwin2.png
--------------------------------------------------------------------------------
/doc/mainwin3.PNG:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/liaanj/ShowTime/2143fddbf4655edbf582bd2c35e8abb7b28a5169/doc/mainwin3.PNG
--------------------------------------------------------------------------------
/doc/mainwin4.PNG:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/liaanj/ShowTime/2143fddbf4655edbf582bd2c35e8abb7b28a5169/doc/mainwin4.PNG
--------------------------------------------------------------------------------
/doc/mainwin5.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/liaanj/ShowTime/2143fddbf4655edbf582bd2c35e8abb7b28a5169/doc/mainwin5.png
--------------------------------------------------------------------------------
/doc/mainwin6.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/liaanj/ShowTime/2143fddbf4655edbf582bd2c35e8abb7b28a5169/doc/mainwin6.png
--------------------------------------------------------------------------------
/doc/time.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/liaanj/ShowTime/2143fddbf4655edbf582bd2c35e8abb7b28a5169/doc/time.png
--------------------------------------------------------------------------------
/showtime.py:
--------------------------------------------------------------------------------
1 | import sys
2 | import threading
3 | import datetime
4 | import time
5 | import os
6 | import json
7 | import win32evtlog
8 | import win32gui
9 | import win32process
10 | import psutil
11 | import ctypes
12 | from PySide6 import QtWidgets, QtCore, QtGui
13 | import xml.etree.ElementTree as ET
14 | import getpass
15 | import subprocess
16 | from PySide6.QtCore import QSharedMemory
17 | import win32api
18 |
19 | # 配置文件路径
20 | CONFIG_FILE_PATH = os.path.join(os.path.expanduser('~'), '.my_app_config.json')
21 |
22 | def is_admin():
23 | """
24 | 检查当前用户是否拥有管理员权限
25 | """
26 | try:
27 | return ctypes.windll.shell32.IsUserAnAdmin()
28 | except:
29 | return False
30 |
31 | def run_as_admin(argv=None):
32 | """
33 | 重新启动程序并请求管理员权限
34 | """
35 | shell32 = ctypes.windll.shell32
36 | if argv is None and sys.argv:
37 | argv = sys.argv
38 | if not argv:
39 | argv = ['']
40 | executable = sys.executable
41 | params = ' '.join([f'"{arg}"' for arg in argv])
42 | show_cmd = 1 # SW_NORMAL
43 | lpVerb = 'runas'
44 | try:
45 | ret = shell32.ShellExecuteW(None, lpVerb, executable, params, None, show_cmd)
46 | if ret <= 32:
47 | return False
48 | return True
49 | except:
50 | return False
51 |
52 | def restart_program():
53 | """
54 | 重启当前程序,保持管理员权限
55 | """
56 | try:
57 | if is_admin():
58 | # 获取当前执行的可执行文件路径
59 | if hasattr(sys, 'frozen'):
60 | executable = sys.executable
61 | else:
62 | executable = sys.argv[0]
63 | # 使用 subprocess 重新启动程序
64 | subprocess.Popen([executable] + sys.argv[1:], shell=True)
65 | sys.exit(0)
66 | else:
67 | # 以管理员身份重新启动程序
68 | if run_as_admin(sys.argv):
69 | sys.exit(0)
70 | else:
71 | QtWidgets.QMessageBox.warning(None, "管理员权限", "需要管理员权限才能运行此程序。")
72 | sys.exit()
73 | except Exception as e:
74 | QtWidgets.QMessageBox.critical(None, "重启错误", f"无法重启程序: {e}")
75 | sys.exit(1)
76 |
77 | def get_current_time():
78 | """
79 | 获取当前本地时间,包含时区信息
80 | """
81 | return datetime.datetime.now(datetime.timezone.utc).astimezone()
82 |
83 | def get_last_unlock_time():
84 | """
85 | 从 Windows 安全日志中获取最近一次解锁或登录事件的时间
86 | """
87 | current_user = getpass.getuser().lower()
88 | query = "*[System/EventID=4624]"
89 | try:
90 | hand = win32evtlog.EvtQuery('Security', win32evtlog.EvtQueryReverseDirection, query)
91 | while True:
92 | events = win32evtlog.EvtNext(hand, 10)
93 | if not events:
94 | break
95 | for event in events:
96 | try:
97 | xml_str = win32evtlog.EvtRender(event, win32evtlog.EvtRenderEventXml)
98 | xml_root = ET.fromstring(xml_str)
99 | except Exception:
100 | continue
101 | # 获取命名空间
102 | namespace = {'ns': 'http://schemas.microsoft.com/win/2004/08/events/event'}
103 | logon_type_elem = xml_root.find(".//ns:Data[@Name='LogonType']", namespaces=namespace)
104 | if logon_type_elem is not None:
105 | logon_type = int(logon_type_elem.text)
106 | if logon_type in [2, 7]:
107 | # 检查 TargetUserName
108 | target_user_elem = xml_root.find(".//ns:Data[@Name='TargetUserName']", namespaces=namespace)
109 | if target_user_elem is not None:
110 | target_user = target_user_elem.text.lower()
111 | if target_user == current_user:
112 | # 提取事件生成时间
113 | time_created_elem = xml_root.find(".//ns:System/ns:TimeCreated", namespaces=namespace)
114 | if time_created_elem is not None:
115 | time_generated_str = time_created_elem.get('SystemTime')
116 | # 处理微秒部分超过 6 位的情况
117 | time_str = time_generated_str.rstrip('Z')
118 | if '.' in time_str:
119 | base_time, microseconds = time_str.split('.')
120 | microseconds = microseconds[:6] # 截取前 6 位
121 | time_str = f"{base_time}.{microseconds}"
122 | when_utc = datetime.datetime.strptime(time_str, '%Y-%m-%dT%H:%M:%S.%f')
123 | else:
124 | when_utc = datetime.datetime.strptime(time_str, '%Y-%m-%dT%H:%M:%S')
125 | # 将 UTC 时间转换为本地时间
126 | when_utc = when_utc.replace(tzinfo=datetime.timezone.utc)
127 | when_local = when_utc.astimezone()
128 | return when_local
129 | else:
130 | continue
131 | return None
132 | except Exception as e:
133 | print(f"Error reading event log: {e}")
134 | return None
135 |
136 | def update_last_unlock_time():
137 | """
138 | 定期更新 last_unlock_time
139 | """
140 | global last_unlock_time
141 | while True:
142 | new_time = get_last_unlock_time()
143 | with last_unlock_time_lock:
144 | if new_time and (not last_unlock_time or new_time > last_unlock_time):
145 | last_unlock_time = new_time
146 | time.sleep(5) # 每 5 秒检查一次
147 |
148 | def get_active_process_name(window):
149 | """
150 | 获取当前活动窗口的进程名称,排除程序自身和系统窗口
151 | """
152 | try:
153 | hwnd = win32gui.GetForegroundWindow()
154 | # 获取窗口类名和窗口标题
155 | class_name = win32gui.GetClassName(hwnd)
156 | window_text = win32gui.GetWindowText(hwnd)
157 |
158 | # 获取当前程序的窗口句柄和进程 ID
159 | current_hwnd = int(window.winId()) # PySide6 中获取窗口句柄
160 | current_pid = os.getpid()
161 |
162 | # 获取活动窗口的进程 ID
163 | _, pid = win32process.GetWindowThreadProcessId(hwnd)
164 |
165 | # 检查是否为程序自身的窗口
166 | if hwnd == current_hwnd or pid == current_pid:
167 | return None
168 |
169 | # 检查是否为系统窗口
170 | system_classes = [
171 | "Shell_TrayWnd", "TrayNotifyWnd", "NotifyIconOverflowWindow",
172 | "SysListView32", "WorkerW", "Progman", "Button",
173 | "Windows.UI.Core.CoreWindow", "MultitaskingViewFrame", "TaskSwitcherWnd"
174 | ]
175 | if class_name in system_classes:
176 | return None
177 |
178 | # 获取进程名称
179 | process = psutil.Process(pid)
180 | process_name = process.name()
181 |
182 | # 排除特定的系统进程
183 | system_processes = ["explorer.exe", "searchui.exe", "startmenuexperiencehost.exe"]
184 | if process_name.lower() in system_processes:
185 | return None
186 |
187 | return process_name
188 | except Exception as e:
189 | print(f"Error getting active process name: {e}")
190 | return None
191 |
192 | def format_timedelta(td):
193 | """
194 | 将时间差格式化为小时:分钟:秒
195 | """
196 | total_seconds = int(td.total_seconds())
197 | hours, remainder = divmod(total_seconds, 3600)
198 | minutes, seconds = divmod(remainder, 60)
199 | return f'{hours:02}:{minutes:02}:{seconds:02}'
200 |
201 | class Reminder:
202 | def __init__(self, reminder_type, duration, start_time):
203 | self.reminder_type = reminder_type # 'unlock_time', 'app_time', 'countdown'
204 | self.duration = duration # timedelta
205 | self.start_time = start_time # 传入的开始时间
206 | self.target_time = self.start_time + self.duration
207 |
208 | class ProgressBarWidget(QtWidgets.QWidget):
209 | def __init__(self, parent=None, filled_color="#64C864", background_color="#C8C8C8"):
210 | super().__init__(parent)
211 | self.progress = 0 # 进度百分比,0 到 100
212 | self.setMinimumWidth(10) # 设置最小宽度
213 | self.setMinimumHeight(30) # 设置最小高度
214 | self.filled_color = filled_color
215 | self.background_color = background_color
216 |
217 | def set_progress(self, progress):
218 | self.progress = progress
219 | self.update() # 触发重绘
220 |
221 | def set_filled_color(self, color):
222 | self.filled_color = color
223 | self.update()
224 |
225 | def set_background_color(self, color):
226 | self.background_color = color
227 | self.update()
228 |
229 | def paintEvent(self, event):
230 | painter = QtGui.QPainter(self)
231 | # 开启反锯齿
232 | painter.setRenderHint(QtGui.QPainter.Antialiasing)
233 | # 绘制背景矩形
234 | rect = self.rect()
235 | painter.setBrush(QtGui.QColor(self.background_color))
236 | painter.setPen(QtCore.Qt.NoPen)
237 | painter.drawRoundedRect(rect, 5, 5)
238 | # 计算已填充部分
239 | filled_height = rect.height() * self.progress / 100
240 | filled_rect = QtCore.QRectF(
241 | rect.x(),
242 | rect.y() + rect.height() - filled_height,
243 | rect.width(),
244 | filled_height
245 | )
246 | # 绘制已填充部分
247 | painter.setBrush(QtGui.QColor(self.filled_color))
248 | painter.drawRoundedRect(filled_rect, 5, 5)
249 |
250 | class ReminderDialog(QtWidgets.QDialog):
251 | def __init__(self, parent=None):
252 | super().__init__(parent)
253 | self.reminder_type = None
254 | self.duration = None
255 | self.initUI()
256 |
257 | def initUI(self):
258 | self.setWindowTitle("设置提醒")
259 | layout = QtWidgets.QVBoxLayout()
260 |
261 | # 提醒类型选择(使用单选按钮)
262 | type_layout = QtWidgets.QHBoxLayout()
263 | type_label = QtWidgets.QLabel("提醒类型:")
264 | self.unlock_time_radio = QtWidgets.QRadioButton("解锁时间")
265 | self.app_time_radio = QtWidgets.QRadioButton("应用时间")
266 | self.countdown_radio = QtWidgets.QRadioButton("倒计时")
267 | self.unlock_time_radio.setChecked(True) # 默认选中解锁时间
268 | type_layout.addWidget(type_label)
269 | type_layout.addWidget(self.unlock_time_radio)
270 | type_layout.addWidget(self.app_time_radio)
271 | type_layout.addWidget(self.countdown_radio)
272 | layout.addLayout(type_layout)
273 |
274 | # 时间输入
275 | duration_layout = QtWidgets.QHBoxLayout()
276 | duration_label = QtWidgets.QLabel("时间 (分钟):")
277 | self.duration_edit = QtWidgets.QLineEdit()
278 | duration_layout.addWidget(duration_label)
279 | duration_layout.addWidget(self.duration_edit)
280 | layout.addLayout(duration_layout)
281 |
282 | # 按钮
283 | button_box = QtWidgets.QDialogButtonBox(QtWidgets.QDialogButtonBox.Ok | QtWidgets.QDialogButtonBox.Cancel)
284 | button_box.accepted.connect(self.accept)
285 | button_box.rejected.connect(self.reject)
286 | layout.addWidget(button_box)
287 |
288 | self.setLayout(layout)
289 |
290 | def accept(self):
291 | try:
292 | duration_value = int(self.duration_edit.text())
293 | if duration_value <= 0:
294 | raise ValueError("时间必须是正数")
295 | self.duration = duration_value * 60 # 将分钟转换为秒
296 | if self.unlock_time_radio.isChecked():
297 | self.reminder_type = "unlock_time"
298 | elif self.app_time_radio.isChecked():
299 | self.reminder_type = "app_time"
300 | elif self.countdown_radio.isChecked():
301 | self.reminder_type = "countdown"
302 | super().accept()
303 | except ValueError:
304 | QtWidgets.QMessageBox.warning(self, "输入错误", "请输入有效的正数时间(分钟)")
305 |
306 | class AppearanceSettingsDialog(QtWidgets.QDialog):
307 | def __init__(self, parent=None, config=None):
308 | super().__init__(parent)
309 | self.config = config or {}
310 | self.main_window = parent # 引用主窗口
311 | self.initUI()
312 | self.connect_signals()
313 |
314 | def initUI(self):
315 | self.setWindowTitle("外观设置")
316 | layout = QtWidgets.QFormLayout()
317 |
318 | # 字体大小
319 | self.font_size_spin = QtWidgets.QSpinBox()
320 | self.font_size_spin.setRange(8, 48)
321 | self.font_size_spin.setValue(self.config.get('font_size', 16))
322 | layout.addRow("字体大小:", self.font_size_spin)
323 |
324 | # 字体颜色
325 | self.font_color_button = QtWidgets.QPushButton("选择颜色")
326 | self.font_color_display = QtWidgets.QLabel()
327 | self.font_color_display.setFixedSize(50, 20)
328 | self.font_color_display.setStyleSheet(f"background-color: {self.config.get('font_color', '#000000')};")
329 | font_color_layout = QtWidgets.QHBoxLayout()
330 | font_color_layout.addWidget(self.font_color_button)
331 | font_color_layout.addWidget(self.font_color_display)
332 | layout.addRow("字体颜色:", font_color_layout)
333 | self.font_color_button.clicked.connect(self.choose_font_color)
334 |
335 | # 窗口宽度
336 | self.window_width_spin = QtWidgets.QSpinBox()
337 | self.window_width_spin.setRange(100, 1000)
338 | self.window_width_spin.setValue(self.config.get('window_width', 200))
339 | layout.addRow("窗口宽度:", self.window_width_spin)
340 |
341 | # 窗口高度
342 | self.window_height_spin = QtWidgets.QSpinBox()
343 | self.window_height_spin.setRange(30, 500) # 设置最低高度为30
344 | self.window_height_spin.setValue(self.config.get('window_height', 60))
345 | layout.addRow("窗口高度:", self.window_height_spin)
346 |
347 | # 进度条位置
348 | self.bar_position_group = QtWidgets.QButtonGroup()
349 | bar_position_layout = QtWidgets.QHBoxLayout()
350 | self.bar_left_radio = QtWidgets.QRadioButton("左侧")
351 | self.bar_right_radio = QtWidgets.QRadioButton("右侧")
352 | self.bar_position_group.addButton(self.bar_left_radio)
353 | self.bar_position_group.addButton(self.bar_right_radio)
354 | bar_position_layout.addWidget(self.bar_left_radio)
355 | bar_position_layout.addWidget(self.bar_right_radio)
356 | bar_position = self.config.get('bar_position', "左侧")
357 | if bar_position == "左侧":
358 | self.bar_left_radio.setChecked(True)
359 | else:
360 | self.bar_right_radio.setChecked(True)
361 | layout.addRow("进度条位置:", bar_position_layout)
362 |
363 | # 控件间距
364 | self.spacing_spin = QtWidgets.QSpinBox()
365 | self.spacing_spin.setRange(0, 20)
366 | self.spacing_spin.setValue(self.config.get('spacing', 2))
367 | layout.addRow("控件间距:", self.spacing_spin)
368 |
369 | # 进度条高度
370 | self.progress_bar_height_spin = QtWidgets.QSpinBox()
371 | self.progress_bar_height_spin.setRange(10, 200)
372 | self.progress_bar_height_spin.setValue(self.config.get('progress_bar_height', 40))
373 | layout.addRow("进度条高度:", self.progress_bar_height_spin)
374 |
375 | # 进度条填充颜色
376 | self.progress_filled_color_button = QtWidgets.QPushButton("选择填充颜色")
377 | self.progress_filled_color_display = QtWidgets.QLabel()
378 | self.progress_filled_color_display.setFixedSize(50, 20)
379 | self.progress_filled_color_display.setStyleSheet(f"background-color: {self.config.get('progress_bar_filled_color', '#64C864')};")
380 | progress_filled_color_layout = QtWidgets.QHBoxLayout()
381 | progress_filled_color_layout.addWidget(self.progress_filled_color_button)
382 | progress_filled_color_layout.addWidget(self.progress_filled_color_display)
383 | layout.addRow("进度条填充颜色:", progress_filled_color_layout)
384 | self.progress_filled_color_button.clicked.connect(self.choose_progress_filled_color)
385 |
386 | # 进度条背景颜色
387 | self.progress_background_color_button = QtWidgets.QPushButton("选择背景颜色")
388 | self.progress_background_color_display = QtWidgets.QLabel()
389 | self.progress_background_color_display.setFixedSize(50, 20)
390 | self.progress_background_color_display.setStyleSheet(f"background-color: {self.config.get('progress_bar_background_color', '#C8C8C8')};")
391 | progress_background_color_layout = QtWidgets.QHBoxLayout()
392 | progress_background_color_layout.addWidget(self.progress_background_color_button)
393 | progress_background_color_layout.addWidget(self.progress_background_color_display)
394 | layout.addRow("进度条背景颜色:", progress_background_color_layout)
395 | self.progress_background_color_button.clicked.connect(self.choose_progress_background_color)
396 |
397 | # 按钮
398 | button_box = QtWidgets.QDialogButtonBox(QtWidgets.QDialogButtonBox.Close)
399 | button_box.rejected.connect(self.reject)
400 | layout.addRow(button_box)
401 |
402 | self.setLayout(layout)
403 |
404 | def connect_signals(self):
405 | # 实时更新
406 | self.font_size_spin.valueChanged.connect(self.update_main_window)
407 | self.window_width_spin.valueChanged.connect(self.update_main_window)
408 | self.window_height_spin.valueChanged.connect(self.update_main_window)
409 | self.bar_position_group.buttonClicked.connect(self.update_main_window)
410 | self.spacing_spin.valueChanged.connect(self.update_main_window)
411 | self.progress_bar_height_spin.valueChanged.connect(self.update_main_window)
412 |
413 | def choose_font_color(self):
414 | color = QtWidgets.QColorDialog.getColor()
415 | if color.isValid():
416 | try:
417 | self.config['font_color'] = color.name()
418 | self.font_color_display.setStyleSheet(f"background-color: {color.name()};")
419 | self.update_main_window()
420 | # 重启程序以应用颜色更改
421 | self.main_window.save_config()
422 | restart_program()
423 | except Exception as e:
424 | QtWidgets.QMessageBox.warning(self, "颜色选择错误", f"无法设置颜色: {e}")
425 |
426 | def choose_progress_filled_color(self):
427 | color = QtWidgets.QColorDialog.getColor()
428 | if color.isValid():
429 | try:
430 | self.config['progress_bar_filled_color'] = color.name()
431 | self.progress_filled_color_display.setStyleSheet(f"background-color: {color.name()};")
432 | self.update_main_window()
433 | # 添加重启程序以应用颜色更改
434 | self.main_window.save_config()
435 | restart_program()
436 | except Exception as e:
437 | QtWidgets.QMessageBox.warning(self, "颜色选择错误", f"无法设置填充颜色: {e}")
438 |
439 | def choose_progress_background_color(self):
440 | color = QtWidgets.QColorDialog.getColor()
441 | if color.isValid():
442 | try:
443 | self.config['progress_bar_background_color'] = color.name()
444 | self.progress_background_color_display.setStyleSheet(f"background-color: {color.name()};")
445 | self.update_main_window()
446 | # 添加重启程序以应用颜色更改
447 | self.main_window.save_config()
448 | restart_program()
449 | except Exception as e:
450 | QtWidgets.QMessageBox.warning(self, "颜色选择错误", f"无法设置背景颜色: {e}")
451 |
452 | def update_main_window(self):
453 | # 更新配置
454 | try:
455 | self.config['font_size'] = self.font_size_spin.value()
456 | self.config['window_width'] = self.window_width_spin.value()
457 | self.config['window_height'] = self.window_height_spin.value()
458 | self.config['bar_position'] = "左侧" if self.bar_left_radio.isChecked() else "右侧"
459 | self.config['spacing'] = self.spacing_spin.value()
460 | self.config['progress_bar_height'] = self.progress_bar_height_spin.value()
461 | self.config['progress_bar_filled_color'] = self.config.get('progress_bar_filled_color', '#64C864')
462 | self.config['progress_bar_background_color'] = self.config.get('progress_bar_background_color', '#C8C8C8')
463 | self.config['font_color'] = self.config.get('font_color', '#000000')
464 | # 应用到主窗口
465 | self.main_window.config.update(self.config)
466 | self.main_window.apply_config()
467 | except Exception as e:
468 | QtWidgets.QMessageBox.warning(self, "应用设置错误", f"无法应用设置: {e}")
469 |
470 | class TransparentWindow(QtWidgets.QWidget):
471 | def __init__(self):
472 | super().__init__()
473 | self.config = self.load_config()
474 | self.recent_apps = {} # 存储最近切换的应用程序
475 | self.reminders = [] # 存储提醒对象
476 | self.is_paused = False # 是否暂停计时
477 | self.is_fullscreen = False # 是否处于全屏状态
478 | self.is_window_shown = True # 窗口是否显示
479 | self.previous_position = None # 记录窗口之前的位置
480 | self.initUI()
481 | # 应用配置
482 | self.apply_config()
483 | # 初始化 last_process_name 和 last_process_start_time
484 | self.last_process_name = get_active_process_name(self)
485 | self.last_process_start_time = get_current_time()
486 | # 启动定时器更新界面
487 | self.timer = QtCore.QTimer()
488 | self.timer.timeout.connect(self.update_time)
489 | self.timer.start(500) # 每 500 毫秒更新一次
490 |
491 | # 启动置顶定时器,每 50 毫秒置顶一次
492 | self.raise_timer = QtCore.QTimer()
493 | self.raise_timer.timeout.connect(self.keep_on_top)
494 | self.raise_timer.start(50) # 每 50 毫秒置顶一次
495 |
496 | # 启动线程定期更新 last_unlock_time
497 | threading.Thread(target=update_last_unlock_time, daemon=True).start()
498 |
499 | # 启动全屏检测定时器
500 | self.fullscreen_timer = QtCore.QTimer()
501 | self.fullscreen_timer.timeout.connect(self.check_fullscreen)
502 | self.fullscreen_timer.start(100) # 每100ms检查一次
503 |
504 | def paintEvent(self, event):
505 | """
506 | 重写 paintEvent 以绘制一个几乎完全透明的背景,
507 | 这样窗口的所有区域都能拦截鼠标事件。
508 | """
509 | painter = QtGui.QPainter(self)
510 | # 设置组合模式为 Source,确保绘制的颜色覆盖所有像素
511 | painter.setCompositionMode(QtGui.QPainter.CompositionMode_Source)
512 | # 绘制一个几乎透明的背景(Alpha 值为1)
513 | painter.fillRect(self.rect(), QtGui.QColor(0, 0, 0, 1))
514 |
515 | def initUI(self):
516 | try:
517 | # 设置窗口无边框、置顶、工具窗口和背景透明
518 | self.setWindowFlags(QtCore.Qt.FramelessWindowHint | QtCore.Qt.WindowStaysOnTopHint | QtCore.Qt.Tool)
519 | self.setAttribute(QtCore.Qt.WA_TranslucentBackground)
520 |
521 | # 使窗口可拖动
522 | self.offset = None
523 |
524 | # 创建标签
525 | self.unlock_time_label = QtWidgets.QLabel("", self)
526 | self.app_time_label = QtWidgets.QLabel("", self)
527 |
528 | # 设置标签样式(字体颜色和大小)
529 | label_style = f"color: {self.config.get('font_color', '#000000')}; font-size: {self.config.get('font_size', 16)}px;"
530 | self.unlock_time_label.setStyleSheet(label_style)
531 | self.app_time_label.setStyleSheet(label_style)
532 |
533 | # 设置标签不拦截鼠标事件
534 | self.unlock_time_label.setAttribute(QtCore.Qt.WA_TransparentForMouseEvents)
535 | self.app_time_label.setAttribute(QtCore.Qt.WA_TransparentForMouseEvents)
536 |
537 | # 设置标签的最小高度,确保在窗口高度降低时能够正常显示
538 | self.unlock_time_label.setMinimumHeight(10)
539 | self.app_time_label.setMinimumHeight(10)
540 |
541 | # 创建进度条小部件
542 | self.progress_bar = ProgressBarWidget(
543 | self,
544 | filled_color=self.config.get('progress_bar_filled_color', '#64C864'),
545 | background_color=self.config.get('progress_bar_background_color', '#C8C8C8')
546 | )
547 | progress_bar_height = self.config.get('progress_bar_height', 40)
548 | self.progress_bar.setFixedSize(10, progress_bar_height) # 调整尺寸
549 |
550 | # 布局设置
551 | self.layout = QtWidgets.QHBoxLayout()
552 | self.layout.setSpacing(self.config.get('spacing', 2))
553 | self.layout.setContentsMargins(0, 0, 0, 0)
554 |
555 | # 动态调整进度条位置
556 | self.update_layout()
557 |
558 | self.setLayout(self.layout)
559 |
560 | # 设置窗口大小和位置
561 | self.update_window_geometry()
562 | except Exception as e:
563 | QtWidgets.QMessageBox.warning(self, "初始化错误", f"初始化界面时发生错误: {e}")
564 |
565 | def show_about_dialog(self):
566 | """
567 | 显示关于对话框,包含作者名称和 GitHub 地址
568 | """
569 | try:
570 | # 创建一个对话框
571 | dialog = QtWidgets.QDialog(self)
572 | dialog.setWindowTitle("关于")
573 | dialog.setFixedSize(300, 150)
574 |
575 | layout = QtWidgets.QVBoxLayout()
576 |
577 | # 添加作者名称
578 | author_label = QtWidgets.QLabel("作者:liaanj")
579 | author_label.setAlignment(QtCore.Qt.AlignCenter)
580 | layout.addWidget(author_label)
581 |
582 | # 添加 GitHub 地址,设置为可点击的链接
583 | github_label = QtWidgets.QLabel()
584 | github_label.setText('GitHub 地址:点击访问项目')
585 | github_label.setAlignment(QtCore.Qt.AlignCenter)
586 | github_label.setOpenExternalLinks(True) # 允许打开外部链接
587 | layout.addWidget(github_label)
588 |
589 | # 添加关闭按钮
590 | button_box = QtWidgets.QDialogButtonBox(QtWidgets.QDialogButtonBox.Close)
591 | button_box.rejected.connect(dialog.reject)
592 | layout.addWidget(button_box)
593 |
594 | dialog.setLayout(layout)
595 | dialog.exec()
596 | except Exception as e:
597 | QtWidgets.QMessageBox.warning(self, "关于对话框错误", f"无法显示关于对话框: {e}")
598 |
599 | def check_fullscreen(self):
600 | """
601 | 检查当前前台窗口是否全屏,并更新状态
602 | """
603 | hwnd = win32gui.GetForegroundWindow()
604 | if hwnd:
605 | self_hwnd = self.effectiveWinId().__int__()
606 | if hwnd == self_hwnd:
607 | # 前台窗口是自身,忽略,不改变 is_fullscreen 状态
608 | return
609 | else:
610 | # 获取前台窗口所在屏幕
611 | monitor_info = win32api.GetMonitorInfo(win32api.MonitorFromWindow(hwnd))
612 | monitor_area = monitor_info['Monitor']
613 | work_area = monitor_info['Work']
614 | # 获取前台窗口大小
615 | rect = win32gui.GetWindowRect(hwnd)
616 | width = rect[2] - rect[0]
617 | height = rect[3] - rect[1]
618 | screen_width = monitor_area[2] - monitor_area[0]
619 | screen_height = monitor_area[3] - monitor_area[1]
620 | new_fullscreen_state = (width >= screen_width and height >= screen_height)
621 | # 排除桌面窗口
622 | window_class = win32gui.GetClassName(hwnd)
623 | desktop_classes = ["Progman", "WorkerW"]
624 | if window_class in desktop_classes:
625 | new_fullscreen_state = False
626 | if new_fullscreen_state != self.is_fullscreen:
627 | self.is_fullscreen = new_fullscreen_state
628 | if self.is_fullscreen:
629 | print("进入全屏模式")
630 | self.on_enter_fullscreen()
631 | else:
632 | print("退出全屏模式")
633 | self.on_exit_fullscreen()
634 | else:
635 | # 无前台窗口,可能性较小,忽略
636 | pass
637 |
638 |
639 | def on_enter_fullscreen(self):
640 | # 移动到记录的全屏位置
641 | pos = self.config.get('fullscreen_position', None)
642 | if pos is not None:
643 | self.move(pos['x'], pos['y'])
644 | # 移除贴边隐藏相关逻辑
645 |
646 | def on_exit_fullscreen(self):
647 | # 移动到记录的非全屏位置
648 | pos = self.config.get('non_fullscreen_position', None)
649 | if pos is not None:
650 | self.move(pos['x'], pos['y'])
651 | # 如果窗口被隐藏,确保它显示出来
652 | self.show()
653 |
654 | def update_layout(self):
655 | try:
656 | # 清除现有布局
657 | while self.layout.count():
658 | item = self.layout.takeAt(0)
659 | widget = item.widget()
660 | if widget is not None:
661 | widget.setParent(None)
662 |
663 | # 更新标签样式
664 | label_style = f"color: {self.config.get('font_color', '#000000')}; font-size: {self.config.get('font_size', 16)}px;"
665 | self.unlock_time_label.setStyleSheet(label_style)
666 | self.app_time_label.setStyleSheet(label_style)
667 |
668 | # 根据配置添加控件
669 | if self.config.get('bar_position', "左侧") == "左侧":
670 | self.layout.addWidget(self.progress_bar)
671 |
672 | text_layout = QtWidgets.QVBoxLayout()
673 | text_layout.addWidget(self.unlock_time_label)
674 | text_layout.addWidget(self.app_time_label)
675 | text_layout.setSpacing(0)
676 | text_layout.setContentsMargins(0, 0, 0, 0)
677 |
678 | self.layout.addLayout(text_layout)
679 |
680 | if self.config.get('bar_position', "左侧") == "右侧":
681 | self.layout.addWidget(self.progress_bar)
682 |
683 | # 更新布局间距
684 | self.layout.setSpacing(self.config.get('spacing', 2))
685 | except Exception as e:
686 | QtWidgets.QMessageBox.warning(self, "布局更新错误", f"更新布局时发生错误: {e}")
687 |
688 | def update_window_geometry(self):
689 | try:
690 | window_width = self.config.get('window_width', 200)
691 | window_height = self.config.get('window_height', 60)
692 | x = self.config.get('window_x', None)
693 | y = self.config.get('window_y', None)
694 |
695 | # 更新窗口尺寸
696 | self.resize(window_width, window_height)
697 |
698 | if x is not None and y is not None:
699 | self.move(x, y)
700 | else:
701 | screen_rect = QtWidgets.QApplication.primaryScreen().availableGeometry()
702 | taskbar_height = 40 # 假设任务栏高度为 40px
703 | offset_from_right = 100 # 从屏幕右侧向左偏移 100 像素
704 | self.move(screen_rect.width() - window_width - offset_from_right,
705 | screen_rect.height() - taskbar_height - window_height)
706 | except Exception as e:
707 | QtWidgets.QMessageBox.warning(self, "窗口几何错误", f"更新窗口几何时发生错误: {e}")
708 |
709 | def apply_config(self):
710 | try:
711 | # 更新布局
712 | self.update_layout()
713 | # 更新窗口尺寸,但保持当前位置
714 | self.update_window_geometry()
715 |
716 | # 更新进度条高度
717 | progress_bar_height = self.config.get('progress_bar_height', 40)
718 | self.progress_bar.setFixedHeight(progress_bar_height)
719 |
720 | # 更新进度条颜色
721 | filled_color = self.config.get('progress_bar_filled_color', '#64C864')
722 | background_color = self.config.get('progress_bar_background_color', '#C8C8C8')
723 | self.progress_bar.set_filled_color(filled_color)
724 | self.progress_bar.set_background_color(background_color)
725 |
726 | # 更新标签样式
727 | label_style = f"color: {self.config.get('font_color', '#000000')}; font-size: {self.config.get('font_size', 16)}px;"
728 | self.unlock_time_label.setStyleSheet(label_style)
729 | self.app_time_label.setStyleSheet(label_style)
730 | except Exception as e:
731 | QtWidgets.QMessageBox.warning(self, "应用配置错误", f"应用配置时发生错误: {e}")
732 |
733 | def load_config(self):
734 | if os.path.exists(CONFIG_FILE_PATH):
735 | try:
736 | with open(CONFIG_FILE_PATH, 'r', encoding='utf-8') as f:
737 | config = json.load(f)
738 | return config
739 | except Exception as e:
740 | print(f"Error loading config: {e}")
741 | return {}
742 | else:
743 | return {}
744 |
745 | def save_config(self):
746 | try:
747 | # 在保存配置前,保存窗口的位置
748 | self.config['window_x'] = self.x()
749 | self.config['window_y'] = self.y()
750 | with open(CONFIG_FILE_PATH, 'w', encoding='utf-8') as f:
751 | json.dump(self.config, f, ensure_ascii=False, indent=4)
752 | except Exception as e:
753 | print(f"Error saving config: {e}")
754 |
755 | def mousePressEvent(self, event):
756 | """
757 | 记录鼠标按下的位置
758 | """
759 | if event.button() == QtCore.Qt.MouseButton.LeftButton:
760 | self.offset = event.position().toPoint()
761 | elif event.button() == QtCore.Qt.MouseButton.RightButton:
762 | self.contextMenuEvent(event)
763 | # if event.button() == QtCore.Qt.RightButton:
764 | # self.contextMenuEvent(event)
765 | # super().mousePressEvent(event)
766 |
767 | def mouseMoveEvent(self, event):
768 | """
769 | 拖动窗口
770 | """
771 | if self.offset is not None and event.buttons() == QtCore.Qt.MouseButton.LeftButton:
772 | self.move(event.globalPosition().toPoint() - self.offset)
773 |
774 | def mouseReleaseEvent(self, event):
775 | """
776 | 释放鼠标时清除偏移
777 | """
778 | self.offset = None
779 |
780 | def moveEvent(self, event):
781 | """
782 | 当窗口移动时,更新配置中的窗口位置
783 | """
784 | super().moveEvent(event)
785 | self.config['window_x'] = self.x()
786 | self.config['window_y'] = self.y()
787 | # 根据全屏状态记录位置
788 | if self.is_fullscreen:
789 | self.config['fullscreen_position'] = {'x': self.x(), 'y': self.y()}
790 | else:
791 | self.config['non_fullscreen_position'] = {'x': self.x(), 'y': self.y()}
792 |
793 | def contextMenuEvent(self, event):
794 | """
795 | 右键菜单
796 | """
797 | try:
798 | menu = QtWidgets.QMenu(self)
799 | reset_time_action = menu.addAction("清除时间")
800 | set_reminder_action = menu.addAction("设置提醒")
801 | appearance_settings_action = menu.addAction("外观设置")
802 | pause_time_action = menu.addAction("暂停计时" if not self.is_paused else "继续计时")
803 | startup_action = menu.addAction("开机自启")
804 | exit_action = menu.addAction("退出应用")
805 | about_action = menu.addAction("关于")
806 |
807 | # 设置开机自启复选框状态
808 |
809 | is_startup = self.is_startup_enabled()
810 | startup_action.setCheckable(True)
811 | startup_action.setChecked(is_startup)
812 |
813 | # 计算菜单位置,避免被遮挡
814 | screen_rect = QtWidgets.QApplication.primaryScreen().availableGeometry()
815 | menu_x = event.globalPos().x()
816 | menu_y = event.globalPos().y()
817 | menu_height = menu.sizeHint().height()
818 | if menu_y + menu_height > screen_rect.height():
819 | menu_y = screen_rect.height() - menu_height
820 |
821 | action = menu.exec(QtCore.QPoint(menu_x, menu_y))
822 | if action == reset_time_action:
823 | self.reset_time()
824 | elif action == set_reminder_action:
825 | self.set_reminder()
826 | elif action == appearance_settings_action:
827 | self.open_appearance_settings()
828 | elif action == pause_time_action:
829 | self.toggle_pause()
830 | elif action == startup_action:
831 | if startup_action.isChecked():
832 | self.enable_startup()
833 | else:
834 | self.disable_startup()
835 | elif action == about_action:
836 | self.show_about_dialog()
837 | elif action == exit_action:
838 | self.close()
839 | except Exception as e:
840 | QtWidgets.QMessageBox.warning(self, "右键菜单错误", f"打开右键菜单时发生错误: {e}")
841 |
842 | def open_appearance_settings(self):
843 | """
844 | 打开外观设置对话框
845 | """
846 | try:
847 | dialog = AppearanceSettingsDialog(self, self.config.copy())
848 | dialog.exec()
849 | # 配置已经在实时更新中保存,不需要在这里再保存
850 | except Exception as e:
851 | QtWidgets.QMessageBox.warning(self, "外观设置错误", f"打开外观设置时发生错误: {e}")
852 |
853 | def reset_time(self):
854 | """
855 | 清除时间
856 | """
857 | try:
858 | # 重置 last_unlock_time
859 | with last_unlock_time_lock:
860 | global last_unlock_time
861 | last_unlock_time = get_current_time()
862 | # 重置应用时间
863 | self.last_process_start_time = get_current_time()
864 | self.last_process_name = get_active_process_name(self)
865 | # 清空提醒
866 | self.reminders.clear()
867 | self.progress_bar.set_progress(0)
868 | except Exception as e:
869 | QtWidgets.QMessageBox.warning(self, "重置时间错误", f"重置时间时发生错误: {e}")
870 |
871 | def set_reminder(self):
872 | try:
873 | dialog = ReminderDialog(self)
874 | if dialog.exec() == QtWidgets.QDialog.Accepted:
875 | reminder_type_map = {
876 | "unlock_time": "unlock_time",
877 | "app_time": "app_time",
878 | "countdown": "countdown"
879 | }
880 | reminder_type = reminder_type_map.get(dialog.reminder_type)
881 | duration = datetime.timedelta(seconds=dialog.duration)
882 | # 获取当前已用时间
883 | current_time = get_current_time()
884 |
885 | if reminder_type == 'unlock_time':
886 | with last_unlock_time_lock:
887 | if last_unlock_time:
888 | elapsed_time = current_time - last_unlock_time
889 | start_time = last_unlock_time
890 | else:
891 | # 无法获取解锁时间,提前记录错误信息
892 | error_message = "无法获取解锁时间"
893 | start_time = None
894 | if start_time is None:
895 | QtWidgets.QMessageBox.warning(self, "设置错误", error_message)
896 | return
897 | if duration <= elapsed_time:
898 | QtWidgets.QMessageBox.warning(self, "输入错误", "提醒时间必须大于当前已用的解锁时间")
899 | return
900 |
901 | elif reminder_type == 'app_time':
902 | if self.last_process_start_time:
903 | elapsed_time = current_time - self.last_process_start_time
904 | if duration <= elapsed_time:
905 | QtWidgets.QMessageBox.warning(self, "输入错误", "提醒时间必须大于当前已用的应用时间")
906 | return
907 | start_time = self.last_process_start_time
908 | else:
909 | QtWidgets.QMessageBox.warning(self, "设置错误", "无法获取应用时间")
910 | return
911 |
912 | elif reminder_type == 'countdown':
913 | start_time = current_time
914 |
915 | # 创建提醒对象
916 | reminder = Reminder(reminder_type, duration, start_time)
917 | self.reminders.append(reminder)
918 | except Exception as e:
919 | QtWidgets.QMessageBox.warning(self, "设置提醒错误", f"设置提醒时发生错误: {e}")
920 |
921 | def toggle_pause(self):
922 | """
923 | 暂停或继续计时
924 | """
925 | try:
926 | self.is_paused = not self.is_paused
927 | if self.is_paused:
928 | self.pause_start_time = get_current_time()
929 | else:
930 | pause_duration = get_current_time() - self.pause_start_time
931 | # 调整计时开始时间,补偿暂停的时间
932 | with last_unlock_time_lock:
933 | global last_unlock_time
934 | if last_unlock_time:
935 | last_unlock_time += pause_duration
936 | if self.last_process_start_time:
937 | self.last_process_start_time += pause_duration
938 | # 更新提醒的开始时间和目标时间
939 | for reminder in self.reminders:
940 | reminder.start_time += pause_duration
941 | reminder.target_time += pause_duration
942 | except Exception as e:
943 | QtWidgets.QMessageBox.warning(self, "暂停计时错误", f"暂停/继续计时时发生错误: {e}")
944 |
945 | def show_notification(self, reminder):
946 | """
947 | 显示提醒通知
948 | """
949 | try:
950 | message_map = {
951 | 'unlock_time': '解锁时间',
952 | 'app_time': '应用时间',
953 | 'countdown': '倒计时'
954 | }
955 | message = f"您的{message_map.get(reminder.reminder_type, '未知类型')}已达到设定时间!"
956 | QtWidgets.QMessageBox.information(self, "提醒", message)
957 | except Exception as e:
958 | QtWidgets.QMessageBox.warning(self, "通知错误", f"显示通知时发生错误: {e}")
959 |
960 | def update_time(self):
961 | try:
962 | if self.is_paused:
963 | # 计时暂停,不更新界面,仅保持进度条不动
964 | return
965 |
966 | # 获取当前本地时间,包含时区信息
967 | current_time = get_current_time()
968 |
969 | # 更新自上次解锁以来的时间
970 | with last_unlock_time_lock:
971 | local_last_unlock_time = last_unlock_time
972 | if local_last_unlock_time:
973 | since_unlock = current_time - local_last_unlock_time
974 | self.unlock_time_label.setText(f"解锁时间: {format_timedelta(since_unlock)}")
975 | else:
976 | self.unlock_time_label.setText("解锁时间: N/A")
977 |
978 | # 更新当前应用使用时间
979 | current_process_name = get_active_process_name(self)
980 |
981 | if current_process_name and current_process_name != self.last_process_name:
982 | # 活动应用程序发生变化
983 | switch_away_time = current_time # 记录离开时间
984 |
985 | # 计算在上一个应用程序上花费的时间
986 | app_time = current_time - self.last_process_start_time
987 |
988 | # 将上一个应用程序的信息存储到 recent_apps
989 | self.recent_apps[self.last_process_name] = {
990 | 'last_start_time': self.last_process_start_time,
991 | 'accumulated_time': app_time,
992 | 'switch_away_time': switch_away_time
993 | }
994 |
995 | # 移除离开时间超过一分钟的应用程序
996 | to_remove = []
997 | for app_name, data in self.recent_apps.items():
998 | time_since_switch = (current_time - data['switch_away_time']).total_seconds()
999 | if time_since_switch > 60:
1000 | to_remove.append(app_name)
1001 | for app_name in to_remove:
1002 | del self.recent_apps[app_name]
1003 |
1004 | # 检查当前应用程序是否在 recent_apps 中(即是否在一分钟内返回)
1005 | if current_process_name in self.recent_apps:
1006 | # 恢复之前的应用程序计时
1007 | prev_data = self.recent_apps[current_process_name]
1008 | self.last_process_start_time = prev_data['last_start_time']
1009 | # 调整开始时间,补偿离开的时间
1010 | time_away = current_time - prev_data['switch_away_time']
1011 | self.last_process_start_time += time_away
1012 | # 从 recent_apps 中移除该应用程序
1013 | del self.recent_apps[current_process_name]
1014 | else:
1015 | # 超过一分钟,重置计时
1016 | self.last_process_start_time = current_time
1017 |
1018 | self.last_process_name = current_process_name
1019 |
1020 | elif current_process_name is None:
1021 | # 活动窗口为系统窗口或自身窗口,不做处理
1022 | pass
1023 | else:
1024 | # 活动应用程序未发生变化,继续计时
1025 | pass
1026 |
1027 | if self.last_process_name:
1028 | app_uptime = current_time - self.last_process_start_time
1029 | self.app_time_label.setText(f"应用时间: {format_timedelta(app_uptime)}")
1030 | else:
1031 | self.app_time_label.setText("应用时间: N/A")
1032 |
1033 | # 检查提醒
1034 | if self.reminders:
1035 | # 更新依赖于解锁时间或应用时间的提醒
1036 | for reminder in self.reminders[:]:
1037 | if reminder.reminder_type == 'unlock_time':
1038 | with last_unlock_time_lock:
1039 | local_last_unlock_time = last_unlock_time
1040 | if local_last_unlock_time and local_last_unlock_time > reminder.start_time:
1041 | reminder.start_time = local_last_unlock_time
1042 | reminder.target_time = reminder.start_time + reminder.duration
1043 | elif reminder.reminder_type == 'app_time':
1044 | if self.last_process_start_time and self.last_process_start_time > reminder.start_time:
1045 | reminder.start_time = self.last_process_start_time
1046 | reminder.target_time = reminder.start_time + reminder.duration
1047 |
1048 | time_remaining = (reminder.target_time - current_time).total_seconds()
1049 | total_duration = (reminder.target_time - reminder.start_time).total_seconds()
1050 | if total_duration > 0:
1051 | progress = max(0, min(100, (1 - time_remaining / total_duration) * 100))
1052 | else:
1053 | progress = 100
1054 | self.progress_bar.set_progress(progress)
1055 |
1056 | # 检查是否有提醒到达
1057 | if reminder.target_time <= current_time:
1058 | # 提醒到达
1059 | self.show_notification(reminder)
1060 | # 删除已完成的提醒
1061 | self.reminders.remove(reminder)
1062 | else:
1063 | # 没有提醒,重置进度条
1064 | self.progress_bar.set_progress(0)
1065 | except Exception as e:
1066 | QtWidgets.QMessageBox.warning(self, "更新时间错误", f"更新时间时发生错误: {e}")
1067 |
1068 | def keep_on_top(self):
1069 | """
1070 | 将窗口置顶
1071 | """
1072 | try:
1073 | self.raise_()
1074 | except Exception as e:
1075 | QtWidgets.QMessageBox.warning(self, "置顶错误", f"保持置顶时发生错误: {e}")
1076 |
1077 | def closeEvent(self, event):
1078 | """
1079 | 窗口关闭事件
1080 | """
1081 | try:
1082 | self.save_config()
1083 | # 停止所有定时器
1084 | self.timer.stop()
1085 | self.raise_timer.stop()
1086 | self.fullscreen_timer.stop()
1087 | # self.mouse_timer.stop()
1088 | # 释放共享内存
1089 | shared_memory.detach()
1090 | event.accept()
1091 | QtWidgets.QApplication.quit() # 确保应用程序退出
1092 | except Exception as e:
1093 | QtWidgets.QMessageBox.warning(self, "关闭错误", f"关闭窗口时发生错误: {e}")
1094 | event.ignore()
1095 |
1096 | def is_startup_enabled(self):
1097 | """
1098 | 检查是否设置了开机自启
1099 | """
1100 | task_name = "MyTransparentAppTask"
1101 | try:
1102 | result = subprocess.run(
1103 | ["schtasks", "/Query", "/TN", task_name],
1104 | stdout=subprocess.PIPE,
1105 | stderr=subprocess.PIPE,
1106 | creationflags=0x08000000,
1107 | text=True,
1108 | encoding='utf-8',
1109 | errors='ignore',
1110 | timeout=1 # 设置超时时间为1秒
1111 | )
1112 | return result.returncode == 0
1113 | except subprocess.TimeoutExpired:
1114 | print("查询开机自启状态超时")
1115 | return False
1116 | except Exception as e:
1117 | print(f"Error in is_startup_enabled: {e}")
1118 | return False
1119 |
1120 |
1121 | def enable_startup(self):
1122 | """
1123 | 设置开机自启
1124 | """
1125 | try:
1126 | # exe_path = sys.executable if getattr(sys, 'frozen', False) else os.path.abspath(sys.argv[0])
1127 | exe_path = sys.argv[0]
1128 | task_name = "MyTransparentAppTask"
1129 |
1130 | # 创建任务计划程序任务
1131 | subprocess.run([
1132 | "schtasks",
1133 | "/Create",
1134 | "/TN", task_name,
1135 | "/TR", f'"{exe_path}"',
1136 | "/SC", "ONLOGON",
1137 | "/RL", "HIGHEST",
1138 | "/F" # 强制创建,覆盖同名任务
1139 | ],
1140 | check=True,
1141 | creationflags=0x08000000)
1142 |
1143 | QtWidgets.QMessageBox.information(self, "开机自启", "已启用开机自启。")
1144 | except subprocess.CalledProcessError as e:
1145 | QtWidgets.QMessageBox.warning(self, "开机自启", f"设置开机自启失败: {e.stderr}")
1146 | except FileNotFoundError as e:
1147 | QtWidgets.QMessageBox.warning(self, "开机自启", f"schtasks 未找到: {e}")
1148 |
1149 | def disable_startup(self):
1150 | """
1151 | 取消开机自启
1152 | """
1153 | try:
1154 | task_name = "MyTransparentAppTask"
1155 |
1156 | # 删除任务计划程序任务
1157 | subprocess.run([
1158 | "schtasks",
1159 | "/Delete",
1160 | "/TN", task_name,
1161 | "/F" # 强制删除,不提示
1162 | ], check=True,creationflags=0x08000000)
1163 |
1164 | QtWidgets.QMessageBox.information(self, "开机自启", "已取消开机自启。")
1165 | except subprocess.CalledProcessError as e:
1166 | QtWidgets.QMessageBox.warning(self, "开机自启", f"取消开机自启失败: {e.stderr}")
1167 | except FileNotFoundError as e:
1168 | QtWidgets.QMessageBox.warning(self, "开机自启", f"schtasks 未找到: {e}")
1169 |
1170 | def main():
1171 | global shared_memory
1172 | # 防止重复启动
1173 | shared_memory = QSharedMemory("MyTransparentAppUniqueKey")
1174 | if not shared_memory.create(1):
1175 | # 共享内存已存在,说明已有实例在运行
1176 | app = QtWidgets.QApplication(sys.argv)
1177 | QtWidgets.QMessageBox.warning(None, "程序已在运行", "程序已经在运行。")
1178 | sys.exit()
1179 |
1180 | try:
1181 | if not is_admin():
1182 | if run_as_admin(sys.argv):
1183 | sys.exit()
1184 | else:
1185 | QtWidgets.QMessageBox.warning(None, "管理员权限", "需要管理员权限才能运行此程序。")
1186 | sys.exit()
1187 |
1188 | app = QtWidgets.QApplication(sys.argv)
1189 | window = TransparentWindow()
1190 | window.show()
1191 | sys.exit(app.exec())
1192 | except Exception as e:
1193 | QtWidgets.QMessageBox.critical(None, "程序错误", f"程序发生未处理的错误: {e}")
1194 | sys.exit(1)
1195 |
1196 |
1197 | # 初始化 last_unlock_time
1198 | last_unlock_time = get_last_unlock_time()
1199 | last_unlock_time_lock = threading.Lock()
1200 |
1201 | if __name__ == "__main__":
1202 | main()
--------------------------------------------------------------------------------