c++ imgui implot绘图使用示例 visual studio

手动环境配置(通过vcpkg安装则无需以下配置步骤)

库下载

从github库下载imgui、implot、vcpkg库然后解压

vcpkg为包管理器,类似python pip,可用来安装一些额外包:glfw库(也可通过这个安装 imgui、implot)

不建议使用visual studio自带的vcpkg,难用

头文件引入

有3种方式:

1.直接将需要的头文件放在项目-源文件目录下(注意不是项目-头文件目录下,此目录没作用,不参与编译)

2.通过vcpkg安装的库可直接#include,不需要额外操作

3.将include路径添加到visual studio,如下图添加路径

链接器

链接器-输入 添加额外依赖 : glfw3dll.lib opengl32.lib

.c文件导入

由于本文是直接下载imgui、implot,没有通过vcpkg库安装,因此需要手动导入必要的imgui.c文件

关于手动配置库文件与vcpkg区别

1 vcpkg安装imgui

2 手动下载imgui库

使用visual studio编程 ,方式2需要手动把一些imgui.c文件复制到项目下,方式1为什么可以不用?怎么导入的?

vcpkg 和手动下载 imgui 库的区别,核心在于"谁来帮你处理头文件路径、库文件路径、链接这些繁琐的工作"。

方式 1:vcpkg 安装 imgui(推荐,自动集成)

你运行:

bash 复制代码
vcpkg install imgui:x64-windows   # 或 imgui[opengl3-binding,glfw-binding,win32-binding]:x64-windows 等,根据你的后端

为什么可以不用手动复制 .cpp 文件?

vcpkg 做了以下几件事(你几乎感觉不到):

  1. 下载 imgui 的最新源码(从 github ocornut/imgui)
  2. 自动编译所有 imgui*.cpp 文件(包括 imgui.cpp、imgui_draw.cpp、imgui_widgets.cpp、imgui_tables.cpp 等核心文件)
  3. 把编译好的 .lib(或静态库)放到 vcpkg 的安装目录(通常是 vcpkg\installed\x64-windows\lib)
  4. 把所有头文件(imgui.h、imconfig.h、imgui_internal.h 等)放到 vcpkg\installed\x64-windows\include
  5. 最关键的一步:运行 vcpkg integrate install 后(或在 VS 项目中使用 manifest 模式),vcpkg 会自动修改 Visual Studio 的项目设置:
  • 把 include 路径自动加入到 C/C++ → General → Additional Include Directories
  • 把 lib 路径自动加入到 Linker → General → Additional Library Directories
  • 把 imgui.lib 自动加入到 Linker → Input → Additional Dependencies

所以你只需要在代码里写 #include <imgui.h>,VS 就能找到头文件,链接时也能找到 .lib 文件。

你不需要手动把任何 .cpp 文件复制到项目里,因为 vcpkg 已经帮你编译好了静态库(或动态库),你直接链接就行。

实际操作步骤(自带VCPKG方式):

下载vcpkg库,根据教程安装,以及配置运行一次 vcpkg integrate install(只需一次,全局生效)

方式 2:手动下载 imgui 库

你从 GitHub 下载 imgui 仓库,把 imgui.h、imgui.cpp 等文件复制到项目目录。
为什么需要手动复制 .cpp 文件?

因为你没有包管理器帮忙编译和链接:

  • imgui 是一个源代码库(header-only + 实现文件),不是预编译好的 .lib
  • Visual Studio 必须自己编译这些 .cpp 文件(imgui.cpp、imgui_draw.cpp 等)
  • 所以你必须把它们加到项目里(作为源文件),让 VS 编译成 obj,再链接到你的 exe/dll

手动方式的典型步骤:

  1. 下载 imgui 仓库
  2. 把 imgui.h、imconfig.h 等头文件放进 include 目录
  3. 把 imgui.cpp、imgui_draw.cpp、imgui_widgets.cpp、imgui_tables.cpp 等复制到项目源文件目录
  4. 在 VS 项目里:右键"源文件" → 添加 → 现有项 → 选这些 .cpp 文件
  5. 项目属性 → C/C++ → General → Additional Include Directories → 加 imgui 头文件路径
  6. 如果用后端(如 opengl3、glfw),也要把对应的 backend 文件(如 imgui_impl_opengl3.cpp、imgui_impl_glfw.cpp)加进来

