【AI插件开发】Notepad++ AI插件开发实践(代码篇):从Dock窗口集成到功能菜单实现

一、引言

上篇文章已经在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编辑器的意思了,下一步将进一步完善功能实现,包括配置加载、配置窗口以及界面功能实现。

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