使用Qt Charts实现高效多系列数据可视化

概述

这是一个基于 Qt Charts 的多系列数据可视化组件,支持同时显示多条数据曲线,每一条曲线可以有不同的颜色、样式和标签。组件采用暗色主题设计,具有良好的视觉效果和数据展示能力。

功能特性

  • 支持多数据系列同时显示
  • 自动分配不同颜色区分数据系列
  • 智能图例显示与管理
  • 暗色主题设计,减少视觉疲劳
  • 高性能数据更新与渲染
  • 自适应坐标轴范围
  • 完善的错误处理机制

类结构

ChartView 类

主要成员变量
cpp 复制代码
QChart *m_chart;                    // 图表对象
QChartView *m_chartView;           // 图表视图
QVector<QLineSeries*> m_seriesList; // 数据系列容器
QValueAxis *m_axisX;               // X轴
QValueAxis *m_axisY;               // Y轴
核心方法
1. 构造函数与析构函数
cpp 复制代码
explicit ChartView(QWidget *parent = nullptr);
~ChartView();
2. 数据更新接口
cpp 复制代码
// 更新单个数据系列
void updateChart(const QStringList &listData, const QString &name, int flag);

// 更新多个数据系列
void updateCharts(const QVector<QStringList> &dataLists, 
                 const QStringList &names, 
                 int flag);
3. 辅助方法
cpp 复制代码
void initData();                    // 初始化数据
void initUI();                      // 初始化界面
void setupAxes();                   // 设置坐标轴
void applyDarkTheme();              // 应用暗色主题
void clearAllSeries();              // 清除所有数据系列
QColor getSeriesColor(int index) const; // 获取系列颜色

使用示例

基本用法

cpp 复制代码
#include "chartview.h"
#include <QApplication>
#include <QMainWindow>

int main(int argc, char *argv[])
{
    QApplication a(argc, argv);
    QMainWindow window;
    
    // 创建图表视图
    ChartView *chartView = new ChartView(&window);
    window.setCentralWidget(chartView);
    window.resize(800, 600);
    window.show();
    
    // 准备测试数据
    QVector<QStringList> dataLists;
    QStringList names;
    
    // 温度数据系列
    QStringList temperatureData;
    for (int i = 0; i < 50; ++i) {
        temperatureData << QString::number(20 + 10 * sin(i * 0.1));
    }
    dataLists.append(temperatureData);
    names.append("温度 (°C)");
    
    // 湿度数据系列
    QStringList humidityData;
    for (int i = 0; i < 50; ++i) {
        humidityData << QString::number(50 + 30 * cos(i * 0.15));
    }
    dataLists.append(humidityData);
    names.append("湿度 (%)");
    
    // 压力数据系列
    QStringList pressureData;
    for (int i = 0; i < 50; ++i) {
        pressureData << QString::number(1000 + 50 * sin(i * 0.2));
    }
    dataLists.append(pressureData);
    names.append("压力 (hPa)");
    
    // 更新图表
    chartView->updateCharts(dataLists, names, 0);
    
    return a.exec();
}

动态更新数据

cpp 复制代码
// 模拟实时数据更新
void updateRealTimeData(ChartView *chartView)
{
    QTimer *timer = new QTimer(chartView);
    QObject::connect(timer, &QTimer::timeout, [chartView]() {
        static int counter = 0;
        
        QVector<QStringList> realTimeData;
        QStringList realTimeNames;
        
        // 生成随机数据
        for (int series = 0; series < 3; ++series) {
            QStringList seriesData;
            for (int i = 0; i < 20; ++i) {
                double value = 0;
                if (series == 0) {
                    value = 50 + 30 * sin((counter + i) * 0.1);
                } else if (series == 1) {
                    value = 70 + 20 * cos((counter + i) * 0.15);
                } else {
                    value = 30 + 40 * sin((counter + i) * 0.2 + 0.5);
                }
                seriesData << QString::number(value);
            }
            realTimeData.append(seriesData);
            realTimeNames.append(QString("传感器 %1").arg(series + 1));
        }
        
        chartView->updateCharts(realTimeData, realTimeNames, 0);
        counter++;
    });
    
    timer->start(1000); // 每秒更新一次
}

自定义样式

