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

CPU温度、风扇转速等应用

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.98/5 (25投票s)

2018年7月27日

CPOL

15分钟阅读

viewsIcon

37040

downloadIcon

1727

监控系统传感器的极简应用

引言

在之前的文章《电脑温度、风扇转速等》中,我讨论了如何充分利用OpenHardwareMonitorLib.dll(以下简称OHM)来监控温度、风扇转速等。正如我当时提到的,我有一台DIY的台式机,使用的是ASUS P6T主板,ASUS已不再支持他们出色的监控应用程序PC Probe II。而且我发现其他应用程序要么奇怪,要么不灵活。

我决定采用小组件(widget)的结构来构建这个应用,JLDProbe II,每个传感器一个组件。用户可以选择监控哪些传感器,设置当传感器值超出最小值或最大值时触发的警报,并以相当灵活的方式配置组件。下面是该应用程序停靠在我屏幕右下角的一些截图:

左边是没有触发警报的情况。右边,CPU Core 1的温度超过了警报阈值——我不得不将阈值设置得很低才能在演示中触发它。出问题的组件会闪烁红色,并且可以选择发出警告声音。

Using the Code

代码实现为一个Visual Studio 2015的独立项目。它包含了OpenHardwareMonitorLib.dll版本0.8.0 Beta。您应该能够下载代码文件,解压缩,在VS中加载它,然后进行编译和执行。

您也可以从bin/Release目录中复制相关的可执行文件和DLL文件使用,但我确实认为您必须安装相应的.NET运行时库。

实现

要理解像ohmDataTreeohmHwNodeohmSensor这样的类,请参考我之前的文章《电脑温度、风扇转速等》。

JLDProbe II实现的关键是主探测窗体,它被命名为ProbeForm,名字有点难以想象;还有一个自定义绘制的UserControl,名为SensorWidget,我在命名时也同样缺乏想象力。还有一个名为ProbeConfig的容器类,它维护着整个探测配置,包括布局、位置以及要监控的传感器列表。

传感器组件

SensorWidget类维护着对象的变量,用于:

  • 单个组件的尺寸和显示
  • 组件在探测窗体内的手动定位——稍后会详细介绍
  • 警报的最大和最小阈值
  • OHM读取的IDSensorType
  • 在遍历ohmDataTree时遇到的传感器的线性索引
  • 指向组件ohmSensor实例的指针
  • 指向ProbeConfig实例中主配置的指针——稍后会详细介绍
  • 一个委托,用于在组件被拖放到不同位置时通知主探测窗体,这是在手动定位时发生的。

SensorWidget还实现了DrawRoundedRectangleFillRoundedRectangle方法——我把它们粗略地组合在了一起——以及它绘制自身的OnPaint重载。这些代码在这里列出:

private void DrawRoundedRectangle(Graphics gr, Pen p, Rectangle r, float cornerRy)
{
    float cornerDy = 2 * cornerRy;
    float cornerRx = gr.DpiX * cornerRy / gr.DpiY;
    float cornerDx = 2 * cornerRx;
    gr.DrawArc(p, r.Left, r.Top, cornerDx, cornerDy, 180, 90);
    gr.DrawArc(p, r.Left + r.Width - cornerDx, r.Top, cornerDx, cornerDy, 270, 90);
    gr.DrawArc(p, r.Left, r.Top + r.Height - cornerDy, cornerDx, cornerDy, 90, 90);
    gr.DrawArc(p, r.Left + r.Width - cornerDx, r.Top + r.Height - cornerDy, cornerDx, cornerDy, 0, 90);
    gr.DrawLine(p, r.Left + cornerRx, r.Top, r.Right - cornerRx, r.Top);
    gr.DrawLine(p, r.Left + cornerRx, r.Bottom, r.Right - cornerRx, r.Bottom);
    gr.DrawLine(p, r.Left, r.Top + cornerRy, r.Left, r.Bottom - cornerRy);
    gr.DrawLine(p, r.Right, r.Top + cornerRy, r.Right, r.Bottom - cornerRy);
}

