#include "stdafx.h" #include "shortcutkeymappingdlg.h" #include "IniTools.h" #include "CoreTools.h" namespace { // 辅助函数:将 UTF-8 编码的 char* 转换为 MFC 的 CString CString ConvertUTF8ToCString(const char *pUTF8, int nLen) { if (nLen == 0) return _T(""); // 1. 获取转换为 Unicode (UTF-16) 所需的缓冲区大小 int nWideLen = MultiByteToWideChar(CP_UTF8, 0, pUTF8, nLen, NULL, 0); if (nWideLen == 0) return _T(""); std::vector vecWide(nWideLen + 1); // 2. 执行转换: UTF-8 -> UTF-16 (wchar_t) MultiByteToWideChar(CP_UTF8, 0, pUTF8, nLen, &vecWide[0], nWideLen); vecWide[nWideLen] = 0; // 确保 null 结尾 // 3. 将 wchar_t 赋值给 CString // 如果工程是 Unicode (默认),CString 就是 wchar_t,直接赋值 // 如果工程是 MBCS,CString 会自动将 wchar_t 转换为 ANSI return CString(&vecWide[0]); } std::string ConvertCStringToUTF8(const CString &strContent) { #ifdef _UNICODE const wchar_t *pSrc = strContent.GetString(); int nLen = strContent.GetLength(); #else CA2W wStr(strContent); const wchar_t *pSrc = wStr; int nLen = wcslen(pSrc); #endif if (nLen == 0) return ""; int nUTF8Len = WideCharToMultiByte(CP_UTF8, 0, pSrc, nLen, NULL, 0, NULL, NULL); if (nUTF8Len == 0) return ""; std::string strUTF8(nUTF8Len, 0); WideCharToMultiByte(CP_UTF8, 0, pSrc, nLen, &strUTF8[0], nUTF8Len, NULL, NULL); return strUTF8; } } IMPLEMENT_DYNAMIC(CShortcutKeyMappingDlg, CDialog) CShortcutKeyMappingDlg::CShortcutKeyMappingDlg(CWnd* pParent /*=NULL*/) : CDialog(CShortcutKeyMappingDlg::IDD, pParent) { } CShortcutKeyMappingDlg::~CShortcutKeyMappingDlg() { } void CShortcutKeyMappingDlg::DoDataExchange(CDataExchange* pDX) { CDialog::DoDataExchange(pDX); DDX_Control(pDX, IDC_STATIC_LIST, m_wndStatic); } BEGIN_MESSAGE_MAP(CShortcutKeyMappingDlg, CDialog) ON_BN_CLICKED(IDOK, &CShortcutKeyMappingDlg::OnBnClickedOk) ON_BN_CLICKED(IDC_EXPORT, &CShortcutKeyMappingDlg::OnBnClickedExport) ON_BN_CLICKED(IDC_IMPORT, &CShortcutKeyMappingDlg::OnBnClickedImport) ON_NOTIFY(TVN_ENDLABELEDIT, IDC_SHORTCUT_MAPPING_LIST, &CShortcutKeyMappingDlg::OnEditEnd) ON_NOTIFY(TVN_BEGINLABELEDIT, IDC_SHORTCUT_MAPPING_LIST, &CShortcutKeyMappingDlg::OnEditBegin) END_MESSAGE_MAP() void CShortcutKeyMappingDlg::OnEditEnd(NMHDR * pNMHDR, LRESULT * pResult) { *pResult = 0; // 默认成功 NMTVDISPINFO *pItem = (NMTVDISPINFO*)pNMHDR; CString sNewText = pItem->item.pszText; if (sNewText.IsEmpty()) { *pResult = 1; return; } CString sCmd = m_wndMappingLst.GetItemText(pItem->item.hItem, 1); std::map::iterator it(m_pgpCmdMapping.begin()); for (; it != m_pgpCmdMapping.end(); ++it) { if (it->first.CompareNoCase(sCmd) != 0 && it->second.CompareNoCase(sNewText) == 0) { CString sMsg; sMsg.Format(_T("“%s”与命令:“%s”的快捷键相同,请重新修改!"), sNewText, sCmd); AfxMessageBox(sMsg); *pResult = 1; // 失败 return; } } m_pgpCmdMapping[sCmd] = sNewText; } void CShortcutKeyMappingDlg::OnEditBegin(NMHDR *pNMHDR, LRESULT *pResult) { NMTVDISPINFO *pItem = (NMTVDISPINFO*)pNMHDR; m_sItemOldText = pItem->item.pszText; } void CShortcutKeyMappingDlg::initListCtrl() { CRect rc; m_wndStatic.GetWindowRect(rc); ScreenToClient(rc); m_wndStatic.ShowWindow(SW_HIDE); unsigned uStyle = TVS_HASBUTTONS |TVS_HASBUTTONS |TVS_HASLINES |TVS_LINESATROOT |TVS_EDITLABELS |TVS_FULLROWSELECT; unsigned uExStyle = TVS_EX_ITEMLINES |TVS_EX_ITEMLINES |TVS_EX_ALTERNATECOLOR |TVS_EX_SUBSELECT |TVS_EX_FULLROWMARK; m_wndMappingLst.Create(uStyle|WS_CHILD|WS_VISIBLE|WS_BORDER, CRect(0, 0, 0, 0), this, IDC_SHORTCUT_MAPPING_LIST); if (!m_wndMappingLst.GetSafeHwnd()) return; m_wndMappingLst.SetExtendedStyle(uExStyle); m_wndMappingLst.MoveWindow(rc); CBitmap bmp; bmp.Attach(::LoadBitmap(_hdllInstance, MAKEINTRESOURCE(IDB_MENUSTATE))); m_imageList.Create(12, 12, ILC_COLOR24|ILC_MASK, 0, 0); m_imageList.Add(&bmp, RGB(255, 0, 255)); m_wndMappingLst.SetImageList(&m_imageList,TVSIL_NORMAL); double dScale = CoreTools::GetDPIScale(); LV_COLUMN lvColumn; lvColumn.mask = LVCF_FMT | LVCF_WIDTH | LVCF_TEXT | LVCF_SUBITEM; lvColumn.cx = 200 * dScale; lvColumn.pszText = const_cast(_T("菜单名称")); lvColumn.fmt = LVCFMT_LEFT; m_wndMappingLst.InsertColumn(0, &lvColumn); lvColumn.cx = 100 * dScale; lvColumn.pszText = const_cast(_T("命令名称")); lvColumn.fmt = LVCFMT_CENTER; m_wndMappingLst.InsertColumn(1, &lvColumn); lvColumn.pszText = const_cast(_T("快捷按键")); m_wndMappingLst.InsertColumn(2, &lvColumn); m_wndMappingLst.SetColumnAutoEdit(2, TVAE_EDIT | TVAE_DBLCLICK); } void CShortcutKeyMappingDlg::OnBnClickedOk() { UpdateData(); WriteToAcadPGPFile(); AfxMessageBox(_T("在弹出的对话框中,勾选上 “PGP 文件(F)” 后点击 “确定” 按钮。")); acedPostCommand(_T("_REINIT ")); OnOK(); } void CShortcutKeyMappingDlg::OnBnClickedExport() { #if ARX > 18 CString pgpFilePath = FilePath::GetSysDir() + _T("\\acad.pgp"); // 1. 基本校验:确保源文件存在 if (_taccess(pgpFilePath, 0) != 0) { AfxMessageBox(_T("源文件不存在,无法导出!")); return; } // 2. 弹出文件夹选择对话框 // 参数1: 默认路径(NULL),参数2: 标志,参数3: 父窗口(NULL),参数4: 安全属性 CFolderPickerDialog dlg(NULL, OFN_FILEMUSTEXIST, NULL, 0); dlg.m_ofn.lpstrTitle = _T("请选择导出文件夹"); if (dlg.DoModal() != IDOK) { return; // 用户取消 } CString strDestFolder = dlg.GetPathName(); // 3. 获取源文件名 // 例如 C:\User\acad.pgp -> acad.pgp CString strSrcPath(pgpFilePath); int nPos = strSrcPath.ReverseFind('\\'); CString strFileName; if (nPos != -1) strFileName = strSrcPath.Mid(nPos + 1); else strFileName = strSrcPath; // 只有文件名的情况 // 4. 构建目标路径 // 简单的路径拼接,处理末尾斜杠问题 CString strDestPath = strDestFolder; if (strDestPath.Right(1) != _T("\\")) strDestPath += _T("\\"); strDestPath += strFileName; // 检查源路径和目标路径是否完全一致(防止用户选了同一个文件夹) if (strSrcPath.CompareNoCase(strDestPath) == 0) { AfxMessageBox(_T("源文件和目标文件路径相同,无需导出。")); return; } // 5. 检查同名文件并询问覆盖 if (_taccess(strDestPath, 0) == 0) { CString strMsg; strMsg.Format(_T("目标文件夹已存在文件:\n%s\n\n是否覆盖该文件?"), strFileName); // IDYES, IDNO if (AfxMessageBox(strMsg, MB_YESNO | MB_ICONQUESTION) == IDNO) { return; // 用户选择不覆盖,取消操作 } } // 6. 执行复制 // CopyFile 参数3: FALSE 表示如果文件存在则直接覆盖(因为我们上面已经询问过用户了) // 如果上面不询问,这里设为 TRUE 会导致复制失败 if (::CopyFile(pgpFilePath, strDestPath, FALSE)) { AfxMessageBox(_T("导出成功!")); } else { // 获取错误信息 DWORD dwError = GetLastError(); CString strErr; strErr.Format(_T("导出失败,错误代码:%d"), dwError); AfxMessageBox(strErr, MB_ICONERROR); } #endif } void CShortcutKeyMappingDlg::OnBnClickedImport() { // 1. 获取系统目录下的目标文件路径 CString strDestPath = FilePath::GetSysDir() + _T("\\acad.pgp"); // 2. 配置文件打开对话框 // 参数1: TRUE 为 "打开" 对话框 // 参数2: 默认扩展名 "pgp" // 参数3: 默认文件名 NULL // 参数4: 标志位 (文件必须存在 | 隐藏只读复选框) // 参数5: 过滤器 LPCTSTR szFilter = _T("PGP Files (*.pgp)|*.pgp|All Files (*.*)|*.*||"); CFileDialog dlg(TRUE, _T("pgp"), NULL, OFN_FILEMUSTEXIST | OFN_HIDEREADONLY, szFilter, this); dlg.m_ofn.lpstrTitle = _T("请选择要导入的 PGP 文件"); // 3. 显示对话框 if (dlg.DoModal() != IDOK) { return; // 用户取消 } CString strSrcPath = dlg.GetPathName(); // 4. 校验:不能选择目标文件自己来覆盖自己 if (strSrcPath.CompareNoCase(strDestPath) == 0) { AfxMessageBox(_T("不能选择当前正在使用的文件进行导入!\n请选择其他备份文件。"), MB_ICONWARNING); return; } // 5. 覆盖确认提示 CString strMsg; strMsg.Format(_T("导入操作将使用选中的文件覆盖当前的配置:\n\n%s\n\n此操作不可撤销,是否继续?"), strDestPath); if (AfxMessageBox(strMsg, MB_YESNO | MB_ICONQUESTION) != IDYES) { return; } // 6. 执行文件复制 (覆盖) // 第三个参数 FALSE 表示:如果目标文件已存在,不失败(即允许覆盖) if (::CopyFile(strSrcPath, strDestPath, FALSE)) { // 7. 导入成功后的处理 AfxMessageBox(_T("导入成功!")); // 【关键步骤】 // 此时磁盘上的文件已经变了,但内存里的 map 还是旧的。 // 你需要清空 map 并重新解析新文件,然后刷新列表控件。 m_wndMappingLst.DeleteAllItems(); m_pgpCmdMapping.clear(); // 清空你的 map ParseAcadPGPFile(); // 重新解析 LoadMenus(); } else { // 获取错误代码 DWORD dwError = GetLastError(); CString strErr; strErr.Format(_T("导入失败,无法覆盖文件。\n错误代码:%d"), dwError); AfxMessageBox(strErr, MB_ICONERROR); } } BOOL CShortcutKeyMappingDlg::OnInitDialog() { CDialog::OnInitDialog(); initListCtrl(); ParseAcadPGPFile(); LoadMenus(); UpdateData(FALSE); return TRUE; } void CShortcutKeyMappingDlg::start() { CAcModuleResourceOverride myResources; CShortcutKeyMappingDlg dlg; dlg.DoModal(); } // 解析单个菜单文件 void CShortcutKeyMappingDlg::ParseMenuFile(const CString& strFilePath, const CString& strFileAlias, HTREEITEM hParent) { CStdioFile file; // 尝试打开文件 if (!file.Open(strFilePath, CFile::modeRead | CFile::shareDenyNone | CFile::typeText)) { return; } // 插入根节点(文件名中文别名) HTREEITEM hRoot = m_wndMappingLst.InsertItem(strFileAlias, 1, 1, TVI_ROOT, TVI_LAST); HTREEITEM hCurrentParent = hRoot; std::map::const_iterator itFind(m_pgpCmdMapping.end()); CString strLine; while (file.ReadString(strLine)) { strLine.Trim(); if (strLine.IsEmpty()) continue; // 忽略分隔符 if (strLine.Find(_T("[--]")) >= 0) continue; if (strLine.Find(_T("//")) == 0) continue; if (strLine.Find(_T("**")) == 0) continue; if (strLine.Find(_T("ID_SCREEN")) == 0) continue; if (strLine.Find(_T("***RESOURCE")) == 0) break; // 查找 [] 结构 int nStart = strLine.Find(_T('[')); int nEnd = strLine.Find(_T(']')); // 如果没有合法的 [],则视为无效行或头部垃圾信息,跳过 if (nStart == -1 || nEnd == -1 || nEnd <= nStart) continue; // 1. 提取显示名称 (去掉 []) CString strMenuText = strLine.Mid(nStart + 1, nEnd - nStart - 1); strMenuText.Trim(); // 2. 提取命令 (] 之后的部分) CString strCommand = strLine.Mid(nEnd + 1); strCommand.Trim(); // 3. 判断层级标记 -> 或 <- bool bIsSubStart = (strMenuText.Left(2) == _T("->")); bool bIsSubEnd = (strMenuText.Left(2) == _T("<-")); // 去掉标记符号,获取纯文本名称 if (bIsSubStart || bIsSubEnd) { strMenuText = strMenuText.Mid(2); strMenuText.Trim(); } // 4. 插入节点 HTREEITEM hNewItem = NULL; if (!strMenuText.IsEmpty()) { // 在当前父节点下插入 hNewItem = m_wndMappingLst.InsertItem( strMenuText , strCommand.IsEmpty() ? 2 : 3 , strCommand.IsEmpty() ? 2 : 3 , hCurrentParent); // 如果有命令,设置到第1列 (索引1,对应“命令名称”) if (!strCommand.IsEmpty()) { m_wndMappingLst.SetItemText(hNewItem, strCommand, 1); strCommand.MakeUpper(); itFind = m_pgpCmdMapping.find(strCommand); if (itFind != m_pgpCmdMapping.end()) m_wndMappingLst.SetItemText(hNewItem, itFind->second, 2); else m_wndMappingLst.SetItemText(hNewItem, _T(""), 2); } } // 5. 调整层级关系 if (bIsSubStart) { // 遇到 ->,说明刚才插入的项是下一级菜单的父节点 if (hNewItem) { hCurrentParent = hNewItem; } } else if (bIsSubEnd) { // 遇到 <-,说明本项是当前层级的最后一项,或者本项结束后返回上一层 // 逻辑通常是:插入当前项,然后层级回退 HTREEITEM hGrandParent = m_wndMappingLst.GetParentItem(hCurrentParent); if (hGrandParent) { hCurrentParent = hGrandParent; } else { // 如果已经退无可退,就回到根节点 hCurrentParent = hRoot; } } } // 展开根节点 m_wndMappingLst.Expand(hRoot, TVE_EXPAND); file.Close(); } // 加载菜单主逻辑 void CShortcutKeyMappingDlg::LoadMenus() { std::vector> mm; IniTools::getSectionContent(FilePath::GetSysDir()+_T("\\product.ini") , _T("MENUS") , mm); if (mm.empty()) return; CString menuPath = FilePath::GetSysDir() + _T("\\"); std::vector>::const_iterator it(mm.begin()); for (; it != mm.end(); ++it) { ParseMenuFile(menuPath + it->first + _T(".mnu"), it->second, NULL); } } void CShortcutKeyMappingDlg::ParseAcadPGPFile() { CFile file; CFileException ex; // 1. 打开文件 if (!file.Open(FilePath::GetSysDir() + _T("\\acad.pgp") , CFile::modeRead | CFile::shareDenyNone , &ex)) { TRACE(_T("无法打开文件: acad.pgp\n")); return; } ULONGLONG uFileSize = file.GetLength(); if (uFileSize == 0) { file.Close(); return; } // 2. 读取二进制数据 std::vector vecBuffer((size_t)uFileSize + 1); file.Read(&vecBuffer[0], (UINT)uFileSize); vecBuffer[(size_t)uFileSize] = 0; file.Close(); // 处理 BOM int nOffset = 0; if (uFileSize >= 3 && (unsigned char)vecBuffer[0] == 0xEF && (unsigned char)vecBuffer[1] == 0xBB && (unsigned char)vecBuffer[2] == 0xBF) { nOffset = 3; } // 3. 转换为 CString CString strContent = ConvertUTF8ToCString(&vecBuffer[nOffset], (int)(uFileSize - nOffset)); // 4. 解析逻辑 int nCurPos = 0; CString strLine; bool bMappingSection = false; // 标记字符串 const CString strBeginMarker = _T(";shanghua soft command shortcut key mapping begin"); const CString strEndMarker = _T(";shanghua soft command shortcut key mapping end"); while (!(strLine = strContent.Tokenize(_T("\r\n"), nCurPos)).IsEmpty()) { // 先备份原始行用于判断标记(因为标记行本身以 ; 开头,不能先去注释) CString strRawLine = strLine; strRawLine.Trim(); // --------------------------------------------------------- // A. 检查区块标记 (Start / End) // --------------------------------------------------------- if (strRawLine.CompareNoCase(strBeginMarker) == 0) { bMappingSection = true; continue; // 跳过开始标记行 } if (strRawLine.CompareNoCase(strEndMarker) == 0) { bMappingSection = false; break; // 遇到结束标记,停止解析 } // 如果还没进入有效区域,跳过 if (!bMappingSection) continue; // --------------------------------------------------------- // B. 去除注释 (核心修改) // --------------------------------------------------------- // 查找分号位置 int nCommentPos = strLine.Find(_T(';')); if (nCommentPos != -1) { // 截取分号前面的部分 // 例如 "DJ, *PL_QTDJ ;注释" -> "DJ, *PL_QTDJ " // 例如 ";整行注释" -> "" (空字符串) strLine = strLine.Left(nCommentPos); } // 去除截取后可能产生的尾部空格 strLine.Trim(); // 如果去注释后为空行,说明该行无数据 if (strLine.IsEmpty()) continue; // --------------------------------------------------------- // C. 解析 Key 和 Value // --------------------------------------------------------- // 格式: VIEWPORTS, *VPORTS int nCommaPos = strLine.Find(_T(',')); int nStarPos = strLine.Find(_T('*')); // 确保既有逗号又有星号,且星号在逗号后面(通常情况) if (nCommaPos != -1 && nStarPos != -1) { // 提取 Value: 逗号前的部分 CString strValue = strLine.Left(nCommaPos); // 提取 Key: 星号后的部分 CString strKey = strLine.Mid(nStarPos + 1); // 清理空格 strValue.Trim(); strKey.Trim(); if (!strKey.IsEmpty()) { // 存入 Map // 注意:如果 Key 重复,这里会覆盖旧值,根据需求可调整 m_pgpCmdMapping[strKey.MakeUpper()] = strValue; } } } } void CShortcutKeyMappingDlg::WriteToAcadPGPFile() { const CString strBeginMarker = _T(";shanghua soft command shortcut key mapping begin"); const CString strEndMarker = _T(";shanghua soft command shortcut key mapping end"); // 1. 读取原文件(为了保留 begin 之前和 end 之后的内容) CString strHeader, strFooter; bool bFoundSection = false; CString path(FilePath::GetSysDir() + _T("\\acad.pgp")); CFile fileRead; CFileException ex; // 尝试打开读取,如果文件不存在则视为新文件 if (fileRead.Open( path , CFile::modeRead | CFile::shareDenyNone , &ex)) { ULONGLONG uLen = fileRead.GetLength(); if (uLen > 0) { std::vector vecBuf((size_t)uLen + 1); fileRead.Read(&vecBuf[0], (UINT)uLen); vecBuf[(size_t)uLen] = 0; fileRead.Close(); // 跳过 BOM int nOffset = 0; if (uLen >= 3 && (unsigned char)vecBuf[0] == 0xEF && (unsigned char)vecBuf[1] == 0xBB && (unsigned char)vecBuf[2] == 0xBF) nOffset = 3; CString strFullContent = ConvertUTF8ToCString(&vecBuf[nOffset], (int)(uLen - nOffset)); int nBeginPos = strFullContent.Find(strBeginMarker); int nEndPos = strFullContent.Find(strEndMarker); if (nBeginPos != -1 && nEndPos != -1 && nEndPos > nBeginPos) { // 标记存在,切分头部和尾部 strHeader = strFullContent.Left(nBeginPos + strBeginMarker.GetLength()); strFooter = strFullContent.Mid(nEndPos); bFoundSection = true; } else { // 标记不完整,保留全文作为 Header strHeader = strFullContent; } } else { fileRead.Close(); } } // 2. 构建中间数据段 CString strMiddle; strMiddle += _T("\r\n"); // Begin 标记后换行 // ----------------------------------------------------------- // 核心逻辑修正区 // Input Mapping: Key = 命令 (Command), Value = 快捷键 (Shortcut) // Target Format: 快捷键, *命令 (Shortcut, *Command) // ----------------------------------------------------------- std::map::const_iterator it(m_pgpCmdMapping.begin()); for (; it != m_pgpCmdMapping.end(); ++it) { const CString &strCommand = it->first; // Key: 命令 (例如 "LINE") const CString &strShortcut = it->second; // Value: 快捷键 (例如 "L") CString strLine; // 格式化:Value + "," + 制表符 + "*" + Key // 结果:L, *LINE strLine.Format(_T("%s,\t*%s"), strShortcut, strCommand); strMiddle += strLine + _T("\r\n"); } // 3. 拼接最终字符串 CString strFinal; if (bFoundSection) { strFinal = strHeader + strMiddle + strEndMarker; } else { // 如果原文件没有标记,追加到末尾 if (!strHeader.IsEmpty() && strHeader.Right(1) != _T("\n")) strHeader += _T("\r\n"); strFinal = strHeader; strFinal += strBeginMarker; strFinal += strMiddle; strFinal += strEndMarker; strFinal += _T("\r\n"); } // 4. 写入文件 (UTF-8 with BOM) CFile fileWrite; if (!fileWrite.Open(path, CFile::modeCreate | CFile::modeWrite | CFile::shareDenyNone, &ex)) { TRACE(_T("无法写入文件: %s\n"), path); return; } const unsigned char bom[] = { 0xEF, 0xBB, 0xBF }; fileWrite.Write(bom, 3); std::string strData = ConvertCStringToUTF8(strFinal); if (!strData.empty()) { fileWrite.Write(strData.c_str(), (UINT)strData.length()); } fileWrite.Close(); }