第7天:GraphView绘图层深入攻略 (2小时版)
目标:用2小时掌握GraphView如何将GraphDataModel中的数据可视化,并实现缩放、平移、标记、光标跟踪等交互功能。
学习时间分配
| 时间段 | 任务 | 时长 |
|---|---|---|
| 0-20分钟 | GraphView架构与初始化分析 | 20分钟 |
| 20-50分钟 | 曲线渲染与数据更新机制 | 30分钟 |
| 50-80分钟 | 缩放、平移交互实现 | 30分钟 |
| 80-110分钟 | 标记、光标与工具提示 | 30分钟 |
| 110-120分钟 | 总结与知识拓展 | 10分钟 |
详细攻略
1. GraphView架构与初始化分析 (0-20分钟)
核心问题:GraphView如何组织各个组件完成复杂绘图功能?
行动步骤:
-
打开GraphView.h,分析类结构:
class GraphView : public QObject { Q_OBJECT private: // 核心模型引用 GuiModel* _pGuiModel; SettingsModel* _pSettingsModel; GraphDataModel* _pGraphDataModel; // 绘图组件(核心!) ScopePlot* _pPlot; // QCustomPlot派生类 // 功能模块 GraphScale* _pGraphScale; // 坐标轴缩放管理 GraphViewZoom* _pGraphViewZoom; // 矩形缩放 GraphMarkers* _pGraphMarkers; // 开始/结束标记 GraphIndicators* _pGraphIndicators; // 数值指示器 NoteHandling* _pNoteHandling; // 注释处理 // 状态变量 bool _bEnableSampleHighlight; // 是否高亮数据点 QPoint _tooltipLocation; // 工具提示位置 }; -
查看构造函数(GraphView.cpp),理解初始化顺序:
GraphView::GraphView(GuiModel* pGuiModel, GraphDataModel* pGraphDataModel, ...) { 1. 初始化模型指针 2. 创建ScopePlot对象 3. 设置ScopePlot性能参数(抗锯齿、缓存等) 4. 创建并初始化各个功能模块 5. 连接信号槽 6. 初始绘图设置 } -
理解ScopePlot的作用:
- ScopePlot继承自QCustomPlot
- 重写了部分事件处理(如
enterEvent) - 是实际显示图形的QWidget
-
分析模块分工:
GraphView (总控制器) ├── ScopePlot (绘图表面) ├── GraphScale (坐标轴管理) ├── GraphViewZoom (矩形缩放) ├── GraphMarkers (标记线) ├── GraphIndicators (数值指示) └── NoteHandling (注释处理)
关键代码查看:
// 在构造函数中查找图层创建
_pPlot->addLayer("topMain", _pPlot->layer("main"), QCustomPlot::limAbove);
_pPlot->addLayer("topAxes", _pPlot->layer("axes"), QCustomPlot::limAbove);
2. 曲线渲染与数据更新机制 (20-50分钟)
核心问题:数据如何从GraphDataModel传递到屏幕上的曲线?
行动步骤:
-
找到核心方法:updateGraphs()
void GraphView::updateGraphs() { // 1. 清空现有图形 _pPlot->clearGraphs(); // 2. 获取激活的曲线索引 QList<quint32> activeGraphs = _pGraphDataModel->activeGraphIndexes(); // 3. 为每个激活的曲线创建QCPGraph对象 for (int i = 0; i < activeGraphs.size(); ++i) { quint32 graphIdx = activeGraphs[i]; GraphData graphData = _pGraphDataModel->getGraphData(graphIdx); // 创建图形对象 QCPGraph* pGraph = _pPlot->addGraph(); // 设置曲线属性 pGraph->setPen(QPen(graphData.color(), 1)); pGraph->setVisible(graphData.isVisible()); // 设置数据(共享指针!) pGraph->setData(graphData.dataContainer()); // 设置值轴(左Y轴或右Y轴) if (graphData.valueAxis() == GraphData::VALUE_AXIS_PRIMARY) { pGraph->setValueAxis(_pPlot->yAxis); } else { pGraph->setValueAxis(_pPlot->yAxis2); } } // 4. 更新右侧Y轴可见性 updateSecondaryAxisVisibility(); // 5. 重绘 _pPlot->replot(); } -
理解plotResults()方法:
- 这是接收GraphDataHandler计算结果的槽函数
- 将新的数据点追加到GraphDataModel
- 触发视图更新
-
查看数据同步机制:
// GraphView中的信号槽连接 connect(_pGraphDataModel, &GraphDataModel::dataChanged, this, &GraphView::handleGraphDataChange); connect(_pGraphDataModel, &GraphDataModel::visibilityChanged, this, &GraphView::handleGraphVisibilityChange); -
分析性能优化:
- 大量数据点时的优化策略(
_cOptimizeThreshold) - 数据点高亮的条件判断(
_cPixelPerPointThreshold)
- 大量数据点时的优化策略(
调试实验:
- 在
updateGraphs()设置断点 - 添加/删除曲线,观察调用栈
- 查看数据容器的传递过程
3. 缩放、平移交互实现 (50-80分钟)
核心问题:用户如何与图形交互?缩放、平移如何实现?
行动步骤:
-
理解GraphScale坐标轴管理:
- 打开
graphscale.cpp,查看rescale()方法 - 三种缩放模式:
enum AxisScaleOptions { SCALE_AUTO, // 自动缩放 SCALE_SLIDING, // 滑动缩放 SCALE_MANUAL, // 手动缩放 SCALE_MINMAX, // 最小值-最大值 SCALE_WINDOW_AUTO // 窗口自动缩放 };
- 打开
-
分析鼠标事件处理链:
ScopePlot鼠标事件 → GraphView槽函数 → 分发到各模块 -
查看GraphView::mousePress方法:
void GraphView::mousePress(QMouseEvent *event) { if (_pGraphViewZoom->handleMousePress(event)) { // 处理矩形缩放 _pGraphScale->disableRangeZoom(); } else if (event->modifiers() & Qt::ControlModifier) { // Ctrl+点击:设置标记 _pGraphScale->disableRangeZoom(); double xPos = pixelToClosestKey(event->pos().x()); if (event->button() == Qt::LeftButton) { _pGuiModel->setStartMarkerPos(xPos); } else if (event->button() == Qt::RightButton) { _pGuiModel->setEndMarkerPos(xPos); } } else if (_pNoteHandling->handleMousePress(event)) { // 处理注释拖动 _pGraphScale->disableRangeZoom(); } else { // 普通拖动 _pGraphScale->configureDragDirection(); } } -
理解GraphViewZoom矩形缩放:
- 查看
handleMousePress、handleMouseMove、handleMouseRelease - 理解QRubberBand的使用
- 查看
-
分析滚动缩放:
void GraphView::mouseWheel() { if (!_pGraphViewZoom->handleMouseWheel()) { // 没有进行矩形缩放时,执行轴缩放 _pGraphScale->zoomGraph(); } }
实践操作:
- 运行程序,尝试不同交互方式
- 在相应处理函数设置断点,观察调用路径
4. 标记、光标与工具提示 (80-110分钟)
核心问题:标记线、光标跟踪、工具提示如何实现?
行动步骤:
-
分析GraphMarkers标记系统:
class GraphMarkers { private: QCPItemStraightLine* _pStartMarker; // 开始标记线 QCPItemStraightLine* _pEndMarker; // 结束标记线 QList<QCPItemTracer*> _startTracerList; // 各曲线在开始标记处的追踪点 QList<QCPItemTracer*> _endTracerList; // 各曲线在结束标记处的追踪点 }; -
查看标记创建流程:
// 在GraphMarkers构造函数中 _pStartMarker = new QCPItemStraightLine(_pPlot); _pStartMarker->setPen(QPen(Qt::green, 1, Qt::DashLine)); -
理解光标跟踪:
- 查看
GraphView::mouseMove方法 - 分析
paintTimeStampToolTip实现 - 理解最近数据点查找算法
- 查看
-
工具提示显示逻辑:
void GraphView::paintTimeStampToolTip(QPoint pos) { if (_pGuiModel->cursorValues() && _pPlot->graphCount() > 0) { // 转换为坐标值 double xPos = pixelToClosestKey(pos.x()); // 检查是否在数据范围内 if (isInDataRange(xPos)) { // 格式化为时间字符串 QString timeStr = formatTime(xPos); QToolTip::showText(_pPlot->mapToGlobal(pos), timeStr); _tooltipLocation = pos; } else { QToolTip::hideText(); _tooltipLocation = QPoint(-1, -1); } } } -
数值指示器(GraphIndicators):
- 查看
GraphIndicators类实现 - 理解如何在曲线上显示当前值点
- 查看
关键代码分析:
// 查找最近数据点
double GraphView::pixelToClosestKey(double pixelX)
{
double xPos = _pPlot->xAxis->pixelToCoord(pixelX);
// 如果有图形数据
if (_pPlot->graphCount() > 0 && _pPlot->graph(0)->data()->size() > 0) {
// 二分查找最接近的key值
auto data = _pPlot->graph(0)->data();
auto it = data->findBegin(xPos);
if (it != data->constEnd()) {
return it->key;
}
}
return xPos;
}
5. 总结与知识拓展 (110-120分钟)
核心知识总结:
-
分层架构清晰:
GraphView (控制器层) ├── 模型层:GuiModel, GraphDataModel, SettingsModel ├── 视图层:ScopePlot (QCustomPlot) └── 功能层:GraphScale, GraphMarkers, GraphIndicators, NoteHandling -
数据流与渲染分离:
- 数据存储:GraphDataModel
- 数据渲染:GraphView + ScopePlot
- 数据同步:信号槽机制
-
交互事件分发:
- 鼠标事件统一入口:GraphView
- 按功能分发:缩放、标记、注释
- 状态判断:根据GuiModel状态决定行为
设计模式应用:
- 组合模式:GraphView组合多个功能模块
- 观察者模式:模型变化通知视图更新
- 策略模式:不同的缩放策略(自动、手动、滑动)
性能优化技巧:
- 数据共享:使用QSharedPointer避免数据拷贝
- 条件渲染:根据数据量调整渲染质量
- 事件过滤:高频事件适当节流
- 缓存利用:QCustomPlot的像素图缓存
常见问题解答:
-
Q: 为什么曲线更新有时会卡顿?
A: 检查数据量是否超过_cOptimizeThreshold(默认100万点),超过后会关闭抗锯齿等效果。 -
Q: 如何添加新的交互功能?
A: 继承相应模块或扩展GraphView的鼠标事件处理。 -
Q: 标记表达式计算在哪里实现?
A: 在MarkerInfoDialog和相关模型中,计算开始标记和结束标记间的差值、斜率等。
扩展思考:
-
如果你想实现"曲线拟合"功能:
- 需要扩展GraphDataModel支持拟合数据
- 添加新的渲染类型(虚线拟合曲线)
- 可能需要新的数学模型类
-
如果你想支持"多Y轴":
- 需要扩展GraphData的valueAxis枚举
- 修改GraphScale支持动态创建Y轴
- 更新GraphView的updateGraphs方法
-
如果你想添加"导出为图片"功能:
- 利用QCustomPlot的savePng/saveJpg方法
- 添加UI入口(工具栏按钮)
- 处理保存对话框和参数设置
学习成果验证:
完成今天学习后,你应该能够:
- 解释GraphView如何协调各模块完成绘图功能
- 描述从数据变化到屏幕更新的完整流程
- 说明缩放、平移、标记等交互的实现原理
- 找到修改曲线样式(颜色、线型)的代码位置
- 理解工具提示显示最近数据点的算法
明日学习预告:
明天将学习导入导出模块(DataFileHandler/DataFileExporter),了解:
- 如何将实时数据记录到文件
- 如何解析导入的数据文件
- 项目文件的保存和加载机制
学习建议:
- 今天重点关注"数据如何变成图形"和"用户如何与图形交互"
- 运行程序时,观察不同操作对应的代码执行路径
- 尝试修改一个简单样式(如标记线颜色),验证理解
2小时学习结束,你已经掌握了ModbusScope的核心绘图与交互机制!

被折叠的 条评论
为什么被折叠?