总结对比

一句话结论:

vcpkg 相当于"帮你把 imgui 编译成现成的 .lib + 自动配置 VS 项目路径",所以你不用复制 .cpp 文件,也不用手动配置链接。

手动方式相当于"把 imgui 当成普通源代码,自己编译、自己配置",所以必须把 .cpp 丢进项目里。

如果你用 Visual Studio 2022 + vcpkg,强烈推荐方式 1,省时省力。

本文总示例代码

cpp 复制代码
// main.cpp
#include <GLFW/glfw3.h>//GLFW 库的核心头文件,提供窗口创建、事件处理、OpenGL 上下文管理
#include <stdio.h>// 提供 printf 等标准输出函数(这里用于调试按钮点击)
// ImGui 核心头文件
#include "imgui.h"//Dear ImGui 的核心头文件,包含所有 UI 控件、绘制函数、上下文管理等
#include "imgui_impl_glfw.h"//ImGui 的 GLFW 后端:处理窗口事件、输入(鼠标、键盘)传递给 ImGui
#include "imgui_impl_opengl3.h"//ImGui 的 OpenGL3 渲染后端:负责把 ImGui 的绘制指令转成 OpenGL 调用
// ImGui 的 GLFW + OpenGL3 后端头文件(必须放在 imgui.h 之后)

#include "implot.h"

#include <math.h>
//#include "imgui_demo.cpp"//直接包含官方 demo 实现(ShowDemoWindow 等函数的定义)

#include "history_buffer.h"
HistoryBuffer buffer;

// 最简单的正弦波,每次调用更新数据
void generateSinData(float values[4]) {
    static float time = 0.0f;  // 静态变量,保持时间连续性

    // 4个通道都是正弦波,频率不同
    values[0] = sin(time * 1.0f);           // 频率1Hz
    values[1] = sin(time * 2.0f);           // 频率2Hz  
    values[2] = sin(time * 5.0f);           // 频率5Hz
    values[3] = sin(time * 10.0f);          // 频率10Hz

    time += 0.05f;  // 时间增量,控制更新速度
    if (time > 99999) { time = 0; }
}

