引言
之前的文章已经介绍实现了AI对话窗口
,但只有个空壳,没有实现功能。本次将集中完成对话窗口的功能,主要内容为:
- 模型动态切换:支持运行时加载配置的AI模型列表
- 交互式输入处理:实现多行文本输入与
Ctrl+Enter
提交逻辑 - 异步模型调用:通过回调机制实现流式输出与界面实时更新
- 状态控制:提供发送/停止双模式按钮,支持任务中断
模型下拉支持
启动时读取插件配置,包括当前模型名称
和平台所支持的模型列表
,然后启动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 // 用户输入文本作为请求参数);
};
- 线程安全:通过
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); // 获取控件IDint 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), // 资源IDIMAGE_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)
- 界面联动:输出时禁用编辑区域,任务结束后自动恢复