private void FillRoundedRectangle(Graphics gr, Brush br, Rectangle r, float cornerRy)
{
    float cornerDy = 2 * cornerRy;
    float cornerRx = gr.DpiX * cornerRy / gr.DpiY;
    float cornerDx = 2 * cornerRx;
    gr.FillEllipse(br, r.Left, r.Top, cornerDx, cornerDy);
    gr.FillEllipse(br, r.Left + r.Width - cornerDx, r.Top, cornerDx, cornerDy);
    gr.FillEllipse(br, r.Left, r.Top + r.Height - cornerDy, cornerDx, cornerDy);
    gr.FillEllipse(br, r.Left + r.Width - cornerDx, r.Top + r.Height - cornerDy, cornerDx, cornerDy);
    int icRx = Convert.ToInt32(cornerRx);
    int icRy = Convert.ToInt32(cornerRy);
    r.Inflate(-icRx, 0);
    gr.FillRectangle(br, r);
    r.Inflate(+icRx, -icRy);
    gr.FillRectangle(br, r);
}

protected override void OnPaint(PaintEventArgs e)
{
    //base.OnPaint(e);

    //This can happen when manually setting context menu in method setContextMenu()
    if (cornerRad == 0)
        return;

    Rectangle cRect = this.ClientRectangle;
    cRect.Width--; //must do this so dark line is included on bottom and right
    cRect.Height--;

    //Paint the widget background Light Gray with a black border
    FillRoundedRectangle(e.Graphics, cScheme.BackNameBrush, cRect, cornerRad);
    DrawRoundedRectangle(e.Graphics, Pens.Black, cRect, cornerRad);
    if (this.Font.Size >= 16)
    {
        //Add a second dark line around the widget
        Rectangle smallerR = cRect;
        smallerR.Inflate(-1, -1);
        DrawRoundedRectangle(e.Graphics, Pens.Black, smallerR, cornerRad - 1);
    }

    //Get the rectangle for the Value string
    Rectangle valueRect = new Rectangle( Convert.ToInt32(gap), 
        Convert.ToInt32(nameY + sizeName.Height + gap), 
        Convert.ToInt32(this.Width - 2 * gap), Convert.ToInt32(sizeValue.Height));

    //If flashing, paint the background red, otherwise use bvBrush
    if (FlashRed)
        FillRoundedRectangle(e.Graphics, Brushes.Red, valueRect, cornerRad);
    else
        FillRoundedRectangle(e.Graphics, cScheme.BackValueBrush, valueRect, cornerRad);
    //Add a black border
    DrawRoundedRectangle(e.Graphics, Pens.Black, valueRect, cornerRad);
    if (this.Font.Size >= 16)
    {
        //Add a second dark line around the Value panel
        Rectangle smallerR = valueRect;
        smallerR.Inflate(-1, -1);
        DrawRoundedRectangle(e.Graphics, Pens.Black, smallerR, cornerRad - 1);
    }

    //Paint the strings
    e.Graphics.DrawString(displayName, nameFont, cScheme.ForeNameBrush, 
                         (this.Width - sizeName.Width) / 2, nameY);

    if (ohms == null)
    {
        //If no ohmSensor found, paint red X in the middle of the value panel
        int L = valueRect.Left;
        int R = valueRect.Right;
        int T = valueRect.Top;
        int B = valueRect.Bottom;
        for (int i = -1; i < 2; i++)
        {
            e.Graphics.DrawLine(Pens.Red, L, T + i, R, B + i);
            e.Graphics.DrawLine(Pens.Red, L, B + i, R, T + i);
        }
    }
    else if (FlashRed)//If flashing, paint the Value string white, otherwise, use the fvBrush
        e.Graphics.DrawString(stValue, this.Font, Brushes.White, 
                             (this.Width - sizeValue.Width) / 2, nameY + sizeName.Height + gap);
    else
        e.Graphics.DrawString(stValue, this.Font, cScheme.ForeValueBrush, 
                             (this.Width - sizeValue.Width) / 2, nameY + sizeName.Height + gap);
}

为了让用户能够拖动窗体在屏幕上移动,SensorWidget通过以下方式重载了WndProc方法:

protected override void WndProc(ref Message m)
{
    const int WM_NCHITTEST = 0x0084;
    const int HTTRANSPARENT = (-1);

    if (m.Msg == WM_NCHITTEST && mousePassThrough)
        m.Result = (IntPtr)HTTRANSPARENT;
    else
        base.WndProc(ref m);
}

这产生了点击测试透明度,因此鼠标事件被组件忽略并传递给父窗体。布尔变量mousePassThrough设置为false,以覆盖此行为,在拖动和手动定位单个组件在窗体上,或在“配置传感器”对话框中。OnMouseDownOnMouseMoveOnMouseUp被重载以在出现此类覆盖时提供拖放行为。

