电能监测上位机的 Qt C++ 实现:界面设计

本文介绍使用 Qt Creator 4.11 实现电能监测上位机的用户界面设计部分。要配置 Qt 开发环境,您可以参考 DirtyRabbit 的文章 Chapter 3 STM32示波器Qt上位机项目简介及开发环境安装

1 项目背景

我国的市电电压为 220Vrms,频率为 50Hz。对于生活中的普遍应用场景来讲,市电通常以电压约 220Vrms 的单相电形式存在,实际的电压会受到电网中发电设备和负载的影响,但保持在合理范围内的电压不会影响电气设备的正常工作。如果要持续监测一台设备的用电情况,并以直观的方式呈现给用户,就需要一款能够在计算机上实时呈现电能相关参数的程序,这些参数包括电压有效值、电流有效值、有功功率、无功功率、功率因数、频率等,有计量需要的还会考虑增加电能计量功能。

本项目可将电能参数中的电压、电流、有功功率、视在功率、功率因数、频率通过数值和折线图的形式直观地展现给用户,能够通过 UART 与下位机建立连接,也能创建服务器供另一个上位机访问,兼顾了现场监控和远程监控的需求。此外,程序还会用折线图的形式呈现本机 CPU、GPU、内存的使用情况,对于监视计算机运行状态有一定帮助。

2 界面规划

程序界面上应有数个显示折线图的区域,与此同时还要能够显示实时数据、监控通讯以及具备必要的控制选项。其中,并不是所有数据都应该特别注意,比如对用电量考虑较多的应着重呈现功率和电能,对电压合格率考虑较多的应着重呈现电压、电流,对关注负载特性的应着重呈现功率因数。本项目着重呈现用电量有关信息,因此功率数据的折线图被安置在界面中央,其它数据的折线图按最易于阅读的形式安置在功率折线图周边,而控制选项和通讯日志安置在界面下方,只有当用户需要调试或者修改设置的时候才会关注这些控件,其它时间则更多地关注折线图。

按照这一设计规则,与计算机有关的折线图控件与电能参数折线图被分别安置在功率折线图的左、右两侧。本程序监视计算机的以下数据:核心使用率、RAM 使用率和温度,且数据同时包括 CPU 和 GPU,因此将同类数据的图线绘制在同一折线图控件中,一共使用 3 个折线图控件。与电能相关的参数除功率外,还有电压有效值、电流有效值、功率因数和频率,且功率分为有功、无功和视在功率。一般而言,电网的频率几乎不会有波动,因此不需要过于在意,而电压有效值、电流有效值则经常需要注意。

基于以上设想,大致设计出如下图所示的界面。在该界面中,最大的是有功功率折线图,左侧是计算机运行信息折线图,右侧和下方是其它电能参数折线图。

UI设计草图

3 Graphics View 的使用

为实现绘制折线图功能,界面中需引入 Graphics View 控件。从 Display Widgets 中拖动相应条目至设计界面中即可放置控件。然而,该控件内的标题、坐标轴和图线都需要使用程序绘制,在设计界面为空白。

3.1 获取 QChart 图表对象

在 Graphics View 的对象中有名为 chart() 的方法,该方法正是用于获取特定控件 QChartViewchartQChartView 继承了 QGraphicsView[1],其方法 chart() 能够返回一个 QChart 对象的指针。调用该 QChart 对象的方法,就能够在对应的控件上添加标题、绘制图象等。本程序中包含多个 Graphics View 控件,因此 QChart 对象也有多个。为便于在程序中调用,使用数组存放 QChart 对象的指针

QChart *chart[6] = {nullptr,nullptr,nullptr,nullptr,nullptr,nullptr};

MainWindow 类的私有成员中。对应地,在 MainWindow 类实例化时初始化该数组:

QChartView *drawObj[6] = {
    ui->graphicsView_Voltage,
    ui->graphicsView_Current,
    ui->graphicsView_VA,
    ui->graphicsView_Power,
    ui->graphicsView_Factor,
    ui->graphicsView_Frequency
};
for(int i=0; i<6; i++) {
    chart[i] = drawObj[i]->chart();
}

之后,还可以调用 QChart 对象的 setRenderHint(QPainter::Antialiasing) 设置图象抗锯齿。

3.2 为 QChart 对象添加标题、坐标轴和折线

要绘制折线图,图中应包含以下几个元素:标题、座标系、折线。标题通过 setTitle() 方法指定,而座标系和折线需要引入一个新的类:QLineSeries。与 QChart 类似,程序中也为其准备一个 QLineSeries * 类型的指针数组,并在 MainWindow 类实例化时初始化。

折线图由一组横轴值唯一的点集描述。在 QLineSeries 中,折线上的点是通过 append(x, y) 逐个添加的,在本程序中,初始化 chart 数组的同时,series 数组亦初始化,且在 chart 之前:

series[i] = new QLineSeries();
series[i]->setPointLabelsVisible(false);
series[i]->append(0, 5);
series[i]->append(1, 4);
series[i]->append(3, 8);
series[i]->append(4, 3);

