【AI插件开发】Notepad++ AI插件开发实践:支持配置界面

news2025/5/18 17:56:59

一、引用

此前的系列文章已基本完成了Notepad++的AI插件的功能开发,但是此前使用的配置为JSON配置文件,不支持界面配置。

本章在此基础上集成支持配置界面,这样不需要手工修改配置文件,直接在界面上操作,方便快捷。

:项目已开源、镜像,欢迎StartFork使用和指正。

二、配置界面设计

在插件的菜单栏中支持参数配置,用户点击该菜单时弹出配置对话框,在该配置界面对配置进行增删改查。

{
    "platform": "infini",
    "timeout": 90,
    "platforms": {
        "infini": {
            "enable_ssl": true,
            "base_url": "cloud.infini-ai.com",
            "authorization": {
                "type": "Bearer",
                "data": "sk-xxx"
            },
            "model_name": "deepseek-r1-distill-qwen-32b",
            "models": [ "deepseek-r1-distill-qwen-32b", "deepseek-r1", "deepseek-v3" ],
            "generate_endpoint": {
                "method": "post",
                "api": "/maas/v1/completions",
                "prompt": ""
            },
            "chat_endpoint": {
                "method": "post",
                "api": "/maas/v1/chat/completions",
                "prompt": ""
            },
            "models_endpoint": {

            }
        }
    }
}

根据现有的配置文件格式,对配置界面分为两块区域,区域一配置插件相关的参数,区域二设计一个AI平台的下拉列表框,支持AI平台相关的参数。

考虑到AI平台有多个接口(虽然现在只用了对话接口),因此接口部分使用表单,但表单不方便修改,因此需要新增一个字段编辑对话框,双击列表行时支持编辑列表行。

这样,界面设计差不多这样了。因为使用原生的Windows编程,因此需要花费较多的时间处理界面、事件,对Windows接口也是半生不熟,一边做一边查,所以还是比较费时间的,整体功能流程如下:

选择平台
添加/删除模型
修改接口参数
确认保存
取消
用户点击参数配置菜单
弹出配置对话框
初始化控件/加载配置
用户操作
加载平台配置
更新模型列表
打开字段编辑对话框
保存字段修改
写入配置文件
关闭对话框
流程结束

先看下效果图:
配置界面

三、参数配置界面

1. 创建对话框资源

  • 先在插件的资源文件中新建一个ID为IDD_DIALOG_PLUG_CONFIG的对话框,设计菜单界面,如下:
    在这里插入图片描述

2. 新建一个类,关联该对话框资源

PluginConfigDlg::PluginConfigDlg(HINSTANCE hInstance, Scintilla::PluginConfig& plugConf) 
    : m_hInstance(hInstance), m_plugConfig(plugConf)
{
    // 创建无模式对话框
    m_hDlg = CreateDialogParam(
        m_hInstance,
        MAKEINTRESOURCE(IDD_DIALOG_PLUG_CONFIG),
        nullptr,
        DlgProc,
        reinterpret_cast<LPARAM>(this)
    );

    if (m_hDlg) 
    {
        InitControls();
        LoadConfig();
    }
}

在构造函数中创建对话框,关联对话框资源ID,并指定消息处理函数为DlgProc

INT_PTR CALLBACK PluginConfigDlg::DlgProc(HWND hDlg, UINT uMsg, WPARAM wParam, LPARAM lParam) {
    if (uMsg == WM_INITDIALOG) 
    {
        // 关联类实例指针到窗口
        SetWindowLongPtr(hDlg, GWLP_USERDATA, lParam);
    }

    // 获取类实例指针
    PluginConfigDlg* pThis = reinterpret_cast<PluginConfigDlg*>(
        GetWindowLongPtr(hDlg, GWLP_USERDATA)
        );

    if (pThis) 
    {
        return pThis->RealDlgProc(hDlg, uMsg, wParam, lParam);
    }
    return FALSE;
}

DlgProc中处理WM_INITDIALOG消息,关联类实例指针到窗口,并将事件透传到类的实际消息处理函数RealDlgProcRealDlgProc可以便捷地访问操作类对象成员变量及函数。

