Files
envi-code/SourceCode/Code2026/EarthWormMain/ShortcutKeyMappingDlg.cpp
T

701 lines
18 KiB
C++
Raw Normal View History

2026-09-28 15:28:50 +08:00
#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<wchar_t> 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<CString, CString>::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<LPTSTR>(_T("菜单名称"));
lvColumn.fmt = LVCFMT_LEFT;
m_wndMappingLst.InsertColumn(0, &lvColumn);
lvColumn.cx = 100 * dScale;
lvColumn.pszText = const_cast<LPTSTR>(_T("命令名称"));
lvColumn.fmt = LVCFMT_CENTER;
m_wndMappingLst.InsertColumn(1, &lvColumn);
lvColumn.pszText = const_cast<LPTSTR>(_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<CString, CString>::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<std::pair<CString, CString>> mm;
IniTools::getSectionContent(FilePath::GetSysDir()+_T("\\product.ini")
, _T("MENUS")
, mm);
if (mm.empty()) return;
CString menuPath = FilePath::GetSysDir() + _T("\\");
std::vector<std::pair<CString, CString>>::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<char> 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<char> 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<CString, CString>::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();
}