QCustomplot
简介
QCustomPlot是一个用于绘制交互式图表和图形的开源C++库。它为Qt应用程序提供了强大的绘图功能,可用于创建各种类型的图表,如线图、柱状图、散点图、饼图等。
QCustomPlot具有灵活的配置选项,可以自定义图表的外观和行为。该库易于使用且功能强大,适用于需要在Qt应用程序中显示和操作图表数据的开发项目。
官网下载:
Qt Plotting Widget QCustomPlot - Download
使用
源码使用
进入上面官网链接:
下载后得到qcustomplot.h与qcustomplot.cpp:
拷贝到工程目录下,右键 -> 添加现有文件…,将这两个文件添加至工程,即可在项目中使用:
.pro文件中需添加printsupport模块:
编译动态库使用
进入上面官网链接:
下载找到sharedlib-compilation目录:
使用和项目相同的编译器编译生成动态库即可:
例: 我项目使用的是Qt5.15.2_msvc2019, 因此这里也用Qt5.15.2_msvc2019编译生成动态库:
将动态库文件和头文件添加到项目中:
不知如何将动态链接库添加到项目中可参考:
动态链接库(三)–动态链接库的使用_动态链接库怎么调用-CSDN博客
同样在.pro中添加printsupport模块:
最后包含头文件即可使用:
需求
因项目需要展示以时间为x轴、以IP值为Y轴的二维坐标系统,而Qt Charts并没有提供专门的对于IP(Internet Protocol)值这样的特殊需求的坐标轴类型,因此考虑使用QCustomPlot。
具体需求:
现有一个Excel文件,内含两张表格,每个表格都有相应的IP及时间记录。需要分析,两种表格中是否存在同一IP,在同一时间段使用的记录。存在则找出这段使用同一IP的时间交集。
需求实现
获取Excel表格数据
这里使用QAxObject 类来获取表格数据。
typedef struct
{QString qsTime;QString qsIP;
}IP_TIME, *PIP_TIME;bool Widget::readExcel()
{// 打开文件对话框,选择文件QString fileName = QFileDialog::getOpenFileName(nullptr, "Open Excel", QDir::currentPath(), "Excel Files (*.xls *.xlsx)");if (fileName.isEmpty()){return false;}//QVector<QList<IP_TIME>> vlIP_Time; //保存每张表,每行数据的信息vlIP_Time.clear();// 创建连接到Excel的对象QAxObject* excel = new QAxObject("Excel.Application");// 打开工作簿QAxObject* workbooks = excel->querySubObject("WorkBooks");//打开文件QAxObject* workbook = workbooks->querySubObject("Open(QString, QVariant)", fileName, 0);// 获取表格对象集合QAxObject* worksheets = workbook->querySubObject("Worksheets");// 计算工作表数量int worksheetCount = worksheets->dynamicCall("Count()").toInt();// 遍历工作表集合for (int i = 1; i <= worksheetCount; i++){qDebug() << u8"\n =============" << "《 sheet" << i << "》============== \n\n";QList<IP_TIME> qlTmp;// 获取工作表QAxObject* worksheet = worksheets->querySubObject("Item(int)", i);// 获取行数QAxObject* usedRange = worksheet->querySubObject("UsedRange");int rowCount = usedRange->querySubObject("Rows")->property("Count").toInt();// 获取列数int columnCount = usedRange->querySubObject("Columns")->property("Count").toInt();qDebug() << "rowCount: " << rowCount << ", columnCount: " << columnCount << "\n";// 遍历工作表的所有行for (int row = 1; row <= rowCount; row++){//这里有多余数据,强制排除一下if (i == 1 && row >= ui->lineEdit_sheet1Max->text().toInt()){continue;}else if (i == 2 && row >= ui->lineEdit_sheet2Max->text().toInt()){continue;}// 遍历工作表的所有列//表格内容固定:第一列是日期 第二列是时间, 第三列是IP//示例: 2023/7/2 18:07:17 +0800 CST 223.104.68.94QString qsTime;for (int column = 1; column <= columnCount; column++){// 读取单元格内容QAxObject* cell = worksheet->querySubObject("Cells(int,int)", row, column);QString cellValue;if (column == 1){QDateTime cellDateTime = cell->dynamicCall("Value()").toDateTime();cellValue = cellDateTime.toString("yyyy/MM/dd");}else{cellValue = cell->dynamicCall("Value()").toString();}if (cellValue.isEmpty() || cellValue == "IP" || cellValue == "时间"){qDebug() << "cellValue: " << cellValue;continue;}if (column == 3){if (cellValue.isEmpty()){//qDebug() << "Row:" << row << "Column:" << column << "value :" << cellValue;continue;}qsTime = qsTime.left(qsTime.size() - 10);IP_TIME ipTime;ipTime.qsTime = qsTime;ipTime.qsIP = cellValue;qlTmp.append(ipTime);}else{qsTime += cellValue;}// 输出单元格内容//qDebug() << "Row:" << row << "Column:" << column << "value :" << cellValue;}}//end for(int row = 1; row <= rowCount; row++)vlIP_Time.append(qlTmp);}// 关闭工作簿并关闭Excel应用workbook->dynamicCall("Close()");excel->dynamicCall("Quit()");delete excel;return true;
}
上面这个函数让用户选择Excel文件,然后获取每个表格内容到成员变量vlIP_Time中。
注意:
表格的格式需固定为:
第一列是日期 第二列是时间 第三列是IP
2023/7/2 18:07:17 +0800 CST 223.104.68.94
使用QCustomPlot展示数据
代码如下:
void Widget::getAllIP(const QList<IP_TIME>& ql, const QList<IP_TIME>& ql2, QList<QString>& qlOut)
{qlOut.clear();QSet<QString> commonSet; // 使用QSet进行去重for (int i = 0; i < ql.size(); i++){commonSet.insert(ql.at(i).qsIP);}for (int j = 0; j < ql2.size(); j++){commonSet.insert(ql2.at(j).qsIP);}// 转换QSet为QListqlOut = commonSet.values();
}void Widget::dataShow()
{qDebug() << "\n dataShoww \n";plot = new QCustomPlot();mainHLayout->addWidget(plot);plot->setInteractions(QCP::iRangeDrag | QCP::iRangeZoom | QCP::iSelectPlottables);//悬浮显示各节点信息//plot->setInteractions(QCP::iRangeDrag | QCP::iRangeZoom | QCP::iSelectPlottables | QCP::iSelectAxes);//plot->setSelectionTolerance(5); // 根据需要调整选择公差,此处值为15像素//connect(plot, SIGNAL(mouseMove(QMouseEvent*)), this, SLOT(showPointToolTip(QMouseEvent*)));//设置时间x轴QSharedPointer<QCPAxisTickerDateTime> dateTicker(new QCPAxisTickerDateTime);dateTicker->setDateTimeFormat("yyyy/MM/dd hh:mm:ss");plot->xAxis->setTicker(dateTicker);plot->xAxis->setLabel("Time");plot->yAxis->setLabel("IP Address");//QList<QString> qlLabels; //保存vlIP_Time中,所有使用到的IP列表,用于IP值y轴构建qlLabels.clear();for (int i = 0; i < vlIP_Time.size(); i++){if (i + 1 >= vlIP_Time.size()){break;}QList<IP_TIME> ql = vlIP_Time[i];QList<IP_TIME> ql2 = vlIP_Time[i + 1];getAllIP(ql, ql2, qlLabels);}// 使用 'qSort' 函数对 QList 进行排序std::sort(qlLabels.begin(), qlLabels.end());qDebug() << "\n qlLabels size: " << qlLabels.size() << "\n";qDebug() << qlLabels << "\n";QVector<double> ticks;QVector<QString> labels;int nIndex = 0;foreach(const QString & ip, qlLabels){ticks << nIndex;labels << ip;//qDebug() << "ip: " << ip;++nIndex;}QSharedPointer<QCPAxisTickerText> textTicker(new QCPAxisTickerText);textTicker->addTicks(ticks, labels);plot->yAxis->setTicker(textTicker);for (int i = 0; i < vlIP_Time.size(); i++){plot->addGraph();//各节点连接成线//plot->graph(i)->setLineStyle(QCPGraph::lsNone);plot->graph(i)->setScatterStyle(QCPScatterStyle(QCPScatterStyle::ssDisc));if (i == 0){plot->graph(i)->setPen(QPen(Qt::blue, 3));plot->graph(i)->setName("zhang");}else if (i == 1){plot->graph(i)->setPen(QPen(Qt::red, 1));plot->graph(i)->setName("ma");}}//添加数据qDebug() << "\n add data \n";for (int i = 0; i < vlIP_Time.size(); i++){QList<IP_TIME> qlIP_TIME = vlIP_Time[i];QString qsOldIP = qlIP_TIME.first().qsIP;QString qsOldTime;for (int j = 0; j < qlIP_TIME.size(); j++){QString qsIP = qlIP_TIME[j].qsIP;QString qsTime = qlIP_TIME[j].qsTime;//只显示重复IPif (qlLabels.indexOf(qsIP) == -1){continue;}QDateTime dateTime = QDateTime::fromString(qsTime, "yyyy/MM/dd HH:mm:ss");double time = dateTime.toMSecsSinceEpoch() / 1000.0;//qDebug() << "qlTime: " << qsTime << ",qsIP: " << qsIP << ", index: " << qlLabels.indexOf(qsIP);if (qsIP != qsOldIP){//添加辅助点,以更好观察plot->graph(i)->addData(time, qlLabels.indexOf(qsOldIP));}qsOldIP = qsIP;qsOldTime = qsTime;plot->graph(i)->addData(time, qlLabels.indexOf(qsIP));}}plot->rescaleAxes();plot->replot();plot->show();
}
效果如下:
分析相同IP使用时间交集
核心思路是:
将每个IP及其对应的使用时间段保存在一个数据结构里,然后对每一个IP,我们比对所有的使用时间段,检查是否存在交集。
存在4种交集情况:
①
②
③
④:
构造hash:
void Widget::createHashMap()
{//QMap<QString, QList<QPair<QDateTime, QDateTime>>> hashMap; //保存重复IP的每个使用时间段//例:(103.116.122.114, {[2023/07/02 12:01:34, 2023/07/02 15:34:55], [2023/09/12 08:37:22, 2023/09/14 22:30:12]})hashMap.clear();//QList<QString> qlLabels; //保存vlIP_Time中,所有使用到的IP列表qDebug() << "createHashMap--qlLabels size: " << qlLabels.size();for (int i = 0; i < vlIP_Time.size(); i++){QList<IP_TIME> qlTime = vlIP_Time[i];QString qsOldIP = qlTime.first().qsIP;QString qsOldTime = qlTime.first().qsTime;for (int j = 0; j < qlTime.count(); j++){IP_TIME ip_time = qlTime[j];QString qsIP = ip_time.qsIP;QString qsTime = ip_time.qsTime;if (qsIP != qsOldIP){QDateTime dateTime_begin = QDateTime::fromString(qsOldTime, "yyyy/MM/dd HH:mm:ss");QDateTime dateTime_end = QDateTime::fromString(qsTime, "yyyy/MM/dd HH:mm:ss");//qDebug() << "IP: " << qsOldIP << ", begin: " << qsOldTime << ", end: " << qsTime;QPair<QDateTime, QDateTime> pair(dateTime_begin, dateTime_end);if (!qlLabels.contains(qsOldIP)){qsOldIP = qsIP;qsOldTime = qsTime;continue;}//IP变化if (hashMap.contains(qsOldIP)){hashMap[qsOldIP].append(pair);}else{QList<QPair<QDateTime, QDateTime>> qlDate;qlDate.append(pair);hashMap.insert(qsOldIP, qlDate);}qsOldIP = qsIP;qsOldTime = qsTime;}else{}}}
}
计算交集:
QMap<QString, QList<QPair<QDateTime, QDateTime>>> Widget::getOverlappingTimePeriods(QMap<QString, QList<QPair<QDateTime, QDateTime>>>& hashMap)
{QMap<QString, QList<QPair<QDateTime, QDateTime>>> resultMap;QList<QString> keys = hashMap.keys();Logger::writeLog(QString(u8"两个表中都有用到的IP数量为:%1").arg(QString::number(keys.count())));Logger::writeLog(QString(u8"具体如下:"));for (int i = 0; i < keys.count(); i++){QString qsLog = QString(u8"%1: %2").arg(QString::number(i + 1)).arg(keys.at(i));Logger::writeLog(qsLog);}//所有使用到的IP列表qDebug() << "qlLabels: " << qlLabels.size();Logger::writeLog(QString(u8"\n\n"));bool bRet = false;for (QString key : keys){QList < QPair<QDateTime, QDateTime> > qlTime = hashMap[key];for (int i = 0; i < qlTime.size(); i++){QPair<QDateTime, QDateTime> pair = qlTime[i];for (int j = i + 1; j < qlTime.size(); j++){QPair<QDateTime, QDateTime> pair2 = qlTime[j];bool bIn = false;//有时间交集, 计算交集区间QPair<QDateTime, QDateTime> pairSame;if (pair.first > pair2.first && pair.second < pair2.second){/*如下:<-----><------------------->*/qDebug() << u8"交集情况①";Logger::writeLog(u8"交集情况①");pairSame = pair;bIn = true;}else if (pair2.first > pair.first && pair2.second < pair.second){/*如下:<----------------><------>*/qDebug() << u8"交集情况②";Logger::writeLog(u8"交集情况②");pairSame = pair2;bIn = true;}else if (pair2.first > pair.first && pair2.first < pair.second){/* 如下:<----><----->*/qDebug() << u8"交集情况③";Logger::writeLog(u8"交集情况③");pairSame.first = pair2.first;pairSame.second = pair.second;bIn = true;}else if (pair.first > pair2.first && pair.first < pair2.second){/* 如下:<-------><------->*/qDebug() << u8"交集情况④";Logger::writeLog(u8"交集情况④");pairSame.first = pair.first;pairSame.second = pair2.second;bIn = true;}bRet |= bIn;if (bIn){qDebug() << u8"当前比较IP: " << key;qDebug() << u8"表1 begin: " << pair.first << ", pair end: " << pair.second;qDebug() << u8"表2 begin: " << pair2.first << ", pair end: " << pair2.second;qDebug() << u8"===时间交集 begin: " << pairSame.first << ", end: " << pairSame.second << " ===\n";//确保交集必记录Logger::writeLog(QString(u8"当前比较IP: %1").arg(key));Logger::writeLog(QString(u8"表1 IP使用时间区间[%1, %2]").arg(pair.first.toString("yyyy/MM/dd hh:mm:ss")).arg(pair.second.toString("yyyy/MM/dd hh:mm:ss")));Logger::writeLog(QString(u8"表2 IP使用时间区间[%1, %2]").arg(pair2.first.toString("yyyy/MM/dd hh:mm:ss")).arg(pair2.second.toString("yyyy/MM/dd hh:mm:ss")));Logger::writeLog(QString(u8"存在时间交集 [%1, %2]").arg(pairSame.first.toString("yyyy/MM/dd hh:mm:ss")).arg(pairSame.second.toString("yyyy/MM/dd hh:mm:ss")));if (!ui->checkBox_logDetail->isChecked()){Logger::writeLog(u8"\n\n");}if (resultMap.contains(key)){resultMap[key].append(pairSame);}else{QList<QPair<QDateTime, QDateTime>> qlTime;qlTime.append(pairSame);resultMap.insert(key, qlTime);}}else{//qDebug() << u8"=====无时间交集=====";if (ui->checkBox_logDetail->isChecked()){Logger::writeLog(QString(u8"当前比较IP: %1").arg(key));Logger::writeLog(QString(u8"表1 IP使用时间区间[%1, %2]").arg(pair.first.toString("yyyy/MM/dd hh:mm:ss")).arg(pair.second.toString("yyyy/MM/dd hh:mm:ss")));Logger::writeLog(QString(u8"表2 IP使用时间区间[%1, %2]").arg(pair2.first.toString("yyyy/MM/dd hh:mm:ss")).arg(pair2.second.toString("yyyy/MM/dd hh:mm:ss")));Logger::writeLog(u8"=====无时间交集=====");}}if (ui->checkBox_logDetail->isChecked()){Logger::writeLog(u8"\n\n");}}//end for (int j = i + 1; j < qlTime.size(); j++)}}//end for (QString key : keys)if (!bRet){Logger::writeLog(u8"\n=====所有IP均无时间交集=====\n");}return resultMap;
}
详细比对信息记录在log文件中。
测试
测试文件:1.xlsx
ps: 测试文件及说明均放在项目输出目录中,项目完整代码见附录下的仓库链接
构造四段(四中情况)交集:
测试文件:1.xlsx
构造4种交集情况:
①
符合IP:223.104.68.233
sheet1: 2023/07/04 14:13:33 到 2023/07/04 15:01:56
sheet2: 2023/07/04 12:08:04 到 2023/07/04 20:35:45
时间交集:2023/07/04 14:13:33 到 2023/07/04 15:01:56
②
符合IP:223.104.68.66
sheet1:2023/07/03 08:37:41 到 2023/07/04 12:05:08
sheet2: 2023/07/03 16:30:04 到 2023/07/03 16:59:43
时间交集:2023/07/03 16:30:04 到 2023/07/03 16:59:43
③
符合IP:103.116.122.50
sheet1: 2023/07/04 21:12:12 到 2023/07/04 22:32:19
sheet2: 2023/07/04 21:52:32 到 2023/07/04 23:45:31
交集时间:2023/07/04 21:52:32 到 2023/07/04 22:32:19
④
符合IP: 121.35.185.77
sheet1: 2023/07/02 18:07:17 到 2023/07/03 08:37:41
sheet2: 2023/07/02 08:55:56 到 2023/07/02 19:58:16
交集时间:2023/07/02 18:07:17 到 2023/07/02 19:58:16
显示比对结果:
详细比对信息记录:
自测基本满足上述需求。
附录
QCustomPlot官网:
Qt Plotting Widget QCustomPlot - Introduction
QCustomPlot官网下载地址:
Qt Plotting Widget QCustomPlot - Download
动态库使用参考:
动态链接库(三)–动态链接库的使用_动态链接库怎么调用-CSDN博客
完整代码及测试文件:
GitHub - SNAKEpg12138/CoordinateWidget