前言
最近开发了一个轻量级的 Windows 系统监控工具,可以实时监控 CPU、内存、GPU、温度、网络流量等硬件信息,并通过曲线图表直观展示。整个项目基于 .NET 10.0 和 WinForms 框架,使用了 LibreHardwareMonitor 硬件监控库和 ScottPlot 图表库。
本文分享一下开发过程中的核心技术点和关键代码实现。
技术栈
- .NET 10.0 - 最新的 .NET 平台
- Windows Forms - 桌面 UI 框架
- LibreHardwareMonitor 0.9.3 - 硬件传感器数据采集
- ScottPlot 5.0 - 高性能实时图表绘制
- PerformanceCounter - Windows 性能计数器
核心功能
- 实时监控 CPU 使用率、主频、温度
- 内存使用情况监控
- GPU 使用率和温度监控
- 网络上传/下载速度统计
- 磁盘 I/O 读写速度
- 电池电量和充电状态(笔记本)
- 历史数据曲线图表展示
- 任务栏悬浮小窗口(支持三种显示模式)
- 配置持久化存储
一、硬件监控实现
1.1 系统监控服务设计
系统监控服务是整个工具的核心,负责采集各类硬件数据。主要使用两种方式:
- PerformanceCounter:采集 CPU 使用率、内存使用率、网络流量等
- LibreHardwareMonitor:采集温度、频率、GPU 等硬件传感器数据
核心数据结构:
csharp
public class MonitorData
{
// CPU信息
public float CpuUsage { get; set; }
public float CpuFrequency { get; set; } // CPU当前主频 (MHz)
public float CpuTemperature { get; set; }
public int CpuCoreCount { get; set; }
public int CpuThreadCount { get; set; }
public string CpuName { get; set; } = string.Empty;
// 内存信息
public float MemoryUsage { get; set; }
public long TotalMemoryMB { get; set; }
public long UsedMemoryMB { get; set; }
public long AvailableMemoryMB { get; set; }
// GPU信息
public float GpuTemperature { get; set; }
public float GpuUsage { get; set; }
public string GpuName { get; set; } = string.Empty;
// 网络信息
public float NetworkUploadSpeed { get; set; } // KB/s
public float NetworkDownloadSpeed { get; set; } // KB/s
// 磁盘信息
public float DiskUsage { get; set; }
public long DiskReadSpeed { get; set; }
public long DiskWriteSpeed { get; set; }
// 电池信息
public int BatteryLevel { get; set; }
public bool IsCharging { get; set; }
}
1.2 性能计数器初始化
csharp
public class SystemMonitor : IDisposable
{
private readonly Computer _computer;
private readonly PerformanceCounter _cpuCounter;
private readonly PerformanceCounter _ramCounter;
private PerformanceCounter? _networkSentCounter;
private PerformanceCounter? _networkReceivedCounter;
public SystemMonitor()
{
// 初始化性能计数器
_cpuCounter = new PerformanceCounter("Processor", "% Processor Time", "_Total");
_ramCounter = new PerformanceCounter("Memory", "% Committed Bytes In Use");
// 初始化网络计数器
InitializeNetworkCounters();
// 初始化 LibreHardwareMonitor
_computer = new Computer
{
IsCpuEnabled = true,
IsGpuEnabled = true,
IsMemoryEnabled = true,
IsBatteryEnabled = true,
IsNetworkEnabled = true,
IsStorageEnabled = true
};
_computer.Open();
}
}
1.3 网络流量监控
网络流量监控需要注意的是 PerformanceCounter 返回的是累计值,需要计算两次采样之间的差值来得到速度:
csharp
private void InitializeNetworkCounters()
{
try
{
var category = new PerformanceCounterCategory("Network Interface");
var instanceNames = category.GetInstanceNames();
if (instanceNames.Length > 0)
{
string networkInterface = instanceNames[0];
_networkSentCounter = new PerformanceCounter(
"Network Interface", "Bytes Sent/sec", networkInterface);
_networkReceivedCounter = new PerformanceCounter(
"Network Interface", "Bytes Received/sec", networkInterface);
}
}
catch { }
}
private void UpdateNetworkData(MonitorData data)
{
try
{
if (_networkSentCounter != null && _networkReceivedCounter != null)
{
long bytesSent = (long)_networkSentCounter.NextValue();
long bytesReceived = (long)_networkReceivedCounter.NextValue();
var now = DateTime.Now;
var timeDiff = (now - _lastNetworkUpdate).TotalSeconds;
if (timeDiff > 0 && _lastBytesSent > 0)
{
// 计算速度 (KB/s)
data.NetworkUploadSpeed =
(float)((bytesSent - _lastBytesSent) / timeDiff / 1024);
data.NetworkDownloadSpeed =
(float)((bytesReceived - _lastBytesReceived) / timeDiff / 1024);
}
_lastBytesSent = bytesSent;
_lastBytesReceived = bytesReceived;
_lastNetworkUpdate = now;
}
}
catch { }
}
1.4 温度和频率监控
使用 LibreHardwareMonitor 获取 CPU/GPU 温度和频率信息:
csharp
private void UpdateData()
{
var data = new MonitorData();
// 获取 CPU 使用率
data.CpuUsage = _cpuCounter.NextValue();
// 更新硬件传感器信息
foreach (var hardware in _computer.Hardware)
{
hardware.Update();
// CPU 温度和频率
if (hardware.HardwareType == HardwareType.Cpu)
{
foreach (var sensor in hardware.Sensors)
{
if (sensor.SensorType == SensorType.Temperature && sensor.Value.HasValue)
{
data.CpuTemperature = Math.Max(data.CpuTemperature, sensor.Value.Value);
}
// 获取 CPU 主频
if (sensor.SensorType == SensorType.Clock &&
sensor.Name.Contains("Core") && sensor.Value.HasValue)
{
data.CpuFrequency = Math.Max(data.CpuFrequency, sensor.Value.Value);
}
}
}
// GPU 温度和使用率
if (hardware.HardwareType == HardwareType.GpuNvidia ||
hardware.HardwareType == HardwareType.GpuAmd ||
hardware.HardwareType == HardwareType.GpuIntel)
{
foreach (var sensor in hardware.Sensors)
{
if (sensor.SensorType == SensorType.Temperature && sensor.Value.HasValue)
{
data.GpuTemperature = Math.Max(data.GpuTemperature, sensor.Value.Value);
}
if (sensor.SensorType == SensorType.Load &&
sensor.Name.Contains("Core") && sensor.Value.HasValue)
{
data.GpuUsage = Math.Max(data.GpuUsage, sensor.Value.Value);
}
}
}
}
DataUpdated?.Invoke(this, data);
}
1.5 电池状态监控(Win11 兼容)
电池状态监控在 Win11 上需要特别处理,使用 SystemInformation.PowerStatus API:
csharp
try
{
var status = System.Windows.Forms.SystemInformation.PowerStatus;
// 检查是否有电池
if (status.BatteryLifePercent >= 0 && status.BatteryLifePercent <= 1 &&
status.BatteryChargeStatus != System.Windows.Forms.BatteryChargeStatus.NoSystemBattery)
{
// 电池百分比
data.BatteryLevel = (int)(status.BatteryLifePercent * 100);
// 充电状态
data.IsCharging = status.PowerLineStatus == System.Windows.Forms.PowerLineStatus.Online ||
status.BatteryChargeStatus.HasFlag(System.Windows.Forms.BatteryChargeStatus.Charging);
}
else
{
// 台式机或没有电池的设备
data.BatteryLevel = -1; // 用 -1 表示无电池
data.IsCharging = false;
}
}
catch
{
data.BatteryLevel = -1;
data.IsCharging = false;
}
二、历史数据管理
为了绘制历史曲线,需要维护一个固定大小的数据队列:
csharp
public class DataHistory
{
private readonly int _maxDataPoints;
private readonly Queue<float> _cpuHistory;
private readonly Queue<float> _memoryHistory;
private readonly Queue<float> _cpuTempHistory;
private readonly Queue<float> _gpuTempHistory;
// ... 其他数据队列
public DataHistory(int maxDataPoints = 60)
{
_maxDataPoints = maxDataPoints;
_cpuHistory = new Queue<float>(maxDataPoints);
_memoryHistory = new Queue<float>(maxDataPoints);
// 初始化其他队列...
}
public void AddData(MonitorData data)
{
AddToQueue(_cpuHistory, data.CpuUsage);
AddToQueue(_memoryHistory, data.MemoryUsage);
AddToQueue(_cpuTempHistory, data.CpuTemperature);
// 添加其他数据...
}
private void AddToQueue(Queue<float> queue, float value)
{
if (queue.Count >= _maxDataPoints)
{
queue.Dequeue();
}
queue.Enqueue(value);
}
public double[] GetCpuHistory() => _cpuHistory.Select(x => (double)x).ToArray();
public double[] GetMemoryHistory() => _memoryHistory.Select(x => (double)x).ToArray();
// 其他获取方法...
}
三、ScottPlot 图表绘制
3.1 图表初始化
ScottPlot 5.0 提供了强大的实时图表绘制能力:
csharp
private void SetupPlot(FormsPlot plot, string title, Color lineColor, double yMin = 0, double yMax = 100)
{
plot.Plot.Title(title);
plot.Plot.Axes.Title.Label.ForeColor = ScottColor.FromColor(Color.White);
plot.Plot.FigureBackground.Color = ScottColor.FromColor(Color.FromArgb(40, 40, 40));
plot.Plot.DataBackground.Color = ScottColor.FromColor(Color.FromArgb(30, 30, 30));
plot.Plot.Axes.Color(ScottColor.FromColor(Color.Gray));
plot.Plot.Grid.MajorLineColor = ScottColor.FromColor(Color.FromArgb(60, 60, 60));
// 设置 Y 轴范围
plot.Plot.Axes.SetLimitsY(yMin, yMax);
}
3.2 单线图表更新
csharp
private void UpdatePlot(FormsPlot? plot, double[] data, Color lineColor)
{
if (plot == null || data.Length == 0) return;
plot.Plot.Clear();
var signal = plot.Plot.Add.Signal(data);
signal.Color = ScottColor.FromColor(lineColor);
signal.LineWidth = 2;
plot.Plot.Axes.SetLimitsX(0, data.Length);
plot.Plot.Axes.SetLimitsY(0, 100);
plot.Refresh();
}
3.3 双线图表(温度/网络)
温度图表同时显示 CPU 和 GPU 温度,网络图表同时显示上传和下载速度:
csharp
private void UpdateDualLinePlot(FormsPlot? plot, double[] data1, double[] data2,
string legend1, string legend2, Color color1, Color color2)
{
if (plot == null) return;
plot.Plot.Clear();
if (data1.Length > 0)
{
var signal1 = plot.Plot.Add.Signal(data1);
signal1.Color = ScottColor.FromColor(color1);
signal1.LineWidth = 2;
signal1.LegendText = legend1;
}
if (data2.Length > 0)
{
var signal2 = plot.Plot.Add.Signal(data2);
signal2.Color = ScottColor.FromColor(color2);
signal2.LineWidth = 2;
signal2.LegendText = legend2;
}
plot.Plot.ShowLegend();
plot.Plot.Axes.SetLimitsX(0, Math.Max(data1.Length, data2.Length));
// 自动调整 Y 轴范围
if (data1.Length > 0 || data2.Length > 0)
{
double maxValue = 0;
if (data1.Length > 0) maxValue = Math.Max(maxValue, data1.Max());
if (data2.Length > 0) maxValue = Math.Max(maxValue, data2.Max());
plot.Plot.Axes.SetLimitsY(0, Math.Max(10, maxValue * 1.2));
}
plot.Refresh();
}
四、任务栏悬浮窗口
4.1 窗口基本设置
任务栏小窗口使用无边框窗体,置顶显示,不在任务栏显示:
csharp
private void InitializeComponent()
{
this.SuspendLayout();
this.AutoScaleDimensions = new SizeF(7F, 17F);
this.AutoScaleMode = AutoScaleMode.Font;
this.ClientSize = new Size(400, 135);
this.FormBorderStyle = FormBorderStyle.None;
this.Name = "TaskbarWindow";
this.TopMost = true;
this.ShowInTaskbar = false;
this.StartPosition = FormStartPosition.Manual;
this.BackColor = Color.FromArgb(30, 30, 30);
this.ResumeLayout(false);
}
protected override CreateParams CreateParams
{
get
{
CreateParams cp = base.CreateParams;
cp.ExStyle |= 0x80; // WS_EX_TOOLWINDOW - 不显示在 Alt+Tab 中
return cp;
}
}
4.2 三种显示模式
小窗口支持三种显示模式,通过右键菜单切换:
模式 0:CPU + 内存图表
csharp
private void SetupMode0_CpuMemory()
{
_cpuPlot = new FormsPlot
{
Location = new Point(5, 30),
Size = new Size(190, 100),
BackColor = Color.FromArgb(40, 40, 40)
};
SetupPlot(_cpuPlot, "CPU", Color.FromArgb(0, 174, 219));
this.Controls.Add(_cpuPlot);
_memoryPlot = new FormsPlot
{
Location = new Point(205, 30),
Size = new Size(190, 100),
BackColor = Color.FromArgb(40, 40, 40)
};
SetupPlot(_memoryPlot, "内存", Color.FromArgb(142, 68, 173));
this.Controls.Add(_memoryPlot);
}
模式 1:温度 + 网络图表
csharp
private void SetupMode1_TempNetwork()
{
_tempPlot = new FormsPlot
{
Location = new Point(5, 30),
Size = new Size(190, 100),
BackColor = Color.FromArgb(40, 40, 40)
};
SetupPlot(_tempPlot, "温度", Color.FromArgb(231, 76, 60));
this.Controls.Add(_tempPlot);
_networkPlot = new FormsPlot
{
Location = new Point(205, 30),
Size = new Size(190, 100),
BackColor = Color.FromArgb(40, 40, 40)
};
SetupPlot(_networkPlot, "网络", Color.FromArgb(52, 152, 219));
this.Controls.Add(_networkPlot);
}
模式 2:详细信息列表
csharp
private void SetupMode2_DetailedInfo()
{
int yPos = 30;
int labelHeight = 13;
var labels = new[]
{
("CPU使用率", "0%"),
("CPU频率", "0 MHz"),
("CPU温度", "0°C"),
("GPU使用率", "0%"),
("GPU温度", "0°C"),
("内存使用率", "0%"),
("网络上传", "0 KB/s"),
("网络下载", "0 KB/s")
};
foreach (var (name, value) in labels)
{
var nameLabel = new Label
{
Text = name + ":",
Location = new Point(10, yPos),
Size = new Size(100, labelHeight),
ForeColor = Color.FromArgb(200, 200, 200),
Font = new Font("Microsoft YaHei UI", 8F),
TextAlign = ContentAlignment.MiddleLeft
};
this.Controls.Add(nameLabel);
var valueLabel = new Label
{
Text = value,
Location = new Point(120, yPos),
Size = new Size(270, labelHeight),
ForeColor = Color.White,
Font = new Font("Microsoft YaHei UI", 8F, FontStyle.Bold),
TextAlign = ContentAlignment.MiddleLeft,
Tag = name // 用于在更新时识别标签
};
this.Controls.Add(valueLabel);
yPos += labelHeight;
}
}
4.3 窗口拖拽功能
实现窗口拖拽移动:
csharp
private Point _dragStartPoint;
private bool _isDragging = false;
private void TaskbarWindow_MouseDown(object? sender, MouseEventArgs e)
{
if (e.Button == MouseButtons.Left)
{
_isDragging = true;
_dragStartPoint = e.Location;
}
}
private void TaskbarWindow_MouseMove(object? sender, MouseEventArgs e)
{
if (_isDragging)
{
Point newLocation = this.Location;
newLocation.X += e.X - _dragStartPoint.X;
newLocation.Y += e.Y - _dragStartPoint.Y;
this.Location = newLocation;
}
if (e.Button == MouseButtons.None)
{
_isDragging = false;
}
}
4.4 右键菜单切换模式
csharp
private void SetupContextMenu()
{
var contextMenu = new ContextMenuStrip();
var modeMenu = new ToolStripMenuItem("切换显示模式");
var mode0 = new ToolStripMenuItem("CPU + 内存图表")
{
Checked = _displayMode == 0
};
mode0.Click += (s, e) => SwitchDisplayMode(0);
var mode1 = new ToolStripMenuItem("温度 + 网络图表")
{
Checked = _displayMode == 1
};
mode1.Click += (s, e) => SwitchDisplayMode(1);
var mode2 = new ToolStripMenuItem("详细信息列表")
{
Checked = _displayMode == 2
};
mode2.Click += (s, e) => SwitchDisplayMode(2);
modeMenu.DropDownItems.Add(mode0);
modeMenu.DropDownItems.Add(mode1);
modeMenu.DropDownItems.Add(mode2);
contextMenu.Items.Add(modeMenu);
contextMenu.Items.Add(new ToolStripSeparator());
contextMenu.Items.Add("关闭", null, (s, e) => this.Close());
this.ContextMenuStrip = contextMenu;
}
private void SwitchDisplayMode(int mode)
{
_displayMode = mode;
_settings.TaskbarWindowDisplayMode = mode;
// 保存配置
Utils.SettingsManager.Save(_settings);
// 重新创建界面
SetupWindow();
}
五、配置持久化
5.1 配置数据结构
csharp
public class AppSettings
{
public bool AutoStart { get; set; } = false;
public int RefreshInterval { get; set; } = 1000;
public bool ShowTaskbarWindow { get; set; } = true;
public int TaskbarWindowX { get; set; } = -1;
public int TaskbarWindowY { get; set; } = -1;
public int HistoryDuration { get; set; } = 120;
public bool StartMinimized { get; set; } = false;
public bool EnableTemperatureMonitoring { get; set; } = true;
public int MainWindowWidth { get; set; } = 900;
public int MainWindowHeight { get; set; } = 600;
public int TaskbarWindowDisplayMode { get; set; } = 0;
}
5.2 JSON 序列化存储
csharp
public static class SettingsManager
{
private static readonly string SettingsPath = Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData),
"WindowsMonitor",
"settings.json"
);
public static AppSettings Load()
{
try
{
if (File.Exists(SettingsPath))
{
string json = File.ReadAllText(SettingsPath);
return JsonSerializer.Deserialize<AppSettings>(json) ?? new AppSettings();
}
}
catch { }
return new AppSettings();
}
public static void Save(AppSettings settings)
{
try
{
string directory = Path.GetDirectoryName(SettingsPath)!;
if (!Directory.Exists(directory))
{
Directory.CreateDirectory(directory);
}
var options = new JsonSerializerOptions { WriteIndented = true };
string json = JsonSerializer.Serialize(settings, options);
File.WriteAllText(SettingsPath, json);
}
catch { }
}
public static string GetSettingsPath() => SettingsPath;
}
六、开机自启动
6.1 注册表方式实现
csharp
public static class AutoStartManager
{
private const string RegistryKey = @"SOFTWARE\Microsoft\Windows\CurrentVersion\Run";
private const string AppName = "WindowsMonitor";
public static void SetAutoStart(bool enable)
{
try
{
using var key = Registry.CurrentUser.OpenSubKey(RegistryKey, true);
if (key == null) return;
if (enable)
{
string exePath = Application.ExecutablePath;
key.SetValue(AppName, $"\"{exePath}\"");
}
else
{
key.DeleteValue(AppName, false);
}
}
catch { }
}
public static bool IsAutoStartEnabled()
{
try
{
using var key = Registry.CurrentUser.OpenSubKey(RegistryKey, false);
return key?.GetValue(AppName) != null;
}
catch
{
return false;
}
}
}
七、UI 布局优化
7.1 避免控件重叠
在 WinForms 中,控件重叠是常见问题。需要精确计算每个控件的位置和大小:
csharp
// 使用固定列位置和行位置
int col1X = 15, col2X = 250, col3X = 485, col4X = 720;
int row1Y = 10, row2Y = 40, row3Y = 70, row4Y = 100;
// 创建信息标签时使用固定大小
private Label CreateInfoLabel(string text, Point location, Panel parent)
{
var label = new Label
{
Text = text,
Location = location,
Size = new Size(210, 25), // 固定宽度避免显示不全
AutoSize = false,
Font = new Font("Microsoft YaHei UI", 9F, FontStyle.Regular),
AutoEllipsis = true // 文字太长时显示省略号
};
parent.Controls.Add(label);
return label;
}
7.2 响应式布局
使用 Anchor 属性实现窗口大小改变时控件自适应:
csharp
var dataPanel = new Panel
{
Location = new Point(10, 10),
Size = new Size(this.ClientSize.Width - 40, 140),
BackColor = Color.White,
BorderStyle = BorderStyle.FixedSingle,
Anchor = AnchorStyles.Top | AnchorStyles.Left | AnchorStyles.Right
};
八、系统托盘功能
8.1 托盘图标和菜单
csharp
private void SetupNotifyIcon()
{
_notifyIcon = new NotifyIcon
{
Icon = SystemIcons.Application,
Visible = true,
Text = "Windows 系统监控"
};
var contextMenu = new ContextMenuStrip();
contextMenu.Items.Add("显示主窗口", null, (s, e) => ShowMainWindow());
contextMenu.Items.Add("任务栏窗口", null, (s, e) => ToggleTaskbarWindow());
contextMenu.Items.Add(new ToolStripSeparator());
contextMenu.Items.Add("退出", null, (s, e) => Application.Exit());
_notifyIcon.ContextMenuStrip = contextMenu;
_notifyIcon.DoubleClick += (s, e) => ShowMainWindow();
}
8.2 窗口最小化到托盘
csharp
private void MainWindow_Resize(object? sender, EventArgs e)
{
if (this.WindowState == FormWindowState.Minimized)
{
this.Hide();
}
}
private void MainWindow_FormClosing(object? sender, FormClosingEventArgs e)
{
if (e.CloseReason == CloseReason.UserClosing)
{
e.Cancel = true;
this.WindowState = FormWindowState.Minimized;
this.Hide();
}
}
总结
这个系统监控工具的开发涉及了以下几个关键技术点:
- 硬件数据采集:结合 PerformanceCounter 和 LibreHardwareMonitor 实现全面的硬件监控
- 数据可视化:使用 ScottPlot 实现高性能的实时曲线绘制
- UI 设计:WinForms 布局优化,避免控件重叠
- 数据管理:使用队列管理历史数据,控制内存占用
- 配置持久化:JSON 序列化存储用户配置
- 系统集成:托盘图标、开机自启、悬浮窗口等系统功能
整个项目代码结构清晰,模块化设计良好,后续可以方便地扩展更多监控项和功能。
运行截图
主窗口展示了多个实时曲线图表,任务栏小窗口可以自由拖拽并切换显示模式,整体界面简洁美观,性能开销低。
开发环境要求:
- Visual Studio 2022 或更高版本
- .NET 10.0 SDK
- Windows 10/11 操作系统
依赖包:
xml
<PackageReference Include="LibreHardwareMonitorLib" Version="0.9.3" />
<PackageReference Include="ScottPlot.WinForms" Version="5.0.47" />
如有问题欢迎留言讨论。