i18n: UTF-8 protocol capability + Unicode rendering on server

This commit is contained in:
yuanyuanxiang
2026-05-06 16:01:16 +02:00
parent 11434653e9
commit 0aa75882d1
11 changed files with 361 additions and 40 deletions

View File

@@ -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 后由系统改发此通知。
// 工程仍是 MBCSCString 内部仍按 CP_ACP这里把要显示的列统一转成宽字符填回
// 让控件直接以 Unicode 渲染,从而在非中文 ACP 服务端上也能正确显示中文。
void CMy2015RemoteDlg::OnGetDispInfoW(NMHDR* pNMHDR, LRESULT* pResult)
{
NMLVDISPINFOW* pDispInfo = reinterpret_cast<NMLVDISPINFOW*>(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 仍是 ANSICP_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<LPNMITEMACTIVATE>(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);