3. 初始化界面

因为界面部分控件是下拉组合框和列表,因此需先初始化该部分,主要是初始化下拉列表数据、表单头,方便后续直接使用:

void PluginConfigDlg::InitControls() 
{
    // 授权类型
    HWND hWnd = GetDlgItem(m_hDlg, IDC_COMBO_AUTH_TYPE);
    SendMessage(hWnd, CB_RESETCONTENT, 0, 0);
    SendMessageW(hWnd, CB_ADDSTRING, 0, (LPARAM)L"无");
    SendMessageW(hWnd, CB_ADDSTRING, 0, (LPARAM)L"Basic");
    SendMessageW(hWnd, CB_ADDSTRING, 0, (LPARAM)L"Bearer");
    SendMessageW(hWnd, CB_ADDSTRING, 0, (LPARAM)L"ApiKey");

    // 接口列表
    HWND hList = GetDlgItem(m_hDlg, IDC_LIST_ENDPOINT);

    // 1. 设置基础样式(必须包含 LVS_REPORT)
    SetWindowLongW(hList, GWL_STYLE, 
        GetWindowLongW(hList, GWL_STYLE) | 
        LVS_REPORT |      // 报表视图
        LVS_SINGLESEL     // 禁止多选
    );

    // 2. 配置扩展样式
    ListView_SetExtendedListViewStyle(hList, 
        LVS_EX_GRIDLINES |    // 显示网格线
        LVS_EX_FULLROWSELECT  // 整行选中
    );

    // 3. 初始化列头
    LVCOLUMNW lvc = {0};
    lvc.mask = LVCF_FMT | LVCF_WIDTH | LVCF_TEXT;
    // 批量添加列
    const struct 
    {
        int width;
        const wchar_t* title;
    } columns[] = 
    {
        {50, L"名称"},
        {50, L"方法"},
        {300, L"接口"},
        {150, L"参数"}
    };
    for (size_t i = 0; i < _countof(columns); ++i) 
    {
        lvc.fmt = LVCFMT_CENTER;
        lvc.cx = columns[i].width;
        lvc.pszText = const_cast<LPWSTR>(columns[i].title);
        ListView_InsertColumn(hList, i, &lvc);
    }
}

然后是将配置文件中的内容在界面上显示,设置超时、设置当前平台:

bool PluginConfigDlg::LoadConfig()
{
    SetDlgItemTextA(m_hDlg, IDC_EDIT_TIMEOUT, std::to_string(m_plugConfig.timeout).c_str());
    int nSel = 0;

    HWND hWnd = GetDlgItem(m_hDlg, IDC_COMBO_PLATFORM);
    SendMessage(hWnd, CB_RESETCONTENT, 0, 0);
    int nIdx = 0;
    for (auto& p : m_plugConfig.platforms)
    {
        SendMessageA(hWnd, CB_ADDSTRING, 0, (LPARAM)p.first.c_str());
        if (!Scintilla::String::icasecompare(p.first, m_plugConfig.platform))
        {
            nSel = nIdx;
        }
        nIdx++;
    }
    SendMessage(hWnd, CB_SETCURSEL, (WPARAM)nSel, 0);

    auto& platform = m_plugConfig.Platform();
    Load(platform);
    return true;
}

其中平台配置信息只显示单个,即当前选中的平台,启动时显示当前配置的平台信息,然后调用Load函数加载该平台信息配置。考虑到配置需要支持增删改查,因此通过平台组合框下拉列表可以切换到不同平台:

