From 86a9e9e81d0856c27f87875ce5ca7c9adc1c9567 Mon Sep 17 00:00:00 2001 From: tzdwindows 7 <3076584115@qq.com> Date: Thu, 21 Aug 2025 16:21:36 +0800 Subject: [PATCH] =?UTF-8?q?feat(RegisterTray):=20=E9=87=8D=E6=9E=84?= =?UTF-8?q?=E5=B9=B6=E6=B7=BB=E5=8A=A0=E6=96=B0=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 重构了 RegisterTray.dll 的核心逻辑,使用更现代的 Windows API - 添加了自定义弹出菜单功能,支持鼠标悬停和点击事件 - 优化了托盘图标的创建和销毁流程 -改进了错误处理和资源管理- 新增 registerEx 方法,支持描述信息 --- library/RegisterTray.dll | Bin 20992 -> 32768 bytes ...m_axis_innovators_box_tools_RegisterTray.h | 11 +- src/main/Cpp/RegisterTray/dllmain.cpp | 924 ++++++++++++++---- .../java/com/axis/innovators/box/Main.java | 2 +- .../innovators/box/tools/RegisterTray.java | 49 +- .../com/axis/innovators/box/util/Tray.java | 13 +- 6 files changed, 750 insertions(+), 249 deletions(-) diff --git a/library/RegisterTray.dll b/library/RegisterTray.dll index 49659b104d34518a1e31a98dfd6a6fef433b187e..dac2ffa0a1afb2899a986f2eba392ee377f6f9e6 100644 GIT binary patch literal 32768 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 yyBco8mIYUZKXVj13X= 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 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){