cpp 复制代码
// 自定义系列颜色和样式
void customizeChart(ChartView *chartView)
{
    // 可以通过修改 getSeriesColor 方法来自定义颜色
    // 或者继承 ChartView 类并重写相关方法
    
    QVector<QStringList> dataLists;
    QStringList names;
    
    // 添加示例数据
    QStringList data1, data2, data3;
    for (int i = 0; i < 30; ++i) {
        data1 << QString::number(10 + i * 0.5);
        data2 << QString::number(20 + i * 0.3);
        data3 << QString::number(30 + i * 0.7);
    }
    
    dataLists << data1 << data2 << data3;
    names << "线性增长" << "缓慢增长" << "快速增长";
    
    chartView->updateCharts(dataLists, names, 0);
}

高级功能

1. 数据验证与错误处理

组件内置数据验证机制,能够处理:

  • 空数据列表
  • 数据转换错误(非数字字符串)
  • 无效的数据点

2. 智能坐标轴调整

自动计算所有数据系列的最小值和最大值,并智能调整坐标轴范围,确保所有数据都能正确显示。

3. 性能优化

  • 使用 replace() 而不是 append() 更新数据点
  • 预分配内存空间
  • 批量处理数据更新

4. 主题定制

通过修改 applyDarkTheme() 方法可以轻松切换主题风格:

cpp 复制代码
void ChartView::applyLightTheme()
{
    m_chart->setBackgroundBrush(Qt::white);
    m_chart->setTitleBrush(Qt::black);
    m_axisX->setTitleBrush(Qt::black);
    m_axisY->setTitleBrush(Qt::black);
    m_axisX->setLabelsColor(Qt::black);
    m_axisY->setLabelsColor(Qt::black);
    m_chartView->setBackgroundBrush(Qt::white);
}

集成指南

1. 项目配置

.pro 文件中添加:

makefile 复制代码
QT += charts

2. 包含头文件

cpp 复制代码
#include "chartview.h"