SensorWidget还公开了布尔属性AutoSizeWandH。该组件维护着许多尺寸变量,用于在绘制时定位和调整组件元素的大小。如果AutoSizeWandHtrue,组件还会将其WidthHeight调整到能容纳传感器NameValuestrings所需的最小值。

主探测器首先为所有组件设置AutoSizeWandHtrue来显示组件。然后它确定最大组件的widthheight,设置AutoSizeWandHfalse,并手动将所有组件的widthheight设置为最大值。这使得所有组件的大小统一,以便它们在窗体上整齐地排列。

传感器组件很大程度上从SensorWidgetColorScheme容器类的实例中获得外观,该类实现如下:

public class SensorWidgetColorScheme
{
    public bool byType = true;
    private Color fnColor, bnColor, fvColor, bvColor;
    private SolidBrush fvBrush, bvBrush; //Used for font and background colors in the Value pane. 
    private SolidBrush fnBrush, bnBrush; //Used for font and background colors in the Name pane.
    
    ...
    ...
    
    public SolidBrush ForeNameBrush { get { return fnBrush; } }
    public SolidBrush BackNameBrush { get { return bnBrush; } }

    public SolidBrush ForeValueBrush { get { return fvBrush; } }
    public SolidBrush BackValueBrush { get { return bvBrush; } }

    public Color NameFontColor
    {
        get { return fnColor; }
        set { fnColor = value; UpdateBrushes(); }
    }
    public Color NameBackgroundColor
    {
        get { return bnColor; }
        set { bnColor = value; UpdateBrushes(); }
    }
    public Color ValueFontColor
    {
        get { return fvColor; }
        set { fvColor = value; UpdateBrushes(); }
    }
    public Color ValueBackgroundColor
    {
        get { return bvColor; }
        set { bvColor = value; UpdateBrushes(); }
    }
}

SensorWidgetColorScheme维护着颜色和画笔,用于:

  • 传感器名称的字体颜色
  • 传感器名称的背景颜色
  • 传感器值的字体颜色
  • 传感器值的背景颜色

这些颜色可以在“配置传感器”对话框中设置,以提供:

  • 一组用于特定类型传感器(电压、温度、风扇等)的颜色,和/或
  • 为单个传感器设置特定颜色。

稍后会详细介绍。

选择传感器

要开始,您必须选择要监控的传感器。

请注意,如果您运行的应用程序使用了我的配置文件,除非您的系统与我的完全相同,否则在组件的值面板中您会看到很多大大的红色X,因为应用程序找不到与我系统ID匹配的传感器。有关这方面的信息,请参见“ID不正确或传感器故障”部分。

当应用程序启动时,如果没有配置文件,或者没有选择传感器,则“选择传感器”对话框会自动打开。或者,您随时可以右键单击窗体或任何组件来更改选定的传感器,然后会出现以下上下文菜单:

您可以选择“选择传感器…”来更改您的选择。“选择传感器”对话框如下所示:

通过勾选左侧的复选框来选择您要监控的传感器,然后单击确定。如果您之前没有配置布局,它将默认为水平——见下文。

配置传感器 & ProbeConfig

ProbeConfig是一个容器类,它维护着要监控的传感器列表,这是系统中可用传感器的一个选定子集。它还维护着用于排列和布局传感器组件的变量。有四种基本布局类型:垂直水平手动垂直水平布局如下图所示:

这三种布局都显示在我屏幕的右下角。在布局这三种配置的组件时,应用程序会按照它们在ProbeConfig类列表中出现的顺序将组件放入一个网格中。您可以在“配置传感器”对话框中更改该顺序,该对话框如下所示:

在此对话框中,您可以:

  • 通过选择一个传感器,然后单击其显示名称一次并编辑该值,为传感器提供一个可识别的显示名称。
  • 通过单击任何标题来按字母顺序或反字母顺序排列传感器。
  • 通过拖放单个传感器到列表中不同的位置来排列传感器的顺序。
  • 将传感器布局选择为水平垂直手动
  • 水平布局设置行数,或为垂直布局设置列数。
  • 通过在相应文本框中输入值并按设置最小/最大值按钮,为选定的传感器设置最小和/或最大警报阈值。
  • 选择发生警报时要听的声音,包括不发声音。
  • 测试各种警报声音。