int main(int, char**)
{
    // 初始化 GLFW
    if (!glfwInit()) // 初始化 GLFW 库(加载动态库、注册窗口类、初始化内部状态等)
        return -1;

    // 建议使用 OpenGL 3.3 core(imgui opengl3-binding 推荐)
    //设置 OpenGL 上下文版本和模式(在创建窗口之前调用)
    const char* glsl_version = "#version 330";
    glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3);
    glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3);
    glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);

    // 创建窗口
    GLFWwindow* window = glfwCreateWindow(1280, 720, "Dear ImGui GLFW+OpenGL3", nullptr, nullptr);
    if (window == nullptr)
    {
        glfwTerminate();
        return -1;
    }

    glfwMakeContextCurrent(window);//告诉 OpenGL 这个窗口是当前要绘制的目标
    glfwSwapInterval(1); // 开启垂直同步

    // 初始化 ImGui
    IMGUI_CHECKVERSION();//检查头文件和库版本是否匹配(防止 dll/so 版本不一致)
    ImGui::CreateContext();//创建全局 ImGui 上下文(存储样式、输入状态、窗口列表等)
    ImGuiIO& io = ImGui::GetIO(); (void)io;//获取输入/输出接口(鼠标位置、按键、DPI、剪贴板等)

    // 启用键盘控制、游戏手柄、多视口等(可选)
    io.ConfigFlags |= ImGuiConfigFlags_NavEnableKeyboard;
    //NavEnableKeyboard:允许用键盘(Tab、箭头)导航 UI
    //ViewportsEnable:允许多个独立窗口(高级功能,需额外平台代码支持)

    // 设置风格(可选)选择 UI 配色主题(暗色/亮色)
    ImGui::StyleColorsDark();
    // ImGui::StyleColorsLight();

    // 初始化后端 
    ImGui_ImplGlfw_InitForOpenGL(window, true);//ImGui_ImplGlfw_InitForOpenGL:把 GLFW 窗口的事件(鼠标、键盘、窗口大小变化)桥接到 ImGui
    ImGui_ImplOpenGL3_Init(glsl_version);//ImGui_ImplOpenGL3_Init:初始化 OpenGL3 渲染器,创建着色器、顶点缓冲等



    // 在 ImGui 初始化完成之後、新的一幀開始前
    ImPlot::CreateContext();



    // 1. 初始化通道(只需调用一次)
    std::vector<std::string> names = { "t1", "tA", "t3", "tt" };
    buffer.init_channels(names);   // ← 这里就完成了 resize + 内存分配 + 清零
    float values[4] = { /* 从设备读取 */ };   // 假设 4 个通道
    // 主循环
    while (!glfwWindowShouldClose(window))
    {
        
        generateSinData(values);
        buffer.push(values);   // 直接写入


        glfwPollEvents();// 处理窗口事件(关闭、移动、鼠标、键盘等)获取用户输入、窗口事件

        // 开始新一帧 ImGui
        ImGui_ImplOpenGL3_NewFrame();
        ImGui_ImplGlfw_NewFrame(); //告诉 ImGui 新的一帧开始了,更新输入状态
        ImGui::NewFrame(); //开始构建新一帧 UI

        // ------------------- 你的界面代码写在这里 -------------------
        // 让窗口占满整个视口(可选,但很常用,看起来更像"主界面")
        ImGui::SetNextWindowPos(ImVec2(0, 0));
        ImGui::SetNextWindowSize(ImGui::GetIO().DisplaySize);
        // 示例:一个简单的窗口
        ImGui::Begin("Hello ImGui + GLFW!");
        ImGui::Text("current : %.1f FPS", io.Framerate);
        if (ImGui::Button("click me"))
        {
            printf("clicked\n");
        }

        ImGui::SliderFloat("transf", &io.DisplayFramebufferScale.x, 0.5f, 2.0f);
        
        //ImPlot::ShowDemoWindow();   // 這一行就會把整個 demo 視窗秀出來
        static float xs1[1001], ys1[1001];
        static int count = 0;
        count++;
        if (count > 1001) count = 0;
        for (int i = 0; i < count; ++i) {
            xs1[i] = i * 0.001f;
            ys1[i] = 0.5f + 0.5f * sinf(50 * xs1[i] );
        }
        static double xs2[20], ys2[20];
        for (int i = 0; i < 20; ++i) {
            xs2[i] = i * 1 / 19.0f;
            ys2[i] = xs2[i] * xs2[i];
        }
        if (ImPlot::BeginPlot("Line Plots")) {
            ImPlot::SetupAxes("x", "y");
            ImPlot::PlotLine("f(x)", xs1, ys1, count);
            //ImPlot::SetNextMarkerStyle(ImPlotMarker_Circle);
            //ImPlot::PlotLine("g(x)", xs2, ys2, 20, ImPlotLineFlags_Segments);
            ImPlot::EndPlot();
        }
        


        if (ImPlot::BeginPlot("实时数据")) {
            ImPlot::SetupAxes("样本索引", "值");

            // 计算环形起始位置
            size_t start = (buffer.head - buffer.size() + buffer.capacity) % buffer.capacity;
            ImPlot::SetupAxisLimits(ImAxis_Y1, 0, 1);
            ImPlot::SetupAxisLimits(ImAxis_X1, 0, 600);

            for (size_t ch = 0; ch < buffer.channel_count(); ++ch) {
                const auto& ch_data = buffer.channels[ch];
                
                ImPlot::PlotLine(
                    buffer.channel_names[ch].c_str(),   // 通道名
                    ch_data.data() ,             // 数据起始
                    buffer.head,                 // 点数
                    1,                            
                    0,                                  // x stride(用索引)
                    sizeof(float)                       // y stride
                );
            }

            ImPlot::EndPlot();
        }













        ImGui::End();


        // 可选:展示 ImGui 官方 demo 窗口(非常有用!)
        //ImGui::ShowDemoWindow();

        // 渲染 // 生成 ImGui 的绘制数据
        ImGui::Render();

        int display_w, display_h;
        glfwGetFramebufferSize(window, &display_w, &display_h); //获取窗口的实际像素大小(考虑 HiDPI / Retina 显示器)
        glViewport(0, 0, display_w, display_h);//设置 OpenGL 渲染区域为整个帧缓冲区 参数:左下角坐标(0, 0),宽度和高度 确保渲染正确适配当前窗口大小
        glClearColor(0.12f, 0.12f, 0.15f, 1.00f);//设置清屏颜色(深灰色,RGBA)
        glClear(GL_COLOR_BUFFER_BIT);//用指定颜色清除颜色缓冲区

        ImGui_ImplOpenGL3_RenderDrawData(ImGui::GetDrawData());//渲染 ImGui

        

        glfwSwapBuffers(window);
    }

    // 清理
    ImGui_ImplOpenGL3_Shutdown();
    ImGui_ImplGlfw_Shutdown();
    ImPlot::DestroyContext(); // 清理ImPlot
    ImGui::DestroyContext();

    glfwDestroyWindow(window);
    glfwTerminate();

    return 0;
}