该初始化过程还向对象中增加了 4 个点,用于在空的控件中绘制示例折线图。在初始化一个 QLineSeries 对象后,相应的 QChart 对象就可以调用它了:

chart[i]->addSeries(series[i]);
chart[i]->createDefaultAxes();

addSeries() 方法用于向 QChart 对象中增加图象,即 QLineSeries 对象。要使两个或更多图象在同一图象中绘制,只需继续调用该方法即可;如果需要重新绘制,可以调用 removeAllSeries() 方法移除所有图象。基于已添加的图象,createDefaultAxes() 方法能够为图象建立坐标轴,并覆盖现有的坐标轴。该坐标轴还可通过访问 axisX()axisY() 后获得对象,调用该对象的方法调整坐标轴:

chart[i]->axisX()->setRange(min, max);
chart[i]->axisY()->setRange(min, max);

以上源代码能够调整横、纵坐标轴的表示范围至 minmax。由于 createDefaultAxes() 方法会使图象尽可能大地呈现在控件中,为了保证图象在界面上稳定呈现,避免出现坐标轴范围忽大忽小的情况,编写刷新程序时需要通过该方法微调由 createDefaultAxes() 方法建立的座标系。

根据以上内容编写控件初始化程序,运行后控件中会出现如图所示的图象。

控件中绘制演示图象

4 连接选项界面实现

程序界面的右下方是设置连接选项的界面。本程序支持 UART 连接和 TCP 客户端两种模式,界面中用 Host 和 Remote 称呼,两种模式的设置界面不会同时出现。在本程序中,使用何种方式是由两个互锁的单选框决定的。本程序在同一区域同时安置了 UART 设置和 TCP 客户端设置两组控件,在平面上相互重叠,并由程序控制其出现或消失,保证任一时刻都有且只有一组设定出现。

本程序默认使用 UART 连接,因此在 MainWindow 类实例化时隐藏 TCP 客户端设置的控件,并使单选框选中 UART 连接,即向其构造函数中添加

ui->radioButtonHost->setChecked(true);
ui->radioButtonRemote->setChecked(false);

ui->textEditIP->setVisible(false);
ui->textEditPort->setVisible(false);
ui->ButtonOpenClose->setVisible(false);

ui->IDC_SearchPortBtn->setVisible(true);
ui->IDC_OpenClosePortBtn->setVisible(true);
ui->portNameBox->setVisible(true);
ui->portSpeedBox->setVisible(true);

ui->groupBox->setTitle("串口设置");

并为两个单选框编写 clicked() 槽函数,使其能够控制设置界面的切换:

void MainWindow::on_radioButtonHost_clicked()
{
    ui->textEditIP->setVisible(false);
    ui->textEditPort->setVisible(false);
    ui->ButtonOpenClose->setVisible(false);
    ui->IDC_SearchPortBtn->setVisible(true);
    ui->IDC_OpenClosePortBtn->setVisible(true);
    ui->portNameBox->setVisible(true);
    ui->portSpeedBox->setVisible(true);
    ui->groupBox->setTitle("串口设置");
    RecLenth=60;
    PowerInfo->clear();
    ValRec->clear();
    CmptInfo->clear();
    CmptRec->clear();
    if(tcpClient)//先关掉Client
    {
        disconnect(tcpClient,&QTcpSocket::connected,this,&MainWindow::on_TCP_Connect);
        disconnect(tcpClient,&QTcpSocket::readyRead,this,&MainWindow::on_TCP_Read);
        disconnect(tcpClient,&QTcpSocket::disconnected,this,&MainWindow::on_TCP_Disconnect);
        if(tcpClient->isOpen())
        {
            tcpClient->disconnectFromHost();
            tcpClient->close();
        }
        ui->ButtonOpenClose->setText("连接");
        delete tcpClient;
        tcpClient=nullptr;
    }
}

void MainWindow::on_radioButtonRemote_clicked()
{
    ui->textEditIP->setVisible(true);
    ui->textEditPort->setVisible(true);
    ui->ButtonOpenClose->setVisible(true);
    ui->IDC_SearchPortBtn->setVisible(false);
    ui->IDC_OpenClosePortBtn->setVisible(false);
    ui->portNameBox->setVisible(false);
    ui->portSpeedBox->setVisible(false);
    ui->groupBox->setTitle("远程设置");
    RecLenth=120;
    PowerInfo->clear();
    ValRec->clear();
    CmptInfo->clear();
    CmptRec->clear();
    //关闭串口
    if(serial_flag)
    {
        ui->portNameBox->setEnabled(true); //串口号下拉按钮使能工作
        myserial->close();
        ui->IDC_OpenClosePortBtn->setText("打开串口"); //按钮显示“打开串口”
        serial_flag = false; //串口标志位置工作
    }
    if(tcpClient == nullptr)
    {
        tcpClient=new QTcpSocket(this);
        connect(tcpClient,&QTcpSocket::connected,this,&MainWindow::on_TCP_Connect);
        connect(tcpClient,&QTcpSocket::readyRead,this,&MainWindow::on_TCP_Read);
    }
}

与此同时,还要对应地关闭相关资源,比如在切换到 TCP 模式时关闭 UART。

参阅:

[1] QChartView Class