|
|
马上注册,结交更多好友,享用更多功能^_^
您需要 登录 才可以下载或查看,没有账号?立即注册
x
本帖最后由 chinajz 于 2026-3-15 10:35 编辑
workbuddy写的硬件监控软件,测试结果,达到预期要求。
- #!/usr/bin/env python3
- # -*- coding: utf-8 -*-
- """
- CPU/GPU 温度监控 — 图形界面 (gui.py)
- 依赖:tkinter(Python 内置)、matplotlib
- 启动方式:
- python main.py --gui
- python gui.py (直接运行)
- """
- import sys
- import time
- import threading
- from datetime import datetime
- from collections import deque
- if sys.platform == "win32":
- try:
- sys.stdout.reconfigure(encoding="utf-8")
- except Exception:
- pass
- try:
- import tkinter as tk
- from tkinter import ttk, font as tkfont
- except ImportError:
- raise ImportError("tkinter 不可用,请确认 Python 安装时包含了 Tk 支持")
- try:
- import matplotlib
- matplotlib.use("TkAgg")
- from matplotlib.figure import Figure
- from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg
- HAS_MPL = True
- except ImportError:
- HAS_MPL = False
- # 导入数据采集模块(与 main.py 同目录)
- import os
- sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
- from main import (
- get_cpu_temperature, get_gpu_temperature,
- get_cpu_info, get_gpu_info,
- CPU_WARN_TEMP, CPU_CRIT_TEMP,
- GPU_WARN_TEMP, GPU_CRIT_TEMP,
- HAS_PYNVML, HAS_PSUTIL,
- )
- # ── 颜色主题 ─────────────────────────────────────────────────────────────────
- COLORS = {
- "bg": "#1e1e2e",
- "panel": "#2a2a3e",
- "border": "#44475a",
- "text": "#cdd6f4",
- "subtext": "#6c7086",
- "green": "#a6e3a1",
- "yellow": "#f9e2af",
- "red": "#f38ba8",
- "blue": "#89b4fa",
- "mauve": "#cba6f7",
- "cpu_line": "#89b4fa",
- "gpu_line": "#f38ba8",
- }
- REFRESH_MS = 2000 # 界面刷新间隔(毫秒)
- HISTORY_LEN = 60 # 曲线历史长度
- # ════════════════════════════════════════════════════════════════════════════
- # 数据模型
- # ════════════════════════════════════════════════════════════════════════════
- class HardwareData:
- def __init__(self):
- self.cpu_temp_history = deque([None] * HISTORY_LEN, maxlen=HISTORY_LEN)
- self.gpu_temp_history = deque([None] * HISTORY_LEN, maxlen=HISTORY_LEN)
- self.lock = threading.Lock()
- # 最新一帧数据
- self.cpu_temps = {}
- self.gpu_temps = {}
- self.cpu_info = {}
- self.gpu_list = []
- self.timestamp = ""
- self.fetching = False
- def fetch(self):
- """在后台线程里采集数据"""
- self.fetching = True
- try:
- cpu_temps = get_cpu_temperature()
- gpu_temps = get_gpu_temperature()
- cpu_info = get_cpu_info()
- gpu_list = get_gpu_info()
- ts = datetime.now().strftime("%H:%M:%S")
- # 取物理核心(过滤 Thread 和 Package/Tdie 聚合传感器)的最高温度作为曲线代表值
- _PKG_KW = ("package", "tdie", "tctl", "average", "max", "distance")
- phys_temps = {k: v for k, v in cpu_temps.items() if "Thread" not in k}
- src = phys_temps if phys_temps else cpu_temps
- core_vals = [v for k, v in src.items()
- if not any(kw in k.lower() for kw in _PKG_KW)]
- cpu_peak = max(core_vals) if core_vals else (max(src.values()) if src else None)
- # 第一块 GPU 的温度
- gpu_peak = None
- if gpu_list:
- i = gpu_list[0]["index"]
- name = gpu_list[0]["name"]
- gpu_peak = gpu_temps.get(f"GPU{i} {name}")
- elif gpu_temps:
- gpu_peak = list(gpu_temps.values())[0]
- with self.lock:
- self.cpu_temps = cpu_temps
- self.gpu_temps = gpu_temps
- self.cpu_info = cpu_info
- self.gpu_list = gpu_list
- self.timestamp = ts
- self.cpu_temp_history.append(cpu_peak)
- self.gpu_temp_history.append(gpu_peak)
- except Exception as e:
- print(f"[数据采集异常] {e}")
- finally:
- self.fetching = False
- # ════════════════════════════════════════════════════════════════════════════
- # 主窗口
- # ════════════════════════════════════════════════════════════════════════════
- class MonitorApp(tk.Tk):
- def __init__(self):
- super().__init__()
- self.title("CPU / GPU 硬件监控")
- self.configure(bg=COLORS["bg"])
- self.resizable(True, True)
- self.geometry("900x640")
- self.data = HardwareData()
- self._build_ui()
- self._schedule_refresh()
- # ── UI 构建 ────────────────────────────────────────────────────────────
- def _build_ui(self):
- # 顶部标题栏
- title_frame = tk.Frame(self, bg=COLORS["bg"])
- title_frame.pack(fill="x", padx=16, pady=(12, 4))
- tk.Label(
- title_frame, text="🖥 硬件温度监控",
- bg=COLORS["bg"], fg=COLORS["text"],
- font=("Segoe UI", 16, "bold"),
- ).pack(side="left")
- self.lbl_time = tk.Label(
- title_frame, text="",
- bg=COLORS["bg"], fg=COLORS["subtext"],
- font=("Segoe UI", 10),
- )
- self.lbl_time.pack(side="right")
- # 主体两列布局
- body = tk.Frame(self, bg=COLORS["bg"])
- body.pack(fill="both", expand=True, padx=16, pady=4)
- left = tk.Frame(body, bg=COLORS["bg"])
- right = tk.Frame(body, bg=COLORS["bg"])
- left.pack(side="left", fill="both", expand=True, padx=(0, 8))
- right.pack(side="right", fill="both", expand=True)
- # 左列:CPU 面板 + GPU 面板
- self._cpu_panel = self._make_panel(left, "📊 CPU", expand=True)
- self._gpu_panel = self._make_panel(left, "🎮 GPU", expand=True)
- # 右列:温度曲线图
- if HAS_MPL:
- chart_frame = self._make_panel(right, "📈 温度趋势(近 60 次)", expand=True)
- self._build_chart(chart_frame)
- else:
- warn = self._make_panel(right, "⚠ 提示", expand=True)
- tk.Label(
- warn, text="安装 matplotlib 以显示温度趋势图\npip install matplotlib",
- bg=COLORS["panel"], fg=COLORS["yellow"],
- font=("Segoe UI", 10), justify="left",
- ).pack(padx=12, pady=12, anchor="w")
- # 底部状态栏
- bar = tk.Frame(self, bg=COLORS["border"], height=1)
- bar.pack(fill="x", padx=16)
- self.lbl_status = tk.Label(
- self, text="正在加载…",
- bg=COLORS["bg"], fg=COLORS["subtext"],
- font=("Segoe UI", 9),
- )
- self.lbl_status.pack(anchor="w", padx=16, pady=(2, 8))
- def _make_panel(self, parent, title: str, expand: bool = False) -> tk.Frame:
- """创建带标题的卡片面板,返回内容区 Frame"""
- outer = tk.Frame(parent, bg=COLORS["border"], bd=0)
- outer.pack(fill="both", expand=expand, pady=4)
- inner = tk.Frame(outer, bg=COLORS["panel"], bd=0)
- inner.pack(fill="both", expand=True, padx=1, pady=1)
- tk.Label(
- inner, text=title,
- bg=COLORS["panel"], fg=COLORS["blue"],
- font=("Segoe UI", 11, "bold"),
- ).pack(anchor="w", padx=12, pady=(10, 4))
- sep = tk.Frame(inner, bg=COLORS["border"], height=1)
- sep.pack(fill="x", padx=12)
- content = tk.Frame(inner, bg=COLORS["panel"])
- content.pack(fill="both", expand=True, padx=12, pady=(6, 10))
- return content
- def _build_chart(self, parent: tk.Frame):
- """在 parent 内嵌入 matplotlib 折线图"""
- fig = Figure(figsize=(4.5, 4), dpi=90, facecolor=COLORS["bg"])
- self._ax = fig.add_subplot(111)
- self._ax.set_facecolor(COLORS["panel"])
- fig.tight_layout(pad=1.5)
- self._canvas = FigureCanvasTkAgg(fig, master=parent)
- self._canvas.get_tk_widget().pack(fill="both", expand=True)
- self._fig = fig
- self._line_cpu, = self._ax.plot(
- [], [], color=COLORS["cpu_line"], linewidth=1.8, label="CPU"
- )
- self._line_gpu, = self._ax.plot(
- [], [], color=COLORS["gpu_line"], linewidth=1.8, label="GPU"
- )
- self._ax.axhline(CPU_WARN_TEMP, color=COLORS["yellow"], linewidth=0.8,
- linestyle="--", alpha=0.6, label=f"警告 {CPU_WARN_TEMP}°C")
- self._ax.axhline(CPU_CRIT_TEMP, color=COLORS["red"], linewidth=0.8,
- linestyle="--", alpha=0.6, label=f"危险 {CPU_CRIT_TEMP}°C")
- self._ax.set_ylabel("温度 (°C)", color=COLORS["text"], fontsize=9)
- self._ax.set_xlabel("历史记录", color=COLORS["text"], fontsize=9)
- self._ax.tick_params(colors=COLORS["subtext"], labelsize=8)
- for spine in self._ax.spines.values():
- spine.set_edgecolor(COLORS["border"])
- self._ax.legend(
- facecolor=COLORS["panel"], edgecolor=COLORS["border"],
- labelcolor=COLORS["text"], fontsize=8
- )
- self._ax.set_ylim(0, 110)
- self._ax.yaxis.label.set_color(COLORS["text"])
- self._ax.xaxis.label.set_color(COLORS["text"])
- # ── 数据刷新 ──────────────────────────────────────────────────────────
- def _schedule_refresh(self):
- """开始后台采集 + 定时 UI 刷新"""
- self._do_refresh()
- def _do_refresh(self):
- """启动后台采集,采集完毕后更新 UI"""
- def worker():
- self.data.fetch()
- self.after(0, self._update_ui)
- t = threading.Thread(target=worker, daemon=True)
- t.start()
- def _update_ui(self):
- """用最新数据更新所有 UI 控件"""
- with self.data.lock:
- cpu_temps = dict(self.data.cpu_temps)
- gpu_temps = dict(self.data.gpu_temps)
- cpu_info = dict(self.data.cpu_info)
- gpu_list = list(self.data.gpu_list)
- ts = self.data.timestamp
- cpu_hist = list(self.data.cpu_temp_history)
- gpu_hist = list(self.data.gpu_temp_history)
- self.lbl_time.config(text=ts)
- self._refresh_cpu_panel(cpu_temps, cpu_info)
- self._refresh_gpu_panel(gpu_temps, gpu_list)
- if HAS_MPL:
- self._refresh_chart(cpu_hist, gpu_hist)
- self.lbl_status.config(text=f"上次更新:{ts} | 刷新间隔:{REFRESH_MS // 1000}s")
- # 安排下次刷新
- self.after(REFRESH_MS, self._do_refresh)
- def _refresh_cpu_panel(self, cpu_temps: dict, cpu_info: dict):
- """清空并重绘 CPU 面板"""
- for w in self._cpu_panel.winfo_children():
- w.destroy()
- def row(label, value, val_color=COLORS["text"]):
- f = tk.Frame(self._cpu_panel, bg=COLORS["panel"])
- f.pack(fill="x", pady=1)
- tk.Label(f, text=label, bg=COLORS["panel"], fg=COLORS["subtext"],
- font=("Segoe UI", 9), width=22, anchor="w").pack(side="left")
- tk.Label(f, text=value, bg=COLORS["panel"], fg=val_color,
- font=("Segoe UI", 9, "bold"), anchor="w").pack(side="left")
- if cpu_info:
- physical = cpu_info.get("cpu_count_physical", "?")
- logical = cpu_info.get("cpu_count_logical", "?")
- row("核心数", f"{physical} 物理 / {logical} 逻辑")
- if "freq_current" in cpu_info:
- row("当前频率", f"{cpu_info['freq_current']:.0f} MHz")
- usage = cpu_info.get("usage_total")
- if usage is not None:
- color = _usage_color(usage)
- row("CPU 使用率", f"{usage:.1f}%", color)
- # 各核使用率——按物理核聚合(超线程取两逻辑核中的较大值)
- per_core = cpu_info.get("usage_per_core", [])
- phys_cnt = cpu_info.get("cpu_count_physical") or 0
- logi_cnt = cpu_info.get("cpu_count_logical") or len(per_core)
- if per_core:
- if phys_cnt and logi_cnt == phys_cnt * 2:
- phys_usage = [
- max(per_core[i * 2], per_core[i * 2 + 1])
- for i in range(phys_cnt)
- ]
- else:
- phys_usage = per_core
- cols = 3
- for start in range(0, len(phys_usage), cols):
- chunk = phys_usage[start:start + cols]
- txt = " ".join(
- f"核{start + j + 1}: {v:>5.1f}%" for j, v in enumerate(chunk)
- )
- tk.Label(
- self._cpu_panel, text=txt,
- bg=COLORS["panel"], fg=COLORS["subtext"],
- font=("Consolas", 8),
- ).pack(anchor="w")
- if "mem_used_gb" in cpu_info:
- mem_color = _usage_color(cpu_info["mem_percent"])
- row("内存使用",
- f"{cpu_info['mem_used_gb']:.1f} / {cpu_info['mem_total_gb']:.1f} GB "
- f"({cpu_info['mem_percent']:.1f}%)", mem_color)
- # 温度——最高核心温度 + Package 温度
- if cpu_temps:
- tk.Frame(self._cpu_panel, bg=COLORS["border"], height=1).pack(fill="x", pady=4)
- phys = {k: v for k, v in cpu_temps.items() if "Thread" not in k}
- src = phys if phys else cpu_temps
- # 最高核心温度(排除 Package/Tdie/Tctl 聚合传感器)
- _PKG_KW = ("package", "tdie", "tctl", "average", "max", "distance")
- core_vals = [v for k, v in src.items()
- if not any(kw in k.lower() for kw in _PKG_KW)]
- peak = max(core_vals) if core_vals else max(src.values())
- row("温度(最高核心)", f"{peak:.1f} °C",
- _temp_color(peak, CPU_WARN_TEMP, CPU_CRIT_TEMP))
- # Package 温度
- for k, v in src.items():
- if any(kw in k.lower() for kw in ("package", "tdie", "tctl")):
- row("温度(Package)", f"{v:.1f} °C",
- _temp_color(v, CPU_WARN_TEMP, CPU_CRIT_TEMP))
- break
- else:
- tk.Label(
- self._cpu_panel,
- text="温度不可用(需 LibreHardwareMonitor)",
- bg=COLORS["panel"], fg=COLORS["yellow"],
- font=("Segoe UI", 9),
- ).pack(anchor="w", pady=4)
- def _refresh_gpu_panel(self, gpu_temps: dict, gpu_list: list):
- """清空并重绘 GPU 面板"""
- for w in self._gpu_panel.winfo_children():
- w.destroy()
- def row(label, value, val_color=COLORS["text"]):
- f = tk.Frame(self._gpu_panel, bg=COLORS["panel"])
- f.pack(fill="x", pady=1)
- tk.Label(f, text=label, bg=COLORS["panel"], fg=COLORS["subtext"],
- font=("Segoe UI", 9), width=22, anchor="w").pack(side="left")
- tk.Label(f, text=value, bg=COLORS["panel"], fg=val_color,
- font=("Segoe UI", 9, "bold"), anchor="w").pack(side="left")
- if not gpu_list and not gpu_temps:
- tk.Label(
- self._gpu_panel,
- text="未检测到 GPU(NVIDIA 需安装 nvidia-ml-py3)",
- bg=COLORS["panel"], fg=COLORS["yellow"],
- font=("Segoe UI", 9),
- ).pack(anchor="w", pady=4)
- return
- for gpu in gpu_list:
- i = gpu["index"]
- name = gpu["name"]
- tk.Label(
- self._gpu_panel, text=f"[{i}] {name}",
- bg=COLORS["panel"], fg=COLORS["mauve"],
- font=("Segoe UI", 9, "bold"),
- ).pack(anchor="w", pady=(4, 1))
- temp_key = f"GPU{i} {name}"
- temp = gpu_temps.get(temp_key)
- if temp is not None:
- color = _temp_color(temp, GPU_WARN_TEMP, GPU_CRIT_TEMP)
- row(" 温度", f"{temp:.1f} °C", color)
- if "usage_gpu" in gpu:
- row(" GPU 使用率", f"{gpu['usage_gpu']:.1f}%",
- _usage_color(gpu["usage_gpu"]))
- if "mem_used_gb" in gpu:
- row(" 显存使用",
- f"{gpu['mem_used_gb']:.1f} / {gpu['mem_total_gb']:.1f} GB "
- f"({gpu['mem_percent']:.1f}%)",
- _usage_color(gpu["mem_percent"]))
- if "power_w" in gpu:
- row(" 功耗", f"{gpu['power_w']:.1f} W / {gpu.get('power_limit_w', '?'):.1f} W")
- if "fan_percent" in gpu:
- row(" 风扇转速", f"{gpu['fan_percent']}%")
- if "clock_gpu_mhz" in gpu:
- row(" 核心频率", f"{gpu['clock_gpu_mhz']} MHz")
- # 其他 GPU(WMI)
- known = {f"GPU{g['index']} {g['name']}" for g in gpu_list}
- for name, temp in gpu_temps.items():
- if name not in known:
- color = _temp_color(temp, GPU_WARN_TEMP, GPU_CRIT_TEMP)
- row(f" {_shorten(name, 22)}", f"{temp:.1f} °C", color)
- def _refresh_chart(self, cpu_hist: list, gpu_hist: list):
- """更新 matplotlib 折线图"""
- xs = list(range(HISTORY_LEN))
- cpu_ys = [v if v is not None else float("nan") for v in cpu_hist]
- gpu_ys = [v if v is not None else float("nan") for v in gpu_hist]
- self._line_cpu.set_data(xs, cpu_ys)
- self._line_gpu.set_data(xs, gpu_ys)
- all_vals = [v for v in cpu_ys + gpu_ys if v == v] # filter nan
- if all_vals:
- y_min = max(0, min(all_vals) - 10)
- y_max = min(120, max(all_vals) + 15)
- self._ax.set_ylim(y_min, y_max)
- self._ax.set_xlim(0, HISTORY_LEN - 1)
- self._canvas.draw_idle()
- # ════════════════════════════════════════════════════════════════════════════
- # 辅助函数
- # ════════════════════════════════════════════════════════════════════════════
- def _temp_color(temp: float, warn: float, crit: float) -> str:
- if temp >= crit:
- return COLORS["red"]
- elif temp >= warn:
- return COLORS["yellow"]
- return COLORS["green"]
- def _usage_color(pct: float) -> str:
- if pct >= 90:
- return COLORS["red"]
- elif pct >= 70:
- return COLORS["yellow"]
- return COLORS["green"]
- def _shorten(text: str, max_len: int) -> str:
- return text if len(text) <= max_len else text[:max_len - 1] + "…"
- # ════════════════════════════════════════════════════════════════════════════
- # 入口
- # ════════════════════════════════════════════════════════════════════════════
- def launch_gui():
- app = MonitorApp()
- app.mainloop()
- if __name__ == "__main__":
- launch_gui()
复制代码 |
-
评分
-
| 参与人数 1 | 荣誉 +6 |
鱼币 +6 |
贡献 +6 |
C币 +6 |
收起
理由
|
不二如是
| + 6 |
+ 6 |
+ 6 |
+ 6 |
鱼C有你更精彩^_^ |
查看全部评分
|