系列文章目录
文章目录
前言
曲线图数据处理离不开游标的使用,利用游标可以进行区间选择,对该区间的数据进行随意处理,例如计算该区间的平均值、面积、方差、拖动、移动等操作。在 Qt 中,曲线图是一种常见的数据可视化方式,它可以用来展示随时间或其他变量而变化的数据。为了提供更多交互性和准确性,Qt 的曲线图通常支持游标功能。
一、什么是游标?
游标是一个可移动的垂直线或标记,用于在曲线图上指示特定数据点或位置。用户可以通过拖动游标,精确查看曲线图上不同数据点的数值。游标可以帮助用户分析曲线图中的数据,并提供更精确的读取和放大功能。
游标的功能:
- 数据展示:游标可以在曲线图上沿着 X 轴(时间轴)或 Y 轴(数值轴)移动,以显示所选数据点的具体数值。
- 交互操作:用户可以通过鼠标拖动游标来选择感兴趣的数据点并获取详细信息。
- 数据对比:通过添加多个游标,用户可以同时比较不同数据点之间的数值差异,从而更好地理解曲线图中的趋势和关系。
二、QCustomPlot中游标的实现。
1.QCPItemStraightLine
QCPItemStraightLine 是 Qt 的 QCustomPlot 库中用于在曲线图中添加直线标记的类。它通过设置起点和终点坐标来确定直线的位置和长度,并支持自定义样式和交互行为。通过使用 QCPItemStraightLine,您可以在曲线图中增加参考线或标记特定位置,以提高数据可视化和用户交互性。
代码如下:
//定义全局变量
QCPItemStraightLine *m_lineLV; //左游标
QCPItemStraightLine *m_lineRV; //右游标
QCPItemStraightLine *m_lineH; //水平游标
/*******************
* @author xxx
* @method
* @param
* 初始化游标
*******************/
void Integrate::PlotWaterfall()
{
try {
//默认曲线选择
graph = customPlot->graph(0);
//设置游标初始位置(可以不设置)
QCPRange keyRange = customPlot->axisRect()->axis(QCPAxis::atBottom)->range();
xValue1 = keyRange.upper * 0.2;
xValue2 = keyRange.upper * 0.7;
QCPRange keyRange2 = customPlot->axisRect()->axis(QCPAxis::atLeft)->range();
yValue = keyRange2.lower;
auto iter1 = graph->data()->findBegin(xValue1);
xValue1 = iter1->mainKey();
auto iter2 = graph->data()->findBegin(xValue2);
xValue2 = iter2->mainKey();
//左游标
QPen pen(graph->pen().color(),1.5,Qt::SolidLine); //颜色 、宽度、样式(直线)
m_lineLV = new QCPItemStraightLine(customPlot);
m_lineLV->setLayer("overlay");
m_lineLV->setPen(pen);//设置游标线的样式
m_lineLV->setClipToAxisRect(true);//自适应范围
m_lineLV->point1->setCoords(xValue1, 1);//起点坐标
m_lineLV->point2->setCoords(xValue1, 1);//终点坐标
m_lineLV->setVisible(true);
//右游标
m_lineRV = new QCPItemStraightLine(customPlot);
m_lineRV->setLayer("overlay");
m_lineRV->setPen(pen);
m_lineRV->setClipToAxisRect(true);
m_lineRV->point1->setCoords(xValue2, 1);
m_lineRV->point2->setCoords(xValue2, 1);
m_lineRV->setVisible(true);
//水平游标
m_lineH = new QCPItemStraightLine(customPlot);
m_lineH->setLayer("overlay");
m_lineH->setPen(pen);
m_lineH->setClipToAxisRect(true);
m_lineH->point1->setCoords(0, yValue);
m_lineH->point2->setCoords(keyRange.upper, yValue);
m_lineH->setVisible(true);
//编辑框显示初始位置
ui->lineEdit_2->setText(QString::number(m_lineLV->point1->key()));
ui->lineEdit_3->setText(QString::number(m_lineRV->point1->key()));
ui->lineEdit_4->setText(QString::number(m_lineH->point1->value()));
customPlot->replot();
} catch (...) {
FERROR("游标初始化异常!");
}
}
至此在图像上就可以生成看到三条游标了,但是此时的游标并不能进行鼠标操作,貌似没有封装好的方法提供,需要手写。
三、游标的鼠标事件。
代码如下(示例):
//建立槽函数
connect(customPlot, SIGNAL(mousePress(QMouseEvent*)), this, SLOT(mousePress(QMouseEvent*)));
connect(customPlot, SIGNAL(mouseMove(QMouseEvent*)), this, SLOT(mouseMove(QMouseEvent*)));
connect(customPlot, SIGNAL(mouseRelease(QMouseEvent*)), this, SLOT(mouseRelease(QMouseEvent*)));
1.鼠标点击事件
/*******************
* @author xxx
* @method
* @param
* 鼠标抓取事件 点击曲线游标显示
*******************/
void Integrate::mousePress(QMouseEvent *event)
{
try {
//定义鼠标距离游标的像素距离
double distanceLV = m_lineLV->selectTest(event->pos(), false);
double distanceRV = m_lineRV->selectTest(event->pos(), false);
double distanceH = m_lineH->selectTest(event->pos(), false);
//定义游标标识,由上面的距离来决定移动那根游标
if (event->button() == Qt::LeftButton && distanceLV <= 5 && customPlot->axisRect()->rect().contains(event->pos())){
lLineV = true;
}
if (event->button() == Qt::LeftButton && distanceRV <= 5 && customPlot->axisRect()->rect().contains(event->pos())){
rLineV = true;
}
if (event->button() == Qt::LeftButton && distanceH <= 5 && customPlot->axisRect()->rect().contains(event->pos())){
bLineH = true;
}
//左右游标重合时,先移动右游标(非必须)
if(event->button() == Qt::LeftButton){//鼠标左键判断
if(customPlot->axisRect()->rect().contains(event->pos())){//鼠标点在图像内
if(distanceLV <= 5 && distanceRV <= 5){//重合时
//这里获取曲线的最后一个值,以判断重合位置是否在末尾,如果是则先移动左游标,否则都先移动右游标
double end = tempXpoinLists[subscript].last();
double now = m_lineLV->point1->key();
if(now == end){
lLineV = true;
rLineV = false;
}else{
lLineV = false;
rLineV = true;
}
}
//三线重合时,先移动水平游标(非必须)
if(distanceLV <= 5 && distanceRV <= 5 && distanceH <= 5){
lLineV = false;
rLineV = false;
bLineH = true;
}
}
}
customPlot->replot();
} catch (...) {
FERROR("系统发异常!");
}
}
2.鼠标移动事件
/*******************
* @author xxx
* @method
* @param
* 游标移动
*******************/
void Integrate::mouseMove(QMouseEvent *event)
{
try {
//当前鼠标位置(像素坐标)
double x_pos = event->pos().x();
double y_pos = event->pos().y();
//像素坐标转成实际的x,y轴的坐标
double x_val = customPlot->xAxis->pixelToCoord(x_pos);
double y_val = customPlot->yAxis->pixelToCoord(y_pos);
//获取当前曲线上最近的X轴点坐标
auto iter = graph->data()->findBegin(x_val);
x_val = iter->mainKey();
//获取当前游标X轴的坐标
double r_point = m_lineRV->point1->key();
double l_point = m_lineLV->point1->key();
//获取距离
double distanceLV = m_lineLV->selectTest(event->pos(), false);
double distanceRV = m_lineRV->selectTest(event->pos(), false);
double distanceH = m_lineH->selectTest(event->pos(), false);
//当鼠标在游标上时改变鼠标形状
if (distanceLV <= 5 || distanceRV <= 5 || distanceH <= 5 && customPlot->axisRect()->rect().contains(event->pos())){
customPlot->setCursor(Qt::ClosedHandCursor);
}else{
customPlot->setCursor(Qt::ArrowCursor);
}
//游标移动判断
if(lLineV)
{//移动左游标
if(x_val > r_point){ //跟右游标左判断,限制边界
m_lineLV->point1->setCoords(r_point, customPlot->yAxis->range().lower);
m_lineLV->point2->setCoords(r_point, customPlot->yAxis->range().upper);
}else {
m_lineLV->point1->setCoords(x_val, customPlot->yAxis->range().lower);
m_lineLV->point2->setCoords(x_val, customPlot->yAxis->range().upper);
}
ui->lineEdit_2->setText(QString::number(m_lineLV->point1->key()));
customPlot->replot();
}
if(rLineV)
{
if (x_val < l_point) {
m_lineRV->point1->setCoords(l_point, customPlot->yAxis->range().lower);
m_lineRV->point2->setCoords(l_point, customPlot->yAxis->range().upper);
}
else {
m_lineRV->point1->setCoords(x_val, customPlot->yAxis->range().lower);
m_lineRV->point2->setCoords(x_val, customPlot->yAxis->range().upper);
}
ui->lineEdit_3->setText(QString::number(m_lineRV->point1->key()));
customPlot->replot();
}
if(bLineH)
{
m_lineH->point1->setCoords(customPlot->xAxis->range().lower,y_val);
m_lineH->point2->setCoords(customPlot->xAxis->range().upper,y_val);
ui->lineEdit_4->setText(QString::number(m_lineH->point1->value()));
customPlot->replot();
}
//进行相应的计算
if(lLineV || rLineV || bLineH){
calculate_integral();
calculate_average();
}
} catch (...) {
FERROR("系统发异常!");
}
}
3.鼠标释放事件
/**
* @brief Average::mouseRelease
* @param e
* 鼠标释放事件 关闭游标移动
*/
void Integrate::mouseRelease(QMouseEvent *evet)
{
lLineV = false;
rLineV = false;
bLineH = false;
}
四、 游标的应用场景
1. 曲线平均值计算
/*******************
* @author xxx
* @method
* @param
* 平均值计算
*******************/
void Integrate::calculate_average()
{
try {
//1、获取左右游标位置坐标
double r_point = m_lineRV->point1->key();
double l_point = m_lineLV->point1->key();
//曲线点
int begin = 0, end = 0;
double sum = 0;
//2、通过两个坐标点获取分别获取在曲线数据点中的下标位置
for (int i = 0; i < tempXPoinLists[subscript].length(); ++i) {
if(tempXPoinLists[subscript][i] == l_point){
begin = i;
}
if(tempXPoinLists[subscript][i] == r_point){
end = i;
break;
}
}
//3、根据两下标位置获取其之间的所有值 再计算平均值
int n = end - begin + 1;
for (int i = begin; i <= end; ++i) {
sum += tempYPointLists[subscript][i];//曲线中的数据总和
}
averge = sum / n;//计算出平均值
ui->avergrValue->setText(QString::number(averge,'E',2));
ui->label_9->setText(QString::number(n));
int range = tempXPoinLists[subscript][end] - tempXPoinLists[subscript][begin];
ui->label_11->setText(QString::number(range));
} catch (...) {
FERROR("系统发异常!");
}
}
2.计算面积
/*******************
* @author xxx
* @method
* @param
* 面积值计算
*******************/
void Integrate::calculate_integral()
{
try {
//1、获取两游标当前的坐标点
int begin = 0, end = 0;
double r_point = m_lineRV->point1->key();
double l_point = m_lineLV->point1->key();
//2、根据坐标点找到曲线种对应的数据点位置 begin是曲线种第一个点位置 end是最后一个点位置
for (int i = 0; i < tempXPoinLists[subscript].length(); ++i) {
if(tempXPoinLists[subscript][i] == l_point){
begin = i;
}
if(tempXPoinLists[subscript][i] == r_point){
end = i;
break;
}
}
int n = end - begin +1;
//我这里采用的是梯形计算法,也可以采用积分等面积算法进行计算,根据自己的需求选择
//3、根据这两个点位置区间计算所有的形成梯形面积 面积公式:(上底 + 下底)* 高 / 2;
double upper; //上底
double lower; //下底
double high; //高
double area = 0; //面积
for (int i = begin; i < end; ++i) {
upper = tempYPoinLists[subscript][i];
lower = tempYPoinLists[subscript][i+1];
high = tempXPoinLists[subscript][i+1] - tempXPoinLists[subscript][i];
area += (upper+ lower)*high / 2;
}
ui->integralValue->setText(QString::number(area,'E',2));
} catch (...) {
FERROR("系统发异常!");
}
}
总结
本文讲述了在QCustomPlot中建立游标和游标的鼠标事件以及游标使用的一些简单场景,游标的框架使用的是QCustomPlot中的QCPItemStraightLine库,该库的本意是创建一个直线项目创建的项目将自动注册到父绘图中。除此之外,还有其他绘制游标的类,大家可多多尝试。