【AI插件开发】Notepad++ AI插件开发实践:实现对话窗口功能

引言

之前的文章已经介绍实现了AI对话窗口,但只有个空壳,没有实现功能。本次将集中完成对话窗口的功能,主要内容为:

  • 模型动态切换:支持运行时加载配置的AI模型列表
  • 交互式输入处理 :实现多行文本输入与Ctrl+Enter提交逻辑
  • 异步模型调用:通过回调机制实现流式输出与界面实时更新
  • 状态控制:提供发送/停止双模式按钮,支持任务中断

用户 输入框 主窗口 模型 界面 按钮 输入内容(Ctrl+Enter) 触发OnInputFinished 调用DirectRequest 流式返回数据 实时追加输出 点击停止 设置g_bRun=false 任务终止通知 恢复初始状态 用户 输入框 主窗口 模型 界面 按钮

模型下拉支持

启动时读取插件配置,包括当前模型名称和平台所支持的模型列表,然后启动AI对话窗口时加载该模型列表,并选中当前模型名称

cpp 复制代码
// 打开AI助手停靠窗口的主入口函数
void OpenAiAssistWnd()
{
    // 检查窗口实例、模块句柄和Notepad++主窗口的有效性
    if (g_pAiWnd == nullptr && g_hModule != nullptr && g_nppData._nppHandle != nullptr)
    {
        // 创建AI助手窗口实例
        // 参数1: 插件模块实例句柄,用于加载资源
        // 参数2: Notepad++核心数据接口,用于窗口绑定
        g_pAiWnd = new AiAssistWnd((HINSTANCE)g_hModule, g_nppData);
        
        // 执行窗口初始化操作
        // 包含创建子控件、设置布局、注册消息处理器等
        g_pAiWnd->init();
        
        // 获取当前配置的平台信息
        auto& platform = g_pluginConf.Platform();
        
        // 在模型列表中查找当前选定的模型
        // 用于初始化界面中的模型选择控件
        auto it = std::find(platform.models.begin(), 
                          platform.models.end(), 
                          platform.model_name);
        
        // 更新界面模型列表并设置默认选中项
        // 当未找到配置的模型时默认选择第一个条目(索引0)
        g_pAiWnd->updateModelList(
            platform.models, 
            (it == platform.models.end()) ? 0 : 
                static_cast<int>(std::distance(platform.models.begin(), it))
        );
    }
    
    // 注1: 此处假设g_pluginConf.Platform().models至少包含一个元素
    // 注2: 窗口关闭时应调用delete g_pAiWnd释放资源
    // 注3: 实际部署需添加异常处理机制
}

提交用户输入

用户输入是一个文本输入框EDIT,一开始我计划通过响应窗口事件函数获取输入的,但是发现文本框的输入根本不触发窗口事件函数,怀疑是被窗口拦截了。因此,我在创建该文本输入框的时候,为该控件创建单独的事件处理过程。

为了支持输入时支持换行,因此采用Ctrl+Enter作为快捷键提交用户输入,且用户输入也会提交到输出窗口。

  • 控件子类化 :通过InputEditSubclassProc拦截编辑框消息,实现:
    • Ctrl+Enter提交与普通回车换行分离
    • 输入内容过滤(空值/纯空白字符校验)
  • 输入输出分离 :用户提问内容自动添加【问】标记并清空输入区

代码实现如下:

cpp 复制代码
// 输入框子类化处理函数,用于自定义编辑控件行为
LRESULT CALLBACK InputEditSubclassProc(
    HWND hWnd,          // 控件窗口句柄
    UINT uMsg,          // 消息类型
    WPARAM wParam,      // 消息参数
    LPARAM lParam, 
    UINT_PTR uIdSubclass, 
    DWORD_PTR dwRefData  // 存储关联的类实例指针
)
{
    // 将保存的指针转换为窗口类实例
    auto pThis = (AiAssistWnd*)dwRefData;

    // 处理键盘按下消息
    if (uMsg == WM_KEYDOWN && wParam == VK_RETURN) 
    {
        // 检测Ctrl键是否被按住
        if (GetKeyState(VK_CONTROL) & 0x8000) 
        {
            // 调用输入完成处理函数,成功则阻止默认回车行为
            if (pThis->OnInputFinished())
            {
                return 0;  // 中断消息传递
            }
        }
    }
    // 处理字符输入消息
    else if (uMsg == WM_CHAR)
    {
        // 当Ctrl键按下时处理特殊字符
        if (GetKeyState(VK_CONTROL) & 0x8000)
        {
            // 屏蔽回车和换行符的输入
            if (wParam == VK_RETURN || wParam == 0x0A)
            {
                return 0;
            }
        }
        
        // 处理普通回车输入
        if (wParam == VK_RETURN)
        {
            // 若Ctrl按下则忽略
            if (GetKeyState(VK_CONTROL) & 0x8000)
            {
                return 0;
            }
            
            // 插入Windows风格换行符
            const wchar_t* pLR = L"\r\n";
            SendMessageW(hWnd, EM_REPLACESEL, FALSE, (LPARAM)pLR); 
        }
    }
    
    // 执行默认消息处理
    return DefSubclassProc(hWnd, uMsg, wParam, lParam);
}

// 输入完成处理函数
bool AiAssistWnd::OnInputFinished()
{
    // 获取输入框文本长度
    int len = GetWindowTextLengthW(_hInputEdit);
    if (len <= 0)
    {
        return false;  // 空输入不处理
    }

    // 分配缓冲区并获取文本内容
    std::wstring buf((size_t)len + 1, L'\0');
    GetWindowTextW(_hInputEdit, &buf[0], (int)buf.size());
    
    // 转换并清理字符串
    auto text = Scintilla::String::TrimAll(
        Scintilla::String::wstring2s(&buf[0], false)
    );
    if (text.empty())
    {
        return false;  // 无效内容过滤
    }

    // 清空输入区域
    SetWindowTextW(_hInputEdit, L"");          // 重置文本内容
    SendMessageW(_hInputEdit, EM_SETSEL, 0, 0); // 重置选择区域
    SendMessageW(_hInputEdit, EM_SCROLLCARET, 0, 0); // 滚动到起始位置
    
    // 更新界面状态
    appendAnswer("【问】\r\n" + text);  // 在回答区域添加问题标记
    SendMessageW(_hActionBtn, BM_SETIMAGE, IMAGE_ICON, (LPARAM)_hStopIcon); // 切换按钮图标
    ::EnableWindow(_hAnswerView, FALSE);  // 禁用回答区域编辑
    
    // 触发外部回调
    if (fnOnInputFinished != nullptr) 
        fnOnInputFinished(text);
        
    return true;
}

// 控件初始化函数
void AiAssistWnd::initControls()
{
    // 创建多行编辑控件
    _hInputEdit = ::CreateWindowExW(
        WS_EX_CLIENTEDGE,   // 带边框样式
        L"EDIT",            // 控件类名
        L"",                // 初始文本
        WS_CHILD | WS_VISIBLE | ES_MULTILINE | // 多行模式
        ES_AUTOVSCROLL | WS_VSCROLL,          // 滚动条支持
        0, 0, 0, 0,        // 初始位置尺寸(由布局管理)
        _hSelf,             // 父窗口句柄
        (HMENU)IDC_INPUT_EDIT, // 控件ID
        _hInst,             // 实例句柄
        nullptr
    );
    
    // 设置子类化处理
    SetWindowSubclass(
        _hInputEdit,        // 目标控件
        InputEditSubclassProc, // 处理函数
        0,                  // 子类ID
        (DWORD_PTR)this     // 传递类实例指针
    );
    
    // ... 其他控件初始化代码
}

模型调用和输出

上述处理用户输入完成后,会触发外部回调,该回调即模型调用及输出,初始化代码如下:

cpp 复制代码
// 配置模型选择变更回调
g_pAiWnd->fnOnModelSelChange = [](const std::string& model) {
    // 更新当前平台的默认模型配置
    // 操作路径:全局配置对象 -> 当前平台 -> 模型名称
    g_pluginConf.platforms[g_pluginConf.platform].model_name = model;
};

