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的时间交集。
这里使用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.94
QString 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
代码如下:
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为QList
qlOut = 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;
//只显示重复IP
if (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,我们比对所有的使用时间段,检查是否存在交集。
存在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博客
完整代码及测试文件: