文章目录
前言
基于VS2019、Qt5.15.5,纯代码实现信号读取与分析界面。
效果
QT——信号读取与分析
步骤
利用信号的发生时间的唯一性,根据时间来找到以时间命名的文件,读取文件,画出时域图,在进行FFT,获取频域图。
一、报警信号的创建
1. 思路
首先,使用定时器产生报警数据,报警数据是自定义的结构。其次,创建表格,设置表格的上下文菜单策略为自定义。最后,创建动作,连接信号与动作。
2. 代码
2.1 alarmMessageTab.h
#pragma once
#include "ui_alarmMessageTab.h"
#include "qtCommon.h"
#include "alarmSignalWin.h"
class alarmMessageTab : public QWidget
{
Q_OBJECT
public:
alarmMessageTab(QWidget *parent = nullptr);
~alarmMessageTab();
QList<AlarmPoint> alarmPoints;
QTimer* timer = nullptr;
QTableWidget* tableWidget = nullptr; // Declare QTableWidget as a private member variable
QMenu* popMenu = nullptr; //右键菜单
QAction* actionCheckSignalInfo = nullptr;
private:
Ui::alarmMessageTabClass ui;
int timerCount = 0;; // 记录定时器触发次数
private slots:
void generateAlarmPoint(); //产生报警数据
void slotContextMenu(QPoint pos);//右键菜单响应函数
void showSelectedAlarmInfo(int row);//展示所选择行数的信号
};
2.2 alarmMessageTab.cpp
#include "alarmMessageTab.h"
alarmMessageTab::alarmMessageTab(QWidget *parent)
: QWidget(parent)
{
ui.setupUi(this);
this->resize(1200, 600);
// Initialize the timer for generating alarm points
timer = new QTimer(this);
//connect(timer, SIGNAL(timeout()), this, SLOT(generateAlarmPoint()));
connect(timer, &QTimer::timeout, this, &alarmMessageTab::generateAlarmPoint);
timer->start(2000); // Generate alarm point every second
// Create and set up the table widget
tableWidget = new QTableWidget(this);
tableWidget->setColumnCount(4); // 4 columns for time, location, type, channel
QStringList headers;
headers << "Time" << "Location" << "Type" << "Channel";
tableWidget->setHorizontalHeaderLabels(headers);
tableWidget->horizontalHeader()->setSectionResizeMode(QHeaderView::Stretch); // Stretch columns to fill the table
tableWidget->setEditTriggers(QAbstractItemView::NoEditTriggers); // Disable editing of table cells
//设置tableWidget的上下文菜单策略为自定义。这意味着我们将会自己创建一个上下文菜单,并在需要时显示它。
tableWidget->setContextMenuPolicy(Qt::CustomContextMenu);
popMenu = new QMenu(tableWidget);//创建一个新的上下文菜单对象popMenu,并将其与tableWidget关联。
actionCheckSignalInfo = new QAction();
actionCheckSignalInfo->setText(QString::fromLocal8Bit("查看扰动信号"));
popMenu->addAction(actionCheckSignalInfo);
connect(tableWidget, SIGNAL(customContextMenuRequested(QPoint)), this, SLOT(slotContextMenu(QPoint)));
connect(actionCheckSignalInfo, &QAction::triggered, this, [this]() {
int row = tableWidget->currentRow();
showSelectedAlarmInfo(row);
});
QVBoxLayout* layout = new QVBoxLayout(this);
layout->addWidget(tableWidget);
setLayout(layout);
}
alarmMessageTab::~alarmMessageTab()
{
delete timer;
timer = nullptr;
delete actionCheckSignalInfo;
actionCheckSignalInfo = nullptr;
delete popMenu;
popMenu = nullptr;
delete tableWidget;
tableWidget = nullptr;
}
void alarmMessageTab::generateAlarmPoint() {
AlarmPoint newAlarm;
//这里时间格式是为了在表格中显示好看,下面有个转换,是方便存储和读取文件,因为文件命名不能有冒号
newAlarm.time = QDateTime::currentDateTime().toString("yyyy-MM-dd hh:mm:ss");
newAlarm.location = 110.1;
newAlarm.type = 2;
newAlarm.channel = 3;
alarmPoints.prepend(newAlarm);
// Update the UI to display the alarm points
tableWidget->setRowCount(alarmPoints.size());
for (int i = 0; i < alarmPoints.size(); ++i) {
const AlarmPoint& alarm = alarmPoints[i];
QTableWidgetItem* itemTime = new QTableWidgetItem(alarm.time);
QTableWidgetItem* itemLocation = new QTableWidgetItem(QString::number(alarm.location));
QTableWidgetItem* itemType = new QTableWidgetItem(QString::number(alarm.type));
QTableWidgetItem* itemChannel = new QTableWidgetItem(QString::number(alarm.channel));
tableWidget->setItem(i, 0, itemTime);
tableWidget->setItem(i, 1, itemLocation);
tableWidget->setItem(i, 2, itemType);
tableWidget->setItem(i, 3, itemChannel);
}
// 更新定时器触发次数
++timerCount;
// 如果触发次数达到2次,则关闭定时器.也就是产生2条数据
if (timerCount >= 2) {
timer->stop();
}
}
void alarmMessageTab::slotContextMenu(QPoint pos)//这个槽函数是为了显示右键菜单
{
//根据传入的鼠标点击位置 pos 在 tableWidget 上获取对应的索引 index。这个索引表示了鼠标点击位置所在的表格行列。
auto index = tableWidget->indexAt(pos);
//检查获取到的索引是否有效。如果索引有效,表示鼠标点击位置在表格内部,可以显示右键菜单。
if (index.isValid())
{
//调用 popMenu 的 exec 函数来显示上下文菜单。QCursor::pos() 返回当前鼠标光标的全局位置,作为菜单出现的位置。
//这意味着右键菜单将显示在鼠标当前位置附近。
popMenu->exec(QCursor::pos());
}
}
void alarmMessageTab::showSelectedAlarmInfo(int row)
{
if (row >= 0 && row < alarmPoints.size())
{
AlarmPoint& selectedAlarm = alarmPoints[row];
// 下面这句代码,可以用在实时程序,本人已经试过。
//QString Time = QDateTime::fromString(selectedAlarm.time, "yyyy-MM-dd hh:mm:ss").toString("yyyy-MM-dd_hh-mm-ss"); // 将原始时间戳转换为适合作为文件名的格式
//由于没有实时产生的数据,自己找了一段数据,命名为:2024-03-07_08-45-54
QString Time = "2024-03-07_08-45-54";
// 这里是不要创建alarmViewer为 成员变量
alarmSignalWin* alarmViewer = new alarmSignalWin(Time);//这是另一个界面类,下面分析
if (!alarmViewer)
{
// 调试信息:确认 alarmViewer 对象创建失败
//qDebug() << "alarmViewer create failed!";
return;
}
// 调试信息:确认 alarmViewer 对象成功创建
//qDebug() << "alarmViewer create successful!";
alarmViewer->setAttribute(Qt::WA_DeleteOnClose);//界面关闭时,自动销毁对象
alarmViewer->show(); // 显示 AlarmViewer 界面
//下面connect 一方面是为了检查是否销毁。另一方面也可以做其他处理,我暂时还没有其他处理。
connect(alarmViewer, &alarmSignalWin::destroyed, this, [&]() {
//qDebug() << "alarmViewer destroyed!";
//我本来是想在打开界面后,对应行关掉右键菜单功能,界面销毁销毁后对应行恢复右键菜单功能,可惜未实现。
});
}
}
二、报警信号的展示
1. 思路
根据传入的时间参数,找到文件,读取文件,做FFT,显示时域和频域图。
2. 代码
2.1 alarmSignalWin.h
#pragma once
#include "ui_alarmSignalWin.h"
#include <qtCommon.h>
class alarmSignalWin : public QWidget
{
Q_OBJECT
public:
alarmSignalWin(QString& time, QWidget *parent = nullptr);
~alarmSignalWin();
void showAlarmInfo(QString& time);//初始化显示
// 创建布局管理器
QVBoxLayout* mainLayout = nullptr;
QTableWidget* tableWidget = nullptr;
QHBoxLayout* plotLayout = nullptr;
QCustomPlot* m_time_plot = nullptr;
QVBoxLayout* freqVLayout = nullptr;
QCustomPlot* m_freq_plot = nullptr;
QHBoxLayout* controlHLayout = nullptr;
QLabel* rangeLabel = nullptr;
QLineEdit * rangeInput1 = nullptr;
QLabel * dashLabel = nullptr;
QLineEdit * rangeInput2 = nullptr;
QLabel * unitLabel = nullptr;
QPushButton * confirmButton = nullptr;
MatrixXd_RowMajor* file_data = nullptr;//获取的文件数据
MatrixXd_RowMajor* fft_data = nullptr;//文件数据后的FFT数据
MatrixXd_RowMajor* freq_data = nullptr;//频率段数据
void plotTimeGraph(QCustomPlot* m_plot,MatrixXd_RowMajor* file_data);//画时域图函数
void plotFreqGraph(QCustomPlot* m_plot, MatrixXd_RowMajor* fft_data, MatrixXd_RowMajor* freq_data, double range1, double range2);//画频域图
void cal_fft_Matrix(MatrixXd_RowMajor* input_data, MatrixXd_RowMajor* output_data);//fft变换
public slots:
//void on_confirmButton_clicked();//这里需要注意,如果自定义的按钮等控件,这种写法,调试运行时,控制台会有警告,但是程序可运行。
void confirmButtonclicked();//为了不想有警告,自己写槽函数函数
private:
Ui::alarmSignalWinClass ui;
};
2.2 alarmSignalWin.cpp
#include "alarmSignalWin.h"
alarmSignalWin::alarmSignalWin(QString& time, QWidget *parent)
: QWidget(parent)
{
ui.setupUi(this);
// 调整整体界面大小
this->resize(1400, 650);
tableWidget = new QTableWidget(this);//后期将一些信号分析的结果放在表格
tableWidget->setColumnCount(4); // 4 columns for time, location, type, channel
tableWidget->setRowCount(1);
QStringList headers;
headers << QString::fromLocal8Bit( "最高幅值(时域)") <<
QString::fromLocal8Bit("最高能量(所属频带)") <<
QString::fromLocal8Bit("峰值频率及其幅值大小") <<
QString::fromLocal8Bit("重心频率");
tableWidget->setHorizontalHeaderLabels(headers);
tableWidget->horizontalHeader()->setSectionResizeMode(QHeaderView::Stretch); // Stretch columns to fill the table
tableWidget->setEditTriggers(QAbstractItemView::NoEditTriggers); // Disable editing of table cells
// Set the title of the tableWidget to "特征"
tableWidget->setWindowTitle(QString::fromLocal8Bit("特征"));
// 隐藏表格的行标
tableWidget->verticalHeader()->setVisible(false);
// 设置表格固定大小
tableWidget->setFixedHeight(80);
m_time_plot = new QCustomPlot();
m_freq_plot = new QCustomPlot();
//m_time_plot->yAxis->setRange(-5, 5);//设置Y轴范围
m_time_plot->xAxis->setLabel("Time (s)");
m_time_plot->yAxis->setLabel("Amplitude");
m_time_plot->plotLayout()->insertRow(0); // 插入一行用于标题
m_time_plot->plotLayout()->addElement(0, 0, new QCPTextElement(m_time_plot, "Time Domain", QFont("sans", 12, QFont::Bold)));
m_freq_plot->xAxis->setLabel("Frequency (Hz)");
m_freq_plot->yAxis->setLabel("Amplitude");
m_freq_plot->plotLayout()->insertRow(0); // 插入一行用于标题
m_freq_plot->plotLayout()->addElement(0, 0, new QCPTextElement(m_freq_plot, "Frequency Spectrum", QFont("sans", 12, QFont::Bold)));
file_data = new MatrixXd_RowMajor(1, 110000);//设置file_data 长度
// 添加选择频率范围的标签、输入框、按钮
rangeLabel = new QLabel(QString::fromLocal8Bit("选择频率范围:"));
rangeInput1 = new QLineEdit("0");
dashLabel = new QLabel(" - ");
rangeInput2 = new QLineEdit("120");
unitLabel = new QLabel("Hz");
confirmButton = new QPushButton(QString::fromLocal8Bit("确定"));
controlHLayout = new QHBoxLayout();
controlHLayout->addWidget(rangeLabel);
controlHLayout->addWidget(rangeInput1);
controlHLayout->addWidget(dashLabel);
controlHLayout->addWidget(rangeInput2);
controlHLayout->addWidget(unitLabel);
controlHLayout->addWidget(confirmButton);
freqVLayout = new QVBoxLayout();
freqVLayout->addLayout(controlHLayout);
freqVLayout->addWidget(m_freq_plot);
plotLayout = new QHBoxLayout();
// 将两个图形部件添加到水平布局中
plotLayout->addWidget(m_time_plot, 1); // 设置拉伸因子为1,使左侧部件可以拉伸
plotLayout->addLayout(freqVLayout, 1); // 设置拉伸因子为1,使右侧部件可以拉伸
// 创建布局管理器
mainLayout = new QVBoxLayout(this);
// 将表格添加到主布局中
mainLayout->addWidget(tableWidget);
// 将水平布局添加到主布局中
mainLayout->addLayout(plotLayout);
// 设置主布局
setLayout(mainLayout);
connect(confirmButton, &QPushButton::clicked, this, &alarmSignalWin::confirmButtonclicked);
showAlarmInfo(time);
}
alarmSignalWin::~alarmSignalWin()
{
//析构函数也得注意,释放的顺序好像有讲究,先释放单个的,在组合的,大概是这样吧
delete freq_data;
freq_data = nullptr;
delete fft_data;
fft_data = nullptr;
delete file_data;
file_data = nullptr;
delete rangeLabel;
rangeLabel = nullptr;
delete rangeInput1;
rangeInput1 = nullptr;
delete rangeInput2;
rangeInput2 = nullptr;
delete dashLabel;
dashLabel = nullptr;
delete confirmButton;
confirmButton = nullptr;
delete controlHLayout;
controlHLayout = nullptr;
delete m_freq_plot;
m_freq_plot = nullptr;
delete freqVLayout;
freqVLayout = nullptr;
delete m_time_plot;
m_time_plot = nullptr;
delete tableWidget;
tableWidget = nullptr;
delete plotLayout;
plotLayout = nullptr;
delete mainLayout;
mainLayout = nullptr;
}
void alarmSignalWin::showAlarmInfo(QString& time)
{
//QString filename = "E:/code_fiOTDR/other_code/test_matlab/ .dat";
// 根据时间参数拼接文件名!!这里是关键
QString filename = "C:/code_fiOTDR/1/" + time + ".dat";
qDebug() << "filenameTime = " << filename;
QFile file(filename); //以用户给定的文件名创建文件设备file
if (file.open(QIODevice::ReadOnly)) //若设备只写打开成功
{
QDataStream stream(&file);
stream.setByteOrder(QDataStream::LittleEndian); // 设置字节顺序为小端序
int numCols = file_data->cols();//110000 file_data->cols()
int colIndex = 0;
while (!stream.atEnd() && colIndex < numCols)
{
double value;
stream >> value;
(*file_data)(0, colIndex) = value;
colIndex++;
}
file.close();
// 裁剪 Input_data 到实际读取的数据长度(40000)
file_data->conservativeResize(1, colIndex);
//std::cout << "列数 = " << colIndex << std::endl;
//画时域图
plotTimeGraph(m_time_plot,file_data);
//画频域图
// 设置采样频率和采样时间【后期需要读取 配置脉冲周期】,我的数据是这个采样率
double T = 100e-6;
double fs = 1 / T;
int N_test = file_data->cols(); //原始数据长度
int fft_num = floor(file_data->cols() / 2) + 1; //fft长度
//分配fft_data内存
fft_data = new MatrixXd_RowMajor(1, fft_num);
fft_data->setZero();
//傅里叶变换
cal_fft_Matrix(file_data, fft_data);
//分配freq_data内存
freq_data = new MatrixXd_RowMajor(1, fft_num);
freq_data->setZero();
//这里多说一句,fft后,想找到需要的频率是需要计算的
// 计算频率轴,为了找到频率对应的索引值
for (int i = 0; i < fft_num; i++) {
(*freq_data)(0, i) = i * (fs / N_test);
}
//获取初始设置的频率范围,并画图
double range1 = rangeInput1->text().toDouble();
double range2 = rangeInput2->text().toDouble();
plotFreqGraph(m_freq_plot, fft_data, freq_data, range1, range2);
}
}
void alarmSignalWin::plotTimeGraph(QCustomPlot* m_plot,MatrixXd_RowMajor* file_data)
{
// 添加图层
m_plot->addGraph();
// 获取画图数据
QVector<double> x(file_data->cols()), y(file_data->cols());
for (int k = 0; k < file_data->cols(); ++k)
{
y[k] = (*file_data)(0, k);
x[k] = k;
}
//qDebug() << "number of x[i]:" << x.size();
m_plot->graph()->setData(x, y);
// 设置Y轴范围
m_plot->xAxis->setRange(0, file_data->cols());
// 获取最大值和最小值 为了设置Y轴范围
double max_val = file_data->maxCoeff();
double min_val = file_data->minCoeff();
m_plot->yAxis->setRange(min_val, max_val);
//画图
m_plot->replot();
}
void alarmSignalWin::plotFreqGraph(QCustomPlot* m_plot, MatrixXd_RowMajor* fft_data, MatrixXd_RowMajor* freq_data, double range1, double range2)
{
//找到 range1和range2对应的索引
// 找到第一个大于等于range1的值的索引
int range1_index = -1;
for (int i = 0; i < fft_data->cols(); i++) {
if ((*freq_data)(0, i) >= range1) {
range1_index = i;
break;
}
}
if (range1_index != -1) {
std::cout << "第一个大于等于range1的值的索引为: " << range1_index << std::endl;
}
else {
std::cout << "未找到大于等于range1的值" << std::endl;
}
// 找到第一个大于range2的值的索引,然后取前一个,就是最大的小于range2的值的索引
int range2_index = -1;
for (int i = 0; i < fft_data->cols(); i++) {
if ((*freq_data)(0, i) > range2) {
range2_index = i - 1;
break;
}
}
if (range2_index != -1) {
std::cout << "第一个大于range2的值的索引为: " << range2_index << std::endl;
}
else {
std::cout << "未找到大于range2的值" << std::endl;
}
//利用qcustomplot 画出range1_index和range2_index 的test_fft的图,横轴为单位Hz
//清空原来的图层
m_plot->clearGraphs();
// 添加图层
m_plot->addGraph();
// 设置数据
QVector<double> xData, yData;
for (int i = range1_index; i <= range2_index; i++) {
xData.push_back((*freq_data)(0, i));
yData.push_back((*fft_data)(0, i));
}
// 添加数据到图层
m_plot->graph()->setData(xData, yData);
// 设置横纵坐标轴的范围
m_plot->xAxis->setRange(range1, range2); // 设置横坐标轴范围为 range1 到 range2
// 获取最大值和最小值
double max_val = fft_data->maxCoeff();
m_plot->yAxis->setRange(0, max_val);
// 显示图表
m_plot->replot();
}
void alarmSignalWin::confirmButtonclicked() {
double range1 = rangeInput1->text().toDouble();
double range2 = rangeInput2->text().toDouble();
plotFreqGraph(m_freq_plot, fft_data, freq_data,range1, range2);
}
void alarmSignalWin::cal_fft_Matrix(MatrixXd_RowMajor* input_data, MatrixXd_RowMajor* output_data)
{
int fft_num = input_data->cols();//表示输入数据的列数,即要进行FFT的数据点个数
double* fft_Data = new double[input_data->cols()];//创建了一个大小为input_data->cols()的double类型的数组fft_Data
memcpy(fft_Data, input_data->data(), sizeof(double) * fft_num);//并使用memcpy()函数将input_data中的数据复制到fft_Data数组中
int num = floor((double)input_data->cols() / 2) + 1;
fftw_complex* out_cpx;//用于存储FFT结果的复数形式,FFTW_DEFINE_API(FFTW_MANGLE_DOUBLE, double, fftw_complex) fftw3.h文件中
fftw_plan fft;
//为什么是(floor((double)input_data->cols() / 2) + 1)?利用FFT的对称性质,只取一半的频谱(正频率部分或负频率部分),从而将结果表示为实数形式
//通过调用fftw_malloc()函数分配了足够的内存空间,大小为(floor((double)input_data->cols() / 2) + 1)个fftw_complex结构体
out_cpx = (fftw_complex*)fftw_malloc(sizeof(fftw_complex) * (num));
fft = fftw_plan_dft_r2c_1d(fft_num, fft_Data, out_cpx, FFTW_ESTIMATE);//FFTW_MEASURE调用fftw_plan_dft_r2c_1d()函数创建一个一维实数到复数的FFT计划,FFTW_ESTIMATE表示使用估算方法来优化计算
fftw_execute(fft);//调用fftw_execute(fft)函数执行FFT计算
//计算每个频率分量的【幅值】,并除以输入数据的列数,然后将结果存储到output_data中的第1行
for (int i = 0; i < (num); i++)
{
(*output_data)(0, i) = std::sqrt(out_cpx[i][0] * out_cpx[i][0] + out_cpx[i][1] * out_cpx[i][1]) ;//取模
}
fftw_destroy_plan(fft);
fftw_free(out_cpx);
delete[] fft_Data;
}
三、主函数和解决方案资源管理器
#include "alarmMessageTab.h"
#include <QtWidgets/QApplication>
int main(int argc, char *argv[])
{
QApplication a(argc, argv);
alarmMessageTab w;
w.show();
return a.exec();
}
其他设置
为了代码能跑起来,下面是我的一些设置,请参考。
qcustomplot设置
-
先将代码qcustomplot.h 和 qcustomplot.cpp 加入到项目中。
-
头文件 qtCommon.h 去包含。
-
将箭头所指的模块加进去。
-
包含qcustomplot.h 和 qcustomplot.cpp所在文件夹的路径
fftw3设置
- 和上图一样将 fftw3 文件夹所在路径包含
- 头文件 qtCommon.h 去包含。
- 附加依赖项设置
Eigen设置
- 和上上面的图一样 包含eigen-3.4.0 文件夹路径
- 头文件 qtCommon.h 去包含。
调试设置
- 为了看qDebug信息,设置控制台输出
代码和数据下载
我已经将我的代码上传,免费下载!支持开源!!第一次上传资源,本来就想免费的,可是要等待审核。
先放百度网盘的链接,有库和数据文件方便测试,自取哈!
链接:https://pan.baidu.com/s/1ijsoxiR65UBt-3DNb8WZDw?pwd=yyds
提取码:yyds
–来自百度网盘超级会员V6的分享
总结
敲代码的过程遇到的问题:
- 信号的显示界面应该怎么设计?
代码说了,为了动态的展示界面,尽量不要将对象设置成员变量。 - 画图的处理
更改频率范围后,为了让信号更快的呈现,FFT不要每次都做一遍。图的显示,没有设置X和Y的范围不会显示数据。图的标题等一些固定不变的属性,在构造函数内先设置好。点击确定按钮后,重新画图,要删除原来的画布。
简单技巧:
- 将需要用的共用的头文件,宏定义等放在一个头文件。
Go on!