// 配置输入完成回调
g_pAiWnd->fnOnInputFinished = [](const std::string& text) {
    // 设置实时输出回调:将模型返回数据流式显示到界面
    g_pAiModel->fnAppentOutput = [](const std::string& ans) {
        // 转换编码格式:UTF8 -> GBK(适配本地字符集)
        // 参数false表示新建行,即时追加,有打字机效果
        g_pAiWnd->appendAnswer(
            Scintilla::String::UTF8ToGBK(ans.c_str(), ans.size()), 
            false
        ); 
    };
    
    // 设置输出完成回调:绑定窗口类的完成处理方法
    // 使用bind保留窗口实例指针和参数传递能力
    g_pAiModel->fnOutputFinished = std::bind(
        &AiAssistWnd::OnOutputFinished, 
        g_pAiWnd, 
        std::placeholders::_1
    );
    
    // 在回答区域添加应答标记
    g_pAiWnd->appendAnswer("【答】");
    
    // 提交异步AI处理任务
    // 通过UI任务队列保证线程安全
    g_pNppImp->RunUiTask(
        // 绑定模型请求方法与参数
        std::bind(
            &AiModel::DirectRequest, 
            g_pAiModel, 
            std::placeholders::_1
        ), 
        text  // 用户输入文本作为请求参数
    );
};

用户提交输入 设置流式输出回调 触发异步AI任务 实时追加模型输出 任务完成恢复界面状态

  • 线程安全 :通过g_pNppImp->RunUiTask确保UI操作在主线程执行
  • 编码转换 :模型返回的UTF-8数据经UTF8ToGBK转换适配本地环境

发送和停止按钮

考虑用户发起提问后,模型就一直嗒嗒嗒地输出,或者不动了也不知道是停止了还是卡顿了,所以做了一个按钮。该按钮可以:

  • 提交用户输入
  • 停止模型调用
  • 显示当前模型后台任务状态

通过全局变量std::atomic<bool> g_bRun控制后台任务状态。

cpp 复制代码
// 对话框消息处理主函数
INT_PTR AiAssistWnd::run_dlgProc(UINT message, WPARAM wParam, LPARAM lParam)
{
    switch (message) 
    {
    case WM_COMMAND:  // 处理控件通知消息
        {
            int wmId = LOWORD(wParam);     // 获取控件ID
            int wmEvent = HIWORD(wParam);  // 获取通知代码
            
            // 处理模型选择下拉框变化事件
            if (wmId == IDC_MODEL_COMBO && wmEvent == CBN_SELCHANGE)
            {
                OnModelComboSelChange();  // 更新选中的模型配置
                return TRUE;              // 已处理该消息
            }
            
            // 处理操作按钮点击事件
            if (wmId == IDC_ACTION_BUTTON && wmEvent == BN_CLICKED)
            {
                // 根据运行状态切换按钮功能
                if (g_bRun.load())  // 检查原子变量状态
                {
                    g_bRun.store(false);  // 设置停止标志
                }
                else 
                {
                    OnInputFinished();    // 触发输入处理流程
                }
                return TRUE;
            }
        }
        break;
    }
    
    // 未处理的消息传递给基类处理
    return DockingDlgInterface::run_dlgProc(message, wParam, lParam);
}