void PluginConfigDlg::Load(const Scintilla::PlatformConfig& platform)
{
    // SSL
    CheckDlgButton(m_hDlg, IDC_CHECK_SSL, platform.enable_ssl ? BST_CHECKED : BST_UNCHECKED);

    // 授权类型
    HWND hWnd = GetDlgItem(m_hDlg, IDC_COMBO_AUTH_TYPE);
    SendMessage(hWnd, CB_SETCURSEL, (WPARAM)(int)platform.authorization.eAuthType, 0);

    // 授权数据
    SetDlgItemTextA(m_hDlg, IDC_EDIT_AUTH_DATA, platform.authorization.auth_data.c_str());

    // 根地址
    SetDlgItemTextA(m_hDlg, IDC_EDIT_ROOT_URL, platform.base_url.c_str());

    // 模型名称
    int nSel = 0;
    int nIdx = 0;
    hWnd = GetDlgItem(m_hDlg, IDC_COMBO_MODEL_NAME);
    SendMessage(hWnd, CB_RESETCONTENT, 0, 0);
    for (auto& m : platform.models)
    {
        SendMessageA(hWnd, CB_ADDSTRING, 0, (LPARAM)m.c_str());
        if (m == platform.model_name)
        {
            nSel = nIdx;
        }
        nIdx++;
    }
    SendMessage(hWnd, CB_SETCURSEL, (WPARAM)nSel, 0);

    hWnd = GetDlgItem(m_hDlg, IDC_LIST_ENDPOINT);
    ListView_DeleteAllItems(hWnd);

    if (!platform.base_url.empty())
    {
        auto pE = &platform.chat_endpoint;
        ListViewAddRow(hWnd, { "对话", pE->method, pE->api, pE->prompt});
        pE = &platform.generate_endpoint;
        ListViewAddRow(hWnd, { "生成", pE->method, pE->api, pE->prompt});
        pE = &platform.models_endpoint;
        ListViewAddRow(hWnd, { "模型", pE->method, pE->api, pE->prompt});
    }
}

4. 切换平台

处理平台切换事件,并显示切换后平台信息

#define OnDlgItemEvent(nItemId, nEventId, fnCall) if(LOWORD(wParam) == nItemId && HIWORD(wParam) == nEventId) { fnCall(); return TRUE; }
INT_PTR PluginConfigDlg::RealDlgProc(HWND hDlg, UINT uMsg, WPARAM wParam, LPARAM lParam) 
{
    switch (uMsg) 
    {
    case WM_CLOSE:
        DestroyWindow(m_hDlg);
        return TRUE;
    case WM_INITDIALOG:
        return TRUE;

    case WM_COMMAND:
        {
            OnDlgItemEvent(IDC_COMBO_PLATFORM, CBN_SELCHANGE, OnPlatformChange);
            OnDlgItemEvent(IDC_COMBO_MODEL_NAME, CBN_SELCHANGE, OnModelChange);
            OnDlgItemEvent(IDC_BUTTON_MODEL_SAVE, BN_CLICKED, OnSaveMode);
            OnDlgItemEvent(IDC_BUTTON_MODEL_DEL, BN_CLICKED, OnRemoveModel);
            OnDlgItemEvent(IDC_BUTTON_PLATFORM_SAVE, BN_CLICKED, OnSavePlatform);
            OnDlgItemEvent(IDC_BUTTON_PLATFORM_DEL, BN_CLICKED, OnRemovePlatform);
            OnDlgItemEvent(IDCANCEL, BN_CLICKED, LoadConfig);
            OnDlgItemEvent(IDOK, BN_CLICKED, SaveConfig);
        }
        break;
    case WM_NOTIFY:
        LPNMITEMACTIVATE pNmItem = (LPNMITEMACTIVATE)lParam;
        if (pNmItem->hdr.idFrom == IDC_LIST_ENDPOINT &&  pNmItem->hdr.code == NM_DBLCLK) 
        {
            OnEndpointListViewDBClick(pNmItem);
            return TRUE;
        }
        break;
    }
    return FALSE;
}

在窗口事件处理函数中,处理WM_COMMAND消息,根据消息参数识别出控件对象和消息类型,处理平台切换是OnDlgItemEvent(IDC_COMBO_PLATFORM, CBN_SELCHANGE, OnPlatformChange),实现内容在函数OnPlatformChange中:

