引言
之前的文章已经介绍实现了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)
- 界面联动:输出时禁用编辑区域,任务结束后自动恢复
效果图