在“字体与杂项”选项卡上,您可以:

  • 选择探针窗体停靠的方式:左上、中上、右上等。
  • 指定传感器值更新的计时器间隔,以及
  • 选择字体名称、大小和样式。

为了在对话框中嵌入字体选择,我不得不创建一个FontPicker自定义控件,它模仿了Windows的字体对话框。请注意,字体大小决定了组件的大小,因此也决定了屏幕上窗体的大小。

颜色方案将在紧随其后的单独一节中介绍。

当“配置传感器”对话框打开时,对配置所做的任何更改会立即在窗体中实现,因此您可以看到它的样子。如果您喜欢这些更改,请按确定保存它们,或者按取消返回到对话框首次打开时生效的配置。

颜色方案

下图说明了“配置传感器”对话框的“颜色方案”页面。

左侧是一个经典的ColorPicker,添加了四个单选按钮来选择由拾色器修改的颜色——这里,用户正在修改传感器名称的背景颜色。右侧顶部的单选按钮表明用户已选择按传感器类型(VoltageClockTemperature等)配置颜色方案。

注意风扇组件周围的选定矩形,这表明它的颜色将由拾色器修改。当对话框首次打开,或在“按类型”和“单独”配置方案之间切换时,没有选择颜色组件,因此拾色器的更改无效。要选择一个颜色组件,只需用鼠标左键单击该组件即可。

在这种情况下,拾色器中的任何更改将立即反映在传感器名称的背景上,用于:

  1. 风扇颜色组件,以及
  2. 实际JLDProbe II窗体中监控的任何风扇传感器。

如果您为特定类型设置了颜色方案,并想将其复制到另一种类型,您只需用鼠标左键单击您想要复制的那个,然后将其拖放到另一个上即可。在下图的图像中,Clock颜色方案将被拖放到,并复制到,Power颜色方案中。

要为正在监控的单个传感器设置颜色,请单击窗体右上角标有“单独”的单选按钮。对于我的配置,窗体最初会变成这样:

JLDProbe II正在监控的实际传感器现在显示在右侧,它们的颜色方案可以像传感器类型颜色方案一样进行修改。请注意,为电压、时钟、温度和风扇类型设置的颜色方案已应用于正在监控的单个传感器。

我的一些CPU风扇出了点问题——我将在稍后更详细地讨论它——为了方便密切关注它,我选择了那个单独的传感器,并为其设置了一个亮红色的背景色,如下图所示:

我还更改了名称字体的颜色为亮白色。最终配置如下所示:

有点花哨,但出于演示目的很有效。

按类型 vs. 单独

JLDProbe II在传感器类型上维护一个颜色方案数组,声明方式如下:

public SensorWidgetColorScheme[] sensorTypeColorSchemes; 

数组的大小等于OHM中SensorType枚举的长度,再加上一个额外的用于维护默认颜色方案,该方案是硬编码的,以生成本文档第一张图中可见的方案。探测窗体中的每个传感器监控组件不会获得自己一份传感器类型方案的副本,而是被给予指向sensorTypeColorSchemes中相应颜色方案的指针。

请注意,探测窗体中的每个监控传感器都默认为“按类型”的颜色方案。但是,如果在其方案在“配置传感器”对话框中被单独修改,它就会获得自己一份新颜色方案的副本。这样,“单独”就优先于“按类型”。

如果监控传感器的颜色方案是单独设置的,您可以通过在“配置传感器”对话框中右键单击它并选择菜单项“恢复为按类型颜色方案”来将其恢复为“按类型”方案。

手动组件定位

手动布局允许您根据自己的选择来定位组件。这是一个相同组件的手动布局,我随意放置了它们,并将整个窗体浮动在屏幕中间:

在手动布局中,应用程序会忽略组件在列表中的出现顺序,而是根据单独的列和行分配来放置它们。

要手动布局组件,请右键单击任何组件并选择定位组件。窗体会扩大,并在下面图示的左侧看起来类似:

现在用鼠标左键单击任何组件并将其拖动到窗体中的某个位置。在手动定位过程中,布尔变量mousePassThrough被设置为false,以允许拖放单个组件。当您放下它时,组件将吸附到一个整数列/行位置。请注意,应用程序不允许您将一个组件拖放到另一个组件之上。