void PluginConfigDlg::OnPlatformChange()
{
    std::string name;
    if (!GetComboSelectedText(GetDlgItem(m_hDlg, IDC_COMBO_PLATFORM), name) || name.empty())
    {
        return;
    }
    auto e = m_plugConfig.platforms.find(name);
    if (e == m_plugConfig.platforms.end())
    {
        return;
    }
    Load(e->second);
}

获取ComboBox的当前选中项,注意:不能取当前控件的文本即GetWindowText,否则取到的是选中前的内容。然后调用Load函数加载切换后的AI平台配置信息。

5. 模型删除

void PluginConfigDlg::OnRemoveModel()
{
    HWND hCombo = GetDlgItem(m_hDlg, IDC_COMBO_MODEL_NAME);
    if (hCombo == nullptr)
    {
        return;
    }

    auto name = String::Trim(GetComboSelectedText(IDC_COMBO_MODEL_NAME));
    if (name.empty())
    {
        ShowConfigError("请输入或选择当前模型名称");
        return;
    }
    auto nRet = ShowMsgBox(String::Format("是否要删除模型【%s】配置?", name.c_str()), "删除确认", MB_YESNO | MB_ICONQUESTION);
    if (nRet != IDYES)
    {
        return;
    }
    SendMessageA(hCombo, CB_DELETESTRING, 0, (LPARAM)name.c_str());
}

删除当前选中项的模型名称,仅是从列表中删除,不是从对象的内存中删除,后续通过保存平台配置时更新删除后的列表。

6. 模型添加

void PluginConfigDlg::OnSaveMode()
{
    HWND hCombo = GetDlgItem(m_hDlg, IDC_COMBO_MODEL_NAME);
    if (hCombo == nullptr)
    {
        return;
    }
    auto name = String::Trim(GetComboSelectedText(IDC_COMBO_MODEL_NAME));
    if (name.empty())
    {
        ShowConfigError("请输入或选择当前模型名称");
        return;
    }
    SendMessageA(hCombo, CB_ADDSTRING, 0, (LPARAM)name.c_str());
}

删除当前选中项的模型名称,和删除类似,仅是从列表中删除。

7.删除平台配置

void PluginConfigDlg::OnRemovePlatform()
{
    HWND hPlatform = GetDlgItem(m_hDlg, IDC_COMBO_PLATFORM);
    if (hPlatform == nullptr)
    {
        return;
    }

    std::string name;
    auto pPlat = GetCurSelPlatform(name);
    if (!pPlat)
    {
        ShowConfigError("请选择当前平台名称");
        return;
    }
    auto nRet = ShowMsgBox(String::Format("是否要删除平台【%s】配置?", name.c_str()), "删除确认", MB_YESNO | MB_ICONQUESTION);
    if (nRet != IDYES)
    {
        return;
    }
    SendMessageA(hPlatform, CB_DELETESTRING, 0, (LPARAM)name.c_str());
    m_plugConfig.platforms.erase(name);
    // 清空数据
    Load(PlatformConfig());
}

获取当前选中的平台配置,然后从列表和内存中删除该平台配置,删除后清空平台配置信息(通过加载一个空对象实现),此外为防止误删,删除前做了弹框确认。

8.修改或新增平台配置

bool PluginConfigDlg::OnSavePlatform()
{
    HWND hPlatform = GetDlgItem(m_hDlg, IDC_COMBO_PLATFORM);
    if (hPlatform == nullptr)
    {
        return false;
    }

    std::string name;
    auto pPlat = GetCurSelPlatform(name);
    if (name.empty())
    {
        ShowConfigError("请输入或选择当前平台名称");
        return false;
    }

    Scintilla::PlatformConfig plat;
    if (!Save(plat))
    {
        return false;
    }
    if (pPlat == nullptr)
    {
        // 新增
        SendMessageA(hPlatform, CB_ADDSTRING, 0, (LPARAM)name.c_str());
        m_plugConfig.platforms[name] = plat;
    }
    else
    {
        *pPlat = plat;
    }
    m_plugConfig.Save("");
    return true;
}

先把界面上平台信息配置保存到一个临时变量中(防止保存了部分就返回),然后根据是否已存在该名称的平台配置决定是新增还是更新信息。

