#include "plotwidget.h" #include #include #include #include #include PlotWidget::PlotWidget(QWidget *parent) : QWidget(parent) , m_totalPoints(0) , m_displayPoints(3000) , m_yMin(0) , m_yMax(65535) , m_autoYRange(true) , m_autoScroll(true) , m_dragging(false) , m_marginLeft(70) , m_marginRight(160) , m_marginTop(30) , m_marginBottom(50) , m_bgColor("#1a1a2e") , m_gridColor("#333355") , m_gridColorMinor("#252545") , m_textColor("#cccccc") , m_axisColor("#888888") , m_dirty(true) { m_data.resize(16); m_channelVisible.fill(false, 16); m_colors = defaultColors(); setMouseTracking(true); setMinimumSize(400, 300); // 定时刷新(约30fps) m_refreshTimer = new QTimer(this); connect(m_refreshTimer, &QTimer::timeout, this, [this]() { if (m_dirty) { m_dirty = false; update(); } }); m_refreshTimer->start(33); } QList PlotWidget::defaultColors() { return { QColor("#FF4444"), QColor("#44FF44"), QColor("#4488FF"), QColor("#FFAA00"), QColor("#FF44FF"), QColor("#44FFFF"), QColor("#FFFF44"), QColor("#FF8888"), QColor("#88FF88"), QColor("#8888FF"), QColor("#CC6600"), QColor("#FF66CC"), QColor("#66FFCC"), QColor("#CCFF66"), QColor("#AA66FF"), QColor("#FF6666"), }; } void PlotWidget::addDataPoint(const QVector &values) { if (values.size() < 16) return; for (int i = 0; i < 16; ++i) { m_data[i].append(values[i]); } m_totalPoints++; m_dirty = true; if (m_autoYRange && m_totalPoints % 50 == 0) { autoRangeY(); } } void PlotWidget::setAllData(const QVector> &channels) { clear(); if (channels.size() >= 16) { for (int i = 0; i < 16; ++i) { m_data[i] = channels[i]; } m_totalPoints = channels.isEmpty() ? 0 : channels[0].size(); } m_autoScroll = true; m_autoYRange = true; autoRangeY(); m_dirty = true; update(); } void PlotWidget::clear() { for (auto &ch : m_data) { ch.clear(); } m_totalPoints = 0; m_autoScroll = true; m_autoYRange = true; m_dirty = true; update(); } void PlotWidget::setChannelVisible(int channel, bool visible) { if (channel >= 0 && channel < 16) { m_channelVisible[channel] = visible; m_dirty = true; update(); // 立即刷新,不等定时器 } } bool PlotWidget::isChannelVisible(int channel) const { return channel >= 0 && channel < 16 ? m_channelVisible[channel] : false; } void PlotWidget::setDisplayPointCount(int count) { m_displayPoints = qMax(10, count); m_dirty = true; } int PlotWidget::displayPointCount() const { return m_displayPoints; } void PlotWidget::setYRange(double min, double max) { m_yMin = min; m_yMax = max; m_autoYRange = false; m_dirty = true; } void PlotWidget::autoRangeY() { if (m_totalPoints == 0) { m_yMin = 0; m_yMax = 65535; return; } double globalMin = 1e18, globalMax = -1e18; int startIdx = qMax(0, m_totalPoints - m_displayPoints); for (int ch = 0; ch < 16; ++ch) { if (!m_channelVisible[ch]) continue; const auto &channel = m_data[ch]; for (int i = startIdx; i < channel.size(); ++i) { double v = channel[i]; if (v < globalMin) globalMin = v; if (v > globalMax) globalMax = v; } } if (globalMax < globalMin) { // 没有可见通道或没有数据 return; } if (globalMax - globalMin < 1.0) { globalMin -= 50; globalMax += 50; } double margin = (globalMax - globalMin) * 0.1; m_yMin = globalMin - margin; m_yMax = globalMax + margin; } void PlotWidget::setTitle(const QString &title) { m_title = title; m_dirty = true; } int PlotWidget::totalPoints() const { return m_totalPoints; } QRectF PlotWidget::plotArea() const { return QRectF(m_marginLeft, m_marginTop, width() - m_marginLeft - m_marginRight, height() - m_marginTop - m_marginBottom); } QPointF PlotWidget::dataToWidget(double x, double y) const { QRectF area = plotArea(); double px = area.left() + x * area.width(); double py = area.bottom() - ((y - m_yMin) / (m_yMax - m_yMin)) * area.height(); return QPointF(px, py); } // ================ Paint ================ void PlotWidget::paintEvent(QPaintEvent *) { QPainter painter(this); painter.setRenderHint(QPainter::Antialiasing, true); drawBackground(painter); drawGrid(painter); drawCurves(painter); drawAxes(painter); drawLegend(painter); // 标题 if (!m_title.isEmpty()) { painter.setPen(m_textColor); QFont titleFont = font(); titleFont.setPointSize(12); titleFont.setBold(true); painter.setFont(titleFont); painter.drawText(QRect(0, 2, width(), m_marginTop - 4), Qt::AlignCenter, m_title); } // 数据点计数 painter.setPen(QColor("#666666")); QFont smallFont = font(); smallFont.setPointSize(8); painter.setFont(smallFont); QString info = QString("总点数: %1 显示: %2 范围: [%3, %4]") .arg(m_totalPoints) .arg(m_displayPoints) .arg(m_yMin, 0, 'f', 0) .arg(m_yMax, 0, 'f', 0); painter.drawText(QRect(m_marginLeft, height() - m_marginBottom + 30, width() - m_marginLeft - m_marginRight, 20), Qt::AlignLeft | Qt::AlignVCenter, info); } void PlotWidget::drawBackground(QPainter &painter) { painter.fillRect(rect(), m_bgColor); QRectF area = plotArea(); painter.fillRect(area, QColor("#16213e")); } void PlotWidget::drawGrid(QPainter &painter) { QRectF area = plotArea(); if (area.width() <= 0 || area.height() <= 0) return; // 计算合适的刻度 double range = m_yMax - m_yMin; if (range <= 0) return; double roughStep = range / 8.0; double exponent = qFloor(log10(roughStep)); double mantissa = roughStep / qPow(10, exponent); double gridStep; if (mantissa < 1.5) gridStep = qPow(10, exponent); else if (mantissa < 3.5) gridStep = 2 * qPow(10, exponent); else if (mantissa < 7.5) gridStep = 5 * qPow(10, exponent); else gridStep = 10 * qPow(10, exponent); if (gridStep <= 0) gridStep = 1; double firstGrid = qCeil(m_yMin / gridStep) * gridStep; QPen majorPen(m_gridColor, 1, Qt::SolidLine); QPen minorPen(m_gridColorMinor, 1, Qt::DotLine); // Y轴网格 for (double y = firstGrid; y <= m_yMax; y += gridStep) { painter.setPen(majorPen); QPointF p = dataToWidget(0, y); painter.drawLine(QPointF(area.left(), p.y()), QPointF(area.right(), p.y())); // 标签 painter.setPen(m_textColor); QFont gridFont = font(); gridFont.setPointSize(8); painter.setFont(gridFont); painter.drawText(QRectF(0, p.y() - 10, m_marginLeft - 5, 20), Qt::AlignRight | Qt::AlignVCenter, QString::number(y, 'f', 0)); } // X轴网格(时间/采样点) int startIdx = qMax(0, m_totalPoints - m_displayPoints); if (m_autoScroll) { startIdx = qMax(0, m_totalPoints - m_displayPoints); } // 画几条竖线 int xStep = m_displayPoints / 5; if (xStep < 1) xStep = 1; // 竖网格线(基于显示的采样点) for (int i = 0; i <= m_displayPoints; i += xStep) { double t = (double)i / m_displayPoints; QPointF p = dataToWidget(t, 0); painter.setPen(minorPen); painter.drawLine(QPointF(p.x(), area.top()), QPointF(p.x(), area.bottom())); painter.setPen(m_textColor); QFont gridFont = font(); gridFont.setPointSize(8); painter.setFont(gridFont); int sampleIdx = startIdx + i; painter.drawText(QRectF(p.x() - 30, area.bottom() + 3, 60, 15), Qt::AlignCenter, QString::number(sampleIdx)); } } void PlotWidget::drawCurves(QPainter &painter) { QRectF area = plotArea(); if (area.width() <= 0 || area.height() <= 0) return; if (m_totalPoints == 0) return; int startIdx = qMax(0, m_totalPoints - m_displayPoints); int visibleCount = m_totalPoints - startIdx; if (visibleCount < 2) return; for (int ch = 0; ch < 16; ++ch) { if (!m_channelVisible[ch]) continue; const auto &channel = m_data[ch]; if (channel.size() <= startIdx) continue; QPen pen(m_colors[ch], 1.2); painter.setPen(pen); QPainterPath path; bool first = true; int endIdx = channel.size(); for (int i = startIdx; i < endIdx; ++i) { double y = channel[i]; double t = (double)(i - startIdx) / (visibleCount - 1); QPointF pt = dataToWidget(t, y); if (first) { path.moveTo(pt); first = false; } else { path.lineTo(pt); } } painter.drawPath(path); } } void PlotWidget::drawAxes(QPainter &painter) { QRectF area = plotArea(); // Y轴 painter.setPen(QPen(m_axisColor, 1.5)); painter.drawLine(area.topLeft(), area.bottomLeft()); // X轴 painter.drawLine(area.bottomLeft(), area.bottomRight()); // Y轴标签 painter.setPen(m_textColor); QFont labelFont = font(); labelFont.setPointSize(9); painter.setFont(labelFont); painter.save(); painter.translate(12, area.center().y()); painter.rotate(-90); painter.drawText(QRectF(-50, -10, 100, 20), Qt::AlignCenter, "ADC Value"); painter.restore(); } void PlotWidget::drawLegend(QPainter &painter) { QRectF area = plotArea(); int legendX = area.right() + 10; int legendY = area.top() + 5; int itemHeight = 18; painter.setPen(m_textColor); QFont legendFont = font(); legendFont.setPointSize(8); painter.setFont(legendFont); for (int ch = 0; ch < 16; ++ch) { int y = legendY + ch * itemHeight; // 颜色方块:勾选=彩色,不勾选=灰白 QColor color = m_channelVisible[ch] ? m_colors[ch] : QColor("#555555"); painter.fillRect(QRect(legendX, y + 2, 12, 12), color); if (!m_channelVisible[ch]) { painter.setPen(QPen(QColor("#888888"), 1)); painter.drawRect(QRect(legendX, y + 2, 12, 12)); } // 通道名 QString label = QString("CH%1").arg(ch+1); painter.setPen(m_channelVisible[ch] ? m_textColor : QColor("#555555")); painter.drawText(QRect(legendX + 16, y, 50, itemHeight), Qt::AlignLeft | Qt::AlignVCenter, label); } } // ================ Mouse Events ================ void PlotWidget::wheelEvent(QWheelEvent *event) { QRectF area = plotArea(); if (!area.contains(event->posF())) return; double zoomFactor = 1.15; if (event->angleDelta().y() < 0) { zoomFactor = 1.0 / zoomFactor; } double centerY = m_yMin + (m_yMax - m_yMin) / 2.0; double halfRange = (m_yMax - m_yMin) / 2.0 * zoomFactor; m_yMin = centerY - halfRange; m_yMax = centerY + halfRange; m_autoYRange = false; m_dirty = true; } void PlotWidget::mousePressEvent(QMouseEvent *event) { if (event->button() == Qt::RightButton) { m_dragging = true; m_dragStart = event->pos(); // mousePressEvent still uses pos() m_dragYMinStart = m_yMin; m_dragYMaxStart = m_yMax; setCursor(Qt::ClosedHandCursor); } } void PlotWidget::mouseMoveEvent(QMouseEvent *event) { if (m_dragging) { QRectF area = plotArea(); double dy = (event->pos().y() - m_dragStart.y()) / area.height() * (m_yMax - m_yMin); m_yMin = m_dragYMinStart - dy; m_yMax = m_dragYMaxStart - dy; m_autoYRange = false; m_dirty = true; } } void PlotWidget::mouseReleaseEvent(QMouseEvent *event) { if (event->button() == Qt::RightButton) { m_dragging = false; setCursor(Qt::ArrowCursor); } } void PlotWidget::mouseDoubleClickEvent(QMouseEvent *) { // 双击恢复自动范围 m_autoYRange = true; m_autoScroll = true; autoRangeY(); m_dirty = true; } void PlotWidget::resizeEvent(QResizeEvent *) { m_dirty = true; }