本文是 sdk/PluginApi.h 的完整说明。插件是独立 DLL,不链接进androeq_win.exe;删除 DLL 即可停用。
- 头文件:
sdk/PluginApi.h - ABI 版本:
AEQ_PLUGIN_API_VERSION == 6 - 入口:
androeq_plugin_entry() - C ABI,可用于 MSVC / Clang / MinGW 的 64 位 DLL。
- 在线版:https://api.androeq.cc/plugin-api.html
1. 最小插件#
#include "PluginApi.h"
namespace {
const AeqHostApi* g_api = nullptr;
void* g_host = nullptr;
AeqWindowId g_window = 0;
void* AEQ_CALL init_plugin(const AeqHostApi* api, void* host) {
g_api = api;
g_host = host;
AeqWindowDesc wd{};
wd.size = sizeof(wd);
wd.title = L"Hello";
wd.x = wd.y = -1; // 居中
wd.width = 520;
wd.height = 300;
wd.standalone = 0; // 0 内置窗口;1 外置悬浮窗口
wd.theme_mode = AEQ_THEME_FOLLOW;
wd.user_data = nullptr;
g_window = api->create_window(host, &wd);
if (!g_window) return nullptr;
AeqControlDesc d{};
d.size = sizeof(d);
d.type = AEQ_CTL_LABEL;
d.x = 24; d.y = 24; d.w = 460; d.h = 40;
d.id = L"hello";
d.text = L"Hello, ANDROEQ+";
d.flags = AEQ_CF_BOLD;
d.font_size = 18.0f;
api->add_control(host, g_window, &d);
return reinterpret_cast<void*>(1);
}
void AEQ_CALL shutdown_plugin(void*) {}
void AEQ_CALL activate_plugin(void*) {
g_api->show_window(g_host, g_window, 1);
}
}
extern "C" __declspec(dllexport) const AeqPluginDescriptor* AEQ_CALL
androeq_plugin_entry() {
static const AeqPluginDescriptor desc = {
sizeof(AeqPluginDescriptor), AEQ_PLUGIN_API_VERSION,
L"hello", L"Hello", L"1.0.0", L"you", L"示例",
&init_plugin, &shutdown_plugin, &activate_plugin, nullptr};
return &desc;
}构建(MSVC):
cmake -S . -B build -G "Visual Studio 18 2026" -A x64
cmake --build build --config Release把 DLL 放入 <exe目录>\plugins 或 %APPDATA%\ANDROEQ+\plugins,在「插件」页刷新、加载、运行。
2. 描述符与生命周期#
typedef struct AeqPluginDescriptor {
uint32_t size;
uint32_t api_version;
const wchar_t* id;
const wchar_t* name;
const wchar_t* version;
const wchar_t* author;
const wchar_t* description;
void* (AEQ_CALL* init)(const AeqHostApi* api, void* host_user);
void (AEQ_CALL* shutdown)(void* plugin_ctx);
void (AEQ_CALL* activate)(void* plugin_ctx);
void (AEQ_CALL* deactivate)(void* plugin_ctx);
} AeqPluginDescriptor;| 字段 | 说明 |
|---|---|
size | sizeof(AeqPluginDescriptor) |
api_version | 必须等于 AEQ_PLUGIN_API_VERSION(当前 6) |
id | 唯一 ASCII id,建议短横线 |
init | 加载时调用;创建窗口/控件、启动线程 |
shutdown | 卸载/退出时调用;必须 join 自己的线程 |
activate | 点「运行」时调用;显示窗口/恢复任务 |
deactivate | 切走/关闭时调用,可空 |
宿主按以下顺序扫描 *.dll:
<exe目录>\plugins
%APPDATA%\ANDROEQ+\plugins
<exe目录>\old_plugins3. 窗口#
typedef struct AeqWindowDesc {
uint32_t size;
const wchar_t* title;
int32_t x;
int32_t y;
int32_t width;
int32_t height;
uint32_t flags;
int32_t standalone;
int32_t theme_mode;
uint32_t color_window;
uint32_t color_header;
uint32_t color_border;
uint32_t color_text;
uint32_t color_muted;
uint32_t color_accent;
void* user_data;
} AeqWindowDesc;- 坐标单位是 96 DPI 逻辑像素;宿主负责 DPI 缩放。
x/y = -1:内置窗口在宿主客户区居中;独立窗口在显示器工作区居中。standalone = 0:内置窗口,宿主内自绘的模态层,打开时主界面变暗并拦截点击。standalone = 1:外置悬浮窗口,独立的顶层 HWND,可拖出主界面并置顶;
不阻塞主界面。标题栏可拖动,右上角关闭。flags:AEQ_WF_NO_CLOSE:隐藏关闭按钮;AEQ_WF_NO_TITLE:隐藏标题栏;AEQ_WF_TRANSPARENT:外置窗口无面板/边框,仅绘制文字/控件,背景逐像素透明;AEQ_WF_DRAG_ANYWHERE:整个客户区可作为拖动区域(适合无标题栏歌词窗)。
- 窗口尺寸由
width/height决定;外置窗口按显示器工作区裁剪到合理范围。
主题#
enum AeqThemeMode {
AEQ_THEME_FOLLOW = 0,
AEQ_THEME_CUSTOM = 1
};AEQ_THEME_FOLLOW:自动跟随主窗口当前主题(含主色、文字、面板、边框);
所有color_*字段忽略。AEQ_THEME_CUSTOM:使用代码指定颜色;某字段为 0 时回退到当前宿主主题。
颜色格式为 0xAARRGGBB,可用宏:
AEQ_RGBA(r, g, b, a)例如:
wd.theme_mode = AEQ_THEME_CUSTOM;
wd.color_window = AEQ_RGBA(28, 34, 52, 255);
wd.color_header = AEQ_RGBA(36, 44, 68, 255);
wd.color_border = AEQ_RGBA(86, 104, 156, 255);
wd.color_text = AEQ_RGBA(238, 242, 255, 255);
wd.color_muted = AEQ_RGBA(158, 172, 205, 255);
wd.color_accent = AEQ_RGBA(104, 137, 255, 255);创建参数#
user_data 是插件自己的任意指针。事件回调里用:
void* ctx = api->get_window_user_data(host, window);取回。宿主不释放该指针;生命周期由插件保证。
4. 控件#
typedef struct AeqControlDesc {
uint32_t size;
int32_t type;
int32_t x, y, w, h;
const wchar_t* id;
const wchar_t* text;
const wchar_t* value;
const wchar_t* items;
int32_t checked;
uint32_t flags;
float font_size;
} AeqControlDesc;| type | 说明 |
|---|---|
AEQ_CTL_LABEL | 文本 |
AEQ_CTL_BUTTON | 按钮;点击触发 AEQ_EV_CLICK |
AEQ_CTL_EDIT | 单行输入;回车触发 AEQ_EV_SUBMIT |
AEQ_CTL_CHECKBOX | 勾选;变化触发 AEQ_EV_CHANGE |
AEQ_CTL_LIST | 换行分隔的列表;选择触发 AEQ_EV_CHANGE,双击 AEQ_EV_DOUBLE_CLICK,滚轮滚动 |
AEQ_CTL_PROGRESS | 进度条,value 0.0–1.0 |
AEQ_CTL_IMAGE | 显示 set_control_image 指定的图片 |
AEQ_CTL_SEPARATOR | 分隔线 |
控件 flags:
| flag | 说明 |
|---|---|
AEQ_CF_READONLY | 输入只读 |
AEQ_CF_PASSWORD | 输入显示为圆点 |
AEQ_CF_MULTILINE | 多行(保留) |
AEQ_CF_ALIGN_RIGHT | 文本右对齐 |
AEQ_CF_BOLD | 粗体 |
AEQ_CF_WRAP | 自动换行 |
AEQ_CF_HIDDEN | 初始隐藏 |
AEQ_CF_SECONDARY | 按钮次要样式:header 底 + 边框 |
AEQ_CF_GHOST | 按钮幽灵样式:透明底 + 描边 |
所有控件都有稳定字符串 id 和数字句柄;字符串 id 在窗口内唯一。
5. 事件#
typedef void (AEQ_CALL* AeqEventCallback)(
void* plugin_ctx, AeqWindowId window,
AeqControlId control, int32_t event);| event | 触发 |
|---|---|
AEQ_EV_CLICK | 按钮 |
AEQ_EV_CHANGE | 输入/勾选/列表选择 |
AEQ_EV_SUBMIT | 编辑框回车 |
AEQ_EV_DOUBLE_CLICK | 列表双击 |
AEQ_EV_CLOSE | 窗口关闭 |
AEQ_EV_RIGHT_CLICK | 窗口/控件右键(SDK v5+) |
回调在 UI 线程执行;回调内可以继续调用宿主 API,但不要做长时间阻塞。
6. 宿主 API#
AeqHostApi 的函数表按顺序追加;不要假设结构体大小固定,使用api->size / api->version。
窗口 / 控件#
| 函数 | 说明 |
|---|---|
create_window(desc) | 创建窗口,返回 AeqWindowId,失败返回 0 |
destroy_window(window) | 销毁窗口 |
show_window(window, show) | 显示/隐藏(外置窗口会显示为置顶悬浮窗) |
set_window_title(window, title) | 修改标题 |
invalidate_window(window) | 请求重绘 |
add_control(window, desc) | 添加控件 |
remove_control(window, control) | 删除控件 |
get_control_id(window, id) | 按字符串 id 查句柄 |
控件状态(线程安全)#
set_control_text(control, text)
get_control_text(control, buffer, capacity)
set_control_value(control, value)
get_control_value(control)
set_control_checked(control, checked)
get_control_checked(control)
set_control_enabled(control, enabled)
get_control_enabled(control)
set_control_visible(control, visible)
get_control_visible(control)
set_control_items(control, newline_items)
get_control_items(control, buffer, capacity)
set_control_image(control, file_path)
set_control_focus(control)
set_control_font_size(control, logical_px)
set_control_rect(control, x, y, w, h)
set_control_opacity(control, 0.0..1.0)set_control_rect / set_control_opacity 用于歌词推挤、渐隐等逐帧动画。
后台线程可以安全调用这些状态函数;宿主会合并重绘。
宿主服务#
| 函数 | 说明 |
|---|---|
set_status(text) | 主窗口底部状态栏 |
add_media_file(path,title,artist) | 把已存在本地音频加入曲库 |
set_today_recommendations(paths,count) | 推送到曲库页「今天听什么」 |
get_data_dir() | %APPDATA%\ANDROEQ+ |
get_plugin_dir() | <exe目录>\plugins |
open_url(url) | 系统浏览器打开 |
set_clipboard_text / get_clipboard_text | 剪贴板 |
now_ms() | GetTickCount64 |
get_window_user_data(window) | 取回窗口创建参数 |
set_window_opacity(window, 0..1) | 外置窗口整体透明度(样式切换动画) |
show_context_menu(window, items, &selected) | 鼠标位置弹原生右键菜单;items 用 \n 分隔,- 为分隔线 |
7. 播放 / 歌词 API(v4 新增)#
int32_t get_playback_state(void* user, AeqPlaybackState* state);
int32_t get_current_track(void* user,
wchar_t* title, int32_t title_capacity,
wchar_t* artist, int32_t artist_capacity);
int32_t get_current_lyric(void* user,
wchar_t* current, int32_t current_capacity,
wchar_t* next, int32_t next_capacity);AeqPlaybackState:
typedef struct AeqPlaybackState {
uint32_t size; // 调用方填 sizeof(AeqPlaybackState)
int32_t playing;
int32_t paused;
double position; // 秒
double duration; // 秒;未打开为 0
} AeqPlaybackState;- 这些函数可在插件后台线程每 100–200 ms 轮询;
- 歌词由宿主按当前曲目自动加载(本地 LRC / 已缓存的 AI 歌词);
- 无歌词时
get_current_lyric返回 0,并把缓冲区置空; - 返回的字符串在调用返回后复制到调用方缓冲区,插件不要保存宿主指针。
外置歌词窗示例#
AeqPlaybackState st{};
st.size = sizeof(st);
api->get_playback_state(host, &st);
wchar_t current[2048]{}, next[2048]{};
api->get_current_lyric(host, current, 2048, next, 2048);
api->set_control_text(host, lyricControl, current);完整示例:plugins/lyrics_float/LyricsFloat.cpp(外置悬浮窗,跟随主题)。
8. 线程与生命周期规则#
- 控件状态函数是线程安全的;窗口创建/销毁、事件回调建议在 UI 线程。
shutdown必须request_stop()+join()自己的线程,之后宿主才FreeLibrary。- 不要保存宿主返回的
wchar_t*指针;立即复制。 - 不要在事件回调里做网络/磁盘长任务;用后台线程 + 控件状态更新。
- 插件与宿主同进程,只有信任的 DLL 才应加载。
- 外置窗口在插件卸载/宿主退出时由宿主销毁;插件不要自行
DestroyWindow。
9. 兼容与版本#
- v2:新增
set_today_recommendations; - v3:新增窗口
standalone、theme_mode、color_*、user_data和get_window_user_data; - v4:新增播放状态、当前曲目、当前歌词 API;
- v5:新增
AEQ_WF_TRANSPARENT/AEQ_WF_DRAG_ANYWHERE、AEQ_EV_RIGHT_CLICK、set_window_opacity、show_context_menu; - v6:新增
set_control_rect/set_control_opacity(歌词推挤/渐隐动画)。
宿主当前要求 api_version == 6;旧插件请用 sdk/PluginApi.h 重新编译。
更偏使用说明的文档见 PLUGINS.md。