9. 响应接口列表行双击事件

INT_PTR PluginConfigDlg::RealDlgProc(HWND hDlg, UINT uMsg, WPARAM wParam, LPARAM lParam) 
{
    switch (uMsg) 
    {
    case WM_NOTIFY:
        LPNMITEMACTIVATE pNmItem = (LPNMITEMACTIVATE)lParam;
        if (pNmItem->hdr.idFrom == IDC_LIST_ENDPOINT &&  pNmItem->hdr.code == NM_DBLCLK) 
        {
            OnEndpointListViewDBClick(pNmItem);
            return TRUE;
        }
        break;
    }
    return FALSE;
}

处理窗口的WM_NOTIFY消息,然后根据控件ID和事件类型识别出是列表双击事件,然后列表双击编辑函数OnEndpointListViewDBClick

四、字段编辑界面

字段编辑框设计为一个通用的编辑窗口,提供一个字段组合,然后界面显示并支持编辑字段信息。
步骤也是和创建配置对话框差不多,不过这里创建的是一个模态对话框,不使用Show显示,而是使用DoModal模态对话框显示。

1. 调用编辑窗口更新字段

void PluginConfigDlg::OnEndpointListViewDBClick(LPNMITEMACTIVATE& pNmItem)
{
    if (pNmItem == nullptr)
    {
        return;
    }
    int nRow = pNmItem->iItem;
    if (nRow < 0)
    {
        return;
    }
    std::map<std::string, std::string> fields;
    if (ListViewGetRow(GetDlgItem(m_hDlg, IDC_LIST_ENDPOINT), nRow, fields) <= 0)
    {
        return;
    }

    FieldEditDlg dlg(m_hInstance, m_hDlg);
    for (auto& [k, v] : fields)
    {
        dlg.m_mapField[k] = { v };
    }
    dlg.m_mapField["方法"].options = { "post", "get" };
    dlg.m_mapField["方法"].type = FieldEditDlg::FieldType::Combo;
    dlg.m_strTitle = "接口参数设置";
    dlg.m_nLabelWidth = 40;
    if (dlg.DoModal() != IDOK)
    {
        return;
    }
    std::vector<std::string> vs;
    std::vector<std::string> names = { "名称", "方法", "接口", "参数" };
    for (auto& name : names)
    {
        auto e = dlg.m_mapField.find(name);
        if (e == dlg.m_mapField.end())
        {
            return;
        }
        vs.push_back(e->second.val);
    }
    ListViewSetRow(GetDlgItem(m_hDlg, IDC_LIST_ENDPOINT), nRow, vs);
}

这里使用map存储字段,感觉使用vector更合适,有序且字段存取方便,后续改一下。

2. 创建模态编辑窗口

#pragma once
#include <windows.h>
#include "PluginConf.h"
#include <commctrl.h>

namespace Ui
{
    class Util
    {
    public:
        // 局长显示窗口
        static void Show(HWND hWnd, bool bShow, HWND hParent = nullptr);
        static std::string GetText(HWND hWnd);

    };
}

class FieldEditDlg
{
public:
    enum class FieldType
    {
        Edit,
        Combo,
    };

    struct Field
    {
        std::string val;
        std::vector<std::string> options;
        FieldType type = FieldType::Edit;
        bool readonly = false;
    };
    FieldEditDlg(HINSTANCE hInstance, HWND hParent);
    ~FieldEditDlg();

    static INT_PTR CALLBACK DlgProc(HWND hDlg, UINT uMsg, WPARAM wParam, LPARAM lParam);
    INT_PTR DoModal();

private:
    INT_PTR RealDlgProc(HWND hDlg, UINT uMsg, WPARAM wParam, LPARAM lParam);
    void CreateDynamicControls();
    void OnInitDialog();
    void OnSave();

public:
    std::map<std::string, Field> m_mapField;
    int m_nLabelWidth = 100;
    int m_nBoxWidth = 300;
    std::string m_strTitle = "字段设置";

private:
    HINSTANCE m_hInstance = nullptr;
    HWND m_hDlg = nullptr;
    HWND m_hParent = nullptr;
    HFONT m_hFont = nullptr;
    std::map<HWND, std::string> m_hwndMap;
};

