一、引言
上篇文章已经在Notepad++的插件开发中集成了选中即问AI
的功能,这一篇文章将在此基础上进一步集成,支持AI对话窗口
以及常见的代码功能菜单:
- 显示AI的Dock窗口,可以用自然语言向 AI 提问或要求执行任务
- 选中代码后使用,AI 会详细解释代码功能
- 需要 AI 帮助改进或修复代码时使用
- 自动生成代码注释
- 选中即问,直接把选中内容丢给AI
- 参数设置,基于插件配置切换AI平台等参数,提供对话框设置及调整平台参数
本篇的主要难点在于如何集成Dock窗口,是自己裸写Window窗口,还是找第三方库,或者从Notepad++的源码中剥离相关代码?
最终选择的是从Notepad++的源码中剥离相关代码的方案,但是直接剥离很难,发现依赖越来越多,因此需要在剥离的基础上做一些改造,删减非必须的功能,比如NppDarkMode
裁剪。
注
:项目已开源、镜像,欢迎使用及指正
二、Notepad++的源码中剥离Dock窗口

1. 剥离后的Dock文件列表
Common.h
Docking.h
DockingDlgInterface.h
dockingResource.h
dpiManagerV2.cpp
dpiManagerV2.h
NppDarkMode.cpp
NppDarkMode.h
StaticDialog.cpp
StaticDialog.h
Window.h
2. 要点说明
直接从Notepad++中拷贝上述文件代码到项目中引用的话,会发现还要包含其他文件,然后试着把别的文件引入的时候,后面发现引入的文件越来越多,所以需要对部分文件进行裁剪,主要裁剪的代码是NppDarkMode.cpp
。 我把NppDarkMode.cpp
的代码全删了,然后自己实现了项目中调用了的函数,采集后的NppDarkMode.cpp
文件内容如下:
cpp
#include "NppDarkMode.h"
enum class SystemVersion
{
Unknown,
Windows10,
Windows11
};
SystemVersion GetWindowsVersion()
{
// 使用RtlGetVersion替代已废弃的GetVersionEx
typedef NTSTATUS(WINAPI* RtlGetVersionPtr)(PRTL_OSVERSIONINFOW);
OSVERSIONINFOW osInfo = { 0 };
HMODULE hMod = GetModuleHandleW(L"ntdll.dll");
if (hMod)
{
auto RtlGetVersion = reinterpret_cast<RtlGetVersionPtr>(
GetProcAddress(hMod, "RtlGetVersion"));
if (RtlGetVersion) {
osInfo.dwOSVersionInfoSize = sizeof(osInfo);
if (RtlGetVersion(&osInfo) == 0)
{ // STATUS_SUCCESS
// Windows 11的版本号为10.0.22000+
if (osInfo.dwMajorVersion == 10 &&
osInfo.dwMinorVersion == 0)
{
if (osInfo.dwBuildNumber >= 22000)
{
return SystemVersion::Windows11;
}
else if (osInfo.dwBuildNumber >= 10240)
{
return SystemVersion::Windows10;
}
}
}
}
}
return SystemVersion::Unknown;
}
bool NppDarkMode::isWindows10() { return GetWindowsVersion() == SystemVersion::Windows10; }
bool NppDarkMode::isWindows11() { return GetWindowsVersion() == SystemVersion::Windows11; }
void NppDarkMode::setDarkTitleBar(HWND hwnd) {}
3. AI窗口实现
现在只需要引入DockingDlgInterface.h
文件,实现一个基于DockingDlgInterface
的窗口即可,其中AiAssistWnd.h
:
cpp
// AiAssistWnd.h
#pragma once
#include "DockingDlgInterface.h"
#include "PluginInterface.h"
class AiAssistWnd : public DockingDlgInterface {
public:
AiAssistWnd(HINSTANCE hInst, const NppData& nppData);
~AiAssistWnd();
// 必需实现的虚函数
virtual void init();
virtual INT_PTR run_dlgProc(UINT message, WPARAM wParam, LPARAM lParam);
// 功能接口
void updateModelList(const std::vector<std::wstring>& models);
void appendAnswer(const std::wstring& answer);
void clearConversation();
private:
void initControls();
void layoutControls(int width, int height);
void handleUserInput();
// Npp
HINSTANCE _hInst;
NppData _nppData;
// 控件句柄
HWND _hModelCombo = nullptr;
HWND _hInputEdit = nullptr;
HWND _hAnswerView = nullptr;
// 配置参数
const int CONTROL_MARGIN = 5;
const int COMBO_HEIGHT = 25;
const int INPUT_HEIGHT = 80;
// 字体资源
HFONT _hFont = nullptr;
HFONT _hBoldFont = nullptr;
// 控件ID定义
enum ControlID {
IDC_MODEL_COMBO = 2000,
IDC_INPUT_EDIT,
IDC_ANSWER_VIEW
};
};
代码实现:
cpp
// AiAssistWnd.cpp
#include "AiAssistWnd.h"
#include "resource.h"
#include <richedit.h>
#include <commctrl.h>
#include <format>
AiAssistWnd::AiAssistWnd(HINSTANCE hInst, const NppData& nppData)
: DockingDlgInterface(IDD_DIALOG_AI_ASSIST), _hInst(hInst), _nppData(nppData)
{
this->_hParent = _nppData._nppHandle;
}
AiAssistWnd::~AiAssistWnd()
{
if (_hFont) DeleteObject(_hFont);
if (_hBoldFont) DeleteObject(_hBoldFont);
}
void AiAssistWnd::init()
{
DockingDlgInterface::init(_hInst, _hParent);
// 注册Dock窗口
tTbData tbData = { 0 };
DockingDlgInterface::create(&tbData);
tbData.uMask = DWS_DF_CONT_RIGHT | DWS_ICONTAB;;
tbData.pszModuleName = L"AI Assistant";;
tbData.dlgID = _dlgID;
::SendMessage(_nppData._nppHandle, NPPM_DMMREGASDCKDLG, 0, (LPARAM)&tbData);
// 初始化UI控件
initControls();
// 设置初始大小
RECT rc;
GetClientRect(_hSelf, &rc);
::SetWindowPos(_hSelf, nullptr, rc.left, rc.top, 300, rc.bottom, SWP_NOZORDER | SWP_NOMOVE);
layoutControls(rc.right, rc.bottom);
}
void AiAssistWnd::initControls()
{
// 创建控件
_hModelCombo = ::CreateWindowExW(0, WC_COMBOBOX, L"",
CBS_DROPDOWNLIST | WS_CHILD | WS_VISIBLE | WS_TABSTOP,
0, 0, 0, 0, _hSelf, (HMENU)IDC_MODEL_COMBO, _hInst, nullptr);
_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, _hInst, nullptr);
// 使用RichEdit 4.1
LoadLibraryW(L"Msftedit.dll");
_hAnswerView = ::CreateWindowExW(WS_EX_CLIENTEDGE, MSFTEDIT_CLASS, L"",
WS_CHILD | WS_VISIBLE | ES_MULTILINE | ES_READONLY | WS_VSCROLL | WS_HSCROLL,
0, 0, 0, 0, _hSelf, (HMENU)IDC_ANSWER_VIEW, _hInst, nullptr);
// 初始化字体
_hFont = CreateFontW(14, 0, 0, 0, FW_NORMAL, FALSE, FALSE, FALSE,
DEFAULT_CHARSET, OUT_DEFAULT_PRECIS, CLIP_DEFAULT_PRECIS,
DEFAULT_QUALITY, DEFAULT_PITCH, L"Segoe UI");
_hBoldFont = CreateFontW(14, 0, 0, 0, FW_SEMIBOLD, FALSE, FALSE, FALSE,
DEFAULT_CHARSET, OUT_DEFAULT_PRECIS, CLIP_DEFAULT_PRECIS,
DEFAULT_QUALITY, DEFAULT_PITCH, L"Segoe UI");
// 应用字体
SendMessageW(_hModelCombo, WM_SETFONT, (WPARAM)_hFont, TRUE);
SendMessageW(_hInputEdit, WM_SETFONT, (WPARAM)_hFont, TRUE);
SendMessageW(_hAnswerView, WM_SETFONT, (WPARAM)_hFont, TRUE);
}
INT_PTR AiAssistWnd::run_dlgProc(UINT message, WPARAM wParam, LPARAM lParam)
{
switch (message) {
case WM_SIZE:
if (wParam != SIZE_MINIMIZED) {
RECT rc;
GetClientRect(_hSelf, &rc);
layoutControls(rc.right, rc.bottom);
}
return TRUE;
case WM_COMMAND:
if (HIWORD(wParam) == EN_MAXTEXT && LOWORD(wParam) == IDC_INPUT_EDIT) {
handleUserInput();
return TRUE;
}
break;
case WM_NOTIFY:
// 处理其他通知消息
break;
case WM_CTLCOLOREDIT:
// 设置输入框背景色
if ((HWND)lParam == _hInputEdit) {
SetBkColor((HDC)wParam, RGB(255, 255, 255));
return (INT_PTR)GetStockObject(WHITE_BRUSH);
}
break;
}
return DockingDlgInterface::run_dlgProc(message, wParam, lParam);
}
void AiAssistWnd::layoutControls(int width, int height)
{
int yPos = CONTROL_MARGIN;
int nHeight = CONTROL_MARGIN;
// 回答区域
int answerHeight = height - 3*CONTROL_MARGIN - COMBO_HEIGHT - INPUT_HEIGHT;
::MoveWindow(_hAnswerView,
CONTROL_MARGIN, yPos,
width - 2 * CONTROL_MARGIN, answerHeight, TRUE);
yPos += CONTROL_MARGIN + answerHeight;
// 模型选择框
::MoveWindow(_hModelCombo,
CONTROL_MARGIN, yPos,
width - 2 * CONTROL_MARGIN, COMBO_HEIGHT, TRUE);
yPos += COMBO_HEIGHT + CONTROL_MARGIN;
// 输入框
::MoveWindow(_hInputEdit,
CONTROL_MARGIN, yPos,
width - 2 * CONTROL_MARGIN, INPUT_HEIGHT, TRUE);
yPos += INPUT_HEIGHT + CONTROL_MARGIN;
}
void AiAssistWnd::updateModelList(const std::vector<std::wstring>& models)
{
SendMessageW(_hModelCombo, CB_RESETCONTENT, 0, 0);
for (const auto& model : models) {
SendMessageW(_hModelCombo, CB_ADDSTRING, 0, (LPARAM)model.c_str());
}
if (!models.empty()) {
SendMessageW(_hModelCombo, CB_SETCURSEL, 0, 0);
}
}
void AiAssistWnd::appendAnswer(const std::wstring& answer)
{
// 添加时间戳
SYSTEMTIME st;
GetLocalTime(&st);
std::wstring timestamp = std::format(L"[{:02}:{:02}:{:02}] ",
st.wHour, st.wMinute, st.wSecond);
// 设置富文本格式
CHARFORMAT2W cf = { sizeof(CHARFORMAT2W) };
cf.dwMask = CFM_COLOR | CFM_BOLD;
cf.crTextColor = RGB(0, 128, 0);
cf.dwEffects = CFE_BOLD;
SendMessageW(_hAnswerView, EM_SETCHARFORMAT, SCF_SELECTION, (LPARAM)&cf);
SendMessageW(_hAnswerView, EM_REPLACESEL, FALSE, (LPARAM)timestamp.c_str());
// 恢复默认格式
cf.dwMask = CFM_COLOR;
cf.crTextColor = RGB(0, 0, 0);
cf.dwEffects = 0;
SendMessageW(_hAnswerView, EM_SETCHARFORMAT, SCF_SELECTION, (LPARAM)&cf);
SendMessageW(_hAnswerView, EM_REPLACESEL, FALSE, (LPARAM)answer.c_str());
// 自动滚动到底部
SendMessageW(_hAnswerView, WM_VSCROLL, SB_BOTTOM, 0);
}
void AiAssistWnd::clearConversation()
{
if (!IsWindow(_hAnswerView))
return;
// 使用SETTEXTEX结构清空内容
SETTEXTEX st = {
ST_DEFAULT, // 标志位
1200 // 使用UTF-16编码
};
// 方法1:直接设置空文本(保留格式)
SendMessageW(_hAnswerView, EM_SETTEXTEX, (WPARAM)&st, (LPARAM)L"");
// 方法2:通过选择全部删除(更彻底)
// SendMessageW(_hAnswerView, EM_SETSEL, 0, -1); // 全选
// SendMessageW(_hAnswerView, EM_REPLACESEL, 0, (LPARAM)L""); // 替换为空
// 可选:重置滚动条位置
SendMessageW(_hAnswerView, WM_VSCROLL, SB_TOP, 0);
// 可选:清除Undo缓冲区
SendMessageW(_hAnswerView, EM_EMPTYUNDOBUFFER, 0, 0);
}
void AiAssistWnd::handleUserInput()
{
// 获取输入文本
int len = GetWindowTextLengthW(_hInputEdit) + 1;
std::wstring input(len, L'\0');
GetWindowTextW(_hInputEdit, &input[0], len);
input.resize(len - 1); // 移除结尾的null字符
if (!input.empty()) {
// TODO: 触发AI处理逻辑
appendAnswer(L"Received: " + input);
// 清空输入框
SetWindowTextW(_hInputEdit, L"");
}
}
三、主要菜单功能实现
1.定义菜单
cpp
// PluginDefinition.h
//-----------------------------------------------//
//-- STEP 2. DEFINE YOUR PLUGIN COMMAND NUMBER --//
//-----------------------------------------------//
//
// Here define the number of your plugin commands
//
const int nbFunc = 6;
//
// Your plugin command functions
//
//
// 参数设置
void PluginConfig();
// 打开Ai助手窗口
void OpenAiAssistWnd();
// 解读代码
void ReadCode();
// 代码优化
void OptimizeCode();
// 添加代码注释
void AddCodeComment();
// 选中即问
void AskBySelectedText();
2.初始化菜单
cpp
// PluginDefinition.cpp
//
// Initialization of your plugin commands
// You should fill your plugins commands here
void commandMenuInit()
{
//--------------------------------------------//
//-- STEP 3. CUSTOMIZE YOUR PLUGIN COMMANDS --//
//--------------------------------------------//
// with function :
// setCommand(int index, // zero based number to indicate the order of command
// TCHAR *commandName, // the command name that you want to see in plugin menu
// PFUNCPLUGINCMD functionPointer, // the symbol of function (function pointer) associated with this command. The body should be defined below. See Step 4.
// ShortcutKey *shortcut, // optional. Define a shortcut to trigger this command
// bool check0nInit // optional. Make this menu item be checked visually
// );
// 初始化数据
g_pNppImp = new NppImp(g_nppData);
// 初始化菜单
ShortcutKey* pSck = new ShortcutKey[nbFunc];
g_pShortcutKeys = pSck;
size_t nCid = 0;
setCommand(nCid, L"参数配置", PluginConfig, NULL, false); ++nCid;
pSck[nCid] = { false, true, false, 'K' };
setCommand(nCid, L"显示窗口", OpenAiAssistWnd, pSck + nCid, false); ++nCid;
pSck[nCid] = { false, true, false, 'J' };
setCommand(nCid, L"解读代码", ReadCode, pSck + nCid, false); ++nCid;
pSck[nCid] = { false, true, false, 'Y' };
setCommand(nCid, L"优化代码", OptimizeCode, pSck + nCid, false); ++nCid;
pSck[nCid] = { false, true, false, 'Z' };
setCommand(nCid, L"代码注释", AddCodeComment, pSck + nCid, false); ++nCid;
pSck[nCid] = { false, true, false, 'A' };
setCommand(nCid, L"选中即问", AskBySelectedText, pSck + nCid, false); ++nCid;
}
3.打开AI窗口
cpp
// PluginDefinition.cpp
// 打开Ai助手窗口
void OpenAiAssistWnd()
{
// 初始化Dock窗口
if (g_pAiWnd == nullptr && g_hModule != nullptr && g_nppData._nppHandle != nullptr)
{
g_pAiWnd = new AiAssistWnd((HINSTANCE)g_hModule, g_nppData);
g_pAiWnd->init();
}
if (g_pAiWnd)
{
g_pAiWnd->display(true);
}
}
四、效果展示
1.插件菜单

2.AI窗口

五、结语
到这里,已经在Notepad++中支持AI对话窗口了,已经有一点Cursor
的AI编辑器的意思了,下一步将进一步完善功能实现,包括配置加载、配置窗口以及界面功能实现。