3. 基本使用流程

  1. 创建 ChartView 实例
  2. 准备数据(QVector<QStringList>
  3. 准备系列名称(QStringList
  4. 调用 updateCharts() 方法更新显示

注意事项

  1. 内存管理:组件会自动管理数据系列的内存,无需手动释放
  2. 线程安全:不建议在多线程中直接调用更新方法,如需跨线程更新,应使用信号槽机制
  3. 性能考虑:对于大量数据点(>10000),建议进行数据采样或使用更高效的数据结构
  4. 错误处理:始终检查输入数据的有效性,避免传入空列表或无效数据

扩展建议

  1. 添加交互功能:可以实现数据点提示、缩放、平移等交互功能
  2. 支持多种图表类型:可以扩展支持柱状图、散点图等其他图表类型
  3. 数据导出:添加数据导出为图片或CS文件的功能
  4. 主题切换:实现亮色/暗色主题切换功能

这个多系列数据可视化组件提供了强大而灵活的数据展示能力,适合各种监控、分析和数据可视化应用场景。

完整代码

chartview.h

cpp 复制代码
#pragma once

#include <QWidget>
#include <QtCharts/QChartView>
#include <QtCharts/QLineSeries>
#include <QtCharts/QValueAxis>
#include <QVector>

QT_CHARTS_USE_NAMESPACE

class ChartView : public QWidget
{
    Q_OBJECT

public:
    explicit ChartView(QWidget *parent = nullptr);
    ~ChartView();

    // 更新单个数据系列
    void updateChart(const QStringList &listData, const QString &name, int flag);
    
    // 更新多个数据系列
    void updateCharts(const QVector<QStringList> &dataLists, 
                     const QStringList &names, 
                     int flag);

private:
    void initData();
    void initUI();
    void setupAxes();
    void applyDarkTheme();
    void clearAllSeries();
    QColor getSeriesColor(int index) const;

private slots:
    void slotsUpdateChart(const QStringList &listData, const QString &name, int flag);
    void slotsUpdateCharts(const QVector<QStringList> &dataLists, 
                          const QStringList &names, 
                          int flag);

private:
    QChart *m_chart;
    QChartView *m_chartView;
    QVector<QLineSeries*> m_seriesList;  // 改为存储多个系列
    QValueAxis *m_axisX;
    QValueAxis *m_axisY;
};

chartview.cpp

cpp 复制代码
#include "chartview.h"
#include <QHBoxLayout>
#include <QDebug>
#include <QDateTime>
#include <QPen>

#pragma execution_character_set("utf-8")

ChartView::ChartView(QWidget* parent)
    : QWidget(parent)
{
    initData();
    initUI();
}

ChartView::~ChartView()
{
    clearAllSeries();
    
    if (m_chart) {
        delete m_chart;
        m_chart = nullptr;
    }
}

void ChartView::updateChart(const QStringList &listData, const QString &name, int flag)
{
    slotsUpdateChart(listData, name, flag);
}

void ChartView::updateCharts(const QVector<QStringList> &dataLists, 
                            const QStringList &names, 
                            int flag)
{
    slotsUpdateCharts(dataLists, names, flag);
}

void ChartView::initData()
{
    m_chartView = new QChartView(this);
    m_chart = new QChart();
    
    m_chart->legend()->setVisible(true);  // 显示图例
    m_chart->legend()->setAlignment(Qt::AlignBottom);
    m_chart->setTitle("等待数据...");
    
    setupAxes();
    applyDarkTheme();
    
    m_chartView->setChart(m_chart);
    m_chartView->setRenderHint(QPainter::Antialiasing, true);
    
    QHBoxLayout* layout = new QHBoxLayout(this);
    layout->addWidget(m_chartView);
    layout->setContentsMargins(0, 0, 0, 0);
    setLayout(layout);
}

void ChartView::initUI()
{
    setMinimumSize(600, 400);
}

void ChartView::setupAxes()
{
    m_axisX = new QValueAxis();
    m_axisY = new QValueAxis();
    
    m_axisX->setTickCount(6);
    m_axisX->setTitleText("时间 (t)");
    m_axisY->setTitleText("数值 (s)");
    
    m_chart->addAxis(m_axisX, Qt::AlignBottom);
    m_chart->addAxis(m_axisY, Qt::AlignLeft);
}

void ChartView::applyDarkTheme()
{
    m_chart->setBackgroundBrush(QColor(25, 25, 35));
    m_chart->setTitleBrush(QColor(220, 220, 220));
    
    m_axisX->setTitleBrush(QColor(200, 200, 200));
    m_axisY->setTitleBrush(QColor(200, 200, 200));
    m_axisX->setLabelsColor(QColor(200, 200, 200));
    m_axisY->setLabelsColor(QColor(200, 200, 200));
    m_axisX->setGridLineColor(QColor(80, 80, 90));
    m_axisY->setGridLineColor(QColor(80, 80, 90));
    m_axisX->setLinePenColor(QColor(120, 120, 130));
    m_axisY->setLinePenColor(QColor(120, 120, 130));
    
    m_chartView->setBackgroundBrush(QColor(35, 35, 45));
    
    // 设置图例样式
    m_chart->legend()->setLabelColor(QColor(200, 200, 200));
    m_chart->legend()->setBackgroundVisible(true);
    m_chart->legend()->setBrush(QColor(35, 35, 45, 200));
    m_chart->legend()->setBorderColor(QColor(80, 80, 90));
}

void ChartView::clearAllSeries()
{
    // 清除所有系列
    for (QLineSeries* series : m_seriesList) {
        m_chart->removeSeries(series);
        delete series;
    }
    m_seriesList.clear();
}

QColor ChartView::getSeriesColor(int index) const
{
    // 为不同系列提供不同的颜色
    static const QVector<QColor> colors = {
        QColor(65, 105, 225),   // 皇家蓝
        QColor(220, 20, 60),    // 深红
        QColor(50, 205, 50),    // 酸橙绿
        QColor(255, 165, 0),    // 橙色
        QColor(138, 43, 226),   // 紫罗兰
        QColor(32, 178, 170),   // 浅海绿
        QColor(255, 99, 71),    // 番茄红
        QColor(147, 112, 219),  // 中紫色
        QColor(60, 179, 113),   // 中海洋绿
        QColor(255, 215, 0)     // 金色
    };
    
    return colors[index % colors.size()];
}

void ChartView::slotsUpdateChart(const QStringList &listData, const QString &name, int flag)
{
    // 为了兼容性,将单个数据转换为多个数据的调用
    QVector<QStringList> dataLists = {listData};
    QStringList names = {name};
    slotsUpdateCharts(dataLists, names, flag);
}

void ChartView::slotsUpdateCharts(const QVector<QStringList> &dataLists, 
                                 const QStringList &names, 
                                 int flag)
{
    if (dataLists.isEmpty()) {
        qWarning() << "数据列表为空!";
        m_chart->setTitle("无有效数据");
        return;
    }
    
    qreal yMin = std::numeric_limits<qreal>::max();
    qreal yMax = std::numeric_limits<qreal>::lowest();
    int maxDataLength = 0;
    
    // 清除现有系列
    clearAllSeries();
    
    // 处理每个数据系列
    for (int seriesIndex = 0; seriesIndex < dataLists.size(); ++seriesIndex) {
        const QStringList &listData = dataLists[seriesIndex];
        
        if (listData.isEmpty()) {
            qWarning() << "系列" << seriesIndex << "数据为空!";
            continue;
        }
        
        QLineSeries *series = new QLineSeries();
        
        // 设置系列名称
        QString seriesName;
        if (seriesIndex < names.size() && !names[seriesIndex].isEmpty()) {
            seriesName = names[seriesIndex];
        } else {
            seriesName = QString("系列 %1").arg(seriesIndex + 1);
        }
        series->setName(seriesName);
        
        // 设置系列颜色
        QPen pen(getSeriesColor(seriesIndex));
        pen.setWidth(2);
        series->setPen(pen);
        
        bool conversionOk = true;
        QVector<QPointF> points;
        points.reserve(listData.size());
        
        for (int i = 0; i < listData.size(); ++i) {
            bool ok;
            qreal y = listData[i].toDouble(&ok);
            
            if (!ok) {
                qWarning() << "数据转换失败:" << listData[i];
                conversionOk = false;
                continue;
            }
            
            qreal x = i;
            points.append(QPointF(x, y));
            
            yMin = qMin(yMin, y);
            yMax = qMax(yMax, y);
        }
        
        if (!conversionOk) {
            seriesName += " (部分数据错误)";
            series->setName(seriesName);
        }
        
        if (points.isEmpty()) {
            qWarning() << "系列" << seriesIndex << "没有有效的数据点";
            delete series;
            continue;
        }
        
        series->replace(points);
        m_chart->addSeries(series);
        m_seriesList.append(series);
        
        // 附加坐标轴
        series->attachAxis(m_axisX);
        series->attachAxis(m_axisY);
        
        // 更新最大数据长度
        maxDataLength = qMax(maxDataLength, listData.size());
    }
    
    if (m_seriesList.isEmpty()) {
        qWarning() << "没有有效的系列数据";
        m_chart->setTitle("无有效数据");
        return;
    }
    
    // 处理特殊情况
    if (qFuzzyCompare(yMin, yMax)) {
        if (qFuzzyIsNull(yMin)) {
            yMin = -1.0;
            yMax = 1.0;
        } else {
            yMin -= qAbs(yMin) * 0.1;
            yMax += qAbs(yMax) * 0.1;
        }
    } else {
        // 添加边距
        qreal margin = (yMax - yMin) * 0.1;
        yMin -= margin;
        yMax += margin;
    }
    
    // 设置坐标轴范围
    m_axisX->setRange(0, maxDataLength - 1);
    m_axisY->setRange(yMin, yMax);
    
    // 设置图表标题
    QString title;
    if (dataLists.size() == 1 && !names.isEmpty() && !names.first().isEmpty()) {
        title = names.first();
    } else {
        title = QString("多数据系列 (%1 个系列)").arg(dataLists.size());
    }
    
    // 添加时间戳到标题
    QString timestamp = QDateTime::currentDateTime().toString("yyyy-MM-dd hh:mm:ss");
    m_chart->setTitle(QString("%1\n更新时间: %2").arg(title).arg(timestamp));
}

使用示例

cpp 复制代码
// 创建图表视图
ChartView *chartView = new ChartView(this);

// 准备多个数据系列
QVector<QStringList> dataLists;
QStringList names;

// 第一个数据系列
QStringList data1;
data1 << "1.2" << "2.5" << "3.7" << "4.1" << "5.9";
dataLists.append(data1);
names.append("温度数据");

// 第二个数据系列
QStringList data2;
data2 << "0.8" << "1.9" << "2.8" << "3.5" << "4.2";
dataLists.append(data2);
names.append("湿度数据");

// 第三个数据系列
QStringList data3;
data3 << "2.1" << "3.2" << "4.5" << "5.1" << "6.8";
dataLists.append(data3);
names.append("压力数据");

// 更新图表显示多个系列
chartView->updateCharts(dataLists, names, 0);

单个图表代码

chartview.h

cpp 复制代码
#pragma once

#include <QWidget>

#include <QtCharts/QChartView>
#include <QtCharts/QLineSeries>
#include <QtCharts/QValueAxis>

QT_CHARTS_USE_NAMESPACE

class ChartView : public QWidget
{
	Q_OBJECT

public:
	ChartView(QWidget *parent);
	~ChartView();

	void updateChart(QStringList listData, QString name, int flag);

private:
	void initData();
	void initUI();

private slots:
	void slotsUpdateChart(QStringList listData, QString name, int flag);

private:
	QChart *m_chart;
	QChartView *m_chartView;
	QLineSeries *m_lineSeries;
	QValueAxis *m_axisX;
	QValueAxis *m_axisY;
};

chartview.cpp

cpp 复制代码
#include "chartview.h"  // 引入自定义的 ChartView 头文件

#include <QHBoxLayout>  // 引入水平布局类
#include <QDebug>		// 引入调试输出类

#pragma execution_character_set("utf-8")  // 设置源代码文件的字符集为 utf-8

// ChartView 类的构造函数,继承自 QWidget
ChartView::ChartView(QWidget* parent)
	: QWidget(parent)  // 调用父类 QWidget 的构造函数,传入父控件
{
	initData();  // 调用初始化数据函数
	initUI();    // 调用初始化 UI 函数
}

// ChartView 类的析构函数
ChartView::~ChartView()
{
}

// 更新图表数据
void ChartView::updateChart(QStringList listData, QString name, int flag)
{
	slotsUpdateChart(listData, name, flag);  // 调用图表更新的槽函数
}

// 初始化数据
void ChartView::initData()
{
	m_lineSeries = new QLineSeries();  // 创建一个新的线条序列(曲线数据)
	m_chartView = new QChartView(this);  // 创建图表视图

	m_chart = new QChart();  // 创建一个新的图表对象
	m_chart->addSeries(m_lineSeries);  // 将线条序列添加到图表中
	m_chart->legend()->hide();  // 隐藏图例
	m_chart->setTitle("--");  // 设置图表标题

	m_axisX = new QValueAxis();  // 创建 X 轴
	m_axisY = new QValueAxis();  // 创建 Y 轴
	m_axisX->setTickCount(6);  // 设置 X 轴上刻度的数量为 6
	m_axisX->setTitleText("t");  // 设置 X 轴的标题为 "t"
	m_axisY->setTitleText("s");  // 设置 Y 轴的标题为 "s"
	m_chart->addAxis(m_axisX, Qt::AlignBottom);  // 将 X 轴添加到图表并设置位置为底部
	m_chart->addAxis(m_axisY, Qt::AlignLeft);  // 将 Y 轴添加到图表并设置位置为左侧
	m_lineSeries->attachAxis(m_axisX);  // 将 X 轴附加到线条序列
	m_lineSeries->attachAxis(m_axisY);  // 将 Y 轴附加到线条序列

	m_chartView->setChart(m_chart);  // 设置图表视图的图表为当前图表
	m_chartView->setRenderHints(QPainter::Antialiasing);  // 设置图表渲染时使用抗锯齿

	QHBoxLayout* layout = new QHBoxLayout(this);  // 创建一个水平布局管理器
	layout->addWidget(m_chartView);  // 将图表视图添加到布局中
	layout->setContentsMargins(0, 0, 0, 0);  // 设置布局的内容边距为0
	setLayout(layout);  // 设置当前窗口的布局为刚创建的布局
}

// 初始化 UI
void ChartView::initUI()
{
	m_lineSeries->setColor(QColor(255, 165, 0));  // 设置线条颜色为橙色

	m_chart->setTitleBrush(QColor(255, 99, 71));  // 设置图表标题文字颜色为番茄红
	m_chart->setBackgroundBrush(QColor(0, 0, 0));  // 设置图表背景颜色为黑色

	// 设置 X 轴和 Y 轴的标题文字和网格线的颜色
	m_axisX->setTitleBrush(QColor(255, 99, 71));
	m_axisY->setTitleBrush(QColor(255, 99, 71));
	m_axisX->setLabelsColor(QColor(255, 255, 255));  // 设置标签文字颜色为白色
	m_axisY->setLabelsColor(QColor(255, 255, 255));  // 设置标签文字颜色为白色
	m_axisX->setGridLineColor(QColor(105, 105, 105));  // 设置网格线为灰色
	m_axisY->setGridLineColor(QColor(105, 105, 105));  // 设置网格线为灰色

	// 设置坐标轴线的颜色为深灰色
	m_axisX->setLinePenColor(QColor(169, 169, 169));
	m_axisY->setLinePenColor(QColor(169, 169, 169));

	// 设置图表背景为深黑色
	m_chartView->setBackgroundBrush(QColor(0, 0, 0));

}

// 更新图表的数据显示
void ChartView::slotsUpdateChart(QStringList listData, QString name, int flag)
{
	qreal yMin = 100.0;  // 设置 Y 轴最小值初始为 100
	qreal yMax = -100.0;  // 设置 Y 轴最大值初始为 -100

	m_lineSeries->clear();  // 清除现有的线条数据

	// 遍历数据列表,更新图表数据
	for (int i = 0; i < listData.length(); ++i)
	{
		qreal x = i;  // X 轴的值为数据的索引
		qreal y = listData[i].toDouble();  // Y 轴的值为数据列表中的值(转换为 double)

		// 更新 Y 轴的最小值和最大值
		yMin = qMin(yMin, y);
		yMax = qMax(yMax, y);

		// 将数据点添加到线条序列中
		m_lineSeries->append(x, y);
	}

	// 如果最小值与最大值相差非常小,扩大 Y 轴范围
	if (fabs(yMin - yMax) <= 0.0001)
	{
		yMin -= fabs(yMin) * 0.2;  // 最小值再减去一定比例
		yMax += fabs(yMax) * 0.2;  // 最大值再加上一部分
	}

	// 如果最小值和最大值都接近 0,设置一个固定范围
	if (fabs(yMax) <= 0.0001 && fabs(yMin) <= 0.0001)
	{
		yMin = -0.1;  // 最小值设为 -0.1
		yMax = 0.1;   // 最大值设为 0.1
	}

	// 为了留出一些空间,扩展 Y 轴的显示范围
	yMin = yMin - (yMax - yMin) * 0.1;
	yMax = yMax + (yMax - yMin) * 0.1;

	// 设置 X 轴和 Y 轴的显示范围
	m_axisX->setRange(0, listData.size()/10);  // X 轴范围为数据的数量
	m_axisY->setRange(yMin, yMax);  // Y 轴范围根据数据计算得出

	// 设置图表标题
	m_chart->setTitle(name);
	m_chart->update();  // 更新图表显示
}

使用示例

cpp 复制代码
// 创建图表视图
ChartView *chartView = new ChartView(this);
QVBoxLayout* layout = new QVBoxLayout(ui->widget);
layout->setContentsMargins(2, 2, 2, 2);
layout->addWidget(chartView);
ui->widget->setLayout(layout);
    
if (chartView) {
	chartView->updateChart(dataList, "Channel 4 Wavelength (nm)", 1);
}
相关推荐
数维学长9863 小时前
【全网最全】《2025国赛/高教杯》C题 思路+代码python和matlab+文献 一到四问 退火算法+遗传算法 NIPT的时点选择与胎儿的异常判定
开发语言·算法·matlab
Pocker_Spades_A3 小时前
Python快速入门专业版(三):print 格式化输出:% 占位符、format 方法与 f-string(谁更高效?)
开发语言·python
nightunderblackcat3 小时前
新手向:AI IDE+AI 辅助编程
开发语言·python·microsoft·信息可视化
Pocker_Spades_A4 小时前
Python快速入门专业版(二):print 函数深度解析:不止于打印字符串(含10+实用案例)
开发语言·python·microsoft
IT毕设梦工厂4 小时前
大数据毕业设计选题推荐-基于大数据的儿童出生体重和妊娠期数据可视化分析系统-Hadoop-Spark-数据可视化-BigData
大数据·hadoop·信息可视化·spark·毕业设计·源码·bigdata
java1234_小锋4 小时前
[免费]基于Python的Django+Vue图书借阅推荐系统【论文+源码+SQL脚本】
开发语言·python·django
max5006004 小时前
YOLOv8主干网络替换为UniConvNet的详细指南
运维·开发语言·人工智能·python·算法·yolo
dlraba8024 小时前
用遗传算法破解一元函数最大值问题:从原理到 MATLAB 实现
开发语言·matlab
潼心1412o4 小时前
C语言(长期更新)第14讲:指针详解(四)
c语言·开发语言