上面右侧的图是相同的窗体,在几个组件被拖放到一些随机位置后。当您按照喜好定位好组件后,右键单击窗体并选择接受定位,在此示例中将生成一个背景透明的窗体,外观如下:

在所有四种布局中,当不手动定位窗体上的组件时,如果用户用鼠标左键单击一个组件并拖动它,所有其他组件都会随之被拖动,因为您实际上在拖动整个窗体,即使窗体的背景是透明的。

杂项菜单项

右键单击组件并选择视图>放大可将字体大小增加25%,或选择视图>缩小可将其减小20%。这是改变组件和窗体大小的快捷方式。

选择视图>隐藏可将窗体停靠在系统托盘

退出会退出应用程序并将配置保存到可执行文件所在文件夹中的一个文件中。请注意,只有当配置已更改时,它才会保存配置。

关于显示JLDProbe II和OHM的版本。

配置文件

该应用程序将所有配置信息保存到可执行文件所在文件夹中的一个名为JLDProbeII.cfg的文件中。它仅在应用程序运行时配置已更改时才保存配置。这些信息本应保存到Windows注册表,但出于本文档的目的,我选择不这样做。

启动时运行

由于传感器数据只有在应用程序以管理员权限运行时才有效,因此在Windows 10中,我找到的唯一一种在启动时执行此操作的方法是通过将其安排为登录时的任务。操作步骤如下:

  1. 打开任务计划程序。
  2. 在“常规”选项卡上,为任务指定一个名称描述,然后勾选“以最高权限运行”复选框。
  3. 在“触发器”选项卡上,单击“新建”按钮,然后在出现的对话框中,在“开始任务”的组合框中选择“登录时”——我没有尝试“启动时”,但那也可能有效。
  4. 在“操作”选项卡上,单击“新建”按钮,然后在出现的对话框中,对于“操作”组合框选择“启动程序”,然后单击“浏览”按钮,浏览并选择bin/Release文件夹中(或您放置它的任何文件夹)的可执行文件“JLDProbe II.exe”。
  5. 在“条件”选项卡上,取消勾选所有选项。
  6. 在“设置”选项卡上,取消勾选“如果任务运行时间超过:则停止任务”
  7. 单击确定,完成。

如果您不这样设置,例如,只是将可执行文件的链接放在Windows启动文件夹中,您的登录过程会卡住,等待您响应用户访问控制(UAC)提示。

如果您有更好的方法在启动或登录时以管理员权限启动它,请告诉我。

ID不正确或传感器故障

我有一个似乎正在失效的风扇——毕竟它已经使用了七八年了。现在,它运行不稳定。但它在我的配置中是我想要监控的传感器之一。如果风扇在应用程序启动时没有运行,应用程序会从配置文件中读取其ID,但在ohmDataTree中找不到它。发生这种情况的任何传感器都会在值面板中显示一个大大的红色X,如下图所示:

发生这种情况时,右键单击出现问题的组件,“错误详细信息…”菜单项现在会出现。选择它,一个对话框将提供传感器的显示名称、传感器类型和ID,以帮助您排除故障。该对话框还提供从配置中删除该传感器的选项。

请记住,如果您对特定传感器收到相同的错误消息,尤其是对于像风扇这样有运动部件的东西,它可能正在失效,需要更换。

同时也要记住,如果您运行的JLDProbe II使用了我的配置文件,应用程序不会崩溃,但您的传感器不太可能与我的ID相同,所以您会看到很多那些大大的红色X。

待办事项

添加功能以:

  1. 后台记录、跟踪和绘制传感器历史记录,
  2. 存储传感器超出可接受阈值运行的警报日志,以及
  3. 在传感器组件上显示视觉指示器,以警告传感器已触发警报。

这些功能现在已完成,并在另一篇文章中提供:CPU温度、风扇转速等应用,第二部分

历史

  • 2018.07.27
    • 首次实现和发布
  • 2018.07.28
    • 修复了图像未显示的问题
  • 2018.08.10
    • 在“配置传感器”对话框中添加了组件颜色自定义功能。
    • 创建了FontPickerColorPicker控件,以在“配置传感器”对话框中嵌入字体和颜色选择。
    • 在发出警报前添加了3秒延迟:传感器必须连续超过警报阈值3秒后才发出警报。这可以防止信号的瞬间峰值触发警报,造成误报。
  • 2018.10.11
  • 2019.02.20
    • 修复了一些小错误
© . All rights reserved.