QCustomPlot(以下简称QCP)绝对是使用Qt开发时绘制数据曲线的福利工具,简单的几行代码就能很好地绘制2d数据曲线。
但有时有这样的应用需求:曲线绘制好了,用户需要利用鼠标在图上移动来拾取曲线上的数据。这个需求可以通过在数据曲线上设置游标,使游标响应鼠标移动来解决。
怎么做到呢?csdn上有部分文章,我觉得有的讲得复杂,有的也没讲清楚。
下面讲一下我的实践情况,希望对大家有帮助。
一、绘图,并添加游标(tracer)和游标数据说明(tracerLabel)
以使用QWidget为例。继承一个QWidget,加入QCP的指针作为成员(cmPlot),它就是负责绘制曲线的对象,可以称之为绘制器。为了实现随鼠标移动的游标功能,还要加入QCPItemTracer的指针(tracer)和QCPItemText的指针(tracerLabel)作为成员,前者就是游标,后者用于显示游标在曲线上的数据。代码如下:
widget.h
#include <QWidget>
#include <QMouseEvent>
#include "../QCustomPlot.h"
class Widget : public QWidget
{
Q_OBJECT
public:
Widget(QWidget *parent = 0);
~Widget();
public slots:
void mouseMove1(QMouseEvent *e);
void mouseMove2(QMouseEvent *e);
private:
QCustomPlot *cmPlot;
QCPItemTracer *tracer;
QCPItemText *tracerLabel;
};
#endif // WIDGET_H
说明一下,widget.h中还申明了两个槽函数
mouseMove1() 和 mouseMove2()
这是两个实现游标的方法,这两个方法不兼容,使用其中一个,就不要用另外一个,使用时根据情况选择。具体情况后续会介绍。
一个简单的绘制数据曲线的方法就是实现widget的构造函数,如下:
Widget::Widget(QWidget *parent)
: QWidget(parent),
cmPlot(new QCustomPlot)
{
QVBoxLayout *vbox = new QVBoxLayout(this); //生成一个布局
vbox->addWidget(cmPlot); //把绘制器加入布局
QPushButton *btn = new QPushButton(QString("This is a push button"), this); //生成一个按钮,没有什么用,配相而已
vbox->addWidget(btn); //把按钮加入布局
setGeometry(100, 100, 800, 500);
QVector<double> xs, ys; //要绘制的数据
for(double x=0.0; x<2.0*M_PI; x+=M_PI/200)
{
//看得出来,数据就是典型正弦曲线上的数据
xs.append(x);
ys.append(sin(x));
}
cmPlot->xAxis->setRange(-0.2, 2.0*M_PI+0.2);
cmPlot->yAxis->setRange(-1.2, 1.2);
cmPlot->addGraph();
cmPlot->graph(0)->setData(xs, ys); //把数据加入到绘制器cmPlot,绘制器会自动绘制曲线
tracer = new QCPItemTracer(cmPlot); //生成游标
//下面的代码就是设置游标的外观
tracer->setPen(QPen(Qt::red));
tracer->setBrush(QBrush(Qt::red));
tracer->setStyle(QCPItemTracer::tsCircle);
tracer->setSize(20.0);
tracerLabel = new QCPItemText(cmPlot); //生成游标说明
//下面的代码就是设置游标说明的外观和对齐方式等状态
tracerLabel->setLayer("overlay");
tracerLabel->setPen(QPen(Qt::red));
tracerLabel->setPositionAlignment(Qt::AlignLeft | Qt::AlignTop);
//下面这个语句很重要,它将游标说明锚固在tracer位置处,实现自动跟随
tracerLabel->position->setParentAnchor(tracer->position);
//这里的信号-槽连接语句很重要
connect(cmPlot, SIGNAL(mouseMove(QMouseEvent*)), this, SLOT(mouseMove1(QMouseEvent*)));
//connect(cmPlot, SIGNAL(mouseMove(QMouseEvent*)), this, SLOT(mouseMove2(QMouseEvent*)));
}
上述代码大部分都好懂。重点就是信号-槽的连接部分,下面说一下。
二、通过信号-槽连接来处理鼠标事件(获得鼠标位置)
一般来说,要获得鼠标移动时的实时位置,一般可以通过改写widget的事件函数 mouseMoveEvent() 来实现。但是如果直接这样做的话,实际上不可行,因为鼠标是在绘制器(cmPlot)上移动时,该事件已经由绘制器处理了,这造成widget的鼠标移动事件函数得不到执行。
要处理这个情况也很简单。当绘制器处理鼠标移动时,它会发送 mouseMove(QMouseEvent*) 信号,只要为widget设计槽函数来接受这个信号,widget 就能够处理鼠标事件了。
所以widget.h中申明了两个槽函数
mouseMove1() 和 mouseMove2()
并用connect语句将cmPlot发出的信号mouseMove(QMouseEvent*)连接到上述两个槽函数上。
如前所述,这两个槽函数代表两种实现方法,而且不兼容,所以实现其中一个,就要将另外一个注释掉,切记。
三、方法1:手动设置tracer的位置
在槽函数mouseMove1()中实现方法1。
void Widget::mouseMove1(QMouseEvent *e)
{
//获得鼠标位置处对应的横坐标数据x
double x = cmPlot->xAxis->pixelToCoord(e->pos().x());
double xValue, yValue;
//xValue就是游标的横坐标
xValue = x;
//yValue就是游标的纵坐标,这里直接根据产生数据的函数(sin)获得
yValue = sin(xValue);
//下面设置游标(tracer)的位置
tracer->position->setCoords(xValue, yValue);
//设置游标说明(tracerLabel)的内容
tracerLabel->setText(QString("x = %1, y = %2").arg(xValue).arg(yValue));
cmPlot->replot();//绘制器一定要重绘,否则看不到游标位置更新情况
}
上面代码也比较好懂。说明一下,这种方法是手动自由设置游标的位置,这个位置可以跟曲线数据没有什么关系。之所以我们可以看到游标粘附在曲线上,是因为我们获得了xValue,并利用xValue和曲线函数计算出了yValue,再利用xVaule和yValue设置了游标的位置,这样游标就粘附在曲线上了,并跟随鼠标移动。如下:
这种方法适合于函数比较简单的情况(比如本例的sin),计算yValue时计算开销不大,鼠标移动时响应比较迅速。如果函数形式比较复杂,计算量大,那么应该采用以下的方法2。
四、方法2:将tracer自动粘附到曲线
在槽函数 mouseMove2() 中实现方法2。代码如下:
void Widget::mouseMove2(QMouseEvent *e)
{
//获得鼠标位置处对应的横坐标数据x
double x = cmPlot->xAxis->pixelToCoord(e->pos().x());
//下面的代码很关键
tracer->setGraph(cmPlot->graph(0)); //将游标和该曲线图层想连接
tracer->setGraphKey(x); //将游标横坐标(key)设置成刚获得的横坐标数据x
tracer->setInterpolating(true); //游标的纵坐标可以通过曲线数据线性插值自动获得(这就不用手动去计算了)
tracer->updatePosition(); //使得刚设置游标的横纵坐标位置生效
//以下代码用于更新游标说明的内容
double xValue = tracer->position->key();
double yValue = tracer->position->value();
tracerLabel->setText(QString("x = %1, y = %2").arg(xValue).arg(yValue));
cmPlot->replot(); //不要忘了重绘
}
这个方法也比较简单,可以自动根据曲线数据给出游标位置,就不用再算一次了,节省了计算开销。
全文完,上述经验希望对大家有用。