许多时候,我们都会用图表显示数据,便于清晰直观的呈现数据走势和变化。某些环境下,更希望能够在图表上拖拽曲线,达到修改数据的目的。之前,使用海思PQ调试工具时,发现gamma模块的曲线拖拽功能做的很好,便用Qt做了一个曲线拖拽的demo(如图1)。一来可以了解海思工具中拖拽曲线的实现方式,二来可以加深对Qt图表操作的理解。
该demo具备以下功能:
- 鼠标在图表中移动时,呈现十字光标状态,并显示鼠标当前位置的坐标值。
- 鼠标为十字光标时,按下左键,选择矩形区域进行放大。单击鼠标右键恢复图表大小(如图2所示)。
- 鼠标移动到数据点附近,光标切换为垂直调整光标,并将数据点的坐标显示在右上角的X,Y编辑框中。
- 鼠标为垂直调整光标时,按下左键,可拖动数据点,调整数据点的纵坐标(如图3所示)。
- 捕获到数据点时,可在Y编辑框中编辑该数据点的纵坐标,点击set按钮后生效(如图4所示)。
- 选中“Force Correctness on Drag”时,当前点的纵坐标yn会被限制在[yn-1,yn+1]范围内。
- 改变Curve Type的类型,在图表上显示不同类型的曲线(如图5所示)。
为了能够实现鼠标拖动曲线的功能,需要在QChartView组件中对鼠标事件进行处理。因此需要自定义一个QChartView继承的类,实现鼠标拖动功能。在文中通过自定义CurveChartView类处理鼠标操作,其定义如下。
#ifndef _CURVECHARTVIEW_H
#define _CURVECHARTVIEW_H
#include <QtCharts/QChartView>
#include <QtCharts/QScatterSeries>
#include <QtCharts/QSplineSeries>
#include <QtCore/QtMath>
QT_CHARTS_USE_NAMESPACE
class CurveChartView : public QChartView
{
Q_OBJECT
public:
CurveChartView(QWidget *parent = Q_NULLPTR);
~CurveChartView();
void setForceCorrectnessOnDrag(bool status); // 限制拖动点范围
void setSeriesIndex(int index); // 更改curve type类型
void setCurrentPointValue(const QPointF &value); // 修改当前坐标数据
void setSeriesList(QList<QScatterSeries *> sList,
QList<QSplineSeries *> lList); // 设置曲线数据
void setYRange(int minVal, int maxVal); // Y轴范围
protected:
void mousePressEvent(QMouseEvent *event);
void mouseMoveEvent(QMouseEvent *event);
void mouseReleaseEvent(QMouseEvent *event);
private:
bool forceCorrectnessFlag; // 拖动范围限制标志
bool dragPointFlag; // 拖动数据点标志
QPoint beginPoint; // 选择矩形区域的起点
QPoint endPoint; // 选择矩形区域的重点
int pointIndex; // 当前数据点序号
int seriesIndex; // 当前曲线类型
int dataCount; // 数据点个数
int minValue; // Y轴最小值
int maxValue; // Y轴最大值
QList<QScatterSeries *> scatterList; // 散点序列集合
QList<QSplineSeries *> lineList; // 曲线序列集合
qreal distance(const QPointF &p1, const QPointF &p2); // 计算距离
void detectDragPoint(const QPointF &point); // 捕获待拖动的数据点
void updateSeriesData(const QPointF &point); // 更新数据点数据
signals:
void signalMouseMovePoint(QPoint point); // 鼠标移动信号
void signalCurrentDragPoint(QPointF point); // 数据点拖动信号
};
#endif
在设计UI界面时,按照图1所示效果进行布局,并将用于显示图表的Widget控件提升为CurveChartView类。
在图表范围内,鼠标默认采用十字光标。此状态按下鼠标,会将拖拽模式设置为橡皮筋模式,并记录坐标;释放鼠标,则会记录鼠标释放坐标,放大预览。当点击鼠标右键时,可将预览恢复到初始状态。
在十字光标状态下,移动鼠标靠近数据点时,鼠标会被数据点捕获,由十字光标切换为垂直调整光标状态。在垂直调整光标状态下,拖动数据点,即可改变当前数据点的值。
/* 按下鼠标 */
void CurveChartView::mousePressEvent(QMouseEvent *event)
{
QCursor currentCusor = this->cursor();
if (event->button() == Qt::LeftButton) // 左键按下
{
// 十字光标状态
if (currentCusor.shape() == Qt::CrossCursor)
{
this->setDragMode(QGraphicsView::RubberBandDrag);
beginPoint = event->pos();
}
else // 垂直调整光标
{
this->setDragMode(QGraphicsView::NoDrag);
dragPointFlag = true;
}
}
QChartView::mousePressEvent(event);
}
/* 移动鼠标 */
void CurveChartView::mouseMoveEvent(QMouseEvent *event)
{
QPoint point = event->pos();
emit signalMouseMovePoint(point);
if (dragPointFlag) // 检测是否正在拖点
{
this->setCursor(Qt::SizeVerCursor);
updateSeriesData(point); // 更新拖动点数值
}
else
{
this->setCursor(Qt::CrossCursor);
detectDragPoint(point); // 捕获拖动点
}
QChartView::mouseMoveEvent(event);
}
/* 释放鼠标 */
void CurveChartView::mouseReleaseEvent(QMouseEvent *event)
{
QCursor currentCusor = this->cursor();
if (event->button() == Qt::LeftButton)
{
if (currentCusor.shape() == Qt::CrossCursor)
{ // 左键释放,十字光标,获取选择矩形终点,缩放图像
endPoint = event->pos();
QRectF rectF;
rectF.setTopLeft(this->beginPoint);
rectF.setBottomRight(this->endPoint);
this->chart()->zoomIn(rectF);
}
else
{
dragPointFlag = false;
}
}
else if (event->button() == Qt::RightButton)
{ // 右键释放,恢复图表大小
this->chart()->zoomReset();
}
QChartView::mouseReleaseEvent(event);
}
鼠标移动时,会计算鼠标所在位置与曲线数据之间的距离,当某个数据点与鼠标所在位置的距离小于设定的阈值时,便会将距离最近的数据点记为可拖动的数据点。
/*
描述: 捕获拖动点
*/
void CurveChartView::detectDragPoint(const QPointF &point)
{
// check data
if (scatterList.isEmpty() || lineList.isEmpty())
return;
// detect drag point
QPointF curPoint = this->chart()->mapToValue(point);
QScatterSeries *curSeries = scatterList.at(seriesIndex);
QVector<QPointF> seriesData = curSeries->pointsVector();
for (int i = 0; i < seriesData.count(); i++)
{
if (distance(curPoint, seriesData.at(i)) <= 1) // 距离检测
{
pointIndex = i;
emit signalCurrentDragPoint(seriesData.at(i)); // 标记拖动点
this->setCursor(Qt::SizeVerCursor); // 切换光标状态
break;
}
}
}
效果示意:
说明:
上述功能,采用VS2013+Qt5.8环境编译,且验证通过。由于代码较多,在博客中贴全部源码会显得冗余,且占篇幅。故将完整代码上传GitHub,其地址为:https://github.com/ShrekLi/Programing/tree/master/Qt_OperateCurve。
参考文献:
[1] 王维波,2018. Qt 5.9 C++开发指南[M]. 北京:人民邮电出版社
个人声明:
以上内容,纯属个人观点,不喜勿喷。未经本人同意,不得私自转载。博客中出现的代码仅供学习参考,不得有其他用途。若文中存在纰漏,或读者有更好的建议,欢迎留言探讨。也可邮箱联系:yxyx_0212@163.com