diff --git a/library/RegisterTray.dll b/library/RegisterTray.dll index 49659b1..dac2ffa 100644 Binary files a/library/RegisterTray.dll and b/library/RegisterTray.dll differ diff --git a/src/main/Cpp/RegisterTray/com_axis_innovators_box_tools_RegisterTray.h b/src/main/Cpp/RegisterTray/com_axis_innovators_box_tools_RegisterTray.h index cf07f79..1f72373 100644 --- a/src/main/Cpp/RegisterTray/com_axis_innovators_box_tools_RegisterTray.h +++ b/src/main/Cpp/RegisterTray/com_axis_innovators_box_tools_RegisterTray.h @@ -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); /* diff --git a/src/main/Cpp/RegisterTray/dllmain.cpp b/src/main/Cpp/RegisterTray/dllmain.cpp index 940bcf2..0ec76e8 100644 --- a/src/main/Cpp/RegisterTray/dllmain.cpp +++ b/src/main/Cpp/RegisterTray/dllmain.cpp @@ -4,288 +4,790 @@ #include #include #include +#include +#include +#include +#include +#include + +#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 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 menuItems; - jobject eventObj = NULL; - jmethodID onClickMethod = NULL; HWND hwnd = NULL; UINT trayId = 0; HICON hIcon = NULL; + std::vector 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 running{ false }; }; -std::vector trayDataList; +static std::vector 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 lk(gTrayListMutex); + for (auto t : gTrayList) if (t->trayId == id) return t; + return nullptr; +} + +// 计算窗口尺寸(基于菜单文本) +SIZE CalcPopupSize(HDC hdc, const std::vector& 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& 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 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) { - 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); - } - 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); - jvm->DetachCurrentThread(); - } - } + case WM_CREATE: + SetTimer(hwnd, kGuardTimerId, kGuardMs, nullptr); + return 0; + + case WM_TIMER: + if (wParam == kGuardTimerId) { + KillTimer(hwnd, kGuardTimerId); 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; - } + + 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(x), work.left), + std::max(work.right - static_cast(sz.cx), work.left) + ); + LONG py = std::min( + std::max(static_cast(y), work.top), + std::max(work.bottom - static_cast(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); + if (td) ShowCustomPopup(td, pt.x, pt.y); + return 0; + } + 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; + } + } + + switch (msg) { + case WM_CREATE: + return 0; case WM_DESTROY: PostQuitMessage(0); return 0; + default: + return DefWindowProcW(hwnd, msg, wParam, lParam); } - 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; +// 线程入口(Win32 线程入口) +DWORD WINAPI TrayMessageThreadProc(LPVOID lpParam) { + TrayData* td = (TrayData*)lpParam; + if (!td) return 0; + TrayMessageThread(td); + return 0; +} - // 创建消息窗口 - HWND hwnd = CreateWindowExW(0, L"TrayWindowClass", L"", 0, 0, 0, 0, 0, - HWND_MESSAGE, NULL, NULL, NULL); - if (!hwnd) return -1; +// 线程主体:为单个托盘创建窗口、图标并运行消息循环 +void TrayMessageThread(TrayData* td) { + if (!td) return; + td->running.store(true); - TrayData* pData = new TrayData(); - pData->hwnd = hwnd; - pData->trayId = GetTickCount(); - pData->hMenu = CreatePopupMenu(); + HINSTANCE hInst = GetModuleHandleW(NULL); + EnsureWindowClassesRegistered(hInst); - // 解析菜单项 - 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); + // 创建消息窗口(消息窗口必须在本线程创建) + 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; } - - // 事件回调 - jclass eventClass = env->GetObjectClass(event); - pData->onClickMethod = env->GetMethodID(eventClass, "onClick", "(J)V"); - pData->eventObj = env->NewGlobalRef(event); - - // 配置托盘 - NOTIFYICONDATAW nid = { sizeof(NOTIFYICONDATAW) }; - nid.hWnd = hwnd; - nid.uID = pData->trayId; - nid.uFlags = NIF_MESSAGE | NIF_ICON | NIF_TIP; - nid.uCallbackMessage = WM_USER + 1; + 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); -TrayData* FindTrayData(UINT trayId) { - for (auto data : trayDataList) { - if (data->trayId == trayId) return data; + // 清理窗口句柄(如果尚未) + if (td->hwnd) { + DestroyWindow(td->hwnd); + td->hwnd = NULL; } - 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 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 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 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 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(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 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; + CloseHandle(td->threadHandle); + td->threadHandle = NULL; + td->threadId = 0; + } + else { + for (int i = 0; i < 50 && td->running.load(); ++i) Sleep(20); } - // 3. 销毁菜单 - if (pData->hMenu) { - if (!DestroyMenu(pData->hMenu)) { - OutputDebugStringW(L"[unregister] 销毁菜单失败"); - } - else { - OutputDebugStringW(L"[unregister] 菜单已销毁"); - } - pData->hMenu = 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; } - // 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; + // 删除全局引用 + if (td->eventObjGlobal) { + env->DeleteGlobalRef(td->eventObjGlobal); + td->eventObjGlobal = NULL; } - - // 5. 释放JNI全局引用 - if (pData->eventObj) { - env->DeleteGlobalRef(pData->eventObj); - pData->eventObj = NULL; - OutputDebugStringW(L"[unregister] 事件全局引用已释放"); - } - - // 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; -} \ No newline at end of file +} diff --git a/src/main/java/com/axis/innovators/box/Main.java b/src/main/java/com/axis/innovators/box/Main.java index 25ee022..8644100 100644 --- a/src/main/java/com/axis/innovators/box/Main.java +++ b/src/main/java/com/axis/innovators/box/Main.java @@ -79,7 +79,7 @@ public class Main { if (!acquireLock()) { return; } - AxisInnovatorsBox.run(args, true); + AxisInnovatorsBox.run(args, debugWindowEnabled); } /** diff --git a/src/main/java/com/axis/innovators/box/tools/RegisterTray.java b/src/main/java/com/axis/innovators/box/tools/RegisterTray.java index 29221a1..e9839ad 100644 --- a/src/main/java/com/axis/innovators/box/tools/RegisterTray.java +++ b/src/main/java/com/axis/innovators/box/tools/RegisterTray.java @@ -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 value, - String icon, String description, Event event); + public static native long register(String name, List items, String icon, Event event); + + /** + * 更强的变体:允许提供 description(可以用于 tooltip 或弹出顶部信息)。 + * 推荐使用 registerEx 来获取现代化圆角弹出菜单。 + */ + public static native long registerEx(String name, List items, String icon, String description, Event event); public static native void unregister(long id); public interface Event { diff --git a/src/main/java/com/axis/innovators/box/util/Tray.java b/src/main/java/com/axis/innovators/box/util/Tray.java index 4bd32c3..10504e8 100644 --- a/src/main/java/com/axis/innovators/box/util/Tray.java +++ b/src/main/java/com/axis/innovators/box/util/Tray.java @@ -60,22 +60,17 @@ public class Tray { * @throws RegisterTray.TrayException 抛出错误 */ public static void load(TrayLabels trayLabels) throws RegisterTray.TrayException { - if (trayLabels == null || trayLabelsList.contains(trayLabels)){ + if (trayLabels == null || trayLabelsList.contains(trayLabels)) { System.err.println("trayLabels is null or trayLabelsList contains trayLabels"); return; } 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){