【Qt开源项目】— ModbusScope-day 7

第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如何组织各个组件完成复杂绘图功能?

行动步骤

  1. 打开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;              // 工具提示位置
    };
    
  2. 查看构造函数(GraphView.cpp),理解初始化顺序:

    GraphView::GraphView(GuiModel* pGuiModel, GraphDataModel* pGraphDataModel, ...)
    {
        1. 初始化模型指针
        2. 创建ScopePlot对象
        3. 设置ScopePlot性能参数(抗锯齿、缓存等)
        4. 创建并初始化各个功能模块
        5. 连接信号槽
        6. 初始绘图设置
    }
    
  3. 理解ScopePlot的作用

    • ScopePlot继承自QCustomPlot
    • 重写了部分事件处理(如enterEvent
    • 是实际显示图形的QWidget
  4. 分析模块分工

    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传递到屏幕上的曲线?

行动步骤

  1. 找到核心方法: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();
    }
    
  2. 理解plotResults()方法

    • 这是接收GraphDataHandler计算结果的槽函数
    • 将新的数据点追加到GraphDataModel
    • 触发视图更新
  3. 查看数据同步机制

    // GraphView中的信号槽连接
    connect(_pGraphDataModel, &GraphDataModel::dataChanged,
            this, &GraphView::handleGraphDataChange);
            
    connect(_pGraphDataModel, &GraphDataModel::visibilityChanged,
            this, &GraphView::handleGraphVisibilityChange);
    
  4. 分析性能优化

    • 大量数据点时的优化策略(_cOptimizeThreshold
    • 数据点高亮的条件判断(_cPixelPerPointThreshold

调试实验

  1. updateGraphs()设置断点
  2. 添加/删除曲线,观察调用栈
  3. 查看数据容器的传递过程

3. 缩放、平移交互实现 (50-80分钟)

核心问题:用户如何与图形交互?缩放、平移如何实现?

行动步骤

  1. 理解GraphScale坐标轴管理

    • 打开graphscale.cpp,查看rescale()方法
    • 三种缩放模式:
      enum AxisScaleOptions {
          SCALE_AUTO,        // 自动缩放
          SCALE_SLIDING,     // 滑动缩放
          SCALE_MANUAL,      // 手动缩放
          SCALE_MINMAX,      // 最小值-最大值
          SCALE_WINDOW_AUTO  // 窗口自动缩放
      };
      
  2. 分析鼠标事件处理链

    ScopePlot鼠标事件 → GraphView槽函数 → 分发到各模块
    
  3. 查看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();
        }
    }
    
  4. 理解GraphViewZoom矩形缩放

    • 查看handleMousePresshandleMouseMovehandleMouseRelease
    • 理解QRubberBand的使用
  5. 分析滚动缩放

    void GraphView::mouseWheel()
    {
        if (!_pGraphViewZoom->handleMouseWheel()) {
            // 没有进行矩形缩放时,执行轴缩放
            _pGraphScale->zoomGraph();
        }
    }
    

实践操作

  1. 运行程序,尝试不同交互方式
  2. 在相应处理函数设置断点,观察调用路径

4. 标记、光标与工具提示 (80-110分钟)

核心问题:标记线、光标跟踪、工具提示如何实现?

行动步骤

  1. 分析GraphMarkers标记系统

    class GraphMarkers {
    private:
        QCPItemStraightLine* _pStartMarker;   // 开始标记线
        QCPItemStraightLine* _pEndMarker;     // 结束标记线
        QList<QCPItemTracer*> _startTracerList;  // 各曲线在开始标记处的追踪点
        QList<QCPItemTracer*> _endTracerList;    // 各曲线在结束标记处的追踪点
    };
    
  2. 查看标记创建流程

    // 在GraphMarkers构造函数中
    _pStartMarker = new QCPItemStraightLine(_pPlot);
    _pStartMarker->setPen(QPen(Qt::green, 1, Qt::DashLine));
    
  3. 理解光标跟踪

    • 查看GraphView::mouseMove方法
    • 分析paintTimeStampToolTip实现
    • 理解最近数据点查找算法
  4. 工具提示显示逻辑

    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);
            }
        }
    }
    
  5. 数值指示器(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分钟)

核心知识总结

  1. 分层架构清晰

    GraphView (控制器层)
    ├── 模型层:GuiModel, GraphDataModel, SettingsModel
    ├── 视图层:ScopePlot (QCustomPlot)
    └── 功能层:GraphScale, GraphMarkers, GraphIndicators, NoteHandling
    
  2. 数据流与渲染分离

    • 数据存储:GraphDataModel
    • 数据渲染:GraphView + ScopePlot
    • 数据同步:信号槽机制
  3. 交互事件分发

    • 鼠标事件统一入口:GraphView
    • 按功能分发:缩放、标记、注释
    • 状态判断:根据GuiModel状态决定行为

设计模式应用

  • 组合模式:GraphView组合多个功能模块
  • 观察者模式:模型变化通知视图更新
  • 策略模式:不同的缩放策略(自动、手动、滑动)

性能优化技巧

  1. 数据共享:使用QSharedPointer避免数据拷贝
  2. 条件渲染:根据数据量调整渲染质量
  3. 事件过滤:高频事件适当节流
  4. 缓存利用:QCustomPlot的像素图缓存

常见问题解答

  1. Q: 为什么曲线更新有时会卡顿?
    A: 检查数据量是否超过_cOptimizeThreshold(默认100万点),超过后会关闭抗锯齿等效果。

  2. Q: 如何添加新的交互功能?
    A: 继承相应模块或扩展GraphView的鼠标事件处理。

  3. Q: 标记表达式计算在哪里实现?
    A: 在MarkerInfoDialog和相关模型中,计算开始标记和结束标记间的差值、斜率等。

扩展思考

  1. 如果你想实现"曲线拟合"功能

    • 需要扩展GraphDataModel支持拟合数据
    • 添加新的渲染类型(虚线拟合曲线)
    • 可能需要新的数学模型类
  2. 如果你想支持"多Y轴"

    • 需要扩展GraphData的valueAxis枚举
    • 修改GraphScale支持动态创建Y轴
    • 更新GraphView的updateGraphs方法
  3. 如果你想添加"导出为图片"功能

    • 利用QCustomPlot的savePng/saveJpg方法
    • 添加UI入口(工具栏按钮)
    • 处理保存对话框和参数设置

学习成果验证
完成今天学习后,你应该能够:

  • 解释GraphView如何协调各模块完成绘图功能
  • 描述从数据变化到屏幕更新的完整流程
  • 说明缩放、平移、标记等交互的实现原理
  • 找到修改曲线样式(颜色、线型)的代码位置
  • 理解工具提示显示最近数据点的算法

明日学习预告
明天将学习导入导出模块(DataFileHandler/DataFileExporter),了解:

  1. 如何将实时数据记录到文件
  2. 如何解析导入的数据文件
  3. 项目文件的保存和加载机制

学习建议

  • 今天重点关注"数据如何变成图形"和"用户如何与图形交互"
  • 运行程序时,观察不同操作对应的代码执行路径
  • 尝试修改一个简单样式(如标记线颜色),验证理解

2小时学习结束,你已经掌握了ModbusScope的核心绘图与交互机制!

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

程序员-King.

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值