绘图使用到的数据结构

cpp 复制代码
// history_buffer.h
#pragma once
#include <vector>
#include <string>

class HistoryBuffer {
public:
    // 配置(你可以外部修改)
    size_t capacity = 600;           // 最大样本数(600秒 × 100Hz)

    // 当前状态(你自己维护写入时更新)
    size_t head = 0;                  // 下一个写入位置
    size_t count = 0;                  // 当前有效样本数(≤ capacity)

    // 数据区
    std::vector<std::vector<float>> channels;

    // 通道名称(可选,用于绘图)
    std::vector<std::string> channel_names;


    // 初始化通道(必须在开始采集前调用一次)
    // 传入通道名称列表,会自动 resize + 分配内存 + 清零
    void init_channels(const std::vector<std::string>& names) {
        channel_names = names;
        size_t n = names.size();

        // 调整通道数量
        channels.resize(n);

        // 为每个通道分配内存并清零
        for (auto& ch : channels) {
            ch.resize(capacity);          // 申请内存并默认初始化为 0.0f
            // 如果你想明确清零或初始化其他值,可以改成:
            // ch.assign(capacity, 0.0f);
        }

        // 重置状态(通道变了就从头开始)
        head = 0;
        count = 0;
    }

    // 采集写入(你自己调用,每10ms一次)
    void push(const float* values) {
        // 写入当前 head 位置
        for (size_t i = 0; i < channels.size(); ++i) {
            channels[i][head] = values[i];
        }

        head = (head + 1) % capacity;
        if (count < capacity) {
            ++count;
        }
    }

    // 获取当前有效样本数(绘图时用)
    size_t size() const {
        return count;
    }

    // 获取通道数量
    size_t channel_count() const {
        return channels.size();
    }
};

数据模拟

用于模拟生成采集数据

cpp 复制代码
// 最简单的正弦波,每次调用更新数据
void generateSinData(float values[4]) {
    static float time = 0.0f;  // 静态变量,保持时间连续性

    // 4个通道都是正弦波,频率不同
    values[0] = sin(time * 1.0f);           // 频率1Hz
    values[1] = sin(time * 2.0f);           // 频率2Hz  
    values[2] = sin(time * 5.0f);           // 频率5Hz
    values[3] = sin(time * 10.0f);          // 频率10Hz

    time += 0.05f;  // 时间增量,控制更新速度
    if (time > 99999) { time = 0; }
}

绘图

implot_demo.cpp内有大量示例绘图函数可用于参考

本文参考此函数绘折线图

implot_demo.cpp示例代码

cpp 复制代码
void Demo_LinePlots() {
    static float xs1[1001], ys1[1001];
    for (int i = 0; i < 1001; ++i) {
        xs1[i] = i * 0.001f;
        ys1[i] = 0.5f + 0.5f * sinf(50 * (xs1[i] + (float)ImGui::GetTime() / 10));
    }
    static double xs2[20], ys2[20];
    for (int i = 0; i < 20; ++i) {
        xs2[i] = i * 1/19.0f;
        ys2[i] = xs2[i] * xs2[i];
    }
    if (ImPlot::BeginPlot("Line Plots")) {
        ImPlot::SetupAxes("x","y");
        ImPlot::PlotLine("f(x)", xs1, ys1, 1001);
        ImPlot::SetNextMarkerStyle(ImPlotMarker_Circle);
        ImPlot::PlotLine("g(x)", xs2, ys2, 20,ImPlotLineFlags_Segments);
        ImPlot::EndPlot();
    }
}

