feat: AiAnalysis - 16通道AI数据UDP实时采集与分析
功能: - UDP端口监听,实时接收数据 - 解析16通道逗号分隔的ADC数据 - 实时波形绘制(支持滚轮缩放、右键拖拽、双击恢复) - 数据自动保存到 data_端口号/ 目录 - 支持打开历史CSV文件回放查看 - 16通道独立颜色,可切换显示/隐藏 - 深色主题界面
This commit is contained in:
456
plotwidget.cpp
Normal file
456
plotwidget.cpp
Normal file
@@ -0,0 +1,456 @@
|
||||
#include "plotwidget.h"
|
||||
#include <QPainter>
|
||||
#include <QPainterPath>
|
||||
#include <QtMath>
|
||||
#include <QFontMetrics>
|
||||
#include <algorithm>
|
||||
|
||||
PlotWidget::PlotWidget(QWidget *parent)
|
||||
: QWidget(parent)
|
||||
, m_totalPoints(0)
|
||||
, m_displayPoints(500)
|
||||
, 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->position())) 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;
|
||||
}
|
||||
Reference in New Issue
Block a user