前言
最近开发了一个轻量级的 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 等硬件传感器数据
核心数据结构:
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 性能计数器初始化
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 返回的是累计值,需要计算两次采样之间的差值来得到速度:
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 温度和频率信息:
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:
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;
}
二、历史数据管理
为了绘制历史曲线,需要维护一个固定大小的数据队列:
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 提供了强大的实时图表绘制能力:
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 单线图表更新
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 温度,网络图表同时显示上传和下载速度:
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 窗口基本设置
任务栏小窗口使用无边框窗体,置顶显示,不在任务栏显示:
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 + 内存图表
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:温度 + 网络图表
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:详细信息列表
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 窗口拖拽功能
实现窗口拖拽移动:
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 右键菜单切换模式
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 配置数据结构
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 序列化存储
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 注册表方式实现
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 中,控件重叠是常见问题。需要精确计算每个控件的位置和大小:
// 使用固定列位置和行位置
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 属性实现窗口大小改变时控件自适应:
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 托盘图标和菜单
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 窗口最小化到托盘
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 操作系统
依赖包:
<packagereference include="librehardwaremonitorlib" version="0.9.3" /> <packagereference include="scottplot.winforms" version="5.0.47" />
以上就是基于c# winforms开发的windows系统监控工具的详细内容,更多关于c# winforms windows系统监控的资料请关注代码网其它相关文章!
发表评论