// ============================================================================= // 诊断日志唯一出口的实现。说明见 Utils/UIDiag.h。 // // 为什么是独立 .cpp(而不是 header-only + inline): // 如果写成头文件里的 inline 函数,每个包含它的 .cpp 都会有一份 static 局部 // 变量副本(s_buf / path 缓存),日志行为虽然仍正确,但每次都重新 // GetModuleFileName + 打开/关闭文件,且在 DLL 边界上语义容易含混。 // 放 .cpp 里只编译一份 —— 这也是"全库只有本 .cpp 真正落盘"的实现基础。 // ============================================================================= #include "StdAfx.h" #include "UIDiag.h" #include #include #include namespace DuiLibDiag { // ★ 全库唯一的日志开关:需要重新开启诊断时把这里改回 true 并重编。 // // 2026-09-24(第 41 轮):Win10 触摸键盘输入问题(第 40 轮修:CEditUI 合成鼠标手势 // 只发 DOWN 不发 UP ⇒ 原生 EDIT 停在半截手势)已真机验收通过,收尾关闭诊断输出。 // 2026-09-25(R37):toast 加圆角后完全消失,靠 R35/R36 三轮埋点现场定位到 // 「UpdateLayeredWindow 返回 FALSE / ERROR_INVALID_PARAMETER(87),位图正确却提交 // 不上去 ⇒ 整窗空白」。真因是 toast_wnd.xml 的 alpha="230"(SetLayeredWindowAttributes // ⇒ 系统重定向模式)与 roundcorner="10,10"(要调 ULW_ALPHA)两套 alpha 机制互斥; // 已在 SetOpacity / SetRoundCorner / RefreshLayeredStyleBit 处根治。取证埋点使命 // 完成,再次关闭;下次排查按同样的「装现场再修」套路走。 // 关闭后 Enabled() 是编译期常量 false,整个 Log 函数体会被当死代码消除 // ⇒ 运行时零开销、零文件 IO,二进制里也不再出现 touch_trace.log / DIAG-BUILD 字样。 // 埋点代码全部保留在源码里,随时改回 true 重编即可复现现场。 inline bool Enabled() { return false; } // 极简 TCHAR* → UTF-8 char* 转换(仅用于日志路径;非线程安全,够用即可) static const char* Narrow(LPCTSTR s) { static char s_buf[MAX_PATH * 3] = { 0 }; #ifdef _UNICODE s_buf[0] = '\0'; if (s != NULL) ::WideCharToMultiByte(CP_UTF8, 0, s, -1, s_buf, (int)sizeof(s_buf) - 1, NULL, NULL); return s_buf; #else return s; #endif } void Log(LPCTSTR fmt, ...) { if (!Enabled()) return; // ★ 版本横幅 —— 整个日志里只写一次。 // // 存在的唯一理由:收银机不是本机,DLL 要手工拷过去。用户说"还是不行"时, // 第一步永远是先确认"他跑的是不是这一轮编出来的 DLL"。 // 只要日志第一行有这行横幅,就说明新库确实被加载了。 static bool s_bBannerWritten = false; const bool bWriteBanner = !s_bBannerWritten; s_bBannerWritten = true; TCHAR path[MAX_PATH] = { 0 }; ::GetModuleFileName(NULL, path, MAX_PATH); TCHAR* p = _tcsrchr(path, _T('\\')); if (p != NULL) *(p + 1) = _T('\0'); _tcscat_s(path, MAX_PATH, _T("touch_trace.log")); TCHAR msg[1024] = { 0 }; va_list ap; va_start(ap, fmt); _vsntprintf_s(msg, _countof(msg), _TRUNCATE, fmt, ap); va_end(ap); char utf8[2048] = { 0 }; #ifdef _UNICODE if (::WideCharToMultiByte(CP_UTF8, 0, msg, -1, utf8, (int)sizeof(utf8) - 1, NULL, NULL) <= 0) return; #else _tcscpy_s(utf8, _countof(utf8), msg); #endif FILE* f = NULL; // 保护:单个日志超过 32MB 就不再追加,避免在收银机上长期运行把日志写到失控。 // (全量 INPUT-TRACE 很啰嗦,阈值从 4MB 放宽到 32MB,否则一轮诊断还没跑完就静默停写了。) // 注意:GetFileAttributesEx 是 TCHAR 宏(Unicode 工程下展开为 ...ExW), // 必须传 TCHAR 的 path,不能传 Narrow(path) 得到的 UTF-8 char*(否则 C2664)。 WIN32_FILE_ATTRIBUTE_DATA fad = { 0 }; if (::GetFileAttributesEx(path, GetFileExInfoStandard, &fad) && fad.nFileSizeHigh == 0 && fad.nFileSizeLow >= (32u * 1024u * 1024u)) return; // fopen_s 只吃 char*(窄字符路径),这里才需要 UTF-8 的 Narrow(path) if (fopen_s(&f, Narrow(path), "a") == 0 && f != NULL) { if (bWriteBanner) { fprintf(f, "======== DIAG-BUILD 2026-09-25 R35-toast (SetFocus(NULL) null-deref fix; diagnostics OFF by default) ========\n"); } fprintf(f, "[%8u][T%5u] %s\n", (unsigned)::GetTickCount(), (unsigned)::GetCurrentThreadId(), utf8); // 立刻冲盘:程序被强杀 / 挂死 / 断电时,日志尾部(往往正是关键现场)不能丢。 fflush(f); fclose(f); } } void LogButtonActivate(LPCTSTR name, bool captureState, unsigned buttonState, bool pushed) { // 位域含义(取自 UIManager.h 的 #define,勿凭记忆写): // UISTATE_DISABLED=0x04 / UISTATE_HOT=0x08 // UISTATE_PUSHED=0x10 / UISTATE_CAPTURED=0x40 Log(_T("BUTTON-ACTIVATE name=%s mgrCaptured=%d state=0x%04X pushed=%d hot=%d captured=%d disabled=%d"), name != NULL ? name : _T("null"), (int)(captureState ? 1 : 0), buttonState, (int)(pushed ? 1 : 0), (int)((buttonState & 0x0008u) ? 1 : 0), (int)((buttonState & 0x0040u) ? 1 : 0), (int)((buttonState & 0x0004u) ? 1 : 0)); } }