65.9K
CodeProject 正在变化。 阅读更多。
Home

系统监视器实现

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.88/5 (46投票s)

2007 年 9 月 7 日

CPOL

6分钟阅读

viewsIcon

172029

downloadIcon

9933

如何检索系统数据并在轻量级自定义图表中绘制数据。

System Watcher Application from Zuoliu Ding

Google Desktop System Monitor

引言

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" 这样的常量字符串。

然而,对于某些性能计数器,它可能具有多个实例,具体取决于您的计算机配置。例如,“网络接口”类别,我在我的计算机上发现了三个实例(您可以使用 Visual Studio 中的服务器资源管理器视图轻松验证您的系统类别、计数器和实例)。
  • 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 被定义为一个枚举类型,包含 ReceivedAndSentReceivedSent。使用 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()

为了指定图表绘制格式,我创建了五个属性。LineColorGridColor 是两种绘图颜色。GridPixels 是网格的像素间距,或者设置为零则不绘制网格。InitialHeight 是开始时估计的最大逻辑值。这有助于在开始时调整垂直比例。ChartType 是一个枚举,包含用于线条的 Line 和用于图表中柱状图的 Stick

以下属性分别在 System Watcher 中为 dataChartCPUdataChartDiskR 设置。

现在我们可以看一下我重写的核心工作,即 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 非零,我将使用 GridColorGridPixels 绘制网格。最后,我根据 ChartType 属性值表示数据,可以是线条或柱状图。

关注点

如果您想使用 DataChart 类,您可以添加自己的属性使其更灵活,例如线条/柱状图的宽度和柱状图的间隔。此外,您还可以绘制具有逻辑值的 xy 坐标。任何增强功能都可以使 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() 中,将一些 '<' 和 '>' 更正为 &lt;&gt;
    • 添加了 pen.Dispose(); 以便在此计时器驱动的应用程序中及时手动释放它
© . All rights reserved.