feat(RegisterTray): 重构并添加新功能

- 重构了 RegisterTray.dll 的核心逻辑,使用更现代的 Windows API
- 添加了自定义弹出菜单功能,支持鼠标悬停和点击事件
- 优化了托盘图标的创建和销毁流程
-改进了错误处理和资源管理- 新增 registerEx 方法,支持描述信息
This commit is contained in:
tzdwindows 7
2025-08-21 16:21:36 +08:00
parent 75f765bb47
commit 86a9e9e81d
6 changed files with 750 additions and 249 deletions

Binary file not shown.

View File

@@ -7,12 +7,21 @@
#ifdef __cplusplus
extern "C" {
#endif
/*
* Class: com_axis_innovators_box_tools_RegisterTray
* Method: register
* Signature: (Ljava/lang/String;Ljava/util/List;Ljava/lang/String;Ljava/lang/String;Lcom/axis/innovators/box/tools/RegisterTray/Event;)J
* Signature: (Ljava/lang/String;Ljava/util/List;Ljava/lang/String;Lcom/axis/innovators/box/tools/RegisterTray/Event;)J
*/
JNIEXPORT jlong JNICALL Java_com_axis_innovators_box_tools_RegisterTray_register
(JNIEnv*, jclass, jstring, jobject, jstring, jobject);
/*
* Class: com_axis_innovators_box_tools_RegisterTray
* Method: registerEx
* Signature: (Ljava/lang/String;Ljava/util/List;Ljava/lang/String;Ljava/lang/String;Lcom/axis/innovators/box/tools/RegisterTray/Event;)J
*/
JNIEXPORT jlong JNICALL Java_com_axis_innovators_box_tools_RegisterTray_registerEx
(JNIEnv*, jclass, jstring, jobject, jstring, jstring, jobject);
/*

View File

@@ -4,288 +4,790 @@
#include <jni.h>
#include <vector>
#include <string>
#include <mutex>
#include <atomic>
#include <algorithm>
#include <uxtheme.h>
#include <windowsx.h>
#pragma comment(lib, "uxtheme.lib")
#include "com_axis_innovators_box_tools_RegisterTray.h"
// 调试输出
#define DEBUG_LOG(msg) OutputDebugStringW(L"[Tray] " msg L"\n")
// 调试输出
#define DEBUG_LOG(msg) { OutputDebugStringW(L"[Tray] " msg L"\n"); }
// 全局 JVM 缓存(懒取)
static std::atomic<JavaVM*> gJvm{ nullptr };
struct MenuItemData {
jint menuId;
jobject eventObjGlobal; // global ref to the Event object for this item
jmethodID onClickMethod;
jobject eventObj;
int menuId;
std::wstring title;
};
struct TrayData {
HMENU hMenu = NULL;
std::vector<MenuItemData> menuItems;
jobject eventObj = NULL;
jmethodID onClickMethod = NULL;
HWND hwnd = NULL;
UINT trayId = 0;
HICON hIcon = NULL;
std::vector<MenuItemData> menuItems;
jobject eventObjGlobal = NULL; // global ref for the primary event callback
jmethodID onClickMethod = NULL;
std::wstring name;
std::wstring iconPath;
std::wstring description;
// 使用 Win32 线程句柄替代 std::thread
HANDLE threadHandle = NULL;
DWORD threadId = 0;
std::mutex mutex;
std::atomic<bool> running{ false };
};
std::vector<TrayData*> trayDataList;
static std::vector<TrayData*> gTrayList;
static std::mutex gTrayListMutex;
HICON LoadTrayIcon(const wchar_t* path) {
HICON hIcon = (HICON)LoadImageW(
NULL, path, IMAGE_ICON,
GetSystemMetrics(SM_CXSMICON),
GetSystemMetrics(SM_CYSMICON),
LR_LOADFROMFILE | LR_SHARED
);
return hIcon ? hIcon : LoadIconW(NULL, IDI_APPLICATION);
// ---------- 主题/字体 ----------
struct ThemeColors {
COLORREF bg;
COLORREF hover;
COLORREF text;
COLORREF border;
};
static bool IsLightTheme()
{
DWORD val = 1; // 默认为浅色
DWORD cb = sizeof(val);
LONG r = RegGetValueW(
HKEY_CURRENT_USER,
L"Software\\Microsoft\\Windows\\CurrentVersion\\Themes\\Personalize",
L"AppsUseLightTheme",
RRF_RT_REG_DWORD, nullptr, &val, &cb);
return (r != ERROR_SUCCESS) ? true : (val != 0);
}
LRESULT CALLBACK WndProc(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam) {
TrayData* pData = (TrayData*)GetWindowLongPtrW(hwnd, GWLP_USERDATA);
static ThemeColors GetThemeColors()
{
if (IsLightTheme()) {
// 浅色主题
return ThemeColors{
RGB(245, 245, 245), // 背景
RGB(225, 225, 225), // 悬停
RGB(32, 32, 32), // 文字
RGB(210, 210, 210) // 边框
};
}
else {
// 深色主题
return ThemeColors{
RGB(30, 30, 30), // 背景
RGB(50, 50, 50), // 悬停
RGB(230, 230, 230), // 文字
RGB(60, 60, 60) // 边框
};
}
}
static HFONT CreatePopupUIFont()
{
LOGFONTW lf{};
// 使用系统图标标题字体作为基准
SystemParametersInfoW(SPI_GETICONTITLELOGFONT, sizeof(lf), &lf, 0);
// 替换为 Segoe UI更现代
wcscpy_s(lf.lfFaceName, L"Segoe UI");
lf.lfHeight = -12; // 约 9pt @96DPI
lf.lfWeight = FW_NORMAL;
lf.lfQuality = CLEARTYPE_NATURAL_QUALITY; // 开启更自然的 ClearType
return CreateFontIndirectW(&lf);
}
// ---------- 前向声明 ----------
LRESULT CALLBACK TrayWndProc(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam);
LRESULT CALLBACK PopupWndProc(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam);
DWORD WINAPI TrayMessageThreadProc(LPVOID lpParam);
void TrayMessageThread(TrayData* td);
// 注册窗口类(线程安全)
void EnsureWindowClassesRegistered(HINSTANCE hInstance) {
static std::atomic_bool registered{ false };
bool expected = false;
if (!registered.compare_exchange_strong(expected, true)) return;
WNDCLASSEXW wc = { 0 };
wc.cbSize = sizeof(wc);
wc.lpfnWndProc = TrayWndProc;
wc.hInstance = hInstance;
wc.lpszClassName = L"ModernTray_TrayWindowClass";
wc.hCursor = LoadCursor(NULL, IDC_ARROW);
WNDCLASSEXW popup = { 0 };
popup.cbSize = sizeof(popup);
popup.lpfnWndProc = PopupWndProc;
popup.hInstance = hInstance;
popup.lpszClassName = L"ModernTray_PopupWindowClass";
popup.hCursor = LoadCursor(NULL, IDC_ARROW);
popup.hbrBackground = NULL; // 自绘背景
RegisterClassExW(&wc);
RegisterClassExW(&popup);
}
// 加载图标(文件或默认)
HICON LoadTrayIconSafe(const std::wstring& path) {
if (!path.empty()) {
HICON h = (HICON)LoadImageW(NULL, path.c_str(), IMAGE_ICON,
GetSystemMetrics(SM_CXSMICON), GetSystemMetrics(SM_CYSMICON),
LR_LOADFROMFILE | LR_SHARED);
if (h) return h;
}
return LoadIconW(NULL, IDI_APPLICATION);
}
// 查找 TrayData
TrayData* FindTrayById(UINT id) {
//std::lock_guard<std::mutex> lk(gTrayListMutex);
for (auto t : gTrayList) if (t->trayId == id) return t;
return nullptr;
}
// 计算窗口尺寸(基于菜单文本)
SIZE CalcPopupSize(HDC hdc, const std::vector<MenuItemData>& items, HFONT hFont, int paddingX = 14, int paddingY = 10, int itemHeight = 28) {
SIZE sz = { 0, 0 };
int maxw = 0;
HFONT old = (HFONT)SelectObject(hdc, hFont);
for (const auto& it : items) {
if (it.title.empty()) continue;
RECT rc = { 0,0,0,0 };
DrawTextW(hdc, it.title.c_str(), -1, &rc, DT_SINGLELINE | DT_LEFT | DT_CALCRECT);
int w = rc.right - rc.left;
if (w > maxw) maxw = w;
}
SelectObject(hdc, old);
sz.cx = maxw + paddingX * 2;
if (sz.cx < 140) sz.cx = 140; // 最小宽度
sz.cy = (int)items.size() * itemHeight + paddingY; // 顶部/底部各留 paddingY/2 的感觉
return sz;
}
// 绘制圆角背景与文本WM_PAINT 在 PopupWndProc 使用)
void PaintPopup(HWND hwnd, const std::vector<MenuItemData>& items, int hoverIndex) {
PAINTSTRUCT ps;
HDC hdc = BeginPaint(hwnd, &ps);
RECT rc;
GetClientRect(hwnd, &rc);
ThemeColors c = GetThemeColors();
// 背景(圆角 + 边框)
HBRUSH bg = CreateSolidBrush(c.bg);
HBRUSH hover = CreateSolidBrush(c.hover);
HPEN border = CreatePen(PS_SOLID, 1, c.border);
HRGN rgn = CreateRoundRectRgn(rc.left, rc.top, rc.right, rc.bottom, 12, 12);
SelectClipRgn(hdc, rgn);
HGDIOBJ oldPen = SelectObject(hdc, border);
HGDIOBJ oldBrush = SelectObject(hdc, bg);
RoundRect(hdc, rc.left, rc.top, rc.right, rc.bottom, 12, 12);
// 文本 & 悬停绘制
SetBkMode(hdc, TRANSPARENT);
SetTextColor(hdc, c.text);
static HFONT sFont = nullptr;
if (!sFont) sFont = CreatePopupUIFont();
HFONT oldFont = (HFONT)SelectObject(hdc, sFont);
int y = 6;
const int itemH = 28;
for (size_t i = 0; i < items.size(); ++i) {
RECT itrc = { 8, y, rc.right - 8, y + itemH - 2 };
if ((int)i == hoverIndex) {
FillRect(hdc, &itrc, hover);
}
DrawTextW(hdc, items[i].title.c_str(), -1, &itrc, DT_VCENTER | DT_SINGLELINE | DT_LEFT | DT_NOPREFIX);
y += itemH;
}
SelectObject(hdc, oldFont);
SelectObject(hdc, oldBrush);
SelectObject(hdc, oldPen);
DeleteObject(bg);
DeleteObject(hover);
DeleteObject(border);
DeleteObject(rgn);
EndPaint(hwnd, &ps);
}
// ---------- Popup window helpers ----------
struct PopupContext {
TrayData* tray;
std::vector<MenuItemData> items;
int hoverIndex;
POINT origin;
};
// 存储 PopupContext 到窗口属性(使用 GWLP_USERDATA
static inline PopupContext* GetPopupContext(HWND hwnd) {
return (PopupContext*)GetWindowLongPtrW(hwnd, GWLP_USERDATA);
}
// PopupWndProc 实现:自绘菜单,鼠标移动、点击处理
LRESULT CALLBACK PopupWndProc(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam) {
PopupContext* ctx = GetPopupContext(hwnd);
static const UINT kGuardTimerId = 1; // 初始防抖计时器
static const UINT kGuardMs = 120; // 防抖时长
switch (msg) {
case WM_USER + 1:
switch (lParam) {
case WM_RBUTTONUP:
if (pData && pData->hMenu) {
case WM_CREATE:
SetTimer(hwnd, kGuardTimerId, kGuardMs, nullptr);
return 0;
case WM_TIMER:
if (wParam == kGuardTimerId) {
KillTimer(hwnd, kGuardTimerId);
return 0;
}
break;
case WM_ACTIVATE:
if (LOWORD(wParam) == WA_INACTIVE) {
// 防抖期内忽略一次失焦
if (!KillTimer(hwnd, kGuardTimerId)) {
DestroyWindow(hwnd);
return 0;
}
else {
SetTimer(hwnd, kGuardTimerId, kGuardMs, nullptr);
}
}
return 0;
case WM_MOUSEMOVE: {
if (!ctx) break;
int my = GET_Y_LPARAM(lParam);
const int itemH = 28;
int count = (int)ctx->items.size();
if (count <= 0) break;
int idx = std::max(0, std::min(count - 1, (my - 6) / itemH));
if (idx != ctx->hoverIndex) {
ctx->hoverIndex = idx;
InvalidateRect(hwnd, NULL, TRUE);
}
return 0;
}
case WM_LBUTTONDOWN: {
if (!ctx) break;
int my = GET_Y_LPARAM(lParam);
const int itemH = 28;
int count = (int)ctx->items.size();
if (count > 0) {
int idx = std::max(0, std::min(count - 1, (my - 6) / itemH));
if (idx >= 0 && idx < count) {
MenuItemData sel = ctx->items[idx];
TrayData* td = ctx->tray;
JavaVM* jvm = gJvm.load();
if (!jvm) {
JNI_GetCreatedJavaVMs(&jvm, 1, NULL);
gJvm.store(jvm);
}
if (jvm && sel.eventObjGlobal && sel.onClickMethod) {
JNIEnv* env = nullptr;
if (jvm->AttachCurrentThread((void**)&env, NULL) == JNI_OK) {
env->CallVoidMethod(sel.eventObjGlobal, sel.onClickMethod, (jlong)td->trayId);
jvm->DetachCurrentThread();
}
}
}
}
DestroyWindow(hwnd);
return 0;
}
case WM_PAINT:
if (ctx) PaintPopup(hwnd, ctx->items, ctx->hoverIndex);
return 0;
case WM_NCDESTROY:
if (ctx) {
delete ctx;
SetWindowLongPtrW(hwnd, GWLP_USERDATA, 0);
}
return DefWindowProcW(hwnd, msg, wParam, lParam);
default:
return DefWindowProcW(hwnd, msg, wParam, lParam);
}
}
// 创建并显示自定义弹出菜单(在托盘窗口线程上下文调用)
void ShowCustomPopup(TrayData* td, int x, int y) {
if (!td) return;
HINSTANCE hInst = GetModuleHandleW(NULL);
HWND popup = CreateWindowExW(
WS_EX_TOOLWINDOW | WS_EX_TOPMOST,
L"ModernTray_PopupWindowClass", L"",
WS_POPUP,
x, y, 200, 200,
td->hwnd /* owner */, NULL, hInst, NULL
);
if (!popup) return;
PopupContext* ctx = new PopupContext();
ctx->tray = td;
ctx->items = td->menuItems; // 保证是完整列表
ctx->hoverIndex = -1;
SetWindowLongPtrW(popup, GWLP_USERDATA, (LONG_PTR)ctx);
// 计算尺寸并防止出屏
HDC hdc = GetDC(popup);
static HFONT sFont = nullptr;
if (!sFont) sFont = CreatePopupUIFont();
SIZE sz = CalcPopupSize(hdc, ctx->items, sFont);
ReleaseDC(popup, hdc);
RECT work{};
SystemParametersInfoW(SPI_GETWORKAREA, 0, &work, 0);
LONG px = std::min(
std::max(static_cast<LONG>(x), work.left),
std::max(work.right - static_cast<LONG>(sz.cx), work.left)
);
LONG py = std::min(
std::max(static_cast<LONG>(y), work.top),
std::max(work.bottom - static_cast<LONG>(sz.cy), work.top)
);
SetWindowPos(popup, HWND_TOPMOST, px, py, sz.cx, sz.cy, SWP_SHOWWINDOW | SWP_NOACTIVATE);
// 圆角
HRGN r = CreateRoundRectRgn(0, 0, sz.cx + 1, sz.cy + 1, 12, 12);
SetWindowRgn(popup, r, TRUE);
// 抢前台以降低“瞬间失焦”概率
if (td && td->hwnd) SetForegroundWindow(td->hwnd);
ShowWindow(popup, SW_SHOWNORMAL);
SetForegroundWindow(popup);
SetFocus(popup);
// 仅用于悬停高亮(不再用离开即关闭)
TRACKMOUSEEVENT tme{ sizeof(TRACKMOUSEEVENT) };
tme.dwFlags = TME_LEAVE;
tme.hwndTrack = popup;
TrackMouseEvent(&tme);
UpdateWindow(popup);
}
// ---------- Tray 窗口处理函数 ----------
LRESULT CALLBACK TrayWndProc(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam) {
TrayData* td = (TrayData*)GetWindowLongPtrW(hwnd, GWLP_USERDATA);
if (msg == (WM_USER + 1)) {
if (lParam == WM_RBUTTONUP) {
if (td && td->hwnd) {
SetForegroundWindow(td->hwnd);
}
else {
SetForegroundWindow(hwnd);
}
POINT pt;
GetCursorPos(&pt);
SetForegroundWindow(hwnd);
TrackPopupMenuEx(pData->hMenu,
TPM_RIGHTALIGN | TPM_BOTTOMALIGN,
pt.x, pt.y, hwnd, NULL);
PostMessageW(hwnd, WM_NULL, 0, 0);
}
if (td) ShowCustomPopup(td, pt.x, pt.y);
return 0;
case WM_LBUTTONDOWN:
if (pData && pData->onClickMethod) {
JavaVM* jvm;
JNIEnv* env;
if (JNI_GetCreatedJavaVMs(&jvm, 1, NULL) == JNI_OK &&
jvm->AttachCurrentThread((void**)&env, NULL) == JNI_OK) {
env->CallVoidMethod(pData->eventObj, pData->onClickMethod, (jlong)pData->trayId);
}
else if (lParam == WM_LBUTTONDOWN) {
if (td && td->eventObjGlobal && td->onClickMethod) {
JavaVM* jvm = gJvm.load();
if (!jvm) {
JNI_GetCreatedJavaVMs(&jvm, 1, NULL);
gJvm.store(jvm);
}
if (jvm) {
JNIEnv* env = nullptr;
if (jvm->AttachCurrentThread((void**)&env, NULL) == JNI_OK) {
env->CallVoidMethod(td->eventObjGlobal, td->onClickMethod, (jlong)td->trayId);
jvm->DetachCurrentThread();
}
}
return 0;
}
break;
case WM_COMMAND: {
int menuId = LOWORD(wParam);
if (pData) {
for (auto& item : pData->menuItems) {
if (item.menuId == menuId) {
JavaVM* jvm;
JNIEnv* env;
if (JNI_GetCreatedJavaVMs(&jvm, 1, NULL) == JNI_OK &&
jvm->AttachCurrentThread((void**)&env, NULL) == JNI_OK) {
env->CallVoidMethod(item.eventObj, item.onClickMethod, (jlong)pData->trayId);
jvm->DetachCurrentThread();
}
break;
}
}
}
return 0;
}
}
switch (msg) {
case WM_CREATE:
return 0;
case WM_DESTROY:
PostQuitMessage(0);
return 0;
}
default:
return DefWindowProcW(hwnd, msg, wParam, lParam);
}
JNIEXPORT jlong JNICALL Java_com_axis_innovators_box_tools_RegisterTray_register
(JNIEnv* env, jclass, jstring name, jobject menuItems, jstring icon, jstring, jobject event) {
// 注册窗口类
WNDCLASSEXW wc = { sizeof(WNDCLASSEXW) };
wc.lpfnWndProc = WndProc;
wc.hInstance = GetModuleHandleW(NULL);
wc.lpszClassName = L"TrayWindowClass";
if (!RegisterClassExW(&wc)) return -1;
// 创建消息窗口
HWND hwnd = CreateWindowExW(0, L"TrayWindowClass", L"", 0, 0, 0, 0, 0,
HWND_MESSAGE, NULL, NULL, NULL);
if (!hwnd) return -1;
TrayData* pData = new TrayData();
pData->hwnd = hwnd;
pData->trayId = GetTickCount();
pData->hMenu = CreatePopupMenu();
// 解析菜单项
jclass listClass = env->GetObjectClass(menuItems);
jint size = env->CallIntMethod(menuItems, env->GetMethodID(listClass, "size", "()I"));
for (int i = 0; i < size; ++i) {
jobject item = env->CallObjectMethod(menuItems,
env->GetMethodID(listClass, "get", "(I)Ljava/lang/Object;"), i);
jstring name = (jstring)env->GetObjectField(item,
env->GetFieldID(env->GetObjectClass(item), "name", "Ljava/lang/String;"));
jobject eventObj = env->GetObjectField(item,
env->GetFieldID(env->GetObjectClass(item), "event",
"Lcom/axis/innovators/box/tools/RegisterTray$Event;"));
const jchar* nameChars = env->GetStringChars(name, NULL);
std::wstring menuName(nameChars, nameChars + env->GetStringLength(name));
env->ReleaseStringChars(name, nameChars);
MenuItemData menuItem;
menuItem.menuId = 1000 + i;
menuItem.eventObj = env->NewGlobalRef(eventObj);
jclass eventClass = env->GetObjectClass(eventObj);
menuItem.onClickMethod = env->GetMethodID(eventClass, "onClick", "(J)V");
AppendMenuW(pData->hMenu, MF_STRING, menuItem.menuId, menuName.c_str());
pData->menuItems.push_back(menuItem);
}
// 事件回调
jclass eventClass = env->GetObjectClass(event);
pData->onClickMethod = env->GetMethodID(eventClass, "onClick", "(J)V");
pData->eventObj = env->NewGlobalRef(event);
// 线程入口Win32 线程入口)
DWORD WINAPI TrayMessageThreadProc(LPVOID lpParam) {
TrayData* td = (TrayData*)lpParam;
if (!td) return 0;
TrayMessageThread(td);
return 0;
}
// 配置托盘
NOTIFYICONDATAW nid = { sizeof(NOTIFYICONDATAW) };
nid.hWnd = hwnd;
nid.uID = pData->trayId;
nid.uFlags = NIF_MESSAGE | NIF_ICON | NIF_TIP;
nid.uCallbackMessage = WM_USER + 1;
// 线程主体:为单个托盘创建窗口、图标并运行消息循环
void TrayMessageThread(TrayData* td) {
if (!td) return;
td->running.store(true);
HINSTANCE hInst = GetModuleHandleW(NULL);
EnsureWindowClassesRegistered(hInst);
// 创建消息窗口(消息窗口必须在本线程创建)
HWND hwnd = CreateWindowExW(0, L"ModernTray_TrayWindowClass", L"", 0,
CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT,
HWND_MESSAGE, NULL, hInst, NULL);
if (!hwnd) {
//DEBUG_LOG(L"CreateWindowExW 失败");
td->running.store(false);
return;
}
td->hwnd = hwnd;
SetWindowLongPtrW(hwnd, GWLP_USERDATA, (LONG_PTR)td);
// 加载图标
const wchar_t* iconPath = (const wchar_t*)env->GetStringChars(icon, NULL);
pData->hIcon = LoadTrayIcon(iconPath);
nid.hIcon = pData->hIcon;
env->ReleaseStringChars(icon, (const jchar*)iconPath);
td->hIcon = LoadTrayIconSafe(td->iconPath);
// 设置提示
const wchar_t* tip = (const wchar_t*)env->GetStringChars(name, NULL);
wcsncpy_s(nid.szTip, _countof(nid.szTip), tip, _TRUNCATE);
env->ReleaseStringChars(name, (const jchar*)tip);
if (!Shell_NotifyIconW(NIM_ADD, &nid)) {
delete pData;
return -1;
// 添加到系统托盘
NOTIFYICONDATAW nid = { 0 };
nid.cbSize = sizeof(nid);
nid.hWnd = hwnd;
nid.uID = td->trayId;
nid.uFlags = NIF_MESSAGE | NIF_ICON | NIF_TIP;
nid.uCallbackMessage = WM_USER + 1;
nid.hIcon = td->hIcon;
// szTip
{
std::wstring tip = td->name.empty() ? L"Tray" : td->name;
wcsncpy_s(nid.szTip, tip.c_str(), _TRUNCATE);
}
SetWindowLongPtrW(hwnd, GWLP_USERDATA, (LONG_PTR)pData);
trayDataList.push_back(pData);
if (!Shell_NotifyIconW(NIM_ADD, &nid)) {
//DEBUG_LOG(L"Shell_NotifyIconW(NIM_ADD) 失败");
}
// 启动消息循环
// 消息循环
MSG msg;
while (GetMessageW(&msg, NULL, 0, 0)) {
TranslateMessage(&msg);
DispatchMessageW(&msg);
}
return (jlong)pData->trayId;
// 在退出前删除托盘图标(再一次保险)
Shell_NotifyIconW(NIM_DELETE, &nid);
// 清理窗口句柄(如果尚未)
if (td->hwnd) {
DestroyWindow(td->hwnd);
td->hwnd = NULL;
}
TrayData* FindTrayData(UINT trayId) {
for (auto data : trayDataList) {
if (data->trayId == trayId) return data;
}
return nullptr;
td->running.store(false);
}
// ---------- JNI 接口实现 ----------
// 辅助:将 jstring 转 wchar_t string
static std::wstring JStringToWString(JNIEnv* env, jstring js) {
if (!js) return L"";
const jchar* chars = env->GetStringChars(js, NULL);
jsize len = env->GetStringLength(js);
std::wstring ws(chars, chars + len);
env->ReleaseStringChars(js, chars);
return ws;
}
/*
* public static native long register(String name, List<Item> value, String icon, Event event);
*/
JNIEXPORT jlong JNICALL Java_com_axis_innovators_box_tools_RegisterTray_register
(JNIEnv* env, jclass, jstring jname, jobject jlist, jstring jicon, jobject jevent) {
// 转换输入
std::wstring name = JStringToWString(env, jname);
std::wstring iconPath = JStringToWString(env, jicon);
// 创建 TrayData
TrayData* td = new TrayData();
td->trayId = (UINT)GetTickCount();
td->name = name;
td->iconPath = iconPath;
// 缓存 JVM
JavaVM* jvm = nullptr;
if (JNI_GetCreatedJavaVMs(&jvm, 1, NULL) == JNI_OK && jvm) gJvm.store(jvm);
// event 全局引用与方法
if (jevent) {
td->eventObjGlobal = env->NewGlobalRef(jevent);
jclass evc = env->GetObjectClass(jevent);
td->onClickMethod = env->GetMethodID(evc, "onClick", "(J)V");
}
// 解析列表 items更鲁棒的遍历
if (jlist) {
jclass listClass = env->GetObjectClass(jlist);
jmethodID sizeMid = env->GetMethodID(listClass, "size", "()I");
jmethodID getMid = env->GetMethodID(listClass, "get", "(I)Ljava/lang/Object;");
jint size = env->CallIntMethod(jlist, sizeMid);
jint parsed = 0;
for (jint i = 0; i < size; ++i) {
jobject item = env->CallObjectMethod(jlist, getMid, i);
if (!item) continue;
jclass itemClass = env->GetObjectClass(item);
jfieldID nameF = env->GetFieldID(itemClass, "name", "Ljava/lang/String;");
jstring jtitle = (jstring)env->GetObjectField(item, nameF);
std::wstring title = JStringToWString(env, jtitle);
jfieldID eventF = env->GetFieldID(itemClass, "event", "Lcom/axis/innovators/box/tools/RegisterTray$Event;");
jobject ievent = env->GetObjectField(item, eventF);
jobject globalEvent = nullptr;
jmethodID onClickMid = nullptr;
if (ievent) {
globalEvent = env->NewGlobalRef(ievent);
jclass evc = env->GetObjectClass(ievent);
onClickMid = env->GetMethodID(evc, "onClick", "(J)V");
}
MenuItemData mid;
mid.menuId = 1000 + i;
mid.eventObjGlobal = globalEvent;
mid.onClickMethod = onClickMid;
mid.title = title;
td->menuItems.push_back(mid);
++parsed;
}
// 打印数量,便于确认 Java 侧是否传入了多项
wchar_t buf[128];
swprintf_s(buf, L"[register] parsed menu items = %d", (int)parsed);
//DEBUG_LOG(buf);
}
// 放入全局列表
{
//std::lock_guard<std::mutex> lk(gTrayListMutex);
gTrayList.push_back(td);
}
// 启动窗口线程
td->threadHandle = CreateThread(NULL, 0, TrayMessageThreadProc, td, 0, &td->threadId);
if (!td->threadHandle) {
DEBUG_LOG(L"CreateThread failed");
// 清理
if (td->eventObjGlobal) {
env->DeleteGlobalRef(td->eventObjGlobal);
td->eventObjGlobal = NULL;
}
for (auto& mi : td->menuItems) {
if (mi.eventObjGlobal) {
env->DeleteGlobalRef(mi.eventObjGlobal);
mi.eventObjGlobal = NULL;
}
}
td->menuItems.clear();
delete td;
return (jlong)0;
}
return (jlong)td->trayId;
}
/*
* public static native long registerEx(String name, List<Item> value, String icon, String description, Event event);
*/
JNIEXPORT jlong JNICALL Java_com_axis_innovators_box_tools_RegisterTray_registerEx
(JNIEnv* env, jclass, jstring jname, jobject jlist, jstring jicon, jstring jdesc, jobject jevent) {
// 功能与 register 相同,只是多了 description
std::wstring name = JStringToWString(env, jname);
std::wstring iconPath = JStringToWString(env, jicon);
std::wstring desc = JStringToWString(env, jdesc);
TrayData* td = new TrayData();
td->trayId = (UINT)GetTickCount();
td->name = name;
td->iconPath = iconPath;
td->description = desc;
// 缓存 JVM
JavaVM* jvm = nullptr;
if (JNI_GetCreatedJavaVMs(&jvm, 1, NULL) == JNI_OK && jvm) gJvm.store(jvm);
// event
if (jevent) {
td->eventObjGlobal = env->NewGlobalRef(jevent);
jclass evc = env->GetObjectClass(jevent);
td->onClickMethod = env->GetMethodID(evc, "onClick", "(J)V");
}
// 解析 list
if (jlist) {
jclass listClass = env->GetObjectClass(jlist);
jmethodID sizeMid = env->GetMethodID(listClass, "size", "()I");
jmethodID getMid = env->GetMethodID(listClass, "get", "(I)Ljava/lang/Object;");
jint size = env->CallIntMethod(jlist, sizeMid);
jint parsed = 0;
for (jint i = 0; i < size; ++i) {
jobject item = env->CallObjectMethod(jlist, getMid, i);
if (!item) continue;
jclass itemClass = env->GetObjectClass(item);
jfieldID nameF = env->GetFieldID(itemClass, "name", "Ljava/lang/String;");
jstring jtitle = (jstring)env->GetObjectField(item, nameF);
std::wstring title = JStringToWString(env, jtitle);
jfieldID eventF = env->GetFieldID(itemClass, "event", "Lcom/axis/innovators/box/tools/RegisterTray$Event;");
jobject ievent = env->GetObjectField(item, eventF);
jobject globalEvent = nullptr;
jmethodID onClickMid = nullptr;
if (ievent) {
globalEvent = env->NewGlobalRef(ievent);
jclass evc = env->GetObjectClass(ievent);
onClickMid = env->GetMethodID(evc, "onClick", "(J)V");
}
MenuItemData mid;
mid.menuId = 2000 + i;
mid.eventObjGlobal = globalEvent;
mid.onClickMethod = onClickMid;
mid.title = title;
td->menuItems.push_back(mid);
++parsed;
}
wchar_t buf[128];
swprintf_s(buf, L"[registerEx] parsed menu items = %d", (int)parsed);
//DEBUG_LOG(buf);
}
{
//std::lock_guard<std::mutex> lk(gTrayListMutex);
gTrayList.push_back(td);
}
td->threadHandle = CreateThread(NULL, 0, TrayMessageThreadProc, td, 0, &td->threadId);
if (!td->threadHandle) {
//DEBUG_LOG(L"CreateThread failed (registerEx)");
// 清理
if (td->eventObjGlobal) {
env->DeleteGlobalRef(td->eventObjGlobal);
td->eventObjGlobal = NULL;
}
for (auto& mi : td->menuItems) {
if (mi.eventObjGlobal) {
env->DeleteGlobalRef(mi.eventObjGlobal);
mi.eventObjGlobal = NULL;
}
}
td->menuItems.clear();
delete td;
return (jlong)0;
}
return (jlong)td->trayId;
}
/*
* public static native void unregister(long id);
*/
JNIEXPORT void JNICALL Java_com_axis_innovators_box_tools_RegisterTray_unregister
(JNIEnv* env, jclass clazz, jlong id) {
const UINT trayId = static_cast<UINT>(id);
TrayData* pData = FindTrayData(trayId);
if (!pData) {
OutputDebugStringW(L"[unregister] 找不到对应的托盘数据");
(JNIEnv* env, jclass, jlong id) {
UINT trayId = (UINT)id;
TrayData* td = nullptr;
{
//std::lock_guard<std::mutex> lk(gTrayListMutex);
auto it = std::find_if(gTrayList.begin(), gTrayList.end(), [trayId](TrayData* t) { return t->trayId == trayId; });
if (it != gTrayList.end()) {
td = *it;
gTrayList.erase(it);
}
}
if (!td) {
DEBUG_LOG(L"[unregister] 找不到对应的托盘数据");
return;
}
// 1. 删除系统托盘图标
NOTIFYICONDATAW nid = { sizeof(NOTIFYICONDATAW) };
nid.hWnd = pData->hwnd;
nid.uID = trayId;
if (!Shell_NotifyIconW(NIM_DELETE, &nid)) {
DWORD err = GetLastError();
wchar_t errMsg[256];
swprintf_s(errMsg, _countof(errMsg), L"[unregister] 销毁窗口失败 (错误码: 0x%08X)", err);
OutputDebugStringW(errMsg);
// 发布消息让线程结束(销毁窗口会退出消息循环)
if (td->hwnd) {
PostMessageW(td->hwnd, WM_CLOSE, 0, 0);
}
// 2. 释放图标资源
if (pData->hIcon) {
if (!DestroyIcon(pData->hIcon)) {
OutputDebugStringW(L"[unregister] 销毁图标失败");
// 等待线程退出
if (td->threadHandle) {
if (GetCurrentThreadId() != td->threadId) {
DWORD waitRes = WaitForSingleObject(td->threadHandle, 5000);
if (waitRes == WAIT_TIMEOUT) {
DEBUG_LOG(L"[unregister] WaitForSingleObject 超时");
}
}
else {
OutputDebugStringW(L"[unregister] 图标资源已释放");
for (int i = 0; i < 100 && td->running.load(); ++i) Sleep(10);
}
pData->hIcon = NULL;
}
// 3. 销毁菜单
if (pData->hMenu) {
if (!DestroyMenu(pData->hMenu)) {
OutputDebugStringW(L"[unregister] 销毁菜单失败");
CloseHandle(td->threadHandle);
td->threadHandle = NULL;
td->threadId = 0;
}
else {
OutputDebugStringW(L"[unregister] 菜单已销毁");
}
pData->hMenu = NULL;
for (int i = 0; i < 50 && td->running.load(); ++i) Sleep(20);
}
// 4. 销毁窗口
if (pData->hwnd) {
if (!DestroyWindow(pData->hwnd)) {
DWORD err = GetLastError();
wchar_t errMsg[256];
swprintf_s(errMsg, _countof(errMsg), // 修改点
L"[unregister] 销毁窗口失败 (错误码: 0x%08X)",
err);
OutputDebugStringW(errMsg);
}
else {
OutputDebugStringW(L"[unregister] 窗口已销毁");
}
pData->hwnd = NULL;
// 删除通知区图标(保险)
NOTIFYICONDATAW nid = { 0 };
nid.cbSize = sizeof(nid);
nid.hWnd = td->hwnd;
nid.uID = td->trayId;
Shell_NotifyIconW(NIM_DELETE, &nid);
// 释放 icon
if (td->hIcon) {
DestroyIcon(td->hIcon);
td->hIcon = NULL;
}
// 5. 释放JNI全局引用
if (pData->eventObj) {
env->DeleteGlobalRef(pData->eventObj);
pData->eventObj = NULL;
OutputDebugStringW(L"[unregister] 事件全局引用已释放");
// 删除全局引用
if (td->eventObjGlobal) {
env->DeleteGlobalRef(td->eventObjGlobal);
td->eventObjGlobal = NULL;
}
// 6. 释放菜单项全局引用
for (auto& item : pData->menuItems) {
if (item.eventObj) {
env->DeleteGlobalRef(item.eventObj);
item.eventObj = NULL;
for (auto& mi : td->menuItems) {
if (mi.eventObjGlobal) {
env->DeleteGlobalRef(mi.eventObjGlobal);
mi.eventObjGlobal = NULL;
}
}
pData->menuItems.clear();
OutputDebugStringW(L"[unregister] 菜单项资源已清理");
td->menuItems.clear();
// 7. 从全局列表移除
auto it = std::remove_if(trayDataList.begin(), trayDataList.end(),
[pData](TrayData* data) { return data == pData; });
if (it != trayDataList.end()) {
trayDataList.erase(it, trayDataList.end());
OutputDebugStringW(L"[unregister] 已从全局列表移除");
if (td->hwnd) {
DestroyWindow(td->hwnd);
td->hwnd = NULL;
}
// 8. 释放内存
delete pData;
OutputDebugStringW(L"[unregister] 内存已释放");
// 9. 强制重绘任务栏
HWND taskbar = FindWindowW(L"Shell_TrayWnd", NULL);
if (taskbar) {
RedrawWindow(taskbar, NULL, NULL,
RDW_INVALIDATE | RDW_ERASE | RDW_ALLCHILDREN);
}
delete td;
DEBUG_LOG(L"[unregister] 已完成清理");
}
BOOL APIENTRY DllMain(HMODULE hModule, DWORD reason, LPVOID lpReserved) {
// DllMain
BOOL APIENTRY DllMain(HMODULE, DWORD, LPVOID) {
return TRUE;
}

View File

@@ -79,7 +79,7 @@ public class Main {
if (!acquireLock()) {
return;
}
AxisInnovatorsBox.run(args, true);
AxisInnovatorsBox.run(args, debugWindowEnabled);
}
/**

View File

@@ -14,6 +14,7 @@ public class RegisterTray {
static {
LibraryLoad.loadLibrary("RegisterTray");
//System.load("C:\\Users\\Administrator\\source\\repos\\RegisterTray\\x64\\Release\\RegisterTray.dll");
}
/**
@@ -26,39 +27,28 @@ public class RegisterTray {
* 添加菜单项
* @param id 菜单项唯一标识符需大于0
* @param label 菜单显示文本
* @param onClick 点击事件处理器
* @param onClick 点击事件处理器(接收 itemId
* @return 当前构建器实例
*/
public MenuBuilder addItem(int id, String label, MenuItemClickListener onClick) {
items.add(new Item(
id,
label,
"", "", "",
(Event) combinedId -> {
int itemId = (int)(combinedId >> 32);
onClick.onClick(itemId);
// 为每一项创建一个 Event 回调对象native 端会在点击时回调此 Event.onClick(long)
// 这里我们忽略 native 传回的 trayId只把 itemId 传给上层的 MenuItemClickListener
Event ev = new Event() {
@Override
public void onClick(long trayId) {
try {
onClick.onClick(id);
} catch (Throwable t) {
t.printStackTrace();
}
));
return this;
}
};
/**
* 添加菜单项
* @param id 菜单项唯一标识符需大于0
* @param label 菜单显示文本
* @param onClick 点击事件处理器
* @return 当前构建器实例
*/
public MenuBuilder addItem(MenuBuilder builder,int id, String label, MenuItemClickListener onClick) {
this.items = builder.items;
items.add(new Item(
id,
label,
"", "", "",
(Event) combinedId -> {
int itemId = (int)(combinedId >> 32);
onClick.onClick(itemId);
}
ev
));
return this;
}
@@ -72,6 +62,7 @@ public class RegisterTray {
}
}
/**
* 托盘配置器(流畅接口)
*/
@@ -143,7 +134,6 @@ public class RegisterTray {
title,
menuItems,
iconPath,
tooltip,
clickListener::onClick
);
} catch (Exception e) {
@@ -185,8 +175,13 @@ public class RegisterTray {
}
}
public static native long register(String name, List<Item> value,
String icon, String description, Event event);
public static native long register(String name, List<Item> items, String icon, Event event);
/**
* 更强的变体:允许提供 description可以用于 tooltip 或弹出顶部信息)。
* 推荐使用 registerEx 来获取现代化圆角弹出菜单。
*/
public static native long registerEx(String name, List<Item> items, String icon, String description, Event event);
public static native void unregister(long id);
public interface Event {

View File

@@ -67,15 +67,10 @@ public class Tray {
trayLabelsList.add(trayLabels);
if (menuBuilders == null) {
RegisterTray.MenuBuilder menuBuilder = new RegisterTray.MenuBuilder()
.addItem(trayLabels.id, trayLabels.name, itemId -> trayLabels.action.run());
menuBuilders = menuBuilder;
menuItems = menuBuilder.build();
} else {
menuBuilders = new RegisterTray.MenuBuilder()
.addItem(menuBuilders, trayLabels.id, trayLabels.name, itemId -> trayLabels.action.run());
menuItems = menuBuilders.build();
menuBuilders = new RegisterTray.MenuBuilder();
}
menuBuilders.addItem(trayLabels.id, trayLabels.name, itemId -> trayLabels.action.run());
menuItems = menuBuilders.build();
}
public static void addAction(Runnable action){