Initial commit: EyeOnPower v1.0.0
This commit is contained in:
+17
@@ -0,0 +1,17 @@
|
|||||||
|
# Python
|
||||||
|
__pycache__/
|
||||||
|
*.py[cod]
|
||||||
|
*.egg-info/
|
||||||
|
dist/
|
||||||
|
build/
|
||||||
|
.venv/
|
||||||
|
venv/
|
||||||
|
env/
|
||||||
|
|
||||||
|
# PyInstaller
|
||||||
|
*.spec
|
||||||
|
|
||||||
|
# System files
|
||||||
|
Thumbs.db
|
||||||
|
.DS_Store
|
||||||
|
desktop.ini
|
||||||
Binary file not shown.
+614
@@ -0,0 +1,614 @@
|
|||||||
|
"""
|
||||||
|
EyeOnPower | 电量守望者
|
||||||
|
Windows 电池电量实时监测工具
|
||||||
|
功能:屏幕右下角置顶显示电量百分比、充电状态、剩余续航时间、当前时间日期
|
||||||
|
支持展开/折叠详细信息,支持切换置顶状态
|
||||||
|
"""
|
||||||
|
|
||||||
|
import sys
|
||||||
|
import time
|
||||||
|
import ctypes
|
||||||
|
from ctypes import wintypes
|
||||||
|
from PyQt5.QtWidgets import (QApplication, QMainWindow, QWidget, QVBoxLayout,
|
||||||
|
QLabel, QProgressBar, QGroupBox, QGridLayout,
|
||||||
|
QScrollArea, QPushButton, QHBoxLayout, QFrame)
|
||||||
|
from PyQt5.QtCore import QTimer, Qt
|
||||||
|
from PyQt5.QtGui import QFont
|
||||||
|
|
||||||
|
# 忽略 PyQt5 的 DeprecationWarning
|
||||||
|
import warnings
|
||||||
|
warnings.filterwarnings("ignore", category=DeprecationWarning)
|
||||||
|
|
||||||
|
# 尝试导入 wmi
|
||||||
|
try:
|
||||||
|
import wmi
|
||||||
|
WMI_AVAILABLE = True
|
||||||
|
except ImportError:
|
||||||
|
WMI_AVAILABLE = False
|
||||||
|
|
||||||
|
# ============== WinAPI 结构体 ==============
|
||||||
|
AC_LINE_ONLINE = 1
|
||||||
|
BATTERY_LIFE_UNKNOWN = 0xFFFFFFFF
|
||||||
|
|
||||||
|
class SYSTEM_POWER_STATUS(ctypes.Structure):
|
||||||
|
_fields_ = [
|
||||||
|
('ACLineStatus', ctypes.c_byte),
|
||||||
|
('BatteryFlag', ctypes.c_byte),
|
||||||
|
('BatteryLifePercent', ctypes.c_byte),
|
||||||
|
('Reserved1', ctypes.c_byte),
|
||||||
|
('BatteryLifeTime', wintypes.DWORD),
|
||||||
|
('BatteryFullLifeTime', wintypes.DWORD),
|
||||||
|
]
|
||||||
|
|
||||||
|
def get_power_status():
|
||||||
|
"""调用 GetSystemPowerStatus API(最稳定)"""
|
||||||
|
try:
|
||||||
|
kernel32 = ctypes.windll.kernel32
|
||||||
|
status = SYSTEM_POWER_STATUS()
|
||||||
|
if kernel32.GetSystemPowerStatus(ctypes.byref(status)):
|
||||||
|
return status
|
||||||
|
except Exception as e:
|
||||||
|
print(f"GetSystemPowerStatus 错误: {e}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
def safe_int(value, default=0):
|
||||||
|
"""安全地转换为整数"""
|
||||||
|
try:
|
||||||
|
if value is None:
|
||||||
|
return default
|
||||||
|
if isinstance(value, (int, float)):
|
||||||
|
return int(value)
|
||||||
|
if isinstance(value, str):
|
||||||
|
if value.strip().isdigit():
|
||||||
|
return int(value)
|
||||||
|
return default
|
||||||
|
return default
|
||||||
|
except:
|
||||||
|
return default
|
||||||
|
|
||||||
|
def safe_str(value, default="未知"):
|
||||||
|
"""安全地转换为字符串"""
|
||||||
|
try:
|
||||||
|
if value is None:
|
||||||
|
return default
|
||||||
|
if isinstance(value, str) and value.strip():
|
||||||
|
return value.strip()
|
||||||
|
if value:
|
||||||
|
return str(value)
|
||||||
|
return default
|
||||||
|
except:
|
||||||
|
return default
|
||||||
|
|
||||||
|
def format_remaining_time(seconds):
|
||||||
|
"""格式化剩余时间 - 过大则显示未知"""
|
||||||
|
try:
|
||||||
|
secs = safe_int(seconds, 0)
|
||||||
|
if secs <= 0 or secs == BATTERY_LIFE_UNKNOWN or secs > 864000:
|
||||||
|
return "未知"
|
||||||
|
|
||||||
|
hours = secs // 3600
|
||||||
|
minutes = (secs % 3600) // 60
|
||||||
|
if hours > 0:
|
||||||
|
return f"{hours}小时 {minutes}分钟"
|
||||||
|
else:
|
||||||
|
return f"{minutes}分钟"
|
||||||
|
except:
|
||||||
|
return "未知"
|
||||||
|
|
||||||
|
def get_current_datetime():
|
||||||
|
"""获取当前日期时间字符串"""
|
||||||
|
now = time.localtime()
|
||||||
|
weekdays = ["周一", "周二", "周三", "周四", "周五", "周六", "周日"]
|
||||||
|
return f"{now.tm_year}-{now.tm_mon:02d}-{now.tm_mday:02d} {weekdays[now.tm_wday]} {now.tm_hour:02d}:{now.tm_min:02d}:{now.tm_sec:02d}"
|
||||||
|
|
||||||
|
class EyeOnPower(QMainWindow):
|
||||||
|
"""电量守望者主窗口"""
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
super().__init__()
|
||||||
|
self.battery_info = {}
|
||||||
|
self.wmi_error = None
|
||||||
|
self.detailed_visible = False # 详细信息是否可见
|
||||||
|
self.is_topmost = True # 默认置顶状态
|
||||||
|
|
||||||
|
# 获取 WMI 信息
|
||||||
|
if WMI_AVAILABLE:
|
||||||
|
try:
|
||||||
|
wmi_conn = wmi.WMI()
|
||||||
|
self.get_wmi_battery_info(wmi_conn)
|
||||||
|
except Exception as e:
|
||||||
|
self.wmi_error = f"WMI 初始化失败: {str(e)}"
|
||||||
|
print(self.wmi_error)
|
||||||
|
else:
|
||||||
|
self.wmi_error = "wmi 模块未安装 (pip install wmi)"
|
||||||
|
|
||||||
|
self.init_ui()
|
||||||
|
|
||||||
|
# 设置定时器,每秒更新一次
|
||||||
|
self.timer = QTimer()
|
||||||
|
self.timer.timeout.connect(self.update_battery_info)
|
||||||
|
self.timer.start(1000)
|
||||||
|
self.update_battery_info()
|
||||||
|
|
||||||
|
def get_wmi_battery_info(self, wmi_conn):
|
||||||
|
"""通过 WMI 获取电池详细信息"""
|
||||||
|
try:
|
||||||
|
batteries = wmi_conn.Win32_Battery()
|
||||||
|
for battery in batteries:
|
||||||
|
# 电池状态
|
||||||
|
self.battery_info['battery_status'] = safe_int(getattr(battery, 'BatteryStatus', None), 0)
|
||||||
|
|
||||||
|
# 化学类型
|
||||||
|
chem = getattr(battery, 'Chemistry', None)
|
||||||
|
chem_int = safe_int(chem, 0)
|
||||||
|
chemistry_map = {1: "其他", 2: "未知", 3: "铅酸", 4: "镍镉", 5: "镍氢",
|
||||||
|
6: "锂离子", 7: "锌空气", 8: "锂聚合物"}
|
||||||
|
self.battery_info['chemistry'] = chemistry_map.get(chem_int, f"类型{chem_int}" if chem_int > 0 else "未知")
|
||||||
|
|
||||||
|
# 容量信息
|
||||||
|
self.battery_info['design_capacity'] = safe_int(getattr(battery, 'DesignCapacity', 0), 0)
|
||||||
|
self.battery_info['full_capacity'] = safe_int(getattr(battery, 'FullChargeCapacity', 0), 0)
|
||||||
|
self.battery_info['design_voltage'] = safe_int(getattr(battery, 'DesignVoltage', 0), 0)
|
||||||
|
|
||||||
|
# 预估信息
|
||||||
|
est_charge = getattr(battery, 'EstimatedChargeRemaining', None)
|
||||||
|
self.battery_info['estimated_charge'] = safe_int(est_charge, None) if est_charge is not None else None
|
||||||
|
|
||||||
|
est_time = getattr(battery, 'EstimatedRunTime', None)
|
||||||
|
self.battery_info['estimated_run_time'] = safe_int(est_time, None) if est_time is not None else None
|
||||||
|
|
||||||
|
time_to_full = getattr(battery, 'TimeToFullCharge', None)
|
||||||
|
self.battery_info['time_to_full'] = safe_int(time_to_full, None) if time_to_full is not None else None
|
||||||
|
|
||||||
|
# 基本信息
|
||||||
|
self.battery_info['name'] = safe_str(getattr(battery, 'Name', None), "未知")
|
||||||
|
self.battery_info['manufacturer'] = safe_str(getattr(battery, 'Manufacturer', None), "未知")
|
||||||
|
self.battery_info['serial'] = safe_str(getattr(battery, 'SerialNumber', None), "未知")
|
||||||
|
self.battery_info['device_id'] = safe_str(getattr(battery, 'DeviceID', None), "未知")
|
||||||
|
self.battery_info['smart_version'] = safe_str(getattr(battery, 'SmartBatteryVersion', None), "未知")
|
||||||
|
|
||||||
|
break
|
||||||
|
|
||||||
|
if not self.battery_info:
|
||||||
|
self.wmi_error = "未检测到电池"
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
self.wmi_error = f"WMI 获取失败: {str(e)}"
|
||||||
|
print(self.wmi_error)
|
||||||
|
|
||||||
|
def toggle_topmost(self):
|
||||||
|
"""切换窗口置顶状态"""
|
||||||
|
if self.is_topmost:
|
||||||
|
# 取消置顶:从窗口标志中移除置顶属性
|
||||||
|
self.setWindowFlags(self.windowFlags() & ~Qt.WindowStaysOnTopHint)
|
||||||
|
self.topmost_btn.setText("📌 置顶 (关)")
|
||||||
|
self.topmost_btn.setStyleSheet("""
|
||||||
|
QPushButton {
|
||||||
|
padding: 6px 10px;
|
||||||
|
background-color: #95a5a6;
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
QPushButton:hover {
|
||||||
|
background-color: #7f8c8d;
|
||||||
|
}
|
||||||
|
""")
|
||||||
|
self.is_topmost = False
|
||||||
|
else:
|
||||||
|
# 设置置顶:添加置顶属性
|
||||||
|
self.setWindowFlags(self.windowFlags() | Qt.WindowStaysOnTopHint)
|
||||||
|
self.topmost_btn.setText("📌 置顶 (开)")
|
||||||
|
self.topmost_btn.setStyleSheet("""
|
||||||
|
QPushButton {
|
||||||
|
padding: 6px 10px;
|
||||||
|
background-color: #27ae60;
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
QPushButton:hover {
|
||||||
|
background-color: #2ecc71;
|
||||||
|
}
|
||||||
|
""")
|
||||||
|
self.is_topmost = True
|
||||||
|
|
||||||
|
# 关键:修改窗口标志后必须重新 show() 才会生效
|
||||||
|
self.show()
|
||||||
|
|
||||||
|
def init_ui(self):
|
||||||
|
"""初始化用户界面"""
|
||||||
|
self.setWindowTitle("EyeOnPower - 电量守望者")
|
||||||
|
|
||||||
|
# 初始小窗口大小
|
||||||
|
self.setFixedSize(340, 270)
|
||||||
|
|
||||||
|
# 默认设置窗口置顶标志
|
||||||
|
self.setWindowFlags(self.windowFlags() | Qt.WindowStaysOnTopHint)
|
||||||
|
|
||||||
|
# 获取屏幕尺寸,计算右下角位置
|
||||||
|
screen = QApplication.primaryScreen().geometry()
|
||||||
|
x = screen.width() - self.width() - 20 # 距离右边20像素
|
||||||
|
y = screen.height() - self.height() - 50 # 距离底部50像素(避开任务栏)
|
||||||
|
self.move(x, y)
|
||||||
|
|
||||||
|
# 创建中央窗口部件
|
||||||
|
central = QWidget()
|
||||||
|
central.setStyleSheet("background: #ecf0f1;")
|
||||||
|
self.setCentralWidget(central)
|
||||||
|
|
||||||
|
# 主布局
|
||||||
|
self.main_layout = QVBoxLayout(central)
|
||||||
|
self.main_layout.setSpacing(8)
|
||||||
|
self.main_layout.setContentsMargins(10, 10, 10, 10)
|
||||||
|
|
||||||
|
# ========== 标题栏(包含标题按钮和置顶按钮) ==========
|
||||||
|
title_layout = QHBoxLayout()
|
||||||
|
|
||||||
|
# 标题按钮(用于展开/折叠详细信息)
|
||||||
|
self.title_button = QPushButton("👁️ 电量守望者 ▼")
|
||||||
|
self.title_button.setFont(QFont("Microsoft YaHei", 10, QFont.Bold))
|
||||||
|
self.title_button.setStyleSheet("""
|
||||||
|
QPushButton {
|
||||||
|
text-align: left;
|
||||||
|
padding: 5px;
|
||||||
|
background-color: #34495e;
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
QPushButton:hover {
|
||||||
|
background-color: #2c3e50;
|
||||||
|
}
|
||||||
|
""")
|
||||||
|
self.title_button.clicked.connect(self.toggle_detailed_info)
|
||||||
|
title_layout.addWidget(self.title_button)
|
||||||
|
|
||||||
|
# 置顶切换按钮
|
||||||
|
self.topmost_btn = QPushButton("📌 置顶 (开)")
|
||||||
|
self.topmost_btn.setFont(QFont("Microsoft YaHei", 8))
|
||||||
|
self.topmost_btn.setStyleSheet("""
|
||||||
|
QPushButton {
|
||||||
|
padding: 5px 8px;
|
||||||
|
background-color: #27ae60;
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
QPushButton:hover {
|
||||||
|
background-color: #2ecc71;
|
||||||
|
}
|
||||||
|
""")
|
||||||
|
self.topmost_btn.clicked.connect(self.toggle_topmost)
|
||||||
|
title_layout.addWidget(self.topmost_btn)
|
||||||
|
|
||||||
|
self.main_layout.addLayout(title_layout)
|
||||||
|
|
||||||
|
# ========== 日期时间区域 ==========
|
||||||
|
datetime_frame = QFrame()
|
||||||
|
datetime_frame.setStyleSheet("""
|
||||||
|
QFrame {
|
||||||
|
background-color: #2c3e50;
|
||||||
|
border-radius: 4px;
|
||||||
|
padding: 5px;
|
||||||
|
}
|
||||||
|
""")
|
||||||
|
datetime_layout = QHBoxLayout(datetime_frame)
|
||||||
|
datetime_layout.setContentsMargins(8, 4, 8, 4)
|
||||||
|
|
||||||
|
self.datetime_label = QLabel(get_current_datetime())
|
||||||
|
self.datetime_label.setFont(QFont("Microsoft YaHei", 9))
|
||||||
|
self.datetime_label.setStyleSheet("color: #ecf0f1;")
|
||||||
|
self.datetime_label.setAlignment(Qt.AlignCenter)
|
||||||
|
datetime_layout.addWidget(self.datetime_label)
|
||||||
|
|
||||||
|
self.main_layout.addWidget(datetime_frame)
|
||||||
|
|
||||||
|
# ========== 关键信息区域(始终显示) ==========
|
||||||
|
key_group = QGroupBox("📊 电量实时监控")
|
||||||
|
key_group.setStyleSheet("""
|
||||||
|
QGroupBox {
|
||||||
|
font-weight: bold;
|
||||||
|
border: 2px solid #3498db;
|
||||||
|
border-radius: 6px;
|
||||||
|
margin-top: 8px;
|
||||||
|
font-size: 11px;
|
||||||
|
}
|
||||||
|
QGroupBox::title {
|
||||||
|
subcontrol-origin: margin;
|
||||||
|
left: 10px;
|
||||||
|
padding: 0 5px;
|
||||||
|
color: #3498db;
|
||||||
|
}
|
||||||
|
""")
|
||||||
|
key_layout = QGridLayout()
|
||||||
|
key_layout.setSpacing(6)
|
||||||
|
|
||||||
|
# 电量百分比(大字体)
|
||||||
|
self.percent_label = QLabel("--%")
|
||||||
|
self.percent_label.setFont(QFont("Arial", 32, QFont.Bold))
|
||||||
|
self.percent_label.setStyleSheet("color: #2ecc71;")
|
||||||
|
key_layout.addWidget(self.percent_label, 0, 0, 1, 2)
|
||||||
|
|
||||||
|
# 进度条
|
||||||
|
self.progress = QProgressBar()
|
||||||
|
self.progress.setRange(0, 100)
|
||||||
|
self.progress.setFixedHeight(18)
|
||||||
|
key_layout.addWidget(self.progress, 1, 0, 1, 2)
|
||||||
|
|
||||||
|
# 电源状态
|
||||||
|
key_layout.addWidget(QLabel("电源状态:"), 2, 0)
|
||||||
|
self.power_status = QLabel("--")
|
||||||
|
self.power_status.setStyleSheet("color: #3498db; font-weight: bold;")
|
||||||
|
key_layout.addWidget(self.power_status, 2, 1)
|
||||||
|
|
||||||
|
# 剩余时间
|
||||||
|
key_layout.addWidget(QLabel("剩余时间:"), 3, 0)
|
||||||
|
self.remaining_time = QLabel("--")
|
||||||
|
key_layout.addWidget(self.remaining_time, 3, 1)
|
||||||
|
|
||||||
|
key_group.setLayout(key_layout)
|
||||||
|
self.main_layout.addWidget(key_group)
|
||||||
|
|
||||||
|
# ========== 详细信息区域(初始隐藏) ==========
|
||||||
|
self.detailed_group = QGroupBox("🔧 电池详细信息")
|
||||||
|
self.detailed_group.setVisible(False)
|
||||||
|
self.detailed_group.setStyleSheet("""
|
||||||
|
QGroupBox {
|
||||||
|
font-weight: bold;
|
||||||
|
border: 2px solid #27ae60;
|
||||||
|
border-radius: 6px;
|
||||||
|
margin-top: 8px;
|
||||||
|
background-color: #f8f9fa;
|
||||||
|
font-size: 10px;
|
||||||
|
}
|
||||||
|
QGroupBox::title {
|
||||||
|
subcontrol-origin: margin;
|
||||||
|
left: 10px;
|
||||||
|
padding: 0 5px;
|
||||||
|
color: #27ae60;
|
||||||
|
}
|
||||||
|
QLabel {
|
||||||
|
padding: 2px;
|
||||||
|
font-size: 10px;
|
||||||
|
}
|
||||||
|
""")
|
||||||
|
|
||||||
|
# 使用 QScrollArea 使详细信息可滚动
|
||||||
|
detail_scroll = QScrollArea()
|
||||||
|
detail_scroll.setWidgetResizable(True)
|
||||||
|
detail_scroll.setStyleSheet("QScrollArea { border: none; background: transparent; }")
|
||||||
|
|
||||||
|
detail_content = QWidget()
|
||||||
|
detail_layout = QGridLayout(detail_content)
|
||||||
|
detail_layout.setSpacing(4)
|
||||||
|
detail_layout.setContentsMargins(6, 6, 6, 6)
|
||||||
|
|
||||||
|
# 添加所有详细信息
|
||||||
|
row = 0
|
||||||
|
|
||||||
|
# 制造商
|
||||||
|
detail_layout.addWidget(QLabel("制造商:"), row, 0)
|
||||||
|
self.detail_manufacturer = QLabel("未知")
|
||||||
|
detail_layout.addWidget(self.detail_manufacturer, row, 1)
|
||||||
|
row += 1
|
||||||
|
|
||||||
|
# 电池名称
|
||||||
|
detail_layout.addWidget(QLabel("电池名称:"), row, 0)
|
||||||
|
self.detail_name = QLabel("未知")
|
||||||
|
detail_layout.addWidget(self.detail_name, row, 1)
|
||||||
|
row += 1
|
||||||
|
|
||||||
|
# 化学类型
|
||||||
|
detail_layout.addWidget(QLabel("化学类型:"), row, 0)
|
||||||
|
self.detail_chemistry = QLabel("未知")
|
||||||
|
detail_layout.addWidget(self.detail_chemistry, row, 1)
|
||||||
|
row += 1
|
||||||
|
|
||||||
|
# 设计容量
|
||||||
|
detail_layout.addWidget(QLabel("设计容量:"), row, 0)
|
||||||
|
self.detail_design_cap = QLabel("未知")
|
||||||
|
detail_layout.addWidget(self.detail_design_cap, row, 1)
|
||||||
|
row += 1
|
||||||
|
|
||||||
|
# 当前满充容量
|
||||||
|
detail_layout.addWidget(QLabel("当前满充容量:"), row, 0)
|
||||||
|
self.detail_full_cap = QLabel("未知")
|
||||||
|
detail_layout.addWidget(self.detail_full_cap, row, 1)
|
||||||
|
row += 1
|
||||||
|
|
||||||
|
# 电池健康度
|
||||||
|
detail_layout.addWidget(QLabel("电池健康度:"), row, 0)
|
||||||
|
self.detail_health = QLabel("未知")
|
||||||
|
self.detail_health.setStyleSheet("font-weight: bold;")
|
||||||
|
detail_layout.addWidget(self.detail_health, row, 1)
|
||||||
|
row += 1
|
||||||
|
|
||||||
|
# 设计电压
|
||||||
|
detail_layout.addWidget(QLabel("设计电压:"), row, 0)
|
||||||
|
self.detail_voltage = QLabel("未知")
|
||||||
|
detail_layout.addWidget(self.detail_voltage, row, 1)
|
||||||
|
row += 1
|
||||||
|
|
||||||
|
# 预估剩余电量
|
||||||
|
detail_layout.addWidget(QLabel("预估剩余电量:"), row, 0)
|
||||||
|
self.detail_estimated = QLabel("未知")
|
||||||
|
detail_layout.addWidget(self.detail_estimated, row, 1)
|
||||||
|
row += 1
|
||||||
|
|
||||||
|
# 序列号
|
||||||
|
detail_layout.addWidget(QLabel("序列号:"), row, 0)
|
||||||
|
self.detail_serial = QLabel("未知")
|
||||||
|
detail_layout.addWidget(self.detail_serial, row, 1)
|
||||||
|
row += 1
|
||||||
|
|
||||||
|
# 设备ID
|
||||||
|
detail_layout.addWidget(QLabel("设备ID:"), row, 0)
|
||||||
|
self.detail_device_id = QLabel("未知")
|
||||||
|
detail_layout.addWidget(self.detail_device_id, row, 1)
|
||||||
|
row += 1
|
||||||
|
|
||||||
|
# 数据来源
|
||||||
|
detail_layout.addWidget(QLabel("数据来源:"), row, 0)
|
||||||
|
self.detail_source = QLabel("Win32 API + WMI")
|
||||||
|
self.detail_source.setStyleSheet("color: #2ecc71; font-weight: bold;")
|
||||||
|
detail_layout.addWidget(self.detail_source, row, 1)
|
||||||
|
row += 1
|
||||||
|
|
||||||
|
# WMI 错误提示
|
||||||
|
if self.wmi_error:
|
||||||
|
error_label = QLabel(f"⚠️ {self.wmi_error}")
|
||||||
|
error_label.setStyleSheet("color: #e74c3c; font-size: 9px;")
|
||||||
|
detail_layout.addWidget(error_label, row, 0, 1, 2)
|
||||||
|
|
||||||
|
detail_content.setLayout(detail_layout)
|
||||||
|
detail_scroll.setWidget(detail_content)
|
||||||
|
|
||||||
|
detail_group_layout = QVBoxLayout()
|
||||||
|
detail_group_layout.addWidget(detail_scroll)
|
||||||
|
self.detailed_group.setLayout(detail_group_layout)
|
||||||
|
|
||||||
|
self.main_layout.addWidget(self.detailed_group)
|
||||||
|
|
||||||
|
# 添加弹性空间
|
||||||
|
self.main_layout.addStretch()
|
||||||
|
|
||||||
|
def toggle_detailed_info(self):
|
||||||
|
"""切换详细信息显示"""
|
||||||
|
self.detailed_visible = not self.detailed_visible
|
||||||
|
self.detailed_group.setVisible(self.detailed_visible)
|
||||||
|
|
||||||
|
# 获取当前屏幕尺寸
|
||||||
|
screen = QApplication.primaryScreen().geometry()
|
||||||
|
|
||||||
|
# 更新按钮文字和窗口大小
|
||||||
|
if self.detailed_visible:
|
||||||
|
self.title_button.setText("👁️ 电量守望者 ▲")
|
||||||
|
self.setFixedSize(380, 550)
|
||||||
|
else:
|
||||||
|
self.title_button.setText("👁️ 电量守望者 ▼")
|
||||||
|
self.setFixedSize(340, 270)
|
||||||
|
|
||||||
|
# 重新计算右下角位置
|
||||||
|
x = screen.width() - self.width() - 20
|
||||||
|
y = screen.height() - self.height() - 50
|
||||||
|
self.move(x, y)
|
||||||
|
|
||||||
|
def update_battery_info(self):
|
||||||
|
"""更新所有电池信息"""
|
||||||
|
try:
|
||||||
|
# 更新时间日期
|
||||||
|
self.datetime_label.setText(get_current_datetime())
|
||||||
|
|
||||||
|
# ===== Win32 API(关键信息) =====
|
||||||
|
status = get_power_status()
|
||||||
|
if status:
|
||||||
|
percent = status.BatteryLifePercent
|
||||||
|
if percent != 255:
|
||||||
|
self.percent_label.setText(f"{percent}%")
|
||||||
|
self.progress.setValue(percent)
|
||||||
|
if percent <= 20:
|
||||||
|
self.progress.setStyleSheet("QProgressBar::chunk { background: #e74c3c; border-radius: 4px; }")
|
||||||
|
elif percent <= 50:
|
||||||
|
self.progress.setStyleSheet("QProgressBar::chunk { background: #f39c12; border-radius: 4px; }")
|
||||||
|
else:
|
||||||
|
self.progress.setStyleSheet("QProgressBar::chunk { background: #2ecc71; border-radius: 4px; }")
|
||||||
|
else:
|
||||||
|
self.percent_label.setText("未知")
|
||||||
|
|
||||||
|
acline = status.ACLineStatus
|
||||||
|
battery_flag = status.BatteryFlag
|
||||||
|
|
||||||
|
if acline == 1:
|
||||||
|
self.power_status.setText("🔌 交流电供电")
|
||||||
|
self.remaining_time.setText("正在充电")
|
||||||
|
else:
|
||||||
|
if battery_flag & 2:
|
||||||
|
self.power_status.setText("⚡ 正在充电")
|
||||||
|
elif battery_flag & 4:
|
||||||
|
self.power_status.setText("✅ 完全充满")
|
||||||
|
elif battery_flag & 8:
|
||||||
|
self.power_status.setText("⚠️ 电量低")
|
||||||
|
else:
|
||||||
|
self.power_status.setText("🔋 使用电池")
|
||||||
|
time_text = format_remaining_time(status.BatteryLifeTime)
|
||||||
|
self.remaining_time.setText(time_text)
|
||||||
|
|
||||||
|
# ===== 更新详细信息(如果可见) =====
|
||||||
|
if self.detailed_visible and self.battery_info and not self.wmi_error:
|
||||||
|
self.detail_manufacturer.setText(self.battery_info.get('manufacturer', '未知'))
|
||||||
|
self.detail_name.setText(self.battery_info.get('name', '未知'))
|
||||||
|
self.detail_chemistry.setText(self.battery_info.get('chemistry', '未知'))
|
||||||
|
|
||||||
|
# 设计容量
|
||||||
|
design = self.battery_info.get('design_capacity', 0)
|
||||||
|
if design and design > 0:
|
||||||
|
self.detail_design_cap.setText(f"{design} mWh ({design/1000:.1f} Wh)")
|
||||||
|
else:
|
||||||
|
self.detail_design_cap.setText("未知")
|
||||||
|
|
||||||
|
# 满充容量
|
||||||
|
full = self.battery_info.get('full_capacity', 0)
|
||||||
|
if full and full > 0:
|
||||||
|
self.detail_full_cap.setText(f"{full} mWh ({full/1000:.1f} Wh)")
|
||||||
|
else:
|
||||||
|
self.detail_full_cap.setText("未知")
|
||||||
|
|
||||||
|
# 电池健康度
|
||||||
|
design_val = self.battery_info.get('design_capacity', 0)
|
||||||
|
full_val = self.battery_info.get('full_capacity', 0)
|
||||||
|
if design_val and design_val > 0 and full_val and full_val > 0:
|
||||||
|
health = (full_val / design_val) * 100
|
||||||
|
if health > 100:
|
||||||
|
health = 100
|
||||||
|
self.detail_health.setText(f"{health:.1f}%")
|
||||||
|
if health >= 80:
|
||||||
|
self.detail_health.setStyleSheet("color: #2ecc71; font-weight: bold;")
|
||||||
|
elif health >= 60:
|
||||||
|
self.detail_health.setStyleSheet("color: #f39c12; font-weight: bold;")
|
||||||
|
else:
|
||||||
|
self.detail_health.setStyleSheet("color: #e74c3c; font-weight: bold;")
|
||||||
|
else:
|
||||||
|
self.detail_health.setText("未知")
|
||||||
|
|
||||||
|
# 设计电压
|
||||||
|
voltage = self.battery_info.get('design_voltage', 0)
|
||||||
|
if voltage and voltage > 0:
|
||||||
|
self.detail_voltage.setText(f"{voltage} mV ({voltage/1000:.2f} V)")
|
||||||
|
else:
|
||||||
|
self.detail_voltage.setText("未知")
|
||||||
|
|
||||||
|
# 预估剩余电量
|
||||||
|
est_charge = self.battery_info.get('estimated_charge')
|
||||||
|
if est_charge is not None and est_charge > 0:
|
||||||
|
self.detail_estimated.setText(f"{est_charge}%")
|
||||||
|
else:
|
||||||
|
self.detail_estimated.setText("未知")
|
||||||
|
|
||||||
|
self.detail_serial.setText(self.battery_info.get('serial', '未知'))
|
||||||
|
self.detail_device_id.setText(self.battery_info.get('device_id', '未知'))
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"更新出错: {e}")
|
||||||
|
|
||||||
|
def closeEvent(self, event):
|
||||||
|
"""关闭事件"""
|
||||||
|
self.timer.stop()
|
||||||
|
event.accept()
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
"""程序入口"""
|
||||||
|
app = QApplication(sys.argv)
|
||||||
|
app.setStyle('Fusion')
|
||||||
|
app.setFont(QFont("Microsoft YaHei", 9))
|
||||||
|
|
||||||
|
window = EyeOnPower()
|
||||||
|
window.show()
|
||||||
|
|
||||||
|
sys.exit(app.exec_())
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2026 DVS (dvs-dvsxt)
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
||||||
@@ -0,0 +1,82 @@
|
|||||||
|
# 🪫 EyeOnPower — Battery Monitor
|
||||||
|
|
||||||
|
> A real-time **Windows battery level monitor** that displays battery percentage, charging status, remaining time, and current date/time in a always-on-top corner widget.
|
||||||
|
|
||||||
|
**EyeOnPower** is a lightweight desktop widget that sits in the bottom-right corner of your screen, showing your battery level at a glance. It supports expanding/collapsing detailed battery info, and toggling window always-on-top.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ✨ Features
|
||||||
|
|
||||||
|
| Feature | Description |
|
||||||
|
|---------|-------------|
|
||||||
|
| 🔋 **Real-time Battery %** | Live battery percentage display with large font & progress bar |
|
||||||
|
| 🔌 **Charging Status** | AC power / charging / fully charged / low battery indicators |
|
||||||
|
| ⏱️ **Remaining Time** | Estimated remaining battery life (hours & minutes) |
|
||||||
|
| 📅 **Date & Time** | Current date, weekday, and time always visible |
|
||||||
|
| 🔍 **Detailed Info** | Expandable battery details (manufacturer, capacity, health, voltage, serial...) |
|
||||||
|
| 📌 **Always-on-Top** | Toggleable window pinning |
|
||||||
|
| 🔗 **Dual Data Source** | Win32 API (`GetSystemPowerStatus`) + WMI (`Win32_Battery`) |
|
||||||
|
|
||||||
|
### Detailed Battery Info (expandable)
|
||||||
|
- Manufacturer / Battery name
|
||||||
|
- Chemistry type (Li-ion, Li-polymer, etc.)
|
||||||
|
- Design capacity & full charge capacity (mWh / Wh)
|
||||||
|
- **Battery health %** (full/design capacity ratio, color-coded)
|
||||||
|
- Design voltage (mV / V)
|
||||||
|
- Estimated charge remaining
|
||||||
|
- Serial & Device ID
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🚀 Quick Start
|
||||||
|
|
||||||
|
### Prerequisites
|
||||||
|
- Windows (laptop / notebook with battery)
|
||||||
|
- Python 3.7+
|
||||||
|
|
||||||
|
### Install & Run from Source
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Install dependencies
|
||||||
|
pip install PyQt5 wmi
|
||||||
|
|
||||||
|
# Run
|
||||||
|
python EyeOnPower.py
|
||||||
|
```
|
||||||
|
|
||||||
|
### Run the Compiled EXE
|
||||||
|
Just double-click `EyeOnPower.exe` — no Python required.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🎮 Usage
|
||||||
|
|
||||||
|
- **Title bar button** (`👁️ 电量守望者`): expand / collapse detailed battery info
|
||||||
|
- **Pin button** (`📌 置顶`): toggle always-on-top
|
||||||
|
- Widget automatically stays in the bottom-right corner
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📦 Project Structure
|
||||||
|
|
||||||
|
```
|
||||||
|
EyeOnPower/
|
||||||
|
├── EyeOnPower.py # Main program (PyQt5 battery monitor)
|
||||||
|
├── EyeOnPower.exe # Compiled executable (no Python needed)
|
||||||
|
└── README.md # This document
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📄 License
|
||||||
|
|
||||||
|
This project is licensed under the **MIT License**. See [LICENSE](LICENSE) for details.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🙏 Note
|
||||||
|
|
||||||
|
> ✓ The widget is **always-on-top** by default, so it stays visible above other windows.
|
||||||
|
> ✓ Low battery warning: progress bar turns **red** at ≤20%, **orange** at ≤50%, green otherwise.
|
||||||
|
> ✓ Battery health is color-coded: **green** ≥80%, **orange** ≥60%, **red** below.
|
||||||
Reference in New Issue
Block a user