【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)
  • 界面联动:输出时禁用编辑区域,任务结束后自动恢复

效果图

相关推荐
NAGNIP9 小时前
一文搞懂深度学习中的通用逼近定理!
人工智能·算法·面试
冬奇Lab11 小时前
一天一个开源项目(第36篇):EverMemOS - 跨 LLM 与平台的长时记忆 OS,让 Agent 会记忆更会推理
人工智能·开源·资讯
冬奇Lab11 小时前
OpenClaw 源码深度解析(一):Gateway——为什么需要一个"中枢"
人工智能·开源·源码阅读
AngelPP14 小时前
OpenClaw 架构深度解析:如何把 AI 助手搬到你的个人设备上
人工智能
宅小年14 小时前
Claude Code 换成了Kimi K2.5后,我再也回不去了
人工智能·ai编程·claude
九狼15 小时前
Flutter URL Scheme 跨平台跳转
人工智能·flutter·github
ZFSS15 小时前
Kimi Chat Completion API 申请及使用
前端·人工智能
天翼云开发者社区16 小时前
春节复工福利就位!天翼云息壤2500万Tokens免费送,全品类大模型一键畅玩!
人工智能·算力服务·息壤
知识浅谈16 小时前
教你如何用 Gemini 将课本图片一键转为精美 PPT
人工智能
Ray Liang17 小时前
被低估的量化版模型,小身材也能干大事
人工智能·ai·ai助手·mindx