#include "StdAfx.h" #include "UIEdit.h" #include "../Core/UIContainer.h" // [R46] 滚轮转发要用 CContainerUI::GetScrollRange #include "../Utils/UIDiag.h" namespace DuiLib { // ================================================================== // 【诊断 · 新建商品弹框「输入文字消失」(Win10 触摸键盘)】 // // 现象:手指点触摸键盘输入,已输入的字(尤其中文输入的组合串)会消失; // 鼠标点同一个键盘则完全正常。 // // 已确认的代码事实(不用日志就能确定的): // · 文本**真实存放**在 CEditWnd —— 一个原生的 EDIT 子窗口里(WS_CHILD), // DuiLib 只通过 EN_CHANGE 把内容同步回 CEditUI::m_sText; // · 该窗口的生命周期**极短**:只要它失去键盘焦点, // CEditWnd::OnKillFocus 就会 PostMessage(WM_CLOSE) **把它销毁**; // · 所以"谁在这一刻抢走了焦点"就是本问题的全部答案。 // 一旦焦点离开 EDIT,输入法正在拼的字会被撤销(中文尤其明显), // 之后到达的字符也无处可去 —— 用户看到的就是"输入完被自动删除"。 // // 因此下面这几个 helper 只做一件事:把"抢焦点的是谁"记清楚。 // 判读要点: // · 若 EDIT-KILLFOCUS 的 newTid != selfTid ⇒ 焦点被**别的进程/线程**抢走 // (触摸键盘 TabTip / TextInputHost 是首要嫌疑); // · newClass 是 "#32770" ⇒ 被我们自己的弹框拿走了; // · 若同一个 owner 出现 INIT → FIN → INIT 的抖动 ⇒ 窗口在反复重建, // 重建时用的是 m_sText(可能落后于 IME 的组合状态)⇒ 字就"没了"。 // ================================================================== static LPCTSTR DiagWndClass(HWND h) { static TCHAR s_cls[128]; s_cls[0] = _T('\0'); if (h != NULL && ::IsWindow(h)) ::GetClassName(h, s_cls, _countof(s_cls)); return s_cls; } static DWORD DiagWndTid(HWND h) { DWORD dwTid = 0; if (h != NULL && ::IsWindow(h)) dwTid = ::GetWindowThreadProcessId(h, NULL); return dwTid; } // r37 新增:抽"前台窗口属于哪个进程" —— 用来把两种"焦点空了"分开: // 前台还是本进程 ⇒ 只是键盘焦点被抢(可以且应该抢回来); // 前台已经是别的进程 ⇒ 用户主动切走了应用(绝不能抢)。 static DWORD DiagWndPid(HWND h) { DWORD dwPid = 0; if (h != NULL && ::IsWindow(h)) ::GetWindowThreadProcessId(h, &dwPid); return dwPid; } // 前向声明:DiagFgIsTouchKb 要用它做进程名白名单,实现放下面(见 DiagProcName)。 static void DiagProcName(HWND h, LPTSTR szOut, int cch); // r38 新增:把"前台窗口是不是触摸键盘"这一件事判死 —— 本轮唯一需要保护的判据。 // // Win10 的现代触摸键盘由 TextInputHost.exe 承载,窗口类就是 // `Windows.UI.Core.CoreWindow`(真机日志实证:fgClass=Windows.UI.Core.CoreWindow // fgPid=7012 fgTid=7080);老式 TabTip 的宿主类名是 `IPTip_Main_Window`。两个都收。 // 只有命中这两个才认为"前台是被触摸键盘拿走的",其余一律返回 false // ⇒ 用户主动切到别的应用时**绝不抢**,正常多任务完全不受影响。 static bool DiagFgIsTouchKb(HWND h) { if (h == NULL || !::IsWindow(h)) return false; TCHAR szCls[128] = { 0 }; ::GetClassName(h, szCls, _countof(szCls)); if (_tcsicmp(szCls, _T("Windows.UI.Core.CoreWindow")) != 0 && _tcsicmp(szCls, _T("IPTip_Main_Window")) != 0) { return false; // 类名不对 ⇒ 与触摸键盘无关(例如用户点了桌面/别的应用) } // 类名只是必要条件 —— `Windows.UI.Core.CoreWindow` 是**所有 UWP 应用**共用的类名, // 单凭它会把"用户切到一个 UWP 应用"也误判成"键盘夺前台"。所以再用进程名判死。 // 进程名取不到("?")时**放行**:取不到是极少数情况,而不抢的代价是用户按键继续 // 没有落点(功能直接坏掉),比偶尔打断一次切换严重得多。 TCHAR szExe[128] = { 0 }; DiagProcName(h, szExe, _countof(szExe)); if (szExe[0] == _T('?') || szExe[0] == _T('\0')) return true; return (_tcsicmp(szExe, _T("TextInputHost.exe")) == 0 || _tcsicmp(szExe, _T("TabTip.exe")) == 0 || _tcsicmp(szExe, _T("InputApp.exe")) == 0); } // r38 新增:取窗口所属进程的**可执行文件名**(只要文件名,不要全路径)。 // // 为什么不直接调 QueryFullProcessImageName:它受 `#if(_WIN32_WINNT >= 0x0600)` // 保护,而本工程(DuiLib_Static)的 WINVER 兜底较低(同上面 WS_EX_NOACTIVATE 那次) // ⇒ 走 GetProcAddress 动态取,零版本依赖、不新增 lib、不改工程设置。 // // 用途(本轮的判据靠它才判得死): // `Windows.UI.Core.CoreWindow` 这个类名**不是触摸键盘专有** —— 所有 UWP 应用都用它。 // 只靠类名,用户切到一个 UWP 应用时我们也会误判成"键盘夺前台"。补上进程名 // (TextInputHost.exe / TabTip.exe / InputApp.exe)才能真正把两种情况分开。 // 取不到就返回 "?",**绝不影响任何行为**(只写日志)。 static void DiagProcName(HWND h, LPTSTR szOut, int cch) { typedef BOOL (WINAPI *PFN_QFPE)(HANDLE, DWORD, LPWSTR, PDWORD); if (szOut == NULL || cch <= 0) return; szOut[0] = _T('\0'); if (h == NULL || !::IsWindow(h)) return; DWORD dwPid = 0; ::GetWindowThreadProcessId(h, &dwPid); if (dwPid == 0) return; HMODULE hK32 = ::GetModuleHandle(_T("kernel32.dll")); PFN_QFPE pfn = (hK32 != NULL) ? (PFN_QFPE)::GetProcAddress(hK32, "QueryFullProcessImageNameW") : NULL; if (pfn == NULL) { szOut[0] = _T('?'); szOut[1] = _T('\0'); return; } // 0x1000 = PROCESS_QUERY_LIMITED_INFORMATION(Vista+;字面量以避开 SDK 版本宏) HANDLE hProc = ::OpenProcess(0x1000, FALSE, dwPid); if (hProc == NULL) { szOut[0] = _T('?'); szOut[1] = _T('\0'); return; } WCHAR szFull[512] = { 0 }; DWORD cchLen = _countof(szFull) - 1; const BOOL bOk = pfn(hProc, 0, szFull, &cchLen); ::CloseHandle(hProc); if (!bOk) { szOut[0] = _T('?'); szOut[1] = _T('\0'); return; } LPCTSTR pSrc = NULL; #ifdef UNICODE pSrc = szFull; #else char szAnsi[512] = { 0 }; ::WideCharToMultiByte(CP_ACP, 0, szFull, -1, szAnsi, _countof(szAnsi), NULL, NULL); pSrc = szAnsi; #endif // 只留文件名(手工截取,不依赖 CRT 的 _tcsrchr / 重叠拷贝语义) int nLast = -1; for (int i = 0; pSrc[i] != _T('\0'); ++i) { if (pSrc[i] == _T('\\')) nLast = i; } int i = 0; for (int k = (nLast >= 0 ? nLast + 1 : 0); pSrc[k] != _T('\0') && i < cch - 1; ++i, ++k) szOut[i] = pSrc[k]; szOut[i] = _T('\0'); } // 调 CPaintManagerUI 的"最近一次输入是不是触摸"的判据(见 UIManager.h)。 // 只用于日志标注,不参与任何判断。 class CEditWnd : public CWindowWnd { public: CEditWnd(); void Init(CEditUI* pOwner); RECT CalPos(); LPCTSTR GetWindowClassName() const; LPCTSTR GetSuperClassName() const; void OnFinalMessage(HWND hWnd); LRESULT HandleMessage(UINT uMsg, WPARAM wParam, LPARAM lParam); LRESULT OnKillFocus(UINT uMsg, WPARAM wParam, LPARAM lParam, BOOL& bHandled); LRESULT OnClose(UINT uMsg, WPARAM wParam, LPARAM lParam, BOOL& bHandled); LRESULT OnEditChanged(UINT uMsg, WPARAM wParam, LPARAM lParam, BOOL& bHandled); protected: enum { DEFAULT_TIMERID = 20, }; CEditUI* m_pOwner; HBRUSH m_hBkBrush; bool m_bInit; bool m_bDrawCaret; // 【分层模式】本窗建成**独立顶层 WS_POPUP**(见 Init)时为 true。 // 顶层窗口的 SetWindowPos 用**屏幕坐标**,而 CalPos() 算出来的是 // 「相对宿主窗口客户区」的坐标 ⇒ 换算必须由 CalPos() 自己补, // 否则 Init 补一次、之后的 SetPos/Move(控件重排、拖动宿主窗口)就漏补, // 浮窗会停在旧位置 —— 就是「拖弹框时输入框不跟着走」的残影。 bool m_bTopLevelWnd; }; CEditWnd::CEditWnd() : m_pOwner(NULL), m_hBkBrush(NULL), m_bInit(false), m_bDrawCaret(false), m_bTopLevelWnd(false) { } // WS_EX_NOACTIVATE 在 winuser.h 里受 #if(WINVER >= 0x0500) 保护; // DuiLib_Static 工程的 WINVER 兜底较低,补一个兜底宏以防万一。 #ifndef WS_EX_NOACTIVATE #define WS_EX_NOACTIVATE 0x08000000L #endif void CEditWnd::Init(CEditUI* pOwner) { m_pOwner = pOwner; { CDuiString sOwnerName = pOwner->GetName(); LPCTSTR pszOwnerText = pOwner->GetText(); DuiLibDiag::Log(_T("EDIT-INIT enter owner=%s focused=%d ownerHwnd(old)=%p text='%s' nFocus=%p focusClass=%s focusTid=%u selfTid=%u"), sOwnerName.GetData(), (int)(pOwner->IsFocused() ? 1 : 0), (void*)pOwner->GetHWND(), pszOwnerText != NULL ? pszOwnerText : _T(""), (void*)::GetFocus(), DiagWndClass(::GetFocus()), (unsigned)DiagWndTid(::GetFocus()), (unsigned)::GetCurrentThreadId()); } RECT rcPos = CalPos(); UINT uStyle = 0; DWORD dwExStyle = 0; if(m_pOwner->GetManager()->IsLayered()) { // ⚠️【必须 NOACTIVATE】分层模式下本编辑窗是**独立顶层 WS_POPUP** // (分层窗口不渲染子窗口,只能靠浮层把它画出来)。 // 独立顶层窗口一旦被 SetFocus,系统会把**线程的活动窗口**改成它 // ⇒ 弹框本体收到 WM_ACTIVATE(WA_INACTIVE) ⇒ 每次点击都要重新激活 // ⇒ Windows 判定激活被撤销,**吞掉 WM_LBUTTONDOWN**(只剩 WM_LBUTTONUP) // ⇒ 弹框里所有按钮和关闭全部点不动。 // ⚠️ 2026-09-23 实测**证伪**:WS_EX_NOACTIVATE 拦不住 SetFocus 改「线程活动窗口」 // —— 真机日志里 SetFocus 本窗之后,宿主弹框照样收到 WM_ACTIVATE(WA_INACTIVE)。 // 真正的止血点在 UIManager.cpp MessageHandler 的 case WM_MOUSEACTIVATE // (回 MA_NOACTIVATE,判据见 ZpActivationWillBeRevoked): // 既然「激活 → 被本窗抢回」必然发生,就干脆别激活,系统便会照常投递鼠标消息。 // 本窗保留 WS_EX_NOACTIVATE 的收益只剩:不被**用户点击**激活、不上任务栏。 // WS_EX_TOOLWINDOW:不进任务栏、不参与 Alt+Tab。 // 非分层时本窗是 WS_CHILD(属于弹框),天然没有这个问题。 uStyle = WS_POPUP | ES_AUTOHSCROLL | WS_VISIBLE; dwExStyle = WS_EX_NOACTIVATE | WS_EX_TOOLWINDOW; // ★ 屏幕坐标换算集中到 CalPos()(见那里的注释)。 // 原先在这里手工补了一次,但 SetPos/Move 两条路径没补 ⇒ 一旦控件重排, // 浮窗会被摆到屏幕左上角附近;拖动宿主窗口时更是完全不动。 // ⚠️ 标量成员要在这里置位,再重新调用 CalPos() 取屏幕坐标。 m_bTopLevelWnd = true; rcPos = CalPos(); } else { uStyle = WS_CHILD | ES_AUTOHSCROLL; // 防御:熔断(DisableLayeredRender)后 IsLayered() 会变假 ⇒ 同一个 CEditWnd // 对象再次 Init 会落到本分支;标志若还停在 true,CalPos() 会多补一次 // 屏幕原点,子窗口就被摆错了。 m_bTopLevelWnd = false; } UINT uTextStyle = m_pOwner->GetTextStyle(); if(uTextStyle & DT_LEFT) uStyle |= ES_LEFT; else if(uTextStyle & DT_CENTER) uStyle |= ES_CENTER; else if(uTextStyle & DT_RIGHT) uStyle |= ES_RIGHT; if( m_pOwner->IsPasswordMode() ) uStyle |= ES_PASSWORD; Create(m_pOwner->GetManager()->GetPaintWindow(), NULL, uStyle, dwExStyle, rcPos); HFONT hFont=NULL; int iFontIndex=m_pOwner->GetFont(); if (iFontIndex!=-1) hFont = m_pOwner->GetManager()->GetFont(iFontIndex); if (hFont == NULL) hFont = m_pOwner->GetManager()->GetDefaultFontInfo()->hFont; SetWindowFont(m_hWnd, hFont, TRUE); Edit_LimitText(m_hWnd, m_pOwner->GetMaxChar()); if( m_pOwner->IsPasswordMode() ) Edit_SetPasswordChar(m_hWnd, m_pOwner->GetPasswordChar()); Edit_SetText(m_hWnd, m_pOwner->GetText()); Edit_SetModify(m_hWnd, FALSE); SendMessage(EM_SETMARGINS, EC_LEFTMARGIN | EC_RIGHTMARGIN, MAKELPARAM(0, 0)); Edit_Enable(m_hWnd, m_pOwner->IsEnabled() == true); Edit_SetReadOnly(m_hWnd, m_pOwner->IsReadOnly() == true); //Styls LONG styleValue = ::GetWindowLong(m_hWnd, GWL_STYLE); styleValue |= pOwner->GetWindowStyls(); ::SetWindowLong(GetHWND(), GWL_STYLE, styleValue); //Styls ::ShowWindow(m_hWnd, SW_SHOWNOACTIVATE); ::SetFocus(m_hWnd); if (m_pOwner->IsAutoSelAll()) { int nSize = GetWindowTextLength(m_hWnd); if( nSize == 0 ) nSize = 1; Edit_SetSel(m_hWnd, 0, nSize); } else { int nSize = GetWindowTextLength(m_hWnd); Edit_SetSel(m_hWnd, nSize, nSize); } m_bInit = true; { CDuiString sOwnerName = m_pOwner->GetName(); // nFocus 应当就是本窗口;若不等,说明有更晚的 SetFocus 把它抢走了。 // sel=[start,end] 是设给 EDIT 的光标/选择范围: // 路 B(正常点击)是 [点击处, 点击处];路 A(已聚焦但窗口被销毁后重建)是 [0, 全文] ⇒ 全选。 DWORD dwSelStart = 0, dwSelEnd = 0; ::SendMessage(m_hWnd, EM_GETSEL, (WPARAM)&dwSelStart, (LPARAM)&dwSelEnd); DuiLibDiag::Log(_T("EDIT-INIT done owner=%s hwnd=%p nFocus=%p focusIsSelf=%d sel=[%u,%u] text='%s'"), sOwnerName.GetData(), (void*)m_hWnd, (void*)::GetFocus(), (int)(::GetFocus() == m_hWnd ? 1 : 0), (unsigned)LOWORD(dwSelStart), (unsigned)LOWORD(dwSelEnd), m_pOwner->GetText()); } // ================================================================== // 【r40 取证 · 只写日志,不参与任何判据】把"编辑窗此刻到底能不能编辑"钉死。 // // 为什么需要这一行:r39 的 EDIT-KEYMSG 已经证明**按键确实到达了本窗口** // (focusIsSelf=1、WM_KEYDOWN/WM_CHAR 都有),可编辑框就是不进字。 // 剩下的可能只有两类,必须一刀切开: // ① **本窗口拒绝这个字符** —— 只读 / 被禁用 / 文本长度上限已满; // ② **本窗口不可见或被摆到了屏幕外** —— 字其实进去了,但用户看不见, // 而 EN_CHANGE 反射回 CEditUI::m_sText 的那条链又没走通 // ⇒ DuiLib 自绘的框当然是空的。 // 字段分工: // ro / en / limit ⇒ 切开 ①; vis / rect ⇒ 切开 ②; // len 是基准值(和 EDIT-KEYAFTER 的 len 一比就知道字符有没有生效); // unicode / oldProc ⇒ 万一 CallWindowProc 的目标过程不对,这里能看出来。 // ================================================================== { const LONG lStyleNow = ::GetWindowLong(m_hWnd, GWL_STYLE); RECT rcWndNow = { 0 }; ::GetWindowRect(m_hWnd, &rcWndNow); HWND hParNow = ::GetParent(m_hWnd); CDuiString sOwnerName = m_pOwner->GetName(); DuiLibDiag::Log(_T("EDIT-STATE phase=init owner=%s hwnd=%p ro=%d en=%d vis=%d limit=%u len=%d style=0x%08X rect=[%d,%d,%d,%d] parent=%p/%s unicode=%d oldProc=%p"), sOwnerName.GetData(), (void*)m_hWnd, (int)((lStyleNow & 0x0800L) ? 1 : 0), // 0x0800 = ES_READONLY (int)(::IsWindowEnabled(m_hWnd) ? 1 : 0), (int)(::IsWindowVisible(m_hWnd) ? 1 : 0), (unsigned)::SendMessage(m_hWnd, 0x00D5, 0, 0), // 0x00D5 = EM_GETLIMITTEXT(字面量以避开 SDK 版本宏) (int)::GetWindowTextLength(m_hWnd), (unsigned)lStyleNow, (int)rcWndNow.left, (int)rcWndNow.top, (int)rcWndNow.right, (int)rcWndNow.bottom, (void*)hParNow, DiagWndClass(hParNow), (int)(::IsWindowUnicode(m_hWnd) ? 1 : 0), (void*)m_OldWndProc); } } RECT CEditWnd::CalPos() { CDuiRect rcPos = m_pOwner->GetPos(); RECT rcInset = m_pOwner->GetTextPadding(); rcPos.left += rcInset.left; rcPos.top += rcInset.top; rcPos.right -= rcInset.right; rcPos.bottom -= rcInset.bottom; LONG lEditHeight = m_pOwner->GetManager()->GetFontInfo(m_pOwner->GetFont())->tm.tmHeight; if( lEditHeight < rcPos.GetHeight() ) { rcPos.top += (rcPos.GetHeight() - lEditHeight) / 2; rcPos.bottom = rcPos.top + lEditHeight; } CControlUI* pParent = m_pOwner; RECT rcParent; while( pParent = pParent->GetParent() ) { if( !pParent->IsVisible() ) { rcPos.left = rcPos.top = rcPos.right = rcPos.bottom = 0; break; } rcParent = pParent->GetClientPos(); if( !::IntersectRect(&rcPos, &rcPos, &rcParent) ) { rcPos.left = rcPos.top = rcPos.right = rcPos.bottom = 0; break; } } // 【分层模式】本窗是独立顶层 WS_POPUP:SetWindowPos 要的是**屏幕坐标**, // 而上面算出来的 rcPos 是「相对宿主窗口客户区」的 —— 这里统一补宿主窗口屏幕原点。 // // ⚠️ 必须集中在 CalPos 里,因为它的调用方有三个: // ① Init(建窗)② CEditUI::SetPos(控件重排)③ CEditUI::Move(控件移动) // 只有集中在此,才不会出现「一个调用方补了、另一个没补」的错位。 // (y 方向那个 -1 是原作者调出来的视觉补偿,原值保持不动。) if (m_bTopLevelWnd) { HWND hHost = m_pOwner->GetManager()->GetPaintWindow(); RECT rcHost = { 0 }; ::GetWindowRect(hHost, &rcHost); rcPos.left += rcHost.left; rcPos.right += rcHost.left; rcPos.top += rcHost.top - 1; rcPos.bottom += rcHost.top - 1; } return rcPos; } LPCTSTR CEditWnd::GetWindowClassName() const { return _T("EditWnd"); } LPCTSTR CEditWnd::GetSuperClassName() const { return WC_EDIT; } void CEditWnd::OnFinalMessage(HWND hWnd) { { CDuiString sOwnerName = (m_pOwner != NULL) ? m_pOwner->GetName() : CDuiString(_T("?")); LPCTSTR pszText = (m_pOwner != NULL) ? m_pOwner->GetText() : (LPCTSTR)NULL; // 同一个 owner 反复出现 FIN → INIT ⇒ 窗口在抖动重建; // 重建走的是 m_sText,而 m_sText 可能落后于 IME 尚未提交的组合串 ⇒ 字"没了"。 DuiLibDiag::Log(_T("EDIT-FIN hwnd=%p owner=%s text='%s' nFocus=%p"), (void*)hWnd, sOwnerName.GetData(), pszText != NULL ? pszText : _T(""), (void*)::GetFocus()); } m_pOwner->Invalidate(); // Clear reference and die if( m_hBkBrush != NULL ) ::DeleteObject(m_hBkBrush); if (m_pOwner->GetManager()->IsLayered()) { m_pOwner->GetManager()->RemoveNativeWindow(hWnd); } m_pOwner->m_pWindow = NULL; delete this; } // ================================================================== // 【R49】把落在原生 EDIT 上的「对话框级导航键」(ESC / 回车)转发给真模态对话框。 // // 根因链(逐环坐实): // ① 焦点在原生 EDIT 上时,WM_KEYDOWN 只投给 EDIT;EDIT 既不处理这两个键, // 也不把它们上抛给父对话框(DefWindowProc 不做转发)⇒ 弹框根本看不到; // ② 真模态(CWindowWnd::ShowModal -> DialogBoxIndirectParam,UIBase.cpp:659) // 期间跑的是系统对话框循环,主循环 CPaintManagerUI::MessageLoop 整个被阻塞 // ⇒ 那条「对每一条队列消息都跑 PreMessageHandler、且不看收件窗口是谁」的 // 通道(UIManager.cpp:8012 TranslateMessage)彻底不存在; // ③ 于是挂在 IMessageFilterUI::MessageHandler 上的老弹框(商品新建/修改等 // 38 个)收不到 ESC / 回车 —— 而它们的按钮照常能用,因为按钮是自绘控件、 // 焦点在对话框上,消息本来就到得了 __DialogProc。 // 假模态时代没有这个问题:ShowModalFake 不建自己的消息循环,走的就是主循环。 // // 判据:只在 paint window 的窗口类是系统对话框类 "#32770" 时转发 ⇒ 只有真模态 // 才转发。假模态下主循环已经跑过 pre-filter,再转发会双触发 // (StartSave() 跑两次、连发两次请求)。 // // 用 PostMessage 而不是 SendMessage:弹框可能在本消息的处理过程中 Close() // 把窗口连同这个 EDIT 一起销毁,异步投递可以彻底避开「回来时 this 已析构」。 // ================================================================== static void ForwardNavKeyToModalDialog(CPaintManagerUI* pMgr, HWND hEdit, UINT uMsg, WPARAM wParam, LPARAM lParam) { if( pMgr == NULL ) return; HWND hDlg = pMgr->GetPaintWindow(); if( hDlg == NULL || hDlg == hEdit ) return; TCHAR szClass[64] = { 0 }; if( ::GetClassName(hDlg, szClass, 63) == 0 ) return; if( _tcscmp(szClass, _T("#32770")) != 0 ) return; ::PostMessage(hDlg, uMsg, wParam, lParam); } LRESULT CEditWnd::HandleMessage(UINT uMsg, WPARAM wParam, LPARAM lParam) { LRESULT lRes = 0; BOOL bHandled = TRUE; // ★【R49】把「对话框级导航键」转发给真模态对话框(见上面 helper 的注释)。 // 放在最前面、且不参与下面的 else-if 链 ⇒ 原有的 VK_RETURN / VK_TAB // 分支语义一字不动(VK_RETURN 仍照旧给控件发 DUI_MSGTYPE_RETURN)。 if( uMsg == WM_KEYDOWN && (TCHAR(wParam) == VK_ESCAPE || TCHAR(wParam) == VK_RETURN) ) { ForwardNavKeyToModalDialog(m_pOwner != NULL ? m_pOwner->GetManager() : NULL, m_hWnd, uMsg, wParam, lParam); } if( uMsg == WM_CREATE ) { bHandled = FALSE; } else if( uMsg == WM_KILLFOCUS ) lRes = OnKillFocus(uMsg, wParam, lParam, bHandled); else if( uMsg == WM_CLOSE ) lRes = OnClose(uMsg, wParam, lParam, bHandled); else if( uMsg == OCM_COMMAND ) { if( GET_WM_COMMAND_CMD(wParam, lParam) == EN_CHANGE ) lRes = OnEditChanged(uMsg, wParam, lParam, bHandled); else if( GET_WM_COMMAND_CMD(wParam, lParam) == EN_UPDATE ) { RECT rcClient; ::GetClientRect(m_hWnd, &rcClient); ::InvalidateRect(m_hWnd, &rcClient, FALSE); } } else if( uMsg == WM_KEYDOWN && TCHAR(wParam) == VK_RETURN ){ m_pOwner->GetManager()->SendNotify(m_pOwner, DUI_MSGTYPE_RETURN); } else if( uMsg == WM_KEYDOWN && TCHAR(wParam) == VK_TAB ){ if (m_pOwner->GetManager()->IsLayered()) { m_pOwner->GetManager()->SetNextTabControl(); } } else if( uMsg == WM_MOUSEWHEEL ) { // ★【R46】原生 EDIT 窗口不会把滚轮冒泡给父窗口 —— Windows 不转发 // WM_MOUSEWHEEL,所以光标停在输入框上时整页滚不动。这里把滚轮 // 直接交给「本输入框的第一个可滚祖先」(就是页面级滚动容器), // 绕过中途会认领滚轮的编辑框自身。 // 最坏情况是找不到可滚祖先 ⇒ 什么都不做,与改动前完全一致。 bool bWheelSent = false; if( m_pOwner != NULL && m_pOwner->GetManager() != NULL ) { CPaintManagerUI* pMgr = m_pOwner->GetManager(); POINT ptWheel = { (short)LOWORD(lParam), (short)HIWORD(lParam) }; ::ScreenToClient(pMgr->GetPaintWindow(), &ptWheel); for( CControlUI* p = m_pOwner->GetParent(); p != NULL; p = p->GetParent() ) { CContainerUI* pContent = dynamic_cast(p); if( pContent == NULL ) continue; if( pContent->GetScrollRange().cy <= 0 ) continue; int zDelta = (int)(short)HIWORD(wParam); TEventUI event = { 0 }; event.Type = UIEVENT_SCROLLWHEEL; event.pSender = p; event.wParam = MAKEWPARAM(zDelta < 0 ? SB_LINEDOWN : SB_LINEUP, zDelta); event.lParam = lParam; event.ptMouse = ptWheel; event.wKeyState = (WORD)wParam; event.dwTimestamp = ::GetTickCount(); p->Event(event); bWheelSent = true; break; } } if( bWheelSent ) return 0; bHandled = FALSE; // 没有可滚祖先 ⇒ 原样交给 DefWindowProc(与改前一致) } else if( uMsg == OCM__BASE + WM_CTLCOLOREDIT || uMsg == OCM__BASE + WM_CTLCOLORSTATIC ) { ::SetBkMode((HDC)wParam, TRANSPARENT); DWORD dwTextColor = m_pOwner->GetTextColor(); ::SetTextColor((HDC)wParam, RGB(GetBValue(dwTextColor),GetGValue(dwTextColor),GetRValue(dwTextColor))); DWORD clrColor = m_pOwner->GetNativeEditBkColor(); if (clrColor < 0xFF000000) { if (m_hBkBrush != NULL) ::DeleteObject(m_hBkBrush); RECT rcWnd = m_pOwner->GetManager()->GetNativeWindowRect(m_hWnd); HBITMAP hBmpEditBk = CRenderEngine::GenerateBitmap(m_pOwner->GetManager(), rcWnd, m_pOwner, clrColor); m_hBkBrush = ::CreatePatternBrush(hBmpEditBk); ::DeleteObject(hBmpEditBk); } else { if (m_hBkBrush == NULL) { m_hBkBrush = ::CreateSolidBrush(RGB(GetBValue(clrColor), GetGValue(clrColor), GetRValue(clrColor))); } } return (LRESULT)m_hBkBrush; } else if( uMsg == WM_PAINT) { bHandled = FALSE; } else if( uMsg == WM_PRINT ) { bHandled = FALSE; } else if( uMsg == WM_TIMER ) { if (wParam == CARET_TIMERID) { m_bDrawCaret = !m_bDrawCaret; RECT rcClient; ::GetClientRect(m_hWnd, &rcClient); ::InvalidateRect(m_hWnd, &rcClient, FALSE); return 0; } bHandled = FALSE; } else if( uMsg == WM_SETFOCUS || uMsg == WM_KEYDOWN || uMsg == WM_CHAR ) { // 【r39 取证 · 只写日志,行为**完全不变**】 // 本分支放在 VK_RETURN / VK_TAB 两支**之后**,所以那两支的语义一字不动; // 末尾照旧 `bHandled = FALSE` ⇒ 与原来"落到 else"的路径完全一致。 // // 目的:把"按键到底有没有进到这个窗口"这一件事一次判死 —— // · 抢回之后敲键盘,这三条**一条都不出现** ⇒ 按键根本没投进来 // (前台/焦点没恢复对,"事后抢回"这条路走不通); // · 出现 WM_KEYDOWN 却没有 EN_CHANGE ⇒ 按键到了、被吞(输入法/业务过滤器层); // · 出现 WM_CHAR 或直接 EDIT-CHANGE ⇒ 落点正常。 CDuiString sKeyOwnerName = (m_pOwner != NULL) ? m_pOwner->GetName() : CDuiString(_T("?")); HWND hKeyFocus = ::GetFocus(); DuiLibDiag::Log(_T("EDIT-KEYMSG msg=%u owner=%s wParam=%u lParam=%u nFocus=%p focusIsSelf=%d"), (unsigned)uMsg, sKeyOwnerName.GetData(), (unsigned)wParam, (unsigned)lParam, (void*)hKeyFocus, (int)(hKeyFocus == m_hWnd ? 1 : 0)); bHandled = FALSE; } else bHandled = FALSE; if( !bHandled ) { // 【r40 取证 · 行为零改变】把默认处理的返回值**原样**带出去(与原写法完全等价), // 只在按键类消息之后补一行"事后状态"。 // // 这一行是整个问题的裁判: // · len 变大 / 变小 ⇒ 编辑框**接受了**这个字符。那么"用户看不见字"就只能 // 是显示或反射链的问题(m_sText 没被 EN_CHANGE 更新 ⇒ DuiLib 自绘为空); // · len 不变 ⇒ 编辑框**拒绝**了它(只读/禁用/上限),或者这条消息压根没走到 // 真正 EDIT 的窗口过程。两者改法完全不同,所以必须先看这一行。 const LRESULT lDefRes = CWindowWnd::HandleMessage(uMsg, wParam, lParam); if( uMsg == WM_CHAR || uMsg == WM_KEYDOWN ) { TCHAR szTxtNow[512] = { 0 }; ::GetWindowText(m_hWnd, szTxtNow, _countof(szTxtNow)); const LONG lStyleAfter = ::GetWindowLong(m_hWnd, GWL_STYLE); CDuiString sOwnerAfter = (m_pOwner != NULL) ? m_pOwner->GetName() : CDuiString(_T("?")); DuiLibDiag::Log(_T("EDIT-KEYAFTER msg=%u wParam=%u owner=%s len=%d ro=%d en=%d vis=%d text='%s'"), (unsigned)uMsg, (unsigned)wParam, sOwnerAfter.GetData(), (int)::GetWindowTextLength(m_hWnd), (int)((lStyleAfter & 0x0800L) ? 1 : 0), (int)(::IsWindowEnabled(m_hWnd) ? 1 : 0), (int)(::IsWindowVisible(m_hWnd) ? 1 : 0), szTxtNow); } return lDefRes; } return lRes; } LRESULT CEditWnd::OnKillFocus(UINT uMsg, WPARAM wParam, LPARAM lParam, BOOL& bHandled) { LRESULT lRes = ::DefWindowProc(m_hWnd, uMsg, wParam, lParam); // ---- r37:把现场一次性记全(变量全部落局部,避免 DiagWndClass 的静态缓冲互相覆盖)---- HWND hNewFocus = ::GetFocus(); HWND hFg = ::GetForegroundWindow(); const DWORD dwSelfTid = ::GetCurrentThreadId(); const DWORD dwSelfPid = ::GetCurrentProcessId(); const DWORD dwNewTid = DiagWndTid(hNewFocus); const DWORD dwFgPid = DiagWndPid(hFg); const DWORD dwFgTid = DiagWndTid(hFg); const bool bFgOurs = (hFg != NULL && dwFgPid == dwSelfPid); CPaintManagerUI* pManager = (m_pOwner != NULL) ? m_pOwner->GetManager() : NULL; const bool bOwnerKeepsFocus = (pManager != NULL && pManager->GetFocus() == m_pOwner); { CDuiString sOwnerName = (m_pOwner != NULL) ? m_pOwner->GetName() : CDuiString(_T("?")); TCHAR szNewCls[128] = { 0 }; TCHAR szFgCls[128] = { 0 }; if (hNewFocus != NULL && ::IsWindow(hNewFocus)) ::GetClassName(hNewFocus, szNewCls, _countof(szNewCls)); if (hFg != NULL && ::IsWindow(hFg)) ::GetClassName(hFg, szFgCls, _countof(szFgCls)); // r38:把"前台到底是哪个程序"也记下来 —— 类名只能说"像 UWP",进程名才定案。 TCHAR szFgExe[128] = { 0 }; DiagProcName(hFg, szFgExe, _countof(szFgExe)); // ★ 这条就是"输入法正在拼的字会不会消失"的现场。 // 失焦本身不致命 —— 致命的是"失焦即销毁"叠加"瞬态失焦"(见 OnClose)。 // 判读: // · newFocus = 00000000 ⇒ 本线程此刻**没有**焦点窗口。 // 此时 sameProc 无意义(下面记成 -1,别再把它读成"被别的进程抢走" // —— NULL 没有进程可言)。真实成因靠下面三个新字段分开。 // · newFocus != NULL 且 sameProc=0 ⇒ 真被**别的进程/线程**抢走; // · newClass = EditWnd ⇒ 我们自己的另一个输入框接走了焦点(正常切框); // · newClass = #32770 ⇒ 被本线程的弹框本体拿走(窗口内部搬家)。 // // ── r37 新增字段:专为把"两种焦点空了"分开(判读已按 r38 真机证据修订)── // · fg / fgClass / fgPid / fgTid / fgExe —— 此刻**前台窗口**是谁。 // ★ r38 真机实证到的形态是:**前台被触摸键盘夺走** // (fgClass=Windows.UI.Core.CoreWindow、fgExe 应为 TextInputHost.exe)。 // 前台一变,系统必然清空**旧**前台线程的键盘焦点 —— 所以同一条日志里 // newFocus=00000000 且 killParam=00000000,两者是同一件事的两面。 // 触摸键盘是按"哪个应用持有输入焦点"投递按键/候选词/退格的;我们此刻 // 既不是前台、焦点也被清空 ⇒ 每一次按键全部丢掉。**这就是本体。** // · fg=00000000 ⇒ 系统当前没有前台窗口(少见,见下面的 EDIT-FGLOST 分支)。 // · fgOurs=1 ⇒ 前台仍是本进程的窗口,只是本线程键盘焦点被清空 // ⇒ 属于"焦点被抢",多发生在窗口内部的焦点搬家途中,会自行恢复。 // · killParam —— WM_KILLFOCUS 的 wParam(系统指定的接手窗口,比 GetFocus() 权威)。 DuiLibDiag::Log(_T("EDIT-KILLFOCUS hwnd=%p owner=%s newFocus=%p newClass=%s newTid=%u selfTid=%u sameProc=%d fg=%p fgClass=%s fgTid=%u fgPid=%u fgExe=%s fgOurs=%d ownerKeepsFocus=%d killParam=%p -> 投递 WM_CLOSE(去留由 OnClose 复核)"), (void*)m_hWnd, sOwnerName.GetData(), (void*)hNewFocus, szNewCls, (unsigned)dwNewTid, (unsigned)dwSelfTid, (int)(hNewFocus == NULL ? -1 : ((dwNewTid == dwSelfTid) ? 1 : 0)), (void*)hFg, szFgCls, (unsigned)dwFgTid, (unsigned)dwFgPid, szFgExe, (int)(bFgOurs ? 1 : 0), (int)(bOwnerKeepsFocus ? 1 : 0), (void*)(HWND)wParam); } PostMessage(WM_CLOSE); // ================================================================== // ★★★【r37 根治 · 把被"莫名清空"的键盘焦点抢回来】(2026-09-24) // // 真机形态(用户原话):「用手指点触摸键盘的回退按钮无法删除输入框的文字, // 但是用鼠标点同一个按钮就可以正常删除」。 // // 触摸键盘是**另一个进程**的窗口(TabTip),手指点它时本进程一行代码都不会执行 // —— 所以"点键盘没反应"绝不可能是我们窗口内部逻辑写错。差别只可能在一个地方: // 触摸键盘把按键投给「**前台窗口所在线程的焦点窗口**」。 // · 鼠标点它:TabTip 是 NOACTIVATE 窗口,点它不改变激活 ⇒ 本进程仍是前台 // ⇒ 焦点窗口是编辑窗 ⇒ 字符/退格/候选词全部正常落点。 // · 手指点它:触摸键盘自己**把前台拿走了**(r38 真机日志实证: // `fgClass=Windows.UI.Core.CoreWindow fgPid=7012 fgOurs=0`)⇒ // ① 前台一变,系统必然清空旧前台线程的键盘焦点(见下方日志 // newFocus=00000000,killParam 同样是 00000000); // ② 触摸键盘是靠"有输入焦点的那个应用"投递按键/候选词/退格的, // 我们这个应用此刻既非前台、焦点也被清空 ⇒ 按键没有落点。 // ⇒ 用户看到的正是"打字母不进、退格删不掉、候选词点了没反应"。 // // 注意:OnClose 那处修复(延后判 owner 是否仍是 DuiLib 焦点)只解决了 // "窗口被销毁",**并没有解决"焦点不见了"** —— 窗口活着但没有键盘焦点, // 按键依旧无处可去。这就是用户反馈"还是不行"的那一半。 // // ── r38 / r39 两轮真机证据的合并结论(**两种形态,同一个根**)── // // r37 版日志看到的形态(形态 A): // EDIT-KILLFOCUS ... newFocus=00000000 // fg=0001020C fgClass=Windows.UI.Core.CoreWindow fgTid=7080 fgPid=7012 // fgOurs=0 ownerKeepsFocus=1 killParam=00000000 // ⇒ 前台被**触摸键盘本人**拿走(Win10 现代触摸键盘 = TextInputHost.exe, // 窗口类正是 Windows.UI.Core.CoreWindow)。 // // r38 版日志看到的形态(形态 B): // EDIT-KILLFOCUS ... newFocus=00000000 fg=00000000 fgExe= fgOurs=0 ownerKeepsFocus=1 // ⇒ 前台**落空**(系统当前没有任何前台窗口)。 // // 两者本质相同:**本进程既不是前台、本线程键盘焦点又被清空** ⇒ 触摸键盘 // 投递按键/候选词/退格时找不到落点。所以判据必须把两种都收进来(见下方 bKbTookFg)。 // r38 只收了形态 A,结果正好把真机上实际出现的那次排除在外 —— 这就是 // "还是不行"的直接原因。 // // 同时这也解释了 r37 两个抢回钩子为什么一行日志都没打 —— 它们**都是死代码**: // · UIManager 的 ACT-RECLAIM 挂在 WM_ACTIVATE 上:弹框在那一刻**早已是 // INACTIVE**(最后一次 WND-ACTIVATE 是 531080953 的 wParam=0), // 系统不会为它再补发一次 ⇒ 钩子永不触发; // · 本文件的 EDIT-REFOCUS 判据要求 fgOurs=1,而真实形态恰恰是 fgOurs=0 // ⇒ 条件恒假。 // // 唯一**确实到达**的信号就是这条 WM_KILLFOCUS(r37 日志里它准时出现)。 // 所以抢回逻辑必须挂在这里。 // // 判据(纯状态、零时间戳,三条缺一不可): // ① newFocus = NULL —— 不是"切到我们自己的另一个框"(那会是另一个 EditWnd); // ② ownerKeepsFocus —— DuiLib 侧焦点仍在这个 EDIT 上 ⇒ 用户还在这个框里打字 // (弹框正在销毁/失焦时该值为假 ⇒ 不抢); // ③ 前台是触摸键盘**或落空** —— 两种都算(形态 A / 形态 B,见上)。 // **用户主动 Alt+Tab、点别的应用或点桌面时,前台是那个应用的窗口 // (既不是 NULL、也不命中键盘类名/进程名)⇒ ③ 为假 ⇒ 绝不抢。** // // 动作顺序**不能反**:先把前台要回来,再复位焦点 —— // 前台变更本身会重置线程的键盘焦点,先 SetFocus 再抢前台等于白设。 // ① SetForegroundWindow(画布窗口):常规路径(前台本就是被键盘临时拿走的); // ② 若被系统"后台进程不得抢前台"的闸门挡住 ⇒ 挂到当前前台线程上重试一次 // (经典配方,用完**立刻脱开**,不常驻、不放钩子); // ③ 前台回来后再 ::SetFocus(本编辑窗)。 // 全程无定时器、无时间戳、无事后清洗;不改几何、不改命中、不碰业务侧键盘显隐。 // ================================================================== if (hNewFocus == NULL && bOwnerKeepsFocus) { CDuiString sOwnerName = (m_pOwner != NULL) ? m_pOwner->GetName() : CDuiString(_T("?")); TCHAR szFgClsR[128] = { 0 }; TCHAR szFgExe[128] = { 0 }; if (hFg != NULL && ::IsWindow(hFg)) ::GetClassName(hFg, szFgClsR, _countof(szFgClsR)); DiagProcName(hFg, szFgExe, _countof(szFgExe)); // ★★★ r39 关键修正:把「前台落空(fg = NULL)」也算作触摸键盘夺前台 ★★★ // // r38 这里写的是 `hFg != NULL && ...` —— 恰好把**真机唯一出现过的那种形态** // 排除在外。r38 真机日志里那两行就是铁证: // EDIT-KILLFOCUS ... newFocus=00000000 fg=00000000 fgExe= fgOurs=0 ownerKeepsFocus=1 // EDIT-FGLOST ... -> 非触摸键盘夺前台,不动作 ← 于是我们一个字都没做 // // 「触摸键盘夺走前台」在真机上有**两种形态**,本质完全相同(本进程既不是 // 前台、本线程键盘焦点又被清空 ⇒ 按键注入没有落点),必须一起收: // · 形态 A(r37 日志实证): // fg = Windows.UI.Core.CoreWindow / fgExe = TextInputHost.exe // —— 键盘**本人持有前台**; // · 形态 B(r38 日志实证): // fg = 00000000 —— **前台落空**(当前没有任何前台窗口)。 // 成因:触摸键盘的窗口是 WS_EX_NOACTIVATE 的,手指触摸它引发的 // 激活交接**没有落点**,系统索性把前台置空。 // ⇔ 对照组正是"鼠标点同一个键盘一切正常":鼠标点 NOACTIVATE 窗口 // 根本不产生激活交接,前台一直留在我们身上。 // // 安全性完全不变:用户主动 Alt+Tab / 点别的应用 / 点桌面时,fg 是 // **那个应用的窗口**(既不是 NULL,也不命中键盘类名)⇒ 判据为假 ⇒ 绝不抢。 const bool bFgIsKb = (hFg != NULL && DiagFgIsTouchKb(hFg)); const bool bKbTookFg = (hFg == NULL) || (!bFgOurs && bFgIsKb); if (!bKbTookFg) { // 焦点确实空了,但不是触摸键盘的账(例如用户切应用/切盘途中)⇒ 一个字都不动。 // 留痕是为了万一还要再查:把"谁拿了前台"直接写进日志。 DuiLibDiag::Log(_T("EDIT-FGLOST hwnd=%p owner=%s newFocus=NULL fg=%p(%s) fgExe=%s fgOurs=%d -> 前台属他方(既非键盘也非落空),不动作"), (void*)m_hWnd, sOwnerName.GetData(), (void*)hFg, szFgClsR, szFgExe, (int)(bFgOurs ? 1 : 0)); } else { HWND hCanvas = (pManager != NULL) ? pManager->GetPaintWindow() : NULL; const bool bCanvasOk = (hCanvas != NULL && ::IsWindow(hCanvas) && ::IsWindowVisible(hCanvas)); // ① 把前台要回来 BOOL bOkFg = FALSE; if (bCanvasOk) bOkFg = ::SetForegroundWindow(hCanvas); HWND hFg1 = ::GetForegroundWindow(); const bool bOurFg1 = (hFg1 != NULL && DiagWndPid(hFg1) == dwSelfPid); // ② 被闸门挡住时,挂到当前前台线程上重试一次(用完立刻脱开) BOOL bAttached = FALSE; BOOL bOkFg2 = FALSE; if (!bOurFg1 && bCanvasOk) { const DWORD dwFgTid2 = DiagWndTid(hFg1); if (dwFgTid2 != 0 && dwFgTid2 != dwSelfTid) { bAttached = ::AttachThreadInput(dwFgTid2, dwSelfTid, TRUE); if (bAttached) { bOkFg2 = ::SetForegroundWindow(hCanvas); ::AttachThreadInput(dwFgTid2, dwSelfTid, FALSE); } } } // ③ 前台回来之后再复位焦点(顺序不能反) ::SetFocus(m_hWnd); HWND hFg2 = ::GetForegroundWindow(); TCHAR szFg2Cls[128] = { 0 }; if (hFg2 != NULL && ::IsWindow(hFg2)) ::GetClassName(hFg2, szFg2Cls, _countof(szFg2Cls)); // hit = 本进程内第几次执行抢回。判读(这是本轮最关键的一栏): // · hit 只涨到 1、之后再没出现 ⇒ 抢回后状态**稳住了**,打字应当恢复; // · hit 每敲一下键盘就 +1 ⇒ 触摸键盘在**每一次点按**时都重新夺走前台, // 抢在投递之前 ⇒ "事后抢回"这条路走不通,必须改成"不让它夺"。 static int s_nReclaimHit = 0; ++s_nReclaimHit; HWND hFocusNow = ::GetFocus(); DuiLibDiag::Log(_T("EDIT-RECLAIM hit=%d shape=%s hwnd=%p owner=%s fgBefore=%p(%s) fgExe=%s fgOk=%d attach=%d fgOk2=%d fgAfter=%p(%s) fgOursAfter=%d nFocus=%p focusIsSelf=%d"), (int)s_nReclaimHit, (hFg == NULL ? _T("fgNULL") : _T("fgKB")), (void*)m_hWnd, sOwnerName.GetData(), (void*)hFg, szFgClsR, szFgExe, (int)bOkFg, (int)bAttached, (int)bOkFg2, (void*)hFg2, szFg2Cls, (int)((hFg2 != NULL && DiagWndPid(hFg2) == dwSelfPid) ? 1 : 0), (void*)hFocusNow, (int)(hFocusNow == m_hWnd ? 1 : 0)); } } return lRes; } // ★ 修复(2026-09-23 · 「手指点触摸键盘输入不进去」) // // 背景(真机日志实证,一次"手指点输入框"就够看清楚): // // PTR-DOWN stealFocus ::SetFocus(m_hWndPaint) ← UIManager.cpp 抢走键盘焦点 // EDIT-KILLFOCUS PostMessage(WM_CLOSE) ← 销毁请求**排队**(异步) // EDITUI-SETFOCUS 复用编辑窗 ::SetFocus(hEdit) ← 焦点已经还给 EDIT // …… 几十毫秒后 …… // EDIT-FIN 窗口还是被那条排队的 WM_CLOSE 销毁了 // // 也就是说:**焦点刚被抢回来,窗口照旧死掉**。而 CPaintManagerUI::SetFocus 有 // `pControl == m_pFocus` 早退,不会再发 UIEVENT_SETFOCUS ⇒ 窗口不再重建。 // 触摸键盘敲出的字符、退格、输入法候选词的提交,就全都失去了落点 // (真机日志里对应"9.7 秒零条 EDIT-CHANGE")。 // // "手指点虚拟键盘"之所以必现、鼠标不犯,是因为手指点它的那一刻本窗口会短暂失活 // (WND-ACTIVATE wParam=0(INACTIVE) fg=00000000 + EDIT-KILLFOCUS newFocus=00000000), // 于是又排上一条 WM_CLOSE;鼠标那一路不会走到这里。 // // ⇒ 判据(纯状态、零时间戳):**延后到 WM_CLOSE 真正被取出来处理时**,再看一眼 // 「我的 owner 现在还是不是 DuiLib 的焦点控件」: // // · 还是 ⇒ 这次失焦只是**瞬态**(窗口内部焦点搬家 / 本窗口短暂失活), // owner 还要接着输入 ⇒ 取消销毁,窗口活着等 SETFOCUS 回来; // · 不是 ⇒ 焦点已被别的控件/别的窗口拿走,或者控件被隐藏/移除、弹框正在销毁 // (那些路径都会先 SetFocus(NULL) 或把 m_pFocus 置 NULL) // ⇒ 按原设计销毁。 // // 为什么必须延后:CPaintManagerUI::SetFocus 的顺序是 // 「先 ::SetFocus(画布窗口) 让旧编辑窗失焦 → 再派发 KILLFOCUS → 最后才把 m_pFocus 换成新控件」。 // 若在原生 WM_KILLFOCUS 里当场判断,读到的一定还是**旧值** ⇒ 区分不了 // "瞬态失焦"和"真的切到另一个控件"。 // // 这条判据对"窗口失活"同样成立 —— 而窗口失活恰恰是手指点触摸键盘时唯一会发生的变数。 LRESULT CEditWnd::OnClose(UINT /*uMsg*/, WPARAM /*wParam*/, LPARAM /*lParam*/, BOOL& bHandled) { CPaintManagerUI* pManager = (m_pOwner != NULL) ? m_pOwner->GetManager() : NULL; CControlUI* pMgrFocus = (pManager != NULL) ? pManager->GetFocus() : (CControlUI*)NULL; const bool bOwnerKeepsFocus = (pManager != NULL && pMgrFocus == m_pOwner); { CDuiString sOwnerName = (m_pOwner != NULL) ? m_pOwner->GetName() : CDuiString(_T("?")); DuiLibDiag::Log(_T("EDIT-CLOSE hwnd=%p owner=%s mgrFocus=%p ownerKeepsFocus=%d -> %s"), (void*)m_hWnd, sOwnerName.GetData(), (void*)pMgrFocus, (int)(bOwnerKeepsFocus ? 1 : 0), bOwnerKeepsFocus ? _T("取消销毁(瞬态失焦,窗口保留)") : _T("销毁")); } bHandled = TRUE; if (bOwnerKeepsFocus) { // 不销毁。焦点会由下面两条既有路径送回来,不需要在这里造新机制: // · 我们自己的 ::SetFocus(m_hWndPaint) → WM_SETFOCUS → 重发 UIEVENT_SETFOCUS // → CEditUI::DoEvent 的"复用编辑窗"分支 Show+SetFocus; // · 整个窗口重新被激活 → MessageHandler 的 WM_ACTIVATE 同理。 return 0; } ::DestroyWindow(m_hWnd); return 0; } LRESULT CEditWnd::OnEditChanged(UINT /*uMsg*/, WPARAM /*wParam*/, LPARAM /*lParam*/, BOOL& /*bHandled*/) { // 【r40 取证】EN_CHANGE 到了、却被"Init 还没走完"挡掉的情形必须留痕。 // 否则在日志里,"反射链根本没通"和"反射通了但窗口尚未初始化完"长得一模一样 // —— 而这两件事的结论完全相反(前者要查 OCM 反射,后者是正常路径)。 if( !m_bInit ) { CDuiString sOwnerName = (m_pOwner != NULL) ? m_pOwner->GetName() : CDuiString(_T("?")); DuiLibDiag::Log(_T("EDIT-CHANGE 忽略(Init未完成) hwnd=%p owner=%s len=%d"), (void*)m_hWnd, sOwnerName.GetData(), (int)::GetWindowTextLength(m_hWnd)); return 0; } if( m_pOwner == NULL ) return 0; // Copy text back int cchLen = ::GetWindowTextLength(m_hWnd) + 1; LPTSTR pstr = static_cast(_alloca(cchLen * sizeof(TCHAR))); ASSERT(pstr); if( pstr == NULL ) return 0; ::GetWindowText(m_hWnd, pstr, cchLen); m_pOwner->m_sText = pstr; { CDuiString sOwnerName = m_pOwner->GetName(); // 每一次内容变化都留痕 —— "字符进来"与"被清空"都能看到。 // 判读: // · append 之后突然出现 len=0 ⇒ 有代码把它清掉了。可能是业务侧的 // 数字过滤(首个字符非法时 SetText("")),也可能是 CEditUI::SetText // 被调用 —— 后者会在 EDITUI-SETTEXT 行留下记录; // · 只看到 append、从没出现 len=0,用户却说"字没了" // ⇒ 字符根本没进到这个窗口 ⇒ 焦点已经不在它身上(查 EDIT-KILLFOCUS)。 DuiLibDiag::Log(_T("EDIT-CHANGE hwnd=%p owner=%s len=%d text='%s'"), (void*)m_hWnd, sOwnerName.GetData(), cchLen - 1, pstr); } m_pOwner->GetManager()->SendNotify(m_pOwner, DUI_MSGTYPE_TEXTCHANGED); if( m_pOwner->GetManager()->IsLayered() ) m_pOwner->Invalidate(); return 0; } ///////////////////////////////////////////////////////////////////////////////////// // // IMPLEMENT_DUICONTROL(CEditUI) CEditUI::CEditUI() : m_pWindow(NULL), m_uMaxChar(255), m_bReadOnly(false), m_bPasswordMode(false), m_cPasswordChar(_T('*')), m_bAutoSelAll(false), m_uButtonState(0), m_dwEditbkColor(0xFFFFFFFF), m_dwEditTextColor(0x00000000), m_iWindowStyls(0),m_dwTipValueColor(0xFFBAC0C5) { SetTextPadding(CDuiRect(4, 3, 4, 3)); SetBkColor(0xFFFFFFFF); } LPCTSTR CEditUI::GetClass() const { return _T("EditUI"); } LPVOID CEditUI::GetInterface(LPCTSTR pstrName) { if( _tcsicmp(pstrName, DUI_CTR_EDIT) == 0 ) return static_cast(this); return CLabelUI::GetInterface(pstrName); } UINT CEditUI::GetControlFlags() const { if( !IsEnabled() ) return CControlUI::GetControlFlags(); return UIFLAG_SETCURSOR | UIFLAG_TABSTOP; } void CEditUI::DoEvent(TEventUI& event) { // 【诊断 · 新建商品弹框「输入文字消失」】只对焦点/点击类事件留痕,避免鼠标移动刷屏。 // // 为什么这条日志值得存在:本控件后面那个 `if( m_pWindow ) return;` 会让 // UIEVENT_SETFOCUS **提前返回**,从而走不到 CLabelUI::DoEvent 里的 // `m_bFocused = true` —— 于是 IsFocused() 与真实焦点可能脱节。 // 而下面 UIEVENT_BUTTONDOWN 的三岔口正是**用 IsFocused() 分流**的: // · IsFocused() && m_pWindow == NULL ⇒ 路 A:新建窗口 + **全选整框** // · m_pWindow != NULL ⇒ 路 B:光标落到点击处(正常点击) // · 两者都不满足 ⇒ 什么都不做(点击被静默丢弃) // m_bFocused / m_pWindow 这两个值必须能在日志里直接看到,否则无从判断走了哪条。 if (event.Type == UIEVENT_SETFOCUS || event.Type == UIEVENT_KILLFOCUS || event.Type == UIEVENT_BUTTONDOWN || event.Type == UIEVENT_BUTTONUP || event.Type == UIEVENT_TOUCHDOWN || event.Type == UIEVENT_TOUCHUP || event.Type == UIEVENT_DBLCLICK || event.Type == UIEVENT_RBUTTONDOWN) { CDuiString sName = GetName(); DuiLibDiag::Log(_T("EDITUI-EVENT type=%d owner=%s m_bFocused=%d m_pWindow=%p editHwnd=%p pt=%d,%d nFocus=%p mgrHwnd=%p"), event.Type, sName.GetData(), (int)(m_bFocused ? 1 : 0), (void*)m_pWindow, (void*)GetHWND(), event.ptMouse.x, event.ptMouse.y, (void*)::GetFocus(), (void*)(m_pManager != NULL ? m_pManager->GetPaintWindow() : (HWND)NULL)); } if( !IsMouseEnabled() && event.Type > UIEVENT__MOUSEBEGIN && event.Type < UIEVENT__MOUSEEND ) { if( m_pParent != NULL ) m_pParent->DoEvent(event); else CLabelUI::DoEvent(event); return; } if( event.Type == UIEVENT_SETCURSOR && IsEnabled() ) { ::SetCursor(::LoadCursor(NULL, MAKEINTRESOURCE(IDC_IBEAM))); return; } if( event.Type == UIEVENT_WINDOWSIZE ) { //if( m_pWindow != NULL ) m_pManager->SetFocusNeeded(this); } if( event.Type == UIEVENT_SCROLLWHEEL ) { if( m_pWindow != NULL ) return; } if( event.Type == UIEVENT_SETFOCUS && IsEnabled() ) { if( m_pWindow != NULL ) { // ★ 修复(见本文件顶部「输入文字消失」说明) // // 这里是原来的 `if( m_pWindow ) return;`。它有两个后果: // (1) 走不到 CLabelUI::DoEvent,`m_bFocused = true` 永不执行 // ⇒ IsFocused() 与真实焦点长期脱节(下面 BUTTONDOWN 曾用它分流); // (2) 编辑窗口**还在**、但键盘焦点已被系统/输入法/触摸键盘挪走时, // 没有任何代码把焦点还回去 —— 而所有按键(含退格)与输入法 // 候选词的提交都必须依赖它。这正是"切换过一次输入框后 // 连退格都输不进去"的那一格。 // // 既然 DuiLib 的焦点此刻仍指向本控件(否则收不到 SETFOCUS), // 就把显示与键盘焦点一并还给它。 HWND hEdit = *m_pWindow; if (hEdit != NULL && ::IsWindow(hEdit)) { if (!::IsWindowVisible(hEdit)) ::ShowWindow(hEdit, SW_SHOWNOACTIVATE); if (::GetFocus() != hEdit) ::SetFocus(hEdit); } { CDuiString sName = GetName(); DuiLibDiag::Log(_T("EDITUI-SETFOCUS 复用编辑窗 owner=%s editHwnd=%p nFocus=%p -> Show+SetFocus"), sName.GetData(), (void*)hEdit, (void*)::GetFocus()); } CLabelUI::DoEvent(event); // 让基类补上 m_bFocused = true Invalidate(); return; } m_pWindow = new CEditWnd(); ASSERT(m_pWindow); m_pWindow->Init(this); Invalidate(); } if( event.Type == UIEVENT_KILLFOCUS && IsEnabled() ) { Invalidate(); } if( event.Type == UIEVENT_BUTTONDOWN || event.Type == UIEVENT_DBLCLICK || event.Type == UIEVENT_RBUTTONDOWN) { if( IsEnabled() ) { GetManager()->ReleaseCapture(); // ★ 修复:判据原来还多要求一个 `IsFocused()`。但那个标志由 // CLabelUI::DoEvent 维护,而上面的 SETFOCUS 分支在"编辑窗已存在" // 时会早退、根本走不到那里 ⇒ IsFocused() 可能长期为假。 // 于是"点击一个还没有编辑窗的输入框"时**两条分支都不走**: // 点击被静默丢弃(体感就是"点了没反应")。 // 现在只看"有没有编辑窗" —— 点了输入框就一定拿得到输入能力。 if( m_pWindow == NULL ) { CDuiString sName = GetName(); DuiLibDiag::Log(_T("EDITUI-BUTTONDOWN 无编辑窗 -> 新建并全选 owner=%s m_bFocused=%d"), sName.GetData(), (int)(m_bFocused ? 1 : 0)); m_pWindow = new CEditWnd(); ASSERT(m_pWindow); m_pWindow->Init(this); if( PtInRect(&m_rcItem, event.ptMouse) ) { int nSize = GetWindowTextLength(*m_pWindow); if( nSize == 0 ) nSize = 1; Edit_SetSel(*m_pWindow, 0, nSize); } } else { CDuiString sName = GetName(); DuiLibDiag::Log(_T("EDITUI-BUTTONDOWN 有编辑窗 -> 光标落到点击处 owner=%s pt=%d,%d autoSelAll=%d"), sName.GetData(), event.ptMouse.x, event.ptMouse.y, (int)(m_bAutoSelAll ? 1 : 0)); if (!m_bAutoSelAll) { RECT rcTextPadding = GetTextPadding(); POINT pt = event.ptMouse; pt.x -= m_rcItem.left + rcTextPadding.left; pt.y -= m_rcItem.top + rcTextPadding.top; Edit_SetSel(*m_pWindow, 0, 0); ::SendMessage(*m_pWindow, WM_LBUTTONDOWN, event.wParam, MAKELPARAM(pt.x, pt.y)); // ========================================================== // ★ r40 修复:**这条鼠标按下消息必须配平**(补发配对的抬起)。 // // 原生 EDIT 收到 WM_LBUTTONDOWN 后会进入"按下拖选"状态 // (内部 bCaptureState = TRUE,并 SetCapture 自己), // 只有收到**配对的 WM_LBUTTONUP** 才会退出这个状态。 // // 而历史上这里只发了按下:本控件的 UIEVENT_BUTTONUP 在下面被 // `return;` 直接丢掉、从不下传。于是编辑框长期停在"鼠标按着" // 的状态里 —— 而这正是**触摸点击专有**的路径: // · 鼠标点击:落点在原生编辑窗自己身上,DOWN/UP 由系统成对 // 投递 ⇒ 状态干净 ⇒ 一切正常; // · 手指点击:指针消息只到画布窗口,由 DuiLib 合成这条 DOWN // ⇒ 半截手势 ⇒ 之后编辑框行为异常(真机表现就是"手指点 // 键盘打不进字、退格也删不掉,鼠标点同一个键盘却正常")。 // 坐标完全相同 ⇒ 光标仍精确落在点击处,与一次普通点击等价。 // ========================================================== ::SendMessage(*m_pWindow, WM_LBUTTONUP, 0, MAKELPARAM(pt.x, pt.y)); } } } return; } if( event.Type == UIEVENT_MOUSEMOVE ) { return; } if( event.Type == UIEVENT_BUTTONUP ) { return; } if( event.Type == UIEVENT_CONTEXTMENU ) { return; } if( event.Type == UIEVENT_MOUSEENTER ) { if( ::PtInRect(&m_rcItem, event.ptMouse ) ) { if( IsEnabled() ) { if( (m_uButtonState & UISTATE_HOT) == 0 ) { m_uButtonState |= UISTATE_HOT; Invalidate(); } } } } if( event.Type == UIEVENT_MOUSELEAVE ) { if( IsEnabled() ) { m_uButtonState &= ~UISTATE_HOT; Invalidate(); } return; } CLabelUI::DoEvent(event); } void CEditUI::SetEnabled(bool bEnable) { CControlUI::SetEnabled(bEnable); if( !IsEnabled() ) { m_uButtonState = 0; } } void CEditUI::SetText(LPCTSTR pstrText) { { CDuiString sName = GetName(); // 谁在改文本、改成什么。业务侧的数字过滤在"首个字符非法"时会 // SetText("") —— 那会把整框内容清空(正是"输入完被自动删除"的一种表现)。 // 注意:本函数无论编辑窗在不在都会执行,m_pWindow=NULL 时只改 m_sText, // 那个新值要等下次 CEditWnd::Init 才会真正写进 EDIT。 DuiLibDiag::Log(_T("EDITUI-SETTEXT owner=%s m_pWindow=%p old='%s' new='%s'"), sName.GetData(), (void*)m_pWindow, m_sText.GetData(), pstrText != NULL ? pstrText : _T("")); } m_sText = pstrText; if( m_pWindow != NULL ) Edit_SetText(*m_pWindow, m_sText); Invalidate(); } void CEditUI::SetMaxChar(UINT uMax) { m_uMaxChar = uMax; if( m_pWindow != NULL ) Edit_LimitText(*m_pWindow, m_uMaxChar); } UINT CEditUI::GetMaxChar() { return m_uMaxChar; } void CEditUI::SetReadOnly(bool bReadOnly) { if( m_bReadOnly == bReadOnly ) return; m_bReadOnly = bReadOnly; if( m_pWindow != NULL ) Edit_SetReadOnly(*m_pWindow, m_bReadOnly); Invalidate(); } bool CEditUI::IsReadOnly() const { return m_bReadOnly; } void CEditUI::SetNumberOnly(bool bNumberOnly) { if( bNumberOnly ) { m_iWindowStyls |= ES_NUMBER; } else { m_iWindowStyls &= ~ES_NUMBER; } } bool CEditUI::IsNumberOnly() const { return (m_iWindowStyls & ES_NUMBER) ? true:false; } int CEditUI::GetWindowStyls() const { return m_iWindowStyls; } void CEditUI::SetPasswordMode(bool bPasswordMode) { if( m_bPasswordMode == bPasswordMode ) return; m_bPasswordMode = bPasswordMode; Invalidate(); if( m_pWindow != NULL ) { LONG styleValue = ::GetWindowLong(*m_pWindow, GWL_STYLE); bPasswordMode ? styleValue |= ES_PASSWORD : styleValue &= ~ES_PASSWORD; ::SetWindowLong(*m_pWindow, GWL_STYLE, styleValue); } } bool CEditUI::IsPasswordMode() const { return m_bPasswordMode; } void CEditUI::SetPasswordChar(TCHAR cPasswordChar) { if( m_cPasswordChar == cPasswordChar ) return; m_cPasswordChar = cPasswordChar; if( m_pWindow != NULL ) Edit_SetPasswordChar(*m_pWindow, m_cPasswordChar); Invalidate(); } TCHAR CEditUI::GetPasswordChar() const { return m_cPasswordChar; } LPCTSTR CEditUI::GetNormalImage() { return m_sNormalImage; } void CEditUI::SetNormalImage(LPCTSTR pStrImage) { m_sNormalImage = pStrImage; Invalidate(); } LPCTSTR CEditUI::GetHotImage() { return m_sHotImage; } void CEditUI::SetHotImage(LPCTSTR pStrImage) { m_sHotImage = pStrImage; Invalidate(); } LPCTSTR CEditUI::GetFocusedImage() { return m_sFocusedImage; } void CEditUI::SetFocusedImage(LPCTSTR pStrImage) { m_sFocusedImage = pStrImage; Invalidate(); } LPCTSTR CEditUI::GetDisabledImage() { return m_sDisabledImage; } void CEditUI::SetDisabledImage(LPCTSTR pStrImage) { m_sDisabledImage = pStrImage; Invalidate(); } void CEditUI::SetNativeEditBkColor(DWORD dwBkColor) { m_dwEditbkColor = dwBkColor; } DWORD CEditUI::GetNativeEditBkColor() const { return m_dwEditbkColor; } void CEditUI::SetNativeEditTextColor( LPCTSTR pStrColor ) { if( *pStrColor == _T('#')) pStrColor = ::CharNext(pStrColor); LPTSTR pstr = NULL; DWORD clrColor = _tcstoul(pStrColor, &pstr, 16); m_dwEditTextColor = clrColor; } DWORD CEditUI::GetNativeEditTextColor() const { return m_dwEditTextColor; } bool CEditUI::IsAutoSelAll() { return m_bAutoSelAll; } void CEditUI::SetAutoSelAll(bool bAutoSelAll) { m_bAutoSelAll = bAutoSelAll; } void CEditUI::SetSel(long nStartChar, long nEndChar) { if( m_pWindow != NULL ) Edit_SetSel(*m_pWindow, nStartChar,nEndChar); } void CEditUI::SetSelAll() { SetSel(0,-1); } void CEditUI::SetReplaceSel(LPCTSTR lpszReplace) { if( m_pWindow != NULL ) Edit_ReplaceSel(*m_pWindow, lpszReplace); } void CEditUI::SetTipValue( LPCTSTR pStrTipValue ) { m_sTipValue = pStrTipValue; } LPCTSTR CEditUI::GetTipValue() { if (!IsResourceText()) return m_sTipValue; return CResourceManager::GetInstance()->GetText(m_sTipValue); } void CEditUI::SetTipValueColor( LPCTSTR pStrColor ) { if( *pStrColor == _T('#')) pStrColor = ::CharNext(pStrColor); LPTSTR pstr = NULL; DWORD clrColor = _tcstoul(pStrColor, &pstr, 16); m_dwTipValueColor = clrColor; } DWORD CEditUI::GetTipValueColor() { return m_dwTipValueColor; } HWND CEditUI::GetHWND() { if(m_pWindow != NULL) { return m_pWindow->GetHWND(); } return NULL; } // 【分层模式】把浮层原生编辑窗重新对齐到本控件所在位置。 // // 顶层浮窗(WS_POPUP)**不会**随宿主窗口移动 —— owner 关系只保证 z 序。 // 所以宿主窗口每次 WM_MOVE / WM_WINDOWPOSCHANGED 都要来这里补位, // 由 CPaintManagerUI::MessageHandler 遍历所有 EditUI 调用(见 UIManager.cpp)。 // 非分层模式本函数是空操作(那时 EditWnd 是 WS_CHILD,跟着父窗口走)。 void CEditUI::SyncNativeWindowPos() { if( m_pWindow == NULL || m_pWindow->GetHWND() == NULL ) return; RECT rcPos = m_pWindow->CalPos(); // 已在位就不要 SetWindowPos —— 否则每次 WM_WINDOWPOSCHANGED 都白刷一次, // 拖动时会看到输入框闪。 RECT rcCur = { 0 }; ::GetWindowRect(m_pWindow->GetHWND(), &rcCur); if( rcPos.left == rcCur.left && rcPos.top == rcCur.top && rcPos.right == rcCur.right && rcPos.bottom == rcCur.bottom ) return; ::SetWindowPos(m_pWindow->GetHWND(), NULL, rcPos.left, rcPos.top, rcPos.right - rcPos.left, rcPos.bottom - rcPos.top, SWP_NOZORDER | SWP_NOACTIVATE); } void CEditUI::SetPos(RECT rc, bool bNeedInvalidate) { CControlUI::SetPos(rc, bNeedInvalidate); if( m_pWindow != NULL ) { RECT rcPos = m_pWindow->CalPos(); ::SetWindowPos(m_pWindow->GetHWND(), NULL, rcPos.left, rcPos.top, rcPos.right - rcPos.left, rcPos.bottom - rcPos.top, SWP_NOZORDER | SWP_NOACTIVATE); } } void CEditUI::Move(SIZE szOffset, bool bNeedInvalidate) { CControlUI::Move(szOffset, bNeedInvalidate); if( m_pWindow != NULL ) { RECT rcPos = m_pWindow->CalPos(); ::SetWindowPos(m_pWindow->GetHWND(), NULL, rcPos.left, rcPos.top, rcPos.right - rcPos.left, rcPos.bottom - rcPos.top, SWP_NOSIZE | SWP_NOZORDER | SWP_NOACTIVATE); } } void CEditUI::SetVisible(bool bVisible) { CControlUI::SetVisible(bVisible); if( !IsVisible() && m_pWindow != NULL ) m_pManager->SetFocus(NULL); } void CEditUI::SetInternVisible(bool bVisible) { if( !IsVisible() && m_pWindow != NULL ) m_pManager->SetFocus(NULL); } SIZE CEditUI::EstimateSize(SIZE szAvailable) { if( m_cxyFixed.cy == 0 ) return CDuiSize(m_cxyFixed.cx, m_pManager->GetFontInfo(GetFont())->tm.tmHeight + 6); return CControlUI::EstimateSize(szAvailable); } void CEditUI::SetAttribute(LPCTSTR pstrName, LPCTSTR pstrValue) { if( _tcsicmp(pstrName, _T("readonly")) == 0 ) SetReadOnly(_tcsicmp(pstrValue, _T("true")) == 0); else if( _tcsicmp(pstrName, _T("numberonly")) == 0 ) SetNumberOnly(_tcsicmp(pstrValue, _T("true")) == 0); else if( _tcscmp(pstrName, _T("autoselall")) == 0 ) SetAutoSelAll(_tcscmp(pstrValue, _T("true")) == 0); else if( _tcsicmp(pstrName, _T("password")) == 0 ) SetPasswordMode(_tcsicmp(pstrValue, _T("true")) == 0); else if( _tcsicmp(pstrName, _T("passwordchar")) == 0 ) SetPasswordChar(*pstrValue); else if( _tcsicmp(pstrName, _T("maxchar")) == 0 ) SetMaxChar(_ttoi(pstrValue)); else if( _tcsicmp(pstrName, _T("normalimage")) == 0 ) SetNormalImage(pstrValue); else if( _tcsicmp(pstrName, _T("hotimage")) == 0 ) SetHotImage(pstrValue); else if( _tcsicmp(pstrName, _T("focusedimage")) == 0 ) SetFocusedImage(pstrValue); else if( _tcsicmp(pstrName, _T("disabledimage")) == 0 ) SetDisabledImage(pstrValue); else if( _tcsicmp(pstrName, _T("tipvalue")) == 0 ) SetTipValue(pstrValue); else if( _tcsicmp(pstrName, _T("tipvaluecolor")) == 0 ) SetTipValueColor(pstrValue); else if( _tcsicmp(pstrName, _T("nativetextcolor")) == 0 ) SetNativeEditTextColor(pstrValue); else if( _tcsicmp(pstrName, _T("nativebkcolor")) == 0 ) { if( *pstrValue == _T('#')) pstrValue = ::CharNext(pstrValue); LPTSTR pstr = NULL; DWORD clrColor = _tcstoul(pstrValue, &pstr, 16); SetNativeEditBkColor(clrColor); } else CLabelUI::SetAttribute(pstrName, pstrValue); } void CEditUI::PaintStatusImage(HDC hDC) { if( IsFocused() ) m_uButtonState |= UISTATE_FOCUSED; else m_uButtonState &= ~ UISTATE_FOCUSED; if( !IsEnabled() ) m_uButtonState |= UISTATE_DISABLED; else m_uButtonState &= ~ UISTATE_DISABLED; if( (m_uButtonState & UISTATE_DISABLED) != 0 ) { if( !m_sDisabledImage.IsEmpty() ) { if( !DrawImage(hDC, (LPCTSTR)m_sDisabledImage) ) {} else return; } } else if( (m_uButtonState & UISTATE_FOCUSED) != 0 ) { if( !m_sFocusedImage.IsEmpty() ) { if( !DrawImage(hDC, (LPCTSTR)m_sFocusedImage) ) {} else return; } } else if( (m_uButtonState & UISTATE_HOT) != 0 ) { if( !m_sHotImage.IsEmpty() ) { if( !DrawImage(hDC, (LPCTSTR)m_sHotImage) ) {} else return; } } if( !m_sNormalImage.IsEmpty() ) { if( !DrawImage(hDC, (LPCTSTR)m_sNormalImage) ) {} else return; } } void CEditUI::PaintText(HDC hDC) { DWORD mCurTextColor = m_dwTextColor; if( m_dwTextColor == 0 ) mCurTextColor = m_dwTextColor = m_pManager->GetDefaultFontColor(); if( m_dwDisabledTextColor == 0 ) m_dwDisabledTextColor = m_pManager->GetDefaultDisabledColor(); CDuiString sDrawText = GetText(); CDuiString sTipValue = GetTipValue(); if(sDrawText == sTipValue || sDrawText == _T("")) { mCurTextColor = m_dwTipValueColor; sDrawText = sTipValue; } else { CDuiString sTemp = sDrawText; if( m_bPasswordMode ) { sDrawText.Empty(); LPCTSTR pStr = sTemp.GetData(); while( *pStr != _T('\0') ) { sDrawText += m_cPasswordChar; pStr = ::CharNext(pStr); } } } RECT rcTextPadding = GetTextPadding(); RECT rc = m_rcItem; rc.left += rcTextPadding.left; rc.right -= rcTextPadding.right; rc.top += rcTextPadding.top; rc.bottom -= rcTextPadding.bottom; if( IsEnabled() ) { CRenderEngine::DrawText(hDC, m_pManager, rc, sDrawText, mCurTextColor, \ m_iFont, DT_SINGLELINE | m_uTextStyle); } else { CRenderEngine::DrawText(hDC, m_pManager, rc, sDrawText, m_dwDisabledTextColor, \ m_iFont, DT_SINGLELINE | m_uTextStyle); } } }