系统监视器实现






4.88/5 (46投票s)
如何检索系统数据并在轻量级自定义图表中绘制数据。


引言
System Watcher 是一个 C# 实现的系统数据监视器,如上面第一个截图所示。您可能已经注意到早期版本的 Google Desktop System Monitor(上面第二个截图),它是一个可下载为桌面插件的小工具。作为一种实践,我将 System Watcher 构建成一个增强的独立工具来监视 PC 性能。通常,它显示 CPU 使用率、虚拟/物理内存使用率、物理磁盘读取/写入 BPS(每秒比特数)和网络接收/发送 BPS。利用这里的讨论,您可以在您的应用程序中轻松监视更多系统数据。
实际上,我想利用这个例子来演示两个方面的代码:首先,如何在您的编程中收集系统数据。三种简单的方法包括检索 Windows Management Instrumentation (WMI) 对象、性能计数器和环境变量。要以图形格式显示数据,我需要一些图表控件。您可能会在这里和那里找到许多复杂的控件。然而,我决定作为一项练习来自定义我自己的轻量级控件,一个用于粗水平条,另一个用于折线图和细垂直条。我将在接下来的部分讨论这些实现。
收集系统数据
系统数据可以是动态的或静态的。动态数据随当前时间变化,例如 CPU 和内存使用率、磁盘和网络吞吐量。通常,您可以从性能计数器对象收集数据。例如,要获取 CPU 使用率,我首先需要
PerformanceCounter _cpuCounter = new PerformanceCounter();
然后我在其中调用它
public string GetProcessorData()
{
double d = GetCounterValue(_cpuCounter,
"Processor", "% Processor Time", "_Total");
return _compactFormat? (int)d +"%": d.ToString("F") +"%";
}
...其中 GetCounterValue()
是我创建的一个通用辅助函数,用于获取性能计数器值
double GetCounterValue(PerformanceCounter pc, string categoryName,
string counterName, string instanceName)
{
pc.CategoryName = categoryName;
pc.CounterName = counterName;
pc.InstanceName = instanceName;
return pc.NextValue();
}
同样,我可以用这种方式获取虚拟内存百分比和物理磁盘读取字节数
d = GetCounterValue(_memoryCounter, "Memory",
"% Committed Bytes In Use", null);
d = GetCounterValue(_diskReadCounter, "PhysicalDisk",
"Disk Read Bytes/sec", "_Total");
如您在 GetCounterValue()
中所见,我将第一个 CategoryName
属性设置为 "PhysicalDisk"
,并将第二个 CounterName
设置为 "Disk Read Bytes/sec"
。对于第三个 InstanceName
属性,在 "Memory"
的情况下它可以是 null
,或者对于 "PhysicalDisk"
,它可以是像 "_Total"
这样的常量字符串。
- Broadcom NetXtreme 57xx Gigabit Controller
- Intel[R] PRO_Wireless 2200BG Network Connection,以及
- Microsoft TCP Loopback interface
因此,我必须首先获取所有实例名称并将它们保存在 _instanceNames
中
PerformanceCounterCategory cat = new PerformanceCounterCategory
("Network Interface");
_instanceNames = cat.GetInstanceNames();
如有必要,您可以显示单个网络实例的不同计数器值。而在我的 System Watcher 中,我只需要获取已发送和接收字节的所有计数器值的总和。获取网络数据的函数如下所示:
public double GetNetData(NetData nd)
{
if (_instanceNames.Length==0)
return 0;
double d =0;
for (int i=0; i<_instanceNames.Length; i++)
{
d += nd==NetData.Received?
GetCounterValue(_netRecvCounters[i], "Network Interface",
"Bytes Received/sec", _instanceNames[i]):
nd==NetData.Sent?
GetCounterValue(_netSentCounters[i], "Network Interface",
"Bytes Sent/sec", _instanceNames[i]):
nd==NetData.ReceivedAndSent?
GetCounterValue(_netRecvCounters[i], "Network Interface",
"Bytes Received/sec", _instanceNames[i]) +
GetCounterValue(_netSentCounters[i], "Network Interface",
"Bytes Sent/sec", _instanceNames[i]):
0;
}
return d;
}
...其中 NetData
被定义为一个枚举类型,包含 ReceivedAndSent
、Received
和 Sent
。使用 ReceivedAndSent
,您可以将接收到的字节和发送的字节相加,这可以用于像 Google Desktop 中的迷你面板那样的紧凑格式,其中系统监视器被缩小到小的侧边栏。这就是为什么我在代码中散布 _compactFormat
标志的原因。
静态系统数据可以从 WMI 中检索,WMI 包含许多类供您查询,例如内存和磁盘空间、计算机和用户信息。其中,Win32_ComputerSystem
在这里很重要,我创建了一个通用的共享函数以方便使用。
public string QueryComputerSystem(string type)
{
string str = null;
ManagementObjectSearcher objCS = new ManagementObjectSearcher
("SELECT * FROM Win32_ComputerSystem");
foreach ( ManagementObject objMgmt in objCS.Get() )
{
str = objMgmt[type].ToString();
}
return str;
}
我调用它来获取计算机制造商、型号和用户名,假设 sd
是包含 QueryComputerSystem()
的类的对象。
labelModel.Text = sd.QueryComputerSystem("manufacturer") +", " +
sd.QueryComputerSystem("model");
labelNames.Text = "User: " +sd.QueryComputerSystem("username");
要获取物理内存使用情况,我必须同时使用 WMI 查询和性能计数器对象。
public string GetMemoryPData()
{
string s = QueryComputerSystem("totalphysicalmemory");
double totalphysicalmemory = Convert.ToDouble(s);
double d = GetCounterValue(_memoryCounter, "Memory",
"Available Bytes", null);
d = totalphysicalmemory - d;
s = _compactFormat? "%": "% (" + FormatBytes(d) +" / " +
FormatBytes(totalphysicalmemory) +")";
d /= totalphysicalmemory;
d *= 100;
return _compactFormat? (int)d +s: d.ToString("F") + s;
}
获取静态系统数据的另一种方法是直接调用 ExpandEnvironmentVariables()
。例如,要获取处理器标识符,请调用:
textBoxProcessor.Text = Environment.ExpandEnvironmentVariables
("%PROCESSOR_IDENTIFIER%");
数据图自定义控件
如我所述,两个自定义的轻量级图表控件类是用于粗水平条的 DataBar
,以及用于细线或柱状图的 DataChart
。由于 DataBar
非常简单,我只讨论 DataChart
,它用作 CPU 和内存使用率历史记录中的折线图,也用作磁盘和网络吞吐量显示中的柱状图。例如,要更新 CPU 使用率,我调用:
string s = sd.GetProcessorData();
labelCpu.Text = s;
double d = double.Parse(s.Substring(0, s.IndexOf("%")));
dataBarCPU.Value = (int)d;
dataChartCPU.UpdateChart(d);
...其中 dataBarCPU
是一个 DataBar
对象,而 dataChartCPU
是一个 DataChart
对象。DataChart
中唯一公开的方法定义如下:
public void UpdateChart(double d)
{
Rectangle rt = this.ClientRectangle;
int dataCount = rt.Width/2;
if (_arrayList.Count >= dataCount)
_arrayList.RemoveAt(0);
_arrayList.Add(d);
Invalidate();
}
在此方法中,我首先根据客户端矩形的宽度计算像素中的最大数据计数,假设绘图中每个值占两个像素。然后,我将新值添加到内部存储 _arrayList
中,并将其作为循环队列。最后,我使控件无效以调用虚拟 OnPaint()
。
为了指定图表绘制格式,我创建了五个属性。LineColor
和 GridColor
是两种绘图颜色。GridPixels
是网格的像素间距,或者设置为零则不绘制网格。InitialHeight
是开始时估计的最大逻辑值。这有助于在开始时调整垂直比例。ChartType
是一个枚举,包含用于线条的 Line
和用于图表中柱状图的 Stick
。
以下属性分别在 System Watcher 中为 dataChartCPU
和 dataChartDiskR
设置。
![]() |
![]() |
现在我们可以看一下我重写的核心工作,即 OnPaint()
。
protected override void OnPaint(PaintEventArgs e)
{
int count = _arrayList.Count;
if (count==0) return;
double y=0, yMax = InitialHeight;
for (int i=0; i<count; i++)
{
y = Convert.ToDouble(_arrayList[i]);
if (y>yMax) yMax = y;
}
Rectangle rt = this.ClientRectangle;
y = yMax==0? 1: rt.Height/yMax; // y ratio
int xStart = rt.Width;
int yStart = rt.Height;
int nX, nY;
Pen pen = null;
e.Graphics.Clear(BackColor);
if (GridPixels!=0)
{
pen = new Pen(GridColor, 1);
nX = rt.Width/GridPixels;
nY = rt.Height/GridPixels;
for (int i=1; i<=nX; i++)
e.Graphics.DrawLine(pen, i*GridPixels, 0, i*GridPixels, yStart);
for (int i=1; i<nY; i++)
e.Graphics.DrawLine(pen, 0, i*GridPixels, xStart, i*GridPixels);
pen.Dispose();
}
// From the most recent data, draw from right to left
// Get data from _arrayList[count-1] to _arrayList[0]
if (ChartType==ChartType.Stick)
{
pen = new Pen(LineColor, 2);
for (int i=count-1; i>=0; i--)
{
nX = xStart - 2*(count-i);
if (nX<=0) break;
nY = (int)(yStart-y*Convert.ToDouble(_arrayList[i]));
e.Graphics.DrawLine(pen, nX, yStart, nX, nY);
}
pen.Dispose();
}
else
if (ChartType==ChartType.Line)
{
pen = new Pen(LineColor, 1);
int nX0 = xStart - 2;
int nY0 = (int)(yStart-y*Convert.ToDouble(_arrayList[count-1]));
for (int i=count-2; i>=0; i--)
{
nX = xStart - 2*(count-i);
if (nX<=0) break;
nY = (int)(yStart-y*Convert.ToDouble(_arrayList[i]));
e.Graphics.DrawLine(pen, nX0, nY0, nX, nY);
nX0 = nX;
nY0 = nY;
}
pen.Dispose();
}
base.OnPaint(e);
}
绘图过程很简单。我首先从 _arrayList
中的数据中查找最大的逻辑值 yMax
。然后,我保存计算出的垂直比率以供后续绘图。如果 GridPixels
非零,我将使用 GridColor
和 GridPixels
绘制网格。最后,我根据 ChartType
属性值表示数据,可以是线条或柱状图。
关注点
如果您想使用 DataChart
类,您可以添加自己的属性使其更灵活,例如线条/柱状图的宽度和柱状图的间隔。此外,您还可以绘制具有逻辑值的 x
和 y
坐标。任何增强功能都可以使 DataChart
变得方便易用。
对于系统数据,我尝试为所有逻辑磁盘添加一个空闲空间监视。我期望得到一个像 "C:10.32 GB, D:5.87GB"
这样的字符串。为了实现这一点,我编写了以下函数,使用 WMI 调用 Win32_LogicalDisk
类。
public string LogicalDisk()
{
string diskSpace = string.Empty;
object device, space;
ManagementObjectSearcher objCS = new ManagementObjectSearcher
("SELECT * FROM Win32_LogicalDisk");
foreach ( ManagementObject objMgmt in objCS.Get() )
{
device = objMgmt["DeviceID"]; // C:
if (null !=device)
{
space = objMgmt["FreeSpace"]; // C:10.32 GB
if (null!=space)
diskSpace += device.ToString() +FormatBytes
(double.Parse(space.ToString())) +", ";
}
}
return diskSpace.Substring(0, diskSpace.Length-2);
// C:10.32 GB, D:5.87GB
}
在大多数情况下,它都能正常工作,无论是通过以太网连接的局域网还是离线。不幸的是,当我切换到无线网络连接时,LogicalDisk()
在 objCS.Get()
处遇到了大约 20 秒的长延迟,这冻结了主线程。我尝试了一些解决方法但没有成功。传递一个委托作为回调或创建工作线程都无法解决此时的长时间阻塞问题。欢迎任何评论和建议。
历史
- 2007 年 9 月 7 日 - 发布原始版本
- 2007 年 9 月 13 日 - 文章更新
- 在
OnPaint()
中,将一些 '<' 和 '>' 更正为<
和>
- 添加了
pen.Dispose();
以便在此计时器驱动的应用程序中及时手动释放它
- 在