[Python] 纯文本查看 复制代码
import tkinter as tk
from tkinter import ttk
import ttkbootstrap as ttkb
from ttkbootstrap.constants import *
import psutil
import wmi
import platform
import time
from datetime import datetime
import os
import tempfile
import sys
import webbrowser
# 常量定义
BYTES_TO_GB = 1024 ** 3
CACHE_DURATION = 60 # 缓存1分钟
LOW_SPACE_THRESHOLD = 10 # 低空间阈值(GB)
class SystemMonitorApp:
"""电脑配置助手应用类
功能:
- 显示硬件信息
- 监控系统性能
- 管理Temp目录
- 导出硬件信息
"""
def __init__(self, root: tk.Tk):
"""初始化应用
Args:
root: tkinter根窗口
"""
self.root = root
self.root.title("电脑配置助手")
# 屏幕居中 - 必须在withdraw之前设置
width = 800
height = 600
x = (self.root.winfo_screenwidth() // 2) - (width // 2)
y = (self.root.winfo_screenheight() // 2) - (height // 2)
self.root.geometry(f"{width}x{height}+{x}+{y}")
# 支持窗口大小调整
self.root.resizable(True, True)
# 统一字体和颜色风格
self.fonts = {
'title': ("Arial", 12, "bold"),
'label': ("Arial", 12),
'small': ("Arial", 10)
}
# 初始化缓存
self.temp_space_cache = {}
self.temp_cache_time = 0
# 创建标签页
self.notebook = ttkb.Notebook(root)
self.notebook.pack(fill=BOTH, expand=True, padx=10, pady=10)
# 硬件信息页
self.hardware_frame = ttkb.Frame(self.notebook)
self.notebook.add(self.hardware_frame, text="硬件信息")
self._create_hardware_tab()
# 性能监控页
self.performance_frame = ttkb.Frame(self.notebook)
self.notebook.add(self.performance_frame, text="性能监控")
self._create_performance_tab()
# 延迟初始化WMI和性能监控(在窗口显示后)
self.root.after(100, self._delayed_init)
def _delayed_init(self):
"""延迟初始化
在窗口显示后初始化WMI和性能监控,避免阻塞UI
"""
# 显示窗口(UI创建完成后)
self.root.deiconify()
# 延迟加载硬件信息,让用户看到加载动画
self.root.after(1000, self._load_hardware_info)
def _load_hardware_info(self):
"""加载硬件信息
初始化WMI对象,加载硬件信息,启动性能监控
"""
# 初始化WMI对象(在窗口显示后执行,不阻塞UI)
self.wmi_obj = wmi.WMI()
self.last_disk_io = psutil.disk_io_counters()
self.last_net_io = psutil.net_io_counters()
self.last_time = time.time()
# 加载硬件信息
self.refresh_hardware_info()
# 启动性能监控
self.update_performance()
def _create_hardware_tab(self):
# 创建滚动条
scrollbar = ttk.Scrollbar(self.hardware_frame, orient="vertical")
# 硬件信息表格
self.hardware_tree = ttkb.Treeview(
self.hardware_frame,
columns=("硬件信息", "硬件内容"),
show="headings",
bootstyle="secondary",
yscrollcommand=scrollbar.set
)
# 关联滚动条
scrollbar.config(command=self.hardware_tree.yview)
self.hardware_tree.heading("硬件信息", text="硬件信息")
self.hardware_tree.heading("硬件内容", text="硬件内容")
self.hardware_tree.column("硬件信息", width=200, anchor=W)
self.hardware_tree.column("硬件内容", width=500, anchor=W)
# 添加表格线 - 使用标准ttk样式
style = ttk.Style()
style.configure("Treeview",
rowheight=25,
borderwidth=1,
relief="solid")
style.configure("Treeview.Heading",
borderwidth=1,
relief="solid")
style.map("Treeview",
background=[("selected", "#0078d7")],
foreground=[("selected", "white")])
style.configure("Treeview.Cell",
borderwidth=1)
# 使用grid布局来放置表格和滚动条
self.hardware_tree.grid(row=0, column=0, sticky="nsew", padx=10, pady=10)
scrollbar.grid(row=0, column=1, sticky="ns", pady=10)
# 配置网格列权重
self.hardware_frame.grid_columnconfigure(0, weight=1)
self.hardware_frame.grid_rowconfigure(0, weight=1)
# 加载中提示
self.loading_label = ttkb.Label(
self.hardware_frame,
text="正在加载硬件信息...",
font=self.fonts['label'],
bootstyle="secondary"
)
self.loading_label.grid(row=1, column=0, columnspan=2, pady=10)
# 加载动画
self.loading_progress = ttkb.Progressbar(
self.hardware_frame,
bootstyle="striped",
mode="indeterminate",
maximum=100
)
self.loading_progress.grid(row=2, column=0, columnspan=2, sticky="ew", padx=50, pady=5)
self.loading_progress.start(10)
# 按钮栏
btn_frame = ttkb.Frame(self.hardware_frame)
btn_frame.grid(row=3, column=0, columnspan=2, sticky="ew", padx=10, pady=5)
refresh_btn = ttkb.Button(
btn_frame,
text="刷新",
command=self.refresh_hardware_info,
bootstyle=PRIMARY
)
refresh_btn.pack(side=LEFT, padx=5)
export_btn = ttkb.Button(
btn_frame,
text="导出",
command=self.export_hardware_info,
bootstyle=INFO
)
export_btn.pack(side=LEFT, padx=5)
def _get_hardware_info(self):
info = []
# 系统信息
info.extend(self._get_system_info())
# CPU信息
info.extend(self._get_cpu_info())
# 主板信息
info.extend(self._get_motherboard_info())
# 内存信息
info.extend(self._get_memory_info())
# 显卡信息
info.extend(self._get_gpu_info())
# 硬盘信息
info.extend(self._get_disk_info())
# 分区信息
info.extend(self._get_partition_info())
# 网卡信息
info.extend(self._get_network_info())
# 电池信息
info.extend(self._get_battery_info())
return info
def _get_system_info(self):
"""获取系统信息"""
info = []
info.append(("操作系统", platform.platform()))
info.append(("系统版本", platform.version()))
info.append(("系统架构", platform.architecture()[0]))
return info
def _get_cpu_info(self):
"""获取CPU信息"""
info = []
try:
cpu_info = platform.processor()
cpu_freq = psutil.cpu_freq().current if psutil.cpu_freq() else "N/A"
cpu_cores = psutil.cpu_count(logical=False)
cpu_logical = psutil.cpu_count(logical=True)
info.append(("处理器", f"{cpu_info}"))
info.append(("处理器频率", f"{cpu_freq:.2f}MHz" if isinstance(cpu_freq, (int, float)) else "N/A"))
info.append(("核心数", f"{cpu_cores} 核心 {cpu_logical} 线程"))
except Exception as e:
info.append(("处理器", f"获取失败: {str(e)}"))
return info
def _get_motherboard_info(self):
"""获取主板信息"""
info = []
try:
for board in self.wmi_obj.Win32_BaseBoard():
info.append(("主板型号", board.Product))
info.append(("主板制造商", board.Manufacturer))
info.append(("主板序列号", board.SerialNumber))
except Exception as e:
info.append(("主板信息", f"获取失败: {str(e)}"))
return info
def _get_memory_info(self):
"""获取内存信息"""
info = []
try:
mem_total = round(psutil.virtual_memory().total / (1024**3), 2)
mem_available = round(psutil.virtual_memory().available / (1024**3), 2)
info.append(("内存总容量", f"{mem_total} GB"))
info.append(("可用内存", f"{mem_available} GB"))
info.append(("内存使用率", f"{psutil.virtual_memory().percent}%"))
slot_count = 1
for mem in self.wmi_obj.Win32_PhysicalMemory():
slot = mem.DeviceLocator if mem.DeviceLocator else f"插槽{slot_count}"
capacity = int(mem.Capacity)//(1024**3)
speed = mem.Speed if mem.Speed else "未知"
manufacturer = mem.Manufacturer if mem.Manufacturer else "未知"
info.append((f"内存{slot}", f"{manufacturer} {capacity}GB {speed}MHz"))
slot_count += 1
except Exception as e:
info.append(("内存信息", f"获取失败: {str(e)}"))
return info
def _get_gpu_info(self):
"""获取显卡信息"""
info = []
try:
gpu_count = 1
for gpu in self.wmi_obj.Win32_VideoController():
info.append((f"显卡{gpu_count}", gpu.Name))
if hasattr(gpu, 'AdapterRAM') and gpu.AdapterRAM:
vram = int(gpu.AdapterRAM)/(1024**3)
info.append((f"显存{gpu_count}", f"{vram:.1f}GB"))
gpu_count += 1
except Exception as e:
info.append(("显卡信息", f"获取失败: {str(e)}"))
return info
def _get_disk_info(self):
"""获取硬盘信息"""
info = []
try:
disk_count = 1
for disk in self.wmi_obj.Win32_DiskDrive():
disk_size = round(int(disk.Size)/(1024**3), 2)
info.append((f"硬盘{disk_count}型号", disk.Model))
info.append((f"硬盘{disk_count}容量", f"{disk_size}GB"))
info.append((f"硬盘{disk_count}序列号", disk.SerialNumber.strip()))
info.append((f"硬盘{disk_count}接口", disk.InterfaceType))
disk_count += 1
except Exception as e:
info.append(("硬盘信息", f"获取失败: {str(e)}"))
return info
def _get_partition_info(self):
"""获取分区信息"""
info = []
try:
for part in psutil.disk_partitions():
if 'cdrom' not in part.opts and part.fstype != '':
try:
usage = psutil.disk_usage(part.mountpoint)
total_gb = round(usage.total/(1024**3), 2)
used_gb = round(usage.used/(1024**3), 2)
used_percent = usage.percent
info.append((
f"分区 {part.mountpoint}",
f"{total_gb}GB 总容量,{used_gb}GB 已用 ({used_percent}%)"
))
except Exception as e:
info.append((f"分区 {part.mountpoint}", f"获取失败: {str(e)}"))
except Exception as e:
info.append(("分区信息", f"获取失败: {str(e)}"))
return info
def _get_network_info(self):
"""获取网卡信息"""
info = []
try:
nic_count = 1
for nic in self.wmi_obj.Win32_NetworkAdapterConfiguration():
if nic.IPEnabled:
info.append((f"网卡{nic_count}", nic.Description))
info.append((f"MAC地址{nic_count}", nic.MACAddress))
if nic.IPAddress:
info.append((f"IP地址{nic_count}", nic.IPAddress[0]))
nic_count += 1
except Exception as e:
info.append(("网卡信息", f"获取失败: {str(e)}"))
return info
def _get_battery_info(self):
"""获取电池信息"""
info = []
try:
battery = psutil.sensors_battery()
if battery:
info.append(("电池电量", f"{battery.percent}%"))
info.append(("电池状态", "充电中" if battery.power_plugged else "放电中"))
except Exception as e:
# 笔记本才会有电池,所以这里的异常可以忽略
pass
return info
def refresh_hardware_info(self):
# 清空表格
for item in self.hardware_tree.get_children():
self.hardware_tree.delete(item)
# 分批插入新信息
hardware_info = self._get_hardware_info()
def insert_batch(start=0, batch_size=10):
end = min(start + batch_size, len(hardware_info))
for i in range(start, end):
component, detail = hardware_info[i]
self.hardware_tree.insert("", END, values=(component, detail))
if end < len(hardware_info):
self.root.after(50, insert_batch, end, batch_size)
else:
# 隐藏加载动画
if hasattr(self, 'loading_label'):
self.loading_label.grid_forget()
if hasattr(self, 'loading_progress'):
self.loading_progress.stop()
self.loading_progress.grid_forget()
insert_batch()
def export_hardware_info(self):
"""导出硬件信息到文本文件
将硬件信息导出到程序所在目录,支持开发环境和打包环境
"""
hardware_info = self._get_hardware_info()
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
# 确保导出到主程序所在目录
# 处理开发环境和打包环境
if getattr(sys, 'frozen', False):
# 打包环境
program_dir = os.path.dirname(sys.executable)
else:
# 开发环境
program_dir = os.path.dirname(os.path.abspath(__file__))
filename = os.path.join(program_dir, f"硬件信息_{timestamp}.txt")
try:
with open(filename, "w", encoding="utf-8") as f:
f.write("电脑硬件信息\n")
f.write("="*30 + "\n")
for component, detail in hardware_info:
f.write(f"{component}: {detail}\n")
ttkb.dialogs.Messagebox.show_info("导出成功", f"硬件信息已导出到:\n{filename}")
except Exception as e:
ttkb.dialogs.Messagebox.show_error("导出失败", f"导出硬件信息时出错:\n{str(e)}")
def _create_performance_tab(self):
# CPU使用率
cpu_label = ttkb.Label(self.performance_frame, text="CPU使用率", font=self.fonts['label'])
cpu_label.grid(row=0, column=0, padx=10, pady=5, sticky=W)
self.cpu_value_label = ttkb.Label(self.performance_frame, text="0%", font=self.fonts['label'])
self.cpu_value_label.grid(row=0, column=1, padx=10, pady=5, sticky=W)
self.cpu_progress = ttkb.Progressbar(
self.performance_frame,
bootstyle=PRIMARY,
maximum=100
)
self.cpu_progress.grid(row=0, column=2, padx=10, pady=5, sticky=EW)
# 内存使用率
mem_label = ttkb.Label(self.performance_frame, text="内存使用率", font=self.fonts['label'])
mem_label.grid(row=1, column=0, padx=10, pady=5, sticky=W)
self.mem_value_label = ttkb.Label(self.performance_frame, text="0%", font=self.fonts['label'])
self.mem_value_label.grid(row=1, column=1, padx=10, pady=5, sticky=W)
self.mem_progress = ttkb.Progressbar(
self.performance_frame,
bootstyle=SUCCESS,
maximum=100
)
self.mem_progress.grid(row=1, column=2, padx=10, pady=5, sticky=EW)
# 磁盘读取
disk_read_label = ttkb.Label(self.performance_frame, text="磁盘读取", font=self.fonts['label'])
disk_read_label.grid(row=2, column=0, padx=10, pady=5, sticky=W)
self.disk_read_value_label = ttkb.Label(self.performance_frame, text="0 KB/s", font=self.fonts['label'])
self.disk_read_value_label.grid(row=2, column=1, padx=10, pady=5, sticky=W)
# 磁盘写入
disk_write_label = ttkb.Label(self.performance_frame, text="磁盘写入", font=self.fonts['label'])
disk_write_label.grid(row=3, column=0, padx=10, pady=5, sticky=W)
self.disk_write_value_label = ttkb.Label(self.performance_frame, text="0 KB/s", font=self.fonts['label'])
self.disk_write_value_label.grid(row=3, column=1, padx=10, pady=5, sticky=W)
# 网络上传速度
net_upload_label = ttkb.Label(self.performance_frame, text="网络上传", font=self.fonts['label'])
net_upload_label.grid(row=4, column=0, padx=10, pady=5, sticky=W)
self.net_upload_label = ttkb.Label(self.performance_frame, text="0 KB/s", font=self.fonts['label'])
self.net_upload_label.grid(row=4, column=1, padx=10, pady=5, sticky=W)
# 网络下载速度
net_download_label = ttkb.Label(self.performance_frame, text="网络下载", font=self.fonts['label'])
net_download_label.grid(row=5, column=0, padx=10, pady=5, sticky=W)
self.net_download_label = ttkb.Label(self.performance_frame, text="0 KB/s", font=self.fonts['label'])
self.net_download_label.grid(row=5, column=1, padx=10, pady=5, sticky=W)
# Temp目录空间
temp_label = ttkb.Label(self.performance_frame, text="Temp目录", font=self.fonts['label'])
temp_label.grid(row=6, column=0, padx=10, pady=5, sticky=W)
self.temp_value_label = ttkb.Label(self.performance_frame, text="计算中...", font=self.fonts['label'])
self.temp_value_label.grid(row=6, column=1, padx=10, pady=5, sticky=W)
# 打开Temp目录按钮
open_temp_btn = ttkb.Button(
self.performance_frame,
text="打开Temp",
command=self.open_temp_directory,
bootstyle=INFO
)
open_temp_btn.grid(row=6, column=2, padx=10, pady=5, sticky=W)
# 硬盘容量监控
disk_frame = ttkb.LabelFrame(self.performance_frame, text="硬盘容量监控")
disk_frame.grid(row=7, column=0, columnspan=3, padx=10, pady=10, sticky=EW)
# 硬盘信息容器
self.disk_info_frame = ttkb.Frame(disk_frame)
self.disk_info_frame.pack(fill=BOTH, expand=True, padx=10, pady=5)
# 网格列权重配置
self.performance_frame.grid_columnconfigure(1, weight=1)
self.performance_frame.grid_columnconfigure(2, weight=2)
def update_performance(self):
# CPU使用率
cpu_percent = psutil.cpu_percent(interval=0)
self.cpu_value_label.config(text=f"{cpu_percent}%")
self.cpu_progress['value'] = cpu_percent
# 内存使用率
mem = psutil.virtual_memory()
mem_percent = mem.percent
self.mem_value_label.config(text=f"{mem_percent}%")
self.mem_progress['value'] = mem_percent
# 磁盘读写速度
current_disk_io = psutil.disk_io_counters()
current_time = time.time()
time_delta = current_time - self.last_time
if time_delta > 0:
read_speed = (current_disk_io.read_bytes - self.last_disk_io.read_bytes) / time_delta / 1024
write_speed = (current_disk_io.write_bytes - self.last_disk_io.write_bytes) / time_delta / 1024
self.disk_read_value_label.config(text=f"{read_speed:.1f} KB/s")
self.disk_write_value_label.config(text=f"{write_speed:.1f} KB/s")
self.last_disk_io = current_disk_io
# 网络速度
current_net_io = psutil.net_io_counters()
if time_delta > 0:
upload_speed = (current_net_io.bytes_sent - self.last_net_io.bytes_sent) / time_delta / 1024
download_speed = (current_net_io.bytes_recv - self.last_net_io.bytes_recv) / time_delta / 1024
self.net_upload_label.config(text=f"{upload_speed:.1f} KB/s")
self.net_download_label.config(text=f"{download_speed:.1f} KB/s")
self.last_net_io = current_net_io
# Temp目录空间
temp_space = self.get_temp_directory_space()
self.temp_value_label.config(text=temp_space)
# 更新硬盘信息
self.update_disk_info()
self.last_time = current_time
# 1秒后刷新
self.root.after(1000, self.update_performance)
def get_temp_directory_space(self) -> str:
"""获取临时目录空间信息
Returns:
str: 临时目录大小,格式为"X.XXGB"
"""
temp_dir = tempfile.gettempdir()
current_time = time.time()
# 检查缓存是否有效
if temp_dir in self.temp_space_cache and current_time - self.temp_cache_time < CACHE_DURATION:
return self.temp_space_cache[temp_dir]
try:
# 计算temp目录的实际大小(使用迭代方式,避免递归栈溢出)
def get_dir_size(path: str) -> int:
"""计算目录大小
Args:
path: 目录路径
Returns:
int: 目录大小(字节)
"""
total = 0
stack = [path]
while stack:
current_path = stack.pop()
try:
for entry in os.scandir(current_path):
if entry.is_file():
try:
total += entry.stat().st_size
except:
pass
elif entry.is_dir():
stack.append(entry.path)
except:
pass
return total
temp_size = get_dir_size(temp_dir)
temp_gb = round(temp_size / BYTES_TO_GB, 2)
result = f"{temp_gb}GB"
# 更新缓存
self.temp_space_cache[temp_dir] = result
self.temp_cache_time = current_time
return result
except Exception as e:
return f"无法获取Temp目录信息: {str(e)}"
def open_temp_directory(self):
"""打开临时目录
使用webbrowser.open打开临时目录,提高安全性
"""
temp_dir = tempfile.gettempdir()
try:
# 使用webbrowser.open打开目录,更加安全
webbrowser.open(f'file://{temp_dir}')
except Exception as e:
# 降级方案
try:
if platform.system() == 'Windows':
os.startfile(temp_dir)
elif platform.system() == 'Darwin':
os.system(f'open "{temp_dir}"')
else:
os.system(f'xdg-open "{temp_dir}"')
except Exception as e2:
ttkb.dialogs.Messagebox.show_error("错误", f"无法打开Temp目录: {str(e2)}")
def update_disk_info(self):
"""更新硬盘容量信息
获取所有磁盘分区信息,更新UI显示,根据剩余空间和是否为系统盘设置不同的样式
"""
# 获取系统盘
system_drive = os.environ.get('SYSTEMDRIVE', 'C:')
# 获取所有磁盘分区
disk_partitions = psutil.disk_partitions()
# 过滤有效分区
valid_partitions = []
for partition in disk_partitions:
if 'cdrom' not in partition.opts and partition.fstype != '':
try:
usage = psutil.disk_usage(partition.mountpoint)
total_gb = round(usage.total / BYTES_TO_GB, 2)
used_gb = round(usage.used / BYTES_TO_GB, 2)
free_gb = round(usage.free / BYTES_TO_GB, 2)
percent = usage.percent
# 确定样式
is_system_disk = partition.mountpoint.startswith(system_drive)
is_low_space = free_gb < LOW_SPACE_THRESHOLD
if is_low_space:
bootstyle = "danger"
elif is_system_disk:
bootstyle = "primary"
else:
bootstyle = "secondary"
valid_partitions.append({
'mountpoint': partition.mountpoint,
'used_gb': used_gb,
'total_gb': total_gb,
'percent': percent,
'bootstyle': bootstyle
})
except Exception as e:
# 记录错误但不中断执行
pass
# 获取现有控件
existing_widgets = self.disk_info_frame.winfo_children()
widget_count = len(existing_widgets) // 2 # 每个分区有两个标签
# 更新或创建控件
for i, partition in enumerate(valid_partitions):
if i * 2 < len(existing_widgets):
# 更新现有控件
disk_label = existing_widgets[i * 2]
disk_info = existing_widgets[i * 2 + 1]
disk_label.config(
text=partition['mountpoint'],
bootstyle=partition['bootstyle']
)
disk_info.config(
text=f"{partition['used_gb']}GB / {partition['total_gb']}GB ({partition['percent']}%)",
bootstyle=partition['bootstyle']
)
else:
# 创建新控件
disk_label = ttkb.Label(
self.disk_info_frame,
text=partition['mountpoint'],
font=self.fonts['small'],
bootstyle=partition['bootstyle']
)
disk_label.grid(row=i, column=0, padx=10, pady=2, sticky=W)
disk_info = ttkb.Label(
self.disk_info_frame,
text=f"{partition['used_gb']}GB / {partition['total_gb']}GB ({partition['percent']}%)",
font=self.fonts['small'],
bootstyle=partition['bootstyle']
)
disk_info.grid(row=i, column=1, padx=10, pady=2, sticky=W)
# 隐藏多余的控件
for i in range(len(valid_partitions), widget_count):
if i * 2 < len(existing_widgets):
existing_widgets[i * 2].grid_forget()
existing_widgets[i * 2 + 1].grid_forget()
# 配置网格列
self.disk_info_frame.grid_columnconfigure(1, weight=1)
if __name__ == "__main__":
# 使用cosmo主题,可替换为flatly、superhero等
root = ttkb.Window(themename="cosmo")
# 立即隐藏窗口,避免闪烁
root.withdraw()
# 设置窗口图标
try:
# 判断是否为打包环境
if getattr(sys, 'frozen', False):
# 打包环境
base_path = sys._MEIPASS
else:
# 开发环境
base_path = os.path.dirname(os.path.abspath(__file__))
icon_path = os.path.join(base_path, "app_ico.ico")
root.iconbitmap(icon_path)
except:
pass
app = SystemMonitorApp(root)
root.mainloop()