// 初始化控件布局和属性
void AiAssistWnd::initControls()
{
    // 创建操作按钮控件
    _hActionBtn = ::CreateWindowExW(
        WS_EX_CLIENTEDGE,       // 带3D边框样式
        L"BUTTON",              // 按钮控件类
        L"",                    // 初始文本为空(使用图标)
        WS_CHILD | WS_VISIBLE | // 必须的窗口样式
        BS_ICON | BS_PUSHBUTTON,// 显示图标的按钮类型
        0, 0, 28, 28,           // 初始位置和尺寸(后续布局调整)
        _hSelf,                 // 父窗口句柄
        (HMENU)IDC_ACTION_BUTTON, // 控件ID
        _hInst,                 // 模块实例句柄
        nullptr
    );
    
    // 设置辅助功能文本(供屏幕阅读器识别)
    SetWindowText(_hActionBtn, _T("发送"));

    // 加载发送状态图标资源
    _hSendIcon = (HICON)LoadImageW(
        _hInst,                      // 资源所在模块
        MAKEINTRESOURCEW(IDI_ICON_SEND), // 资源ID
        IMAGE_ICON,                  // 资源类型为图标
        24, 24,                     // 请求的图标尺寸
        LR_DEFAULTCOLOR             // 保留原始颜色
    );
    
    // 加载停止状态图标资源
    _hStopIcon = (HICON)LoadImageW(
        _hInst,
        MAKEINTRESOURCEW(IDI_ICON_STOP),
        IMAGE_ICON,
        24, 24,
        LR_DEFAULTCOLOR
    );

    // 设置按钮初始图标为发送状态
    SendMessageW(
        _hActionBtn, 
        BM_SETIMAGE,        // 设置按钮图像消息
        IMAGE_ICON,         // 指定图像类型为图标
        (LPARAM)_hSendIcon  // 传递图标句柄
    );
}

// 处理用户输入完成事件
bool AiAssistWnd::OnInputFinished()
{
    // 在回答区域添加问题标识
    appendAnswer("【问】\r\n" + text);
    
    // 切换按钮图标为停止状态
    SendMessageW(
        _hActionBtn, 
        BM_SETIMAGE, 
        IMAGE_ICON, 
        (LPARAM)_hStopIcon
    );
    
    // 禁用回答区域编辑功能
    ::EnableWindow(_hAnswerView, FALSE);
    
    // 触发外部输入完成回调
    if (fnOnInputFinished != nullptr) 
        fnOnInputFinished(text);
    
    return true;  // 事件已处理
}

// 处理模型输出完成事件
void AiAssistWnd::OnOutputFinished(const std::string& end)
{
    // 追加最终输出内容
    appendAnswer(end, false);
    
    // 重新启用回答区域编辑
    ::EnableWindow(_hAnswerView, TRUE);
    
    // 恢复按钮图标为发送状态
    SendMessageW(
        _hActionBtn, 
        BM_SETIMAGE, 
        IMAGE_ICON, 
        (LPARAM)_hSendIcon
    );
}
  • 原子变量std::atomic<bool> g_bRun控制后台任务中断
  • 按钮多态
    • 发送状态:显示纸飞机图标,绑定输入提交逻辑
    • 停止状态:显示方块图标,触发g_bRun.store(false)
  • 界面联动:输出时禁用编辑区域,任务结束后自动恢复

效果图

相关推荐
陈奕昆32 分钟前
4.3【LLaMA-Factory实战】教育大模型:个性化学习路径生成系统全解析
人工智能·python·学习·llama·大模型微调
wzx_Eleven34 分钟前
【论文阅读】基于客户端数据子空间主角度的聚类联邦学习分布相似性高效识别
论文阅读·人工智能·机器学习·网络安全·聚类
ykjhr_3d35 分钟前
场景可视化与数据编辑器:构建数据应用情境
人工智能
补三补四37 分钟前
遗传算法(GA)
人工智能·算法·机器学习·启发式算法
梁小憨憨40 分钟前
循环卷积(Circular Convolutions)
人工智能·笔记·深度学习·机器学习
非凡ghost44 分钟前
水印云:AI赋能,让图像处理变得简单高效
图像处理·人工智能
EQ-雪梨蛋花汤1 小时前
【相机标定】OpenCV 相机标定中的重投影误差与角点三维坐标计算详解
人工智能·opencv
向哆哆2 小时前
YOLOv8目标检测性能优化:损失函数改进的深度剖析
人工智能·yolo·目标检测·yolov8
threelab2 小时前
01.three官方示例+编辑器+AI快速学习webgl_animation_keyframes
人工智能·学习·编辑器
小马过河R2 小时前
在Cline上调用MCP服务之MCP实践篇
人工智能·microsoft·语言模型