Files
AiAnalysis/plotwidget.cpp
iorebuild b6cd730feb refactor: 去掉多余的显示点数控件,来一个点画一个点,自动滚动
- 移除SpinBox和onDisplayPointsChanged
- 内部滚动窗口固定3000个点,数据无限追加
- 收到UDP包立即刷新绘图
2026-04-30 13:09:01 +08:00

457 lines
12 KiB
C++
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#include "plotwidget.h"
#include <QPainter>
#include <QPainterPath>
#include <QtMath>
#include <QFontMetrics>
#include <algorithm>
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(true, 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<QColor> 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<double> &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<QVector<double>> &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;
}
}
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 < 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("#444444");
painter.fillRect(QRect(legendX, y + 2, 12, 12), color);
// 通道名
QString label = QString("ch%1").arg(ch);
painter.setPen(m_channelVisible[ch] ? m_textColor : QColor("#666666"));
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;
}