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

news2025/5/20 20:00:16

引言

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

  • 模型动态切换:支持运行时加载配置的AI模型列表
  • 交互式输入处理:实现多行文本输入与Ctrl+Enter提交逻辑
  • 异步模型调用:通过回调机制实现流式输出与界面实时更新
  • 状态控制:提供发送/停止双模式按钮,支持任务中断
用户 输入框 主窗口 模型 界面 按钮 输入内容(Ctrl+Enter) 触发OnInputFinished 调用DirectRequest 流式返回数据 实时追加输出 点击停止 设置g_bRun=false 任务终止通知 恢复初始状态 用户 输入框 主窗口 模型 界面 按钮

模型下拉支持

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

// 打开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提交与普通回车换行分离
    • 输入内容过滤(空值/纯空白字符校验)
  • 输入输出分离:用户提问内容自动添加【问】标记并清空输入区

代码实现如下:

// 输入框子类化处理函数,用于自定义编辑控件行为
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     // 传递类实例指针
    );
    
    // ... 其他控件初始化代码
}

模型调用和输出

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

// 配置模型选择变更回调
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控制后台任务状态。

// 对话框消息处理主函数
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)
  • 界面联动:输出时禁用编辑区域,任务结束后自动恢复

效果图

在这里插入图片描述

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

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

相关文章

在激烈竞争下B端HMI设计怎样打造独特用户体验?

在当今数字化高度发展的时代&#xff0c;B 端市场竞争愈发激烈。对于 B 端 HMI&#xff08;人机界面&#xff09;设计而言&#xff0c;打造独特的用户体验已成为在竞争中脱颖而出的关键因素。B 端用户在复杂的工作场景中&#xff0c;对 HMI 设计有着独特的需求和期望&#xff0…

【Netty篇】Handler Pipeline 详解

目录 一、 Handler & Pipeline——流水线上的“特种部队”与“生产线”1、 ChannelHandler —— 流水线上的“特种兵”&#x1f46e;‍♂️2、 ChannelPipeline —— 生产线上的“接力赛跑”&#x1f3c3;‍♀️&#x1f3c3;‍♂️ 二、 代码实例1、 服务端代码示例2、 客…

16-算法打卡-哈希表-两个数组的交集-leetcode(349)-第十六天

1 题目地址 349. 两个数组的交集 - 力扣&#xff08;LeetCode&#xff09;349. 两个数组的交集 - 给定两个数组 nums1 和 nums2 &#xff0c;返回 它们的 交集 。输出结果中的每个元素一定是 唯一 的。我们可以 不考虑输出结果的顺序 。 示例 1&#xff1a;输入&#xff1a;nu…

java + spring boot + mybatis 通过时间段进行查询

前端传来的只有日期内容&#xff0c;如&#xff1a;2025-04-17 需要在日期内容的基础上补充时间部分&#xff0c;代码示例&#xff1a; /*** 日志查询&#xff08;分页查询&#xff09;* param recordLogQueryDTO 查询参数对象* return 日志列表*/Overridepublic PageBean<…

helm账号密码加密

1、安装工具 sudo apt update sudo apt install gnupg -y wget https://github.com/getsops/sops/releases/download/v3.10.2/sops-v3.10.2.linux.amd64 mv sops-v3.10.2.linux.amd64 /usr/local/bin/sops chmod x /usr/local/bin/sops2、生成加密文件 gpg --full-generate-…

Flink 编程基础:Scala 版 DataStream API 入门

大家好&#xff01;我是心海 流处理技术在大数据时代正变得越来越重要&#xff0c;而 Apache Flink 作为领先的流处理引擎&#xff0c;凭借其高性能、低延迟和丰富的 API 受到了广泛关注。本文将以 Scala 语言为例&#xff0c;详细讲解 Flink DataStream API 的基本编程模型&am…

HTML5好看的水果蔬菜在线商城网站源码系列模板5

文章目录 1.设计来源1.1 主界面1.2 关于我们1.3 商品服务1.4 果蔬展示1.5 联系我们1.6 商品具体信息1.7 登录注册 2.效果和源码2.1 动态效果2.2 源代码 源码下载万套模板&#xff0c;程序开发&#xff0c;在线开发&#xff0c;在线沟通 作者&#xff1a;xcLeigh 文章地址&#…

宜搭与金蝶互通——连接器建立

一、 进入连接器工厂 图1 连接器入口 二、 新建连接器 图2 新建连接器第一步 1、 连接器显示名,如图2中①所示; 2、 图2中②域名,是金蝶系统API接口里面的“完整服务地址”com之前的信息,不含“https”,如图3中①所示; 3、 Base Url通常为“/”,如图2…

SP7733:HPYNOS - Happy Numbers I(参考我之前的文章,哈希)