绘图函数

本文总示例代码中绘图函数如下

cpp 复制代码
        // ------------------- 你的界面代码写在这里 -------------------
        // 让窗口占满整个视口(可选,但很常用,看起来更像"主界面")
        ImGui::SetNextWindowPos(ImVec2(0, 0));
        ImGui::SetNextWindowSize(ImGui::GetIO().DisplaySize);
        // 示例:一个简单的窗口
        ImGui::Begin("Hello ImGui + GLFW!");
        ImGui::Text("current : %.1f FPS", io.Framerate);
        if (ImGui::Button("click me"))
        {
            printf("clicked\n");
        }

        ImGui::SliderFloat("transf", &io.DisplayFramebufferScale.x, 0.5f, 2.0f);
        
        //ImPlot::ShowDemoWindow();   // 這一行就會把整個 demo 視窗秀出來
        static float xs1[1001], ys1[1001];
        static int count = 0;
        count++;
        if (count > 1001) count = 0;
        for (int i = 0; i < count; ++i) {
            xs1[i] = i * 0.001f;
            ys1[i] = 0.5f + 0.5f * sinf(50 * xs1[i] );
        }
        static double xs2[20], ys2[20];
        for (int i = 0; i < 20; ++i) {
            xs2[i] = i * 1 / 19.0f;
            ys2[i] = xs2[i] * xs2[i];
        }
        if (ImPlot::BeginPlot("Line Plots")) {
            ImPlot::SetupAxes("x", "y");
            ImPlot::PlotLine("f(x)", xs1, ys1, count);
            //ImPlot::SetNextMarkerStyle(ImPlotMarker_Circle);
            //ImPlot::PlotLine("g(x)", xs2, ys2, 20, ImPlotLineFlags_Segments);
            ImPlot::EndPlot();
        }
        


        if (ImPlot::BeginPlot("实时数据")) {
            ImPlot::SetupAxes("样本索引", "值");

            // 计算环形起始位置
            size_t start = (buffer.head - buffer.size() + buffer.capacity) % buffer.capacity;
            ImPlot::SetupAxisLimits(ImAxis_Y1, 0, 1);
            ImPlot::SetupAxisLimits(ImAxis_X1, 0, 600);

            for (size_t ch = 0; ch < buffer.channel_count(); ++ch) {
                const auto& ch_data = buffer.channels[ch];
                
                ImPlot::PlotLine(
                    buffer.channel_names[ch].c_str(),   // 通道名
                    ch_data.data() ,             // 数据起始
                    buffer.head,                 // 点数
                    1,                            
                    0,                                  // x stride(用索引)
                    sizeof(float)                       // y stride
                );
            }

一共绘制两组图:

一组以implot_demo.cpp为基础;一组模拟绘制4条采集数据的曲线。

ImPlot::PlotLine

两组绘图使用的都是 ImPlot::PlotLine,在库内有两个实现,一个绘制:x\y曲线;一个绘制单个数组(x轴通过一个间隔参数控制,本例为1)

效果如下

相关推荐
dyyx1111 小时前
C++中的过滤器模式
开发语言·c++·算法
星夜泊客2 小时前
C# 基础:为什么类可以在静态方法中创建自己的实例?
开发语言·经验分享·笔记·unity·c#·游戏引擎
CappuccinoRose2 小时前
React框架学习文档(七)
开发语言·前端·javascript·react.js·前端框架·reactjs·react router
机器视觉知识推荐、就业指导2 小时前
用惯了QTimer定时器,如何快速在纯 C++ 项目中替换?
c++
消失的旧时光-19432 小时前
从拷贝到移动:C++ 移动构造与移动赋值是怎么被逼出来的?(附完整示例)
开发语言·c++
古译汉书2 小时前
部分.exe文件打开但是一直显示界面,点击任务栏持续无反应
开发语言·单片机·嵌入式硬件
2301_817497332 小时前
C++中的装饰器模式高级应用
开发语言·c++·算法
m0_549416662 小时前
C++编译期字符串处理
开发语言·c++·算法
m0_581124192 小时前
C++中的适配器模式实战
开发语言·c++·算法