#pragma once #include #include #include #include // 语言管理类 - 支持多语言切换 class CLangManager { private: std::map m_strings; // 中文 -> 目标语言 CString m_currentLang; // 当前语言代码 CString m_langDir; // 语言文件目录 CLangManager() {} CLangManager(const CLangManager&) = delete; CLangManager& operator=(const CLangManager&) = delete; public: static CLangManager& Instance() { static CLangManager instance; return instance; } // 初始化语言目录 void Init(const CString& langDir = _T("")) { if (langDir.IsEmpty()) { // 默认使用 exe 所在目录下的 lang 文件夹 TCHAR path[MAX_PATH]; GetModuleFileName(NULL, path, MAX_PATH); CString exePath(path); int pos = exePath.ReverseFind(_T('\\')); if (pos > 0) { m_langDir = exePath.Left(pos) + _T("\\lang"); } } else { m_langDir = langDir; } // 确保目录存在 CreateDirectory(m_langDir, NULL); } // 获取可用的语言列表 std::vector GetAvailableLanguages() { std::vector langs; CString searchPath = m_langDir + _T("\\*.ini"); WIN32_FIND_DATA fd; HANDLE hFind = FindFirstFile(searchPath, &fd); if (hFind != INVALID_HANDLE_VALUE) { do { CString filename(fd.cFileName); int dotPos = filename.ReverseFind(_T('.')); if (dotPos > 0) { langs.push_back(filename.Left(dotPos)); } } while (FindNextFile(hFind, &fd)); FindClose(hFind); } return langs; } // 检查语言文件编码是否为 ANSI // 返回 false 表示文件不存在或编码不是 ANSI(检测 BOM 和 UTF-8 无 BOM) bool CheckEncoding(const CString& langCode) { if (langCode == _T("zh_CN") || langCode.IsEmpty()) { TRACE("[LangEnc] zh_CN or empty, skip check\n"); return true; } CString langFile = m_langDir + _T("\\") + langCode + _T(".ini"); TRACE("[LangEnc] Checking: %s\n", (LPCSTR)langFile); FILE* f = nullptr; if (fopen_s(&f, (LPCSTR)langFile, "rb") != 0 || !f) { TRACE("[LangEnc] fopen failed\n"); return false; } // 读取文件内容(最多检测前 4KB 即可判断) unsigned char buf[4096]; size_t n = fread(buf, 1, sizeof(buf), f); fclose(f); TRACE("[LangEnc] Read %zu bytes\n", n); if (n == 0) return false; // 检测 BOM if (n >= 3 && buf[0] == 0xEF && buf[1] == 0xBB && buf[2] == 0xBF) { TRACE("[LangEnc] Detected UTF-8 BOM\n"); return false; } if (n >= 2 && buf[0] == 0xFF && buf[1] == 0xFE) { TRACE("[LangEnc] Detected UTF-16 LE BOM\n"); return false; } if (n >= 2 && buf[0] == 0xFE && buf[1] == 0xFF) { TRACE("[LangEnc] Detected UTF-16 BE BOM\n"); return false; } // 检测 UTF-8 无 BOM:扫描是否存在合法的 UTF-8 多字节序列 // 中文 UTF-8 为 3 字节 (E0-EF + 80-BF + 80-BF) // GBK 为 2 字节 (81-FE + 40-FE),字节模式不同 int utf8SeqCount = 0; for (size_t i = 0; i < n; ) { unsigned char c = buf[i]; if (c < 0x80) { i++; } else if ((c & 0xE0) == 0xC0 && i + 1 < n && (buf[i+1] & 0xC0) == 0x80) { utf8SeqCount++; // 2 字节 UTF-8 i += 2; } else if ((c & 0xF0) == 0xE0 && i + 2 < n && (buf[i+1] & 0xC0) == 0x80 && (buf[i+2] & 0xC0) == 0x80) { utf8SeqCount++; // 3 字节 UTF-8(中文) i += 3; } else if ((c & 0xF8) == 0xF0 && i + 3 < n && (buf[i+1] & 0xC0) == 0x80 && (buf[i+2] & 0xC0) == 0x80 && (buf[i+3] & 0xC0) == 0x80) { utf8SeqCount++; // 4 字节 UTF-8 i += 4; } else { // 高字节不符合 UTF-8 规则 // 但如果在缓冲区末尾,可能是多字节序列被截断,不应误判 if (i + 3 >= n && c >= 0xC0) { TRACE("[LangEnc] Truncated at offset %zu: 0x%02X, skip\n", i, c); break; // 缓冲区尾部截断,跳出循环按已有结果判断 } // 确实是 ANSI/GBK TRACE("[LangEnc] GBK byte at offset %zu: 0x%02X → ANSI\n", i, c); return true; } } TRACE("[LangEnc] utf8SeqCount=%d → %s\n", utf8SeqCount, utf8SeqCount > 0 ? "UTF-8 (not ANSI)" : "pure ASCII (ANSI)"); // 存在多字节序列且全部符合 UTF-8 规则 → 判定为 UTF-8 return (utf8SeqCount == 0); } // 加载语言文件 bool Load(const CString& langCode) { m_strings.clear(); m_currentLang = langCode; // 如果是中文,不需要加载翻译 if (langCode == _T("zh_CN") || langCode.IsEmpty()) { return true; } CString langFile = m_langDir + _T("\\") + langCode + _T(".ini"); // 检查文件是否存在 if (GetFileAttributes(langFile) == INVALID_FILE_ATTRIBUTES) { return false; } // 读取 [Strings] 节的所有键值对 TCHAR buffer[32768] = { 0 }; // 用于获取所有键名 GetPrivateProfileSection(_T("Strings"), buffer, sizeof(buffer)/sizeof(TCHAR), langFile); // 解析键值对 (格式: key=value\0key=value\0\0) TCHAR* p = buffer; while (*p) { CString line(p); int eqPos = line.Find(_T('=')); if (eqPos > 0) { CString key = line.Left(eqPos); CString value = line.Mid(eqPos + 1); m_strings[key] = value; } p += _tcslen(p) + 1; } return true; } // 获取翻译字符串 // key: 中文原文 // 返回: 翻译后的文本,如果没有翻译则返回原文 CString Get(const CString& key) { if (m_currentLang == _T("zh_CN") || m_currentLang.IsEmpty()) { return key; // 中文直接返回 } auto it = m_strings.find(key); if (it != m_strings.end()) { return it->second; } return key; // 没有翻译则返回原文 } // 获取翻译字符串 (std::string 版本) std::string Get(const std::string& key) { CString result = Get(CString(key.c_str())); CT2A ansi(result); return std::string(ansi); } // 获取当前语言 CString GetCurrentLanguage() const { return m_currentLang; } // 是否为中文模式(无需翻译) bool IsChinese() const { return m_currentLang.IsEmpty() || m_currentLang == _T("zh_CN"); } // 获取可用语言数量(包括内置的简体中文) // 返回 1 表示只有简体中文,无其他语言文件 size_t GetLanguageCount() { auto langs = GetAvailableLanguages(); // 检查是否有 zh_CN.ini 文件 bool hasZhCN = false; for (const auto& lang : langs) { if (lang == _T("zh_CN")) { hasZhCN = true; break; } } // 简体中文始终存在,若 zh_CN.ini 不存在则额外 +1 return hasZhCN ? langs.size() : langs.size() + 1; } }; // 全局访问宏 #define g_Lang CLangManager::Instance() // 翻译宏 - 用于代码中的字符串字面量 // 用法: _TR("中文字符串") #define _TR(str) g_Lang.Get(CString(_T(str))) // 翻译宏 - 用于格式化函数 (sprintf_s, _stprintf_s 等可变参数函数) // 用法: _stprintf_s(buf, _TRF("连接 %s 失败"), ip); #define _TRF(str) ((LPCTSTR)_TR(str)) // 翻译函数 - 用于 CString 变量或 LPCTSTR // 用法: _L(strVar) 或 _L(_T("中文")) inline CString _L(const CString& str) { return g_Lang.Get(str); } inline CString _L(LPCTSTR str) { return g_Lang.Get(CString(str)); } // 翻译宏 - 用于格式化函数中的变量 (返回 LPCTSTR) // 用法: _stprintf_s(buf, _LF(strVar), arg); // 注意: 必须是宏,函数版本会导致悬空指针 #define _LF(str) ((LPCTSTR)_L(str)) // CString::Format 的多语言版本 (用于全局替换 .FormatL) // 用法: str.FormatLL("连接 %s 失败", ip); // 展开: str.FormatL(_TR("连接 %s 失败"), ip); // 注意: 不需要翻译的字符串也可以用,找不到翻译会返回原文 #define FormatL(fmt, ...) Format(_TR(fmt), __VA_ARGS__) // ============================================ // 带自动翻译的 MFC 函数宏 (L 后缀 = Language) // ============================================ // MessageBox 系列 // MFC 成员函数版本 (CWnd::MessageBox) #define MessageBoxL(text, caption, type) \ MessageBox(_TR(text), _TR(caption), type) // 全局 API 版本 (::MessageBox / ::MessageBoxA / ::MessageBoxW) #define MessageBoxAPI_L(hwnd, text, caption, type) \ ::MessageBox(hwnd, _TR(text), _TR(caption), type) // 简写:hwnd 为 NULL 时 #define MsgBoxL(text, caption, type) \ ::MessageBox(NULL, _TR(text), _TR(caption), type) #define AfxMessageBoxL(text, type) \ AfxMessageBox(_TR(text), type) // SetWindowText / SetDlgItemText #define SetWindowTextL(text) \ SetWindowText(_TR(text)) #define SetDlgItemTextL(id, text) \ SetDlgItemText(id, _TR(text)) // 列表控件 #define InsertColumnL(index, text, format, width) \ InsertColumn(index, _TR(text), format, width) #define InsertItemL(index, text) \ InsertItem(index, _TR(text)) #define SetItemTextL(item, subitem, text) \ SetItemText(item, subitem, _TR(text)) // ComboBox / ListBox #define AddStringL(text) \ AddString(_TR(text)) #define InsertStringL(index, text) \ InsertString(index, _TR(text)) // Tab 控件 #define InsertTabItemL(index, text) \ InsertItem(index, _TR(text)) // 状态栏 #define SetPaneTextL(index, text) \ SetPaneText(index, _TR(text)) // 菜单 #define AppendMenuL(flags, id, text) \ AppendMenu(flags, id, _TR(text)) #define AppendMenuSeparator(p) \ AppendMenu(p) #define InsertMenuL(pos, flags, id, text) \ InsertMenu(pos, flags, id, _TR(text)) #define ModifyMenuL(pos, flags, id, text) \ ModifyMenu(pos, flags, id, _TR(text)) // 翻译对话框所有控件 inline void TranslateDialog(CWnd* pWnd) { if (g_Lang.IsChinese()) { return; // 中文模式不需要翻译 } if (!pWnd || !pWnd->GetSafeHwnd()) { return; } // 翻译对话框标题 CString title; pWnd->GetWindowText(title); if (!title.IsEmpty()) { CString newTitle = g_Lang.Get(title); if (newTitle != title) { pWnd->SetWindowText(newTitle); } } // 遍历所有子控件 CWnd* pChild = pWnd->GetWindow(GW_CHILD); while (pChild) { // 获取控件文本 CString text; pChild->GetWindowText(text); if (!text.IsEmpty()) { CString newText = g_Lang.Get(text); if (newText != text) { pChild->SetWindowText(newText); } } // 如果是菜单按钮或有子菜单,也需要处理 // 递归处理子窗口(如 GroupBox 内的控件) if (pChild->GetWindow(GW_CHILD)) { TranslateDialog(pChild); } pChild = pChild->GetNextWindow(); } } // 翻译菜单 inline void TranslateMenu(CMenu* pMenu) { if (!pMenu || !pMenu->GetSafeHmenu() || g_Lang.IsChinese()) { return; } UINT count = pMenu->GetMenuItemCount(); for (UINT i = 0; i < count; i++) { CString text; pMenu->GetMenuString(i, text, MF_BYPOSITION); if (!text.IsEmpty()) { CString newText = g_Lang.Get(text); if (newText != text) { // 保留快捷键部分 (Tab 后的内容,如 Ctrl+S) int tabPos = text.Find(_T('\t')); if (tabPos > 0) { CString shortcut = text.Mid(tabPos); int newTabPos = newText.Find(_T('\t')); if (newTabPos < 0) { newText += shortcut; } } // 检查是否是弹出菜单(有子菜单) CMenu* pSubMenu = pMenu->GetSubMenu(i); if (pSubMenu) { // 弹出菜单使用 MF_POPUP pMenu->ModifyMenu(i, MF_BYPOSITION | MF_POPUP | MF_STRING, (UINT_PTR)pSubMenu->GetSafeHmenu(), newText); } else { // 普通菜单项 pMenu->ModifyMenu(i, MF_BYPOSITION | MF_STRING, pMenu->GetMenuItemID(i), newText); } } } // 递归处理子菜单 CMenu* pSubMenu = pMenu->GetSubMenu(i); if (pSubMenu) { TranslateMenu(pSubMenu); } } } // 加载菜单并翻译 (用于 LoadMenu 动态加载菜单后自动翻译) inline BOOL LoadMenuL(CMenu& menu, UINT nIDResource) { if (!menu.LoadMenu(nIDResource)) return FALSE; TranslateMenu(&menu); return TRUE; } inline BOOL LoadMenuL(CMenu* pMenu, UINT nIDResource) { if (!pMenu || !pMenu->LoadMenu(nIDResource)) return FALSE; TranslateMenu(pMenu); return TRUE; } // 翻译列表控件表头 inline void TranslateListHeader(CListCtrl* pList) { if (!pList || g_Lang.IsChinese()) { return; } CHeaderCtrl* pHeader = pList->GetHeaderCtrl(); if (!pHeader) { return; } int count = pHeader->GetItemCount(); for (int i = 0; i < count; i++) { HDITEM hdi; TCHAR text[256] = { 0 }; hdi.mask = HDI_TEXT; hdi.pszText = text; hdi.cchTextMax = 256; if (pHeader->GetItem(i, &hdi)) { CString newText = g_Lang.Get(CString(text)); if (newText != text) { hdi.pszText = (LPTSTR)(LPCTSTR)newText; pHeader->SetItem(i, &hdi); } } } } // 支持多语言的对话框基类 (基于 CDialog) // 用法: 将 class CMyDlg : public CDialog 改为 class CMyDlg : public CDialogLang class CDialogLang : public CDialog { public: CDialogLang(){} CDialogLang(UINT nIDTemplate, CWnd* pParent = NULL) : CDialog(nIDTemplate, pParent) {} CDialogLang(LPCTSTR lpszTemplateName, CWnd* pParent = NULL) : CDialog(lpszTemplateName, pParent) {} protected: virtual BOOL OnInitDialog() override { BOOL ret = __super::OnInitDialog(); TranslateDialog(this); TranslateMenu(GetMenu()); // 自动翻译菜单 return ret; } }; // 支持多语言的对话框基类 (基于 CDialogEx) // 用法: 将 class CMyDlg : public CDialogEx 改为 class CMyDlg : public CDialogLangEx class CDialogLangEx : public CDialogEx { public: CDialogLangEx(UINT nIDTemplate, CWnd* pParent = NULL) : CDialogEx(nIDTemplate, pParent) {} CDialogLangEx(LPCTSTR lpszTemplateName, CWnd* pParent = NULL) : CDialogEx(lpszTemplateName, pParent) {} protected: virtual BOOL OnInitDialog() override { BOOL ret = __super::OnInitDialog(); TranslateDialog(this); TranslateMenu(GetMenu()); // 自动翻译菜单 return ret; } }; // ============================================ // 语言选择对话框(动态创建,无需 RC 资源) // ============================================ class CLangSelectDlg : public CDialog { public: CString m_strSelectedLang; CComboBox m_comboLang; CLangSelectDlg(CWnd* pParent = NULL) : CDialog(), m_pParent(pParent) {} virtual INT_PTR DoModal() override { InitModalIndirect(CreateDialogTemplate(), m_pParent); return CDialog::DoModal(); } // 静态方法:显示对话框并返回选择的语言代码 // 返回空字符串表示用户取消 static CString Show(CWnd* pParent = NULL) { CLangSelectDlg dlg(pParent); if (dlg.DoModal() == IDOK) { return dlg.m_strSelectedLang; } return _T(""); } protected: CWnd* m_pParent; std::vector m_templateBuffer; std::vector m_langCodes; // 语言代码到显示名称的映射 static CString GetLanguageDisplayName(const CString& langCode) { if (langCode == _T("zh_CN")) return _T("简体中文"); if (langCode == _T("zh_TW")) return _T("繁體中文"); if (langCode == _T("en_US")) return _T("English"); return langCode; } LPCDLGTEMPLATE CreateDialogTemplate() { const WORD DLG_WIDTH = 200; const WORD DLG_HEIGHT = 75; m_templateBuffer.clear(); DLGTEMPLATE dlgTemplate = { 0 }; dlgTemplate.style = DS_MODALFRAME | DS_CENTER | WS_POPUP | WS_CAPTION | WS_SYSMENU; dlgTemplate.cdit = 4; dlgTemplate.cx = DLG_WIDTH; dlgTemplate.cy = DLG_HEIGHT; AppendData(&dlgTemplate, sizeof(DLGTEMPLATE)); AppendWord(0); // 菜单 AppendWord(0); // 窗口类 AppendString(_T("选择语言 / Select Language")); AlignToDword(); // 静态文本 AddControl(0x0082, 15, 15, 40, 12, (WORD)-1, SS_LEFT | WS_CHILD | WS_VISIBLE, _T("语言:")); // ComboBox AddControl(0x0085, 55, 13, 130, 150, 1001, CBS_DROPDOWNLIST | WS_CHILD | WS_VISIBLE | WS_TABSTOP | WS_VSCROLL, _T("")); // 确定按钮 AddControl(0x0080, 45, 50, 50, 14, IDOK, BS_DEFPUSHBUTTON | WS_CHILD | WS_VISIBLE | WS_TABSTOP, _T("确定")); // 取消按钮 AddControl(0x0080, 105, 50, 50, 14, IDCANCEL, BS_PUSHBUTTON | WS_CHILD | WS_VISIBLE | WS_TABSTOP, _T("取消")); return (LPCDLGTEMPLATE)m_templateBuffer.data(); } void AppendData(const void* data, size_t size) { const BYTE* p = (const BYTE*)data; m_templateBuffer.insert(m_templateBuffer.end(), p, p + size); } void AppendWord(WORD w) { AppendData(&w, sizeof(WORD)); } void AppendString(LPCTSTR str) { #ifdef UNICODE AppendData(str, (_tcslen(str) + 1) * sizeof(WCHAR)); #else int len = MultiByteToWideChar(CP_ACP, 0, str, -1, NULL, 0); std::vector wstr(len); MultiByteToWideChar(CP_ACP, 0, str, -1, wstr.data(), len); AppendData(wstr.data(), len * sizeof(WCHAR)); #endif } void AlignToDword() { while (m_templateBuffer.size() % 4 != 0) m_templateBuffer.push_back(0); } void AddControl(WORD classAtom, short x, short y, short cx, short cy, WORD id, DWORD style, LPCTSTR text) { AlignToDword(); DLGITEMTEMPLATE item = { 0 }; item.style = style; item.x = x; item.y = y; item.cx = cx; item.cy = cy; item.id = id; AppendData(&item, sizeof(DLGITEMTEMPLATE)); AppendWord(0xFFFF); AppendWord(classAtom); AppendString(text); AppendWord(0); } virtual BOOL OnInitDialog() override { CDialog::OnInitDialog(); // 翻译对话框控件(标题、标签、按钮) TranslateDialog(this); m_comboLang.SubclassDlgItem(1001, this); // 添加简体中文 int idx = m_comboLang.AddString(_T("简体中文")); m_langCodes.push_back(_T("zh_CN")); m_comboLang.SetItemData(idx, 0); // 添加其他语言 auto langs = g_Lang.GetAvailableLanguages(); for (const auto& lang : langs) { if (lang == _T("zh_CN")) continue; CString displayName = GetLanguageDisplayName(lang); idx = m_comboLang.AddString(displayName); m_comboLang.SetItemData(idx, m_langCodes.size()); m_langCodes.push_back(lang); } // 选中当前语言 CString currentLang = g_Lang.GetCurrentLanguage(); if (currentLang.IsEmpty()) currentLang = _T("zh_CN"); for (size_t i = 0; i < m_langCodes.size(); i++) { if (m_langCodes[i] == currentLang) { m_comboLang.SetCurSel((int)i); break; } } CenterWindow(); return TRUE; } virtual void OnOK() override { int sel = m_comboLang.GetCurSel(); if (sel >= 0) { size_t idx = (size_t)m_comboLang.GetItemData(sel); if (idx < m_langCodes.size()) { m_strSelectedLang = m_langCodes[idx]; } } CDialog::OnOK(); } };