题目大意 我们定义“破坏”整数的过程是对其每一位上的数字的平方求和成为一个新数&#xff0c;如果一个数在经过若干次“破坏”以后变成了 1&#xff0c;那么这个数就是一个高兴的数字&#xff0c;输出变化次数&#xff0c;否则如果永远不会变成 1&#xff0c;输出 −1。 例如…

【JavaWeb】详细讲解 HTTP 协议

文章目录 一、HTTP简介1.1 概念1.2 特点 二、协议2.1 HTTP-请求协议&#xff08;1&#xff09;GET方式&#xff08;2&#xff09;POST方式&#xff08;3&#xff09;GET和POST的区别&#xff1a; 2.2 HTTP-响应协议&#xff08;1&#xff09;格式&#xff08;2&#xff09;响应…

“星睿O6” AI PC开发套件评测 - Windows on Arm 安装指南和性能测评

引言 Radxa联合此芯科技和安谋科技推出全新的"星睿O6"迷你 ITX 主板。该系统搭载了 CIX P1&#xff08;CD8180&#xff09;12 核 Armv9 处理器&#xff0c;拥有高达30T算力的NPU和高性能的GPU&#xff0c;最高配备64GB LPDDR内存&#xff0c;并提供了如 5GbE、HDMI …

Python 调用 YOLOv11 ONNX

Python 调用 YOLO ONNX 1 下载ONNX文件2 Python代码 1 下载ONNX文件 ONNX下载地址 2 Python代码 import cv2 from ultralytics import YOLOdef check(yolo:str, path:str):# 加载 YOLOv11model YOLO(yolo)# 读取图片img cv2.imread(path)# 推理&#xff08;可以传文件路径…

数据通信学习笔记之OSPF路由汇总

区域间路由汇总 路由汇总又被称为路由聚合&#xff0c;即是将一组前缀相同的路由汇聚成一条路由&#xff0c;从而达到减小路由表规模以及优化设备资源利用率的目的&#xff0c;我们把汇聚之前的这组路由称为精细路由或明细路由&#xff0c;把汇聚之后的这条路由称为汇总路由或…

ASP.NET Core Web API 配置系统集成

文章目录 前言一、配置源与默认设置二、使用步骤1&#xff09;创建项目并添加配置2&#xff09;配置文件3&#xff09;强类型配置类4&#xff09;配置Program.cs5&#xff09;控制器中使用配置6&#xff09;配置优先级测试7&#xff09;动态重载配置测试8&#xff09;运行结果示…

如何判断单片机性能极限?

目录 1、CPU 负载 2、内存使用情况 3、实时性能 4、外设带宽 5、功耗与温度 在嵌入式系统设计中&#xff0c;当系统变得复杂、功能增加时&#xff0c;单片机可能会逐渐逼近其性能极限。及时识别这些极限点对于保证产品质量、稳定性和用户体验至关重要。 当你的嵌入式系统…

AI在多Agent协同领域的核心概念、技术方法、应用场景及挑战 的详细解析

以下是 AI在多Agent协同领域的核心概念、技术方法、应用场景及挑战 的详细解析&#xff1a; 1. 多Agent协同的定义与核心目标 多Agent系统&#xff08;MAS, Multi-Agent System&#xff09;&#xff1a; 由多个独立或协作的智能体&#xff08;Agent&#xff09;组成&#xff…

1.凸包、极点、极边基础概念

目录 1.凸包 2.调色问题 3.极性(Extrem) 4.凸组合(Convex Combination) 5.问题转化(Strategy)​编辑 6.In-Triangle test 7.To-Left-test 8.极边&#xff08;Extream Edges&#xff09; 1.凸包 凸包就是上面蓝色皮筋围出来的范围 这些钉子可以转换到坐标轴中&#xff0…

OSCP - Proving Grounds - DriftingBlues6

主要知识点 路径爆破dirtycow内核漏洞提权 具体步骤 总体来讲&#xff0c;这台靶机还是比较直接的&#xff0c;没有那么多的陷阱,非常适合用来学习 依旧是nmap开始,只开放了80端口 Nmap scan report for 192.168.192.219 Host is up (0.42s latency). Not shown: 65534 cl…

深度理解指针之例题

文章目录 前言题目分析与讲解涉及知识点 前言 对指针有一定了解后&#xff0c;讲一下一道初学者的易错题 题目分析与讲解 先定义一个数组跟一个指针变量 然后把数组名赋值给指针变量————也就是把首地址传到pulPtr中 重点是分析这一句&#xff1a; *&#xff08;pulPtr…

LeetCode算法题(Go语言实现)_51

题目 给你两个下标从 0 开始的整数数组 nums1 和 nums2 &#xff0c;两者长度都是 n &#xff0c;再给你一个正整数 k 。你必须从 nums1 中选一个长度为 k 的 子序列 对应的下标。 对于选择的下标 i0 &#xff0c;i1 &#xff0c;…&#xff0c; ik - 1 &#xff0c;你的 分数 …