注意,这里不在构造函数中初始化创建窗口,而是在DoModal中创建,并等待窗口结束:

INT_PTR FieldEditDlg::DoModal()
{
    // 创建模态对话框(需提前定义对话框模板ID,假设为IDD_FIELD_EDIT_DLG)
    return DialogBoxParam(
        m_hInstance, 
        MAKEINTRESOURCE(IDD_DIALOG_EDIT_FIELD),
        m_hParent,
        FieldEditDlg::DlgProc,
        reinterpret_cast<LPARAM>(this)
    );
}

// 对话框消息处理
INT_PTR CALLBACK FieldEditDlg::DlgProc(HWND hDlg, UINT uMsg, WPARAM wParam, LPARAM lParam)
{
    if (uMsg == WM_INITDIALOG) 
    {
        // 关联类实例指针到窗口
        SetWindowLongPtr(hDlg, GWLP_USERDATA, lParam);
        auto* pThis = reinterpret_cast<FieldEditDlg*>(lParam);
        pThis->m_hDlg = hDlg;
        pThis->OnInitDialog();
        Ui::Util::Show(hDlg, true);
        return TRUE;
    }

    // 获取类实例指针
    auto pThis = reinterpret_cast<FieldEditDlg*>(
        GetWindowLongPtr(hDlg, GWLP_USERDATA)
        );

    if (pThis) 
    {
        return pThis->RealDlgProc(hDlg, uMsg, wParam, lParam);
    }
    return FALSE;
}

INT_PTR FieldEditDlg::RealDlgProc(HWND hDlg, UINT uMsg, WPARAM wParam, LPARAM lParam)
{
    switch (uMsg) 
    {
    case WM_INITDIALOG: 
        {
            SetWindowLongPtr(hDlg, GWLP_USERDATA, (LONG_PTR)this);
            OnInitDialog();
            return TRUE;
        }
    case WM_COMMAND: 
        {
            int wmId = LOWORD(wParam);
            int wmEvent = HIWORD(wParam);
            if (wmId == IDOK && wmEvent == BN_CLICKED) 
            {
                OnSave();
                EndDialog(m_hDlg, IDOK);
                return TRUE;
            } 
            else if (wmId == IDCANCEL && wmEvent == BN_CLICKED) 
            {
                EndDialog(m_hDlg, IDCANCEL);
                return TRUE;
            }
            break;
        }
    case WM_CLOSE:
        EndDialog(m_hDlg, IDCLOSE);
        return TRUE;
    }
    return FALSE;
}

void FieldEditDlg::OnInitDialog()
{
    if(!m_mapField.empty()) CreateDynamicControls();
    SetWindowTextA(m_hDlg, m_strTitle.c_str());
}

3. 更新保存数据

void FieldEditDlg::OnSave()
{
    for (auto& [k, v] : m_hwndMap)
    {
        m_mapField[v].val = Ui::Util::GetText(k);
    }
}

5. 总结说明

这一篇文章主要介绍了关于配置的两个对话框的实现,完成了手工编辑JSON配置文件到界面快捷配置的革命转换,本文主要涉及的技术要点如下:

核心架构设计
对话框资源系统
配置数据管理
动态控件引擎
事件处理中枢
Windows API创建对话框
类实例与窗口绑定
消息循环处理
JSON结构内存映射
平台配置对象树
双向数据同步机制
智能列表视图
动态组合框
多态控件渲染
WM_COMMAND处理
WM_NOTIFY响应
异步操作队列
关键技术实现
核心创新点
原生窗口性能优化
多层级配置继承
零拷贝数据交换
字段类型自适配
B1,B2,B3

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2337727.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

数据库原理及应用mysql版陈业斌实验四

&#x1f3dd;️专栏&#xff1a;Mysql_猫咪-9527的博客-CSDN博客 &#x1f305;主页&#xff1a;猫咪-9527-CSDN博客 “欲穷千里目&#xff0c;更上一层楼。会当凌绝顶&#xff0c;一览众山小。” 目录 实验四索引与视图 1.实验数据如下 student 表&#xff08;学生表&…

华为OD机试真题——最长的顺子(2025A卷:100分)Java/python/JavaScript/C++/C语言/GO六种最佳实现

2025 A卷 100分 题型 本文涵盖详细的问题分析、解题思路、代码实现、代码详解、测试用例以及综合分析&#xff1b; 并提供Java、python、JavaScript、C、C语言、GO六种语言的最佳实现方式&#xff01; 本文收录于专栏&#xff1a;《2025华为OD真题目录全流程解析/备考攻略/经验…

足球AI模型:一款用数据分析赛事的模型

2023 年欧冠决赛前&#xff0c;某体育数据平台的 AI 模型以 78% 的概率预测曼城夺冠 —— 最终瓜迪奥拉的球队首次捧起大耳朵杯。当足球遇上 AI&#xff0c;那些看似玄学的 "足球是圆的"&#xff0c;正在被数据与算法拆解成可计算的概率命题。今天我们就来聊聊&#…

【ESP32|音频】一文读懂WAV音频文件格式【详解】

简介 最近在学习I2S音频相关内容&#xff0c;无可避免会涉及到关于音频格式的内容&#xff0c;所以刚开始接触的时候有点一头雾水&#xff0c;后面了解了下WAV相关内容&#xff0c;大致能够看懂wav音频格式是怎么样的了。本文主要为后面ESP32 I2S音频系列文章做铺垫&#xff0…

42.[前端开发-JavaScript高级]Day07-手写apply-call-bind-块级作用域

手写apply-call-bind <!DOCTYPE html> <html lang"en"> <head><meta charset"UTF-8"><meta http-equiv"X-UA-Compatible" content"IEedge"><meta name"viewport" content"widthdevi…

Vscode 插件开发

文章目录 1、使用vscode官方插件生成框架&#xff0c;下载脚手架2、使用脚手架初始化项目&#xff0c;这里我选择的是js3、生成的文件结构如下&#xff0c;重要的就是以下两个文件4、代码5、打包使用6、发布官网地址7、publisher ID undefined provided in the extension manif…

RTT添加一个RTC时钟驱动,以DS1307为例

添加一个外部时钟芯片 这里多了一个选项 复制drv_rtc.c,重命名为drv_rtc_ds1307.c 添加到工程中 /*** @file drv_rtc_ds1307.c* @brief * @author jiache (wanghuan3037@fiberhome.com)* @version 1.0* @date 2025-01-08* * @copyright Copyright (c) 2025 58* */ #

常见的低代码策略整理

低代码策略通过简化开发流程、降低技术门槛、提升效率&#xff0c;帮助用户快速构建灵活可靠的应用。这些策略的核心优势体现在以下方面&#xff1a; 快速交付与降本增效 减少编码需求&#xff1a;通过可视化配置&#xff08;如变量替换、表达式函数&#xff09;替代传统编码…

从彩色打印单行标准九九表学习〖代码情书〗的书写范式(Python/DeepSeek)

写给python终端的情书&#xff0c;学习代码设计/书写秘笈。 笔记模板由python脚本于2025-04-17 12:49:08创建&#xff0c;本篇笔记适合有python编程基础的coder翻阅。 【学习的细节是欢悦的历程】 博客的核心价值&#xff1a;在于输出思考与经验&#xff0c;而不仅仅是知识的简…

QML与C++:基于ListView调用外部模型进行增删改查(附自定义组件)

目录 引言相关阅读项目结构文件组织 核心技术实现1. 数据模型设计联系人项目类 (datamodel.h)数据模型类 (datamodel.h)数据模型实现 (datamodel.cpp) 2. 主程序入口点 (main.cpp)3. 主界面设计 (Main.qml)4. 联系人对话框 (ContactDialog.qml)5. 自定义组件CustomTextField.qm…

postman莫名奇妙报错,可能是注释引起的。postman 过滤请求体中的注释。

postman莫名奇妙报错&#xff0c;可能是注释引起的。postman 过滤请求体中的注释。 1、问题描述2、问题分析3、解决方法 1、问题描述 postman http请求测试时&#xff0c;如果在请求体中添加了注释&#xff0c;那么这个注释会被带到服务端执行&#xff0c;导致服务端接口返回报…

扩增子分析|基于R语言microeco包进行微生物群落网络分析(network网络、Zi-Pi关键物种和subnet子网络图)

一、引言 microeco包是福建农林大学姚敏杰教授团队开发的扩增子测序集成分析。该包综合了扩增子测序下游分析的多种功能包括群落组成、多样性、网络分析、零模型等等。通过简单的几行代码可实现复杂的分析。因此&#xff0c;microeco包发表以来被学界广泛关注&#xff0c;截止2…

中间件--ClickHouse-4--向量化执行(什么是向量?为什么向量化执行的更快?)

1、向量&#xff08;Vector&#xff09;的概念 &#xff08;1&#xff09;、向量的定义 向量&#xff1a;在计算机科学中&#xff0c;向量是一组同类型数据的有序集合&#xff0c;例如一个包含多个数值的数组。在数据库中&#xff0c;向量通常指批量数据&#xff08;如一列数…

【SpringBoot+Vue自学笔记】001

跟着这位老师学习的&#xff1a;https://www.bilibili.com/video/BV1nV4y1s7ZN?vd_sourceaf46ae3e8740f44ad87ced5536fc1a45 前后端开发技术的全栈课程&#xff1a; Java EE企业级框架&#xff1a;SpringBootMyBatisPlus Web前端核心框架&#xff1a;VueElement UI 公共云…

第十节:性能优化-如何排查组件不必要的重复渲染?

工具&#xff1a;React DevTools Profiler 方法&#xff1a;memo、shouldComponentUpdate深度对比 React 组件性能优化&#xff1a;排查与解决重复渲染问题指南 一、定位性能问题&#xff1a;React DevTools 高级用法 使用 React Developer Tools Profiler 精准定位问题组件&…

MATLAB项目实战(一)

题目&#xff1a; 某公司有6个建筑工地要开工&#xff0c;每个工地的位置&#xff08;用平面坐标系a&#xff0c;b表示&#xff0c;距离单位&#xff1a;km&#xff09;及水泥日用量d(t)由下表给出&#xff0e;目前有两个临时料场位于A(5,1)&#xff0c;B(2,7)&#xff0c;日储…

spring boot 文件下载

1.添加文件下载工具依赖 Commons IO is a library of utilities to assist with developing IO functionality. <dependency><groupId>commons-io</groupId><artifactId>commons-io</artifactId><version>2.6</version> </depe…

微服务链路追踪:SleuthZipkin

文章目录 Sleuth & Zipkin一、Sleuth\&Zipkin介绍二、搭建环境三、Sleuth入门操作四、Zipkin搭建及操作五、RabbitMQ方式发送信息六、Elasticsearch持久化 SpringBootAdmin一、Actuator介绍二、Actuator快速入门三、SpringBootAdmin介绍四、SpringBootAdmin快速入门4.1…

java面试篇 4.9(mybatis+微服务+线程安全+线程池)

目录 mybatis&#xff1a; 1、mybatis的执行流程 2、mybatis是否支持延迟加载&#xff1f; 当我们需要去开启全局的懒加载时&#xff1a; 3、mybatis的一级和二级缓存 微服务 1、springcloud五大组件有哪些 2、服务注册和发现是什么意思&#xff1f;springcloud如何实现…

基于电子等排体的3D分子生成模型 ShEPhERD - 评测

一、背景介绍 ShEPhERD 是一个由 MIT 开发的一个 3D 相互作用感知的 ligand-based的分子生成模型&#xff0c;以 arXiv 预印本的形式发表于 2024 年&#xff0c;被ICLR2025 会议接收。文章链接&#xff1a;https://openreview.net/pdf?idKSLkFYHlYg ShEPhERD 是一种基于去噪扩…