UIEdit.cpp 60 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415
  1. #include "StdAfx.h"
  2. #include "UIEdit.h"
  3. #include "../Utils/UIDiag.h"
  4. namespace DuiLib
  5. {
  6. // ==================================================================
  7. // 【诊断 · 新建商品弹框「输入文字消失」(Win10 触摸键盘)】
  8. //
  9. // 现象:手指点触摸键盘输入,已输入的字(尤其中文输入的组合串)会消失;
  10. // 鼠标点同一个键盘则完全正常。
  11. //
  12. // 已确认的代码事实(不用日志就能确定的):
  13. // · 文本**真实存放**在 CEditWnd —— 一个原生的 EDIT 子窗口里(WS_CHILD),
  14. // DuiLib 只通过 EN_CHANGE 把内容同步回 CEditUI::m_sText;
  15. // · 该窗口的生命周期**极短**:只要它失去键盘焦点,
  16. // CEditWnd::OnKillFocus 就会 PostMessage(WM_CLOSE) **把它销毁**;
  17. // · 所以"谁在这一刻抢走了焦点"就是本问题的全部答案。
  18. // 一旦焦点离开 EDIT,输入法正在拼的字会被撤销(中文尤其明显),
  19. // 之后到达的字符也无处可去 —— 用户看到的就是"输入完被自动删除"。
  20. //
  21. // 因此下面这几个 helper 只做一件事:把"抢焦点的是谁"记清楚。
  22. // 判读要点:
  23. // · 若 EDIT-KILLFOCUS 的 newTid != selfTid ⇒ 焦点被**别的进程/线程**抢走
  24. // (触摸键盘 TabTip / TextInputHost 是首要嫌疑);
  25. // · newClass 是 "#32770" ⇒ 被我们自己的弹框拿走了;
  26. // · 若同一个 owner 出现 INIT → FIN → INIT 的抖动 ⇒ 窗口在反复重建,
  27. // 重建时用的是 m_sText(可能落后于 IME 的组合状态)⇒ 字就"没了"。
  28. // ==================================================================
  29. static LPCTSTR DiagWndClass(HWND h)
  30. {
  31. static TCHAR s_cls[128];
  32. s_cls[0] = _T('\0');
  33. if (h != NULL && ::IsWindow(h)) ::GetClassName(h, s_cls, _countof(s_cls));
  34. return s_cls;
  35. }
  36. static DWORD DiagWndTid(HWND h)
  37. {
  38. DWORD dwTid = 0;
  39. if (h != NULL && ::IsWindow(h)) dwTid = ::GetWindowThreadProcessId(h, NULL);
  40. return dwTid;
  41. }
  42. // r37 新增:抽"前台窗口属于哪个进程" —— 用来把两种"焦点空了"分开:
  43. // 前台还是本进程 ⇒ 只是键盘焦点被抢(可以且应该抢回来);
  44. // 前台已经是别的进程 ⇒ 用户主动切走了应用(绝不能抢)。
  45. static DWORD DiagWndPid(HWND h)
  46. {
  47. DWORD dwPid = 0;
  48. if (h != NULL && ::IsWindow(h)) ::GetWindowThreadProcessId(h, &dwPid);
  49. return dwPid;
  50. }
  51. // 前向声明:DiagFgIsTouchKb 要用它做进程名白名单,实现放下面(见 DiagProcName)。
  52. static void DiagProcName(HWND h, LPTSTR szOut, int cch);
  53. // r38 新增:把"前台窗口是不是触摸键盘"这一件事判死 —— 本轮唯一需要保护的判据。
  54. //
  55. // Win10 的现代触摸键盘由 TextInputHost.exe 承载,窗口类就是
  56. // `Windows.UI.Core.CoreWindow`(真机日志实证:fgClass=Windows.UI.Core.CoreWindow
  57. // fgPid=7012 fgTid=7080);老式 TabTip 的宿主类名是 `IPTip_Main_Window`。两个都收。
  58. // 只有命中这两个才认为"前台是被触摸键盘拿走的",其余一律返回 false
  59. // ⇒ 用户主动切到别的应用时**绝不抢**,正常多任务完全不受影响。
  60. static bool DiagFgIsTouchKb(HWND h)
  61. {
  62. if (h == NULL || !::IsWindow(h)) return false;
  63. TCHAR szCls[128] = { 0 };
  64. ::GetClassName(h, szCls, _countof(szCls));
  65. if (_tcsicmp(szCls, _T("Windows.UI.Core.CoreWindow")) != 0
  66. && _tcsicmp(szCls, _T("IPTip_Main_Window")) != 0)
  67. {
  68. return false; // 类名不对 ⇒ 与触摸键盘无关(例如用户点了桌面/别的应用)
  69. }
  70. // 类名只是必要条件 —— `Windows.UI.Core.CoreWindow` 是**所有 UWP 应用**共用的类名,
  71. // 单凭它会把"用户切到一个 UWP 应用"也误判成"键盘夺前台"。所以再用进程名判死。
  72. // 进程名取不到("?")时**放行**:取不到是极少数情况,而不抢的代价是用户按键继续
  73. // 没有落点(功能直接坏掉),比偶尔打断一次切换严重得多。
  74. TCHAR szExe[128] = { 0 };
  75. DiagProcName(h, szExe, _countof(szExe));
  76. if (szExe[0] == _T('?') || szExe[0] == _T('\0')) return true;
  77. return (_tcsicmp(szExe, _T("TextInputHost.exe")) == 0
  78. || _tcsicmp(szExe, _T("TabTip.exe")) == 0
  79. || _tcsicmp(szExe, _T("InputApp.exe")) == 0);
  80. }
  81. // r38 新增:取窗口所属进程的**可执行文件名**(只要文件名,不要全路径)。
  82. //
  83. // 为什么不直接调 QueryFullProcessImageName:它受 `#if(_WIN32_WINNT >= 0x0600)`
  84. // 保护,而本工程(DuiLib_Static)的 WINVER 兜底较低(同上面 WS_EX_NOACTIVATE 那次)
  85. // ⇒ 走 GetProcAddress 动态取,零版本依赖、不新增 lib、不改工程设置。
  86. //
  87. // 用途(本轮的判据靠它才判得死):
  88. // `Windows.UI.Core.CoreWindow` 这个类名**不是触摸键盘专有** —— 所有 UWP 应用都用它。
  89. // 只靠类名,用户切到一个 UWP 应用时我们也会误判成"键盘夺前台"。补上进程名
  90. // (TextInputHost.exe / TabTip.exe / InputApp.exe)才能真正把两种情况分开。
  91. // 取不到就返回 "?",**绝不影响任何行为**(只写日志)。
  92. static void DiagProcName(HWND h, LPTSTR szOut, int cch)
  93. {
  94. typedef BOOL (WINAPI *PFN_QFPE)(HANDLE, DWORD, LPWSTR, PDWORD);
  95. if (szOut == NULL || cch <= 0) return;
  96. szOut[0] = _T('\0');
  97. if (h == NULL || !::IsWindow(h)) return;
  98. DWORD dwPid = 0;
  99. ::GetWindowThreadProcessId(h, &dwPid);
  100. if (dwPid == 0) return;
  101. HMODULE hK32 = ::GetModuleHandle(_T("kernel32.dll"));
  102. PFN_QFPE pfn = (hK32 != NULL) ? (PFN_QFPE)::GetProcAddress(hK32, "QueryFullProcessImageNameW") : NULL;
  103. if (pfn == NULL) { szOut[0] = _T('?'); szOut[1] = _T('\0'); return; }
  104. // 0x1000 = PROCESS_QUERY_LIMITED_INFORMATION(Vista+;字面量以避开 SDK 版本宏)
  105. HANDLE hProc = ::OpenProcess(0x1000, FALSE, dwPid);
  106. if (hProc == NULL) { szOut[0] = _T('?'); szOut[1] = _T('\0'); return; }
  107. WCHAR szFull[512] = { 0 };
  108. DWORD cchLen = _countof(szFull) - 1;
  109. const BOOL bOk = pfn(hProc, 0, szFull, &cchLen);
  110. ::CloseHandle(hProc);
  111. if (!bOk) { szOut[0] = _T('?'); szOut[1] = _T('\0'); return; }
  112. LPCTSTR pSrc = NULL;
  113. #ifdef UNICODE
  114. pSrc = szFull;
  115. #else
  116. char szAnsi[512] = { 0 };
  117. ::WideCharToMultiByte(CP_ACP, 0, szFull, -1, szAnsi, _countof(szAnsi), NULL, NULL);
  118. pSrc = szAnsi;
  119. #endif
  120. // 只留文件名(手工截取,不依赖 CRT 的 _tcsrchr / 重叠拷贝语义)
  121. int nLast = -1;
  122. for (int i = 0; pSrc[i] != _T('\0'); ++i) { if (pSrc[i] == _T('\\')) nLast = i; }
  123. int i = 0;
  124. for (int k = (nLast >= 0 ? nLast + 1 : 0); pSrc[k] != _T('\0') && i < cch - 1; ++i, ++k) szOut[i] = pSrc[k];
  125. szOut[i] = _T('\0');
  126. }
  127. // 调 CPaintManagerUI 的"最近一次输入是不是触摸"的判据(见 UIManager.h)。
  128. // 只用于日志标注,不参与任何判断。
  129. class CEditWnd : public CWindowWnd
  130. {
  131. public:
  132. CEditWnd();
  133. void Init(CEditUI* pOwner);
  134. RECT CalPos();
  135. LPCTSTR GetWindowClassName() const;
  136. LPCTSTR GetSuperClassName() const;
  137. void OnFinalMessage(HWND hWnd);
  138. LRESULT HandleMessage(UINT uMsg, WPARAM wParam, LPARAM lParam);
  139. LRESULT OnKillFocus(UINT uMsg, WPARAM wParam, LPARAM lParam, BOOL& bHandled);
  140. LRESULT OnClose(UINT uMsg, WPARAM wParam, LPARAM lParam, BOOL& bHandled);
  141. LRESULT OnEditChanged(UINT uMsg, WPARAM wParam, LPARAM lParam, BOOL& bHandled);
  142. protected:
  143. enum {
  144. DEFAULT_TIMERID = 20,
  145. };
  146. CEditUI* m_pOwner;
  147. HBRUSH m_hBkBrush;
  148. bool m_bInit;
  149. bool m_bDrawCaret;
  150. // 【分层模式】本窗建成**独立顶层 WS_POPUP**(见 Init)时为 true。
  151. // 顶层窗口的 SetWindowPos 用**屏幕坐标**,而 CalPos() 算出来的是
  152. // 「相对宿主窗口客户区」的坐标 ⇒ 换算必须由 CalPos() 自己补,
  153. // 否则 Init 补一次、之后的 SetPos/Move(控件重排、拖动宿主窗口)就漏补,
  154. // 浮窗会停在旧位置 —— 就是「拖弹框时输入框不跟着走」的残影。
  155. bool m_bTopLevelWnd;
  156. };
  157. CEditWnd::CEditWnd() : m_pOwner(NULL), m_hBkBrush(NULL), m_bInit(false), m_bDrawCaret(false), m_bTopLevelWnd(false)
  158. {
  159. }
  160. // WS_EX_NOACTIVATE 在 winuser.h 里受 #if(WINVER >= 0x0500) 保护;
  161. // DuiLib_Static 工程的 WINVER 兜底较低,补一个兜底宏以防万一。
  162. #ifndef WS_EX_NOACTIVATE
  163. #define WS_EX_NOACTIVATE 0x08000000L
  164. #endif
  165. void CEditWnd::Init(CEditUI* pOwner)
  166. {
  167. m_pOwner = pOwner;
  168. {
  169. CDuiString sOwnerName = pOwner->GetName();
  170. LPCTSTR pszOwnerText = pOwner->GetText();
  171. DuiLibDiag::Log(_T("EDIT-INIT enter owner=%s focused=%d ownerHwnd(old)=%p text='%s' nFocus=%p focusClass=%s focusTid=%u selfTid=%u"),
  172. sOwnerName.GetData(), (int)(pOwner->IsFocused() ? 1 : 0), (void*)pOwner->GetHWND(),
  173. pszOwnerText != NULL ? pszOwnerText : _T(""),
  174. (void*)::GetFocus(), DiagWndClass(::GetFocus()), (unsigned)DiagWndTid(::GetFocus()),
  175. (unsigned)::GetCurrentThreadId());
  176. }
  177. RECT rcPos = CalPos();
  178. UINT uStyle = 0;
  179. DWORD dwExStyle = 0;
  180. if(m_pOwner->GetManager()->IsLayered()) {
  181. // ⚠️【必须 NOACTIVATE】分层模式下本编辑窗是**独立顶层 WS_POPUP**
  182. // (分层窗口不渲染子窗口,只能靠浮层把它画出来)。
  183. // 独立顶层窗口一旦被 SetFocus,系统会把**线程的活动窗口**改成它
  184. // ⇒ 弹框本体收到 WM_ACTIVATE(WA_INACTIVE) ⇒ 每次点击都要重新激活
  185. // ⇒ Windows 判定激活被撤销,**吞掉 WM_LBUTTONDOWN**(只剩 WM_LBUTTONUP)
  186. // ⇒ 弹框里所有按钮和关闭全部点不动。
  187. // ⚠️ 2026-09-23 实测**证伪**:WS_EX_NOACTIVATE 拦不住 SetFocus 改「线程活动窗口」
  188. // —— 真机日志里 SetFocus 本窗之后,宿主弹框照样收到 WM_ACTIVATE(WA_INACTIVE)。
  189. // 真正的止血点在 UIManager.cpp MessageHandler 的 case WM_MOUSEACTIVATE
  190. // (回 MA_NOACTIVATE,判据见 ZpActivationWillBeRevoked):
  191. // 既然「激活 → 被本窗抢回」必然发生,就干脆别激活,系统便会照常投递鼠标消息。
  192. // 本窗保留 WS_EX_NOACTIVATE 的收益只剩:不被**用户点击**激活、不上任务栏。
  193. // WS_EX_TOOLWINDOW:不进任务栏、不参与 Alt+Tab。
  194. // 非分层时本窗是 WS_CHILD(属于弹框),天然没有这个问题。
  195. uStyle = WS_POPUP | ES_AUTOHSCROLL | WS_VISIBLE;
  196. dwExStyle = WS_EX_NOACTIVATE | WS_EX_TOOLWINDOW;
  197. // ★ 屏幕坐标换算集中到 CalPos()(见那里的注释)。
  198. // 原先在这里手工补了一次,但 SetPos/Move 两条路径没补 ⇒ 一旦控件重排,
  199. // 浮窗会被摆到屏幕左上角附近;拖动宿主窗口时更是完全不动。
  200. // ⚠️ 标量成员要在这里置位,再重新调用 CalPos() 取屏幕坐标。
  201. m_bTopLevelWnd = true;
  202. rcPos = CalPos();
  203. }
  204. else {
  205. uStyle = WS_CHILD | ES_AUTOHSCROLL;
  206. // 防御:熔断(DisableLayeredRender)后 IsLayered() 会变假 ⇒ 同一个 CEditWnd
  207. // 对象再次 Init 会落到本分支;标志若还停在 true,CalPos() 会多补一次
  208. // 屏幕原点,子窗口就被摆错了。
  209. m_bTopLevelWnd = false;
  210. }
  211. UINT uTextStyle = m_pOwner->GetTextStyle();
  212. if(uTextStyle & DT_LEFT) uStyle |= ES_LEFT;
  213. else if(uTextStyle & DT_CENTER) uStyle |= ES_CENTER;
  214. else if(uTextStyle & DT_RIGHT) uStyle |= ES_RIGHT;
  215. if( m_pOwner->IsPasswordMode() ) uStyle |= ES_PASSWORD;
  216. Create(m_pOwner->GetManager()->GetPaintWindow(), NULL, uStyle, dwExStyle, rcPos);
  217. HFONT hFont=NULL;
  218. int iFontIndex=m_pOwner->GetFont();
  219. if (iFontIndex!=-1)
  220. hFont = m_pOwner->GetManager()->GetFont(iFontIndex);
  221. if (hFont == NULL)
  222. hFont = m_pOwner->GetManager()->GetDefaultFontInfo()->hFont;
  223. SetWindowFont(m_hWnd, hFont, TRUE);
  224. Edit_LimitText(m_hWnd, m_pOwner->GetMaxChar());
  225. if( m_pOwner->IsPasswordMode() ) Edit_SetPasswordChar(m_hWnd, m_pOwner->GetPasswordChar());
  226. Edit_SetText(m_hWnd, m_pOwner->GetText());
  227. Edit_SetModify(m_hWnd, FALSE);
  228. SendMessage(EM_SETMARGINS, EC_LEFTMARGIN | EC_RIGHTMARGIN, MAKELPARAM(0, 0));
  229. Edit_Enable(m_hWnd, m_pOwner->IsEnabled() == true);
  230. Edit_SetReadOnly(m_hWnd, m_pOwner->IsReadOnly() == true);
  231. //Styls
  232. LONG styleValue = ::GetWindowLong(m_hWnd, GWL_STYLE);
  233. styleValue |= pOwner->GetWindowStyls();
  234. ::SetWindowLong(GetHWND(), GWL_STYLE, styleValue);
  235. //Styls
  236. ::ShowWindow(m_hWnd, SW_SHOWNOACTIVATE);
  237. ::SetFocus(m_hWnd);
  238. if (m_pOwner->IsAutoSelAll()) {
  239. int nSize = GetWindowTextLength(m_hWnd);
  240. if( nSize == 0 ) nSize = 1;
  241. Edit_SetSel(m_hWnd, 0, nSize);
  242. }
  243. else {
  244. int nSize = GetWindowTextLength(m_hWnd);
  245. Edit_SetSel(m_hWnd, nSize, nSize);
  246. }
  247. m_bInit = true;
  248. {
  249. CDuiString sOwnerName = m_pOwner->GetName();
  250. // nFocus 应当就是本窗口;若不等,说明有更晚的 SetFocus 把它抢走了。
  251. // sel=[start,end] 是设给 EDIT 的光标/选择范围:
  252. // 路 B(正常点击)是 [点击处, 点击处];路 A(已聚焦但窗口被销毁后重建)是 [0, 全文] ⇒ 全选。
  253. DWORD dwSelStart = 0, dwSelEnd = 0;
  254. ::SendMessage(m_hWnd, EM_GETSEL, (WPARAM)&dwSelStart, (LPARAM)&dwSelEnd);
  255. DuiLibDiag::Log(_T("EDIT-INIT done owner=%s hwnd=%p nFocus=%p focusIsSelf=%d sel=[%u,%u] text='%s'"),
  256. sOwnerName.GetData(), (void*)m_hWnd, (void*)::GetFocus(),
  257. (int)(::GetFocus() == m_hWnd ? 1 : 0),
  258. (unsigned)LOWORD(dwSelStart), (unsigned)LOWORD(dwSelEnd), m_pOwner->GetText());
  259. }
  260. // ==================================================================
  261. // 【r40 取证 · 只写日志,不参与任何判据】把"编辑窗此刻到底能不能编辑"钉死。
  262. //
  263. // 为什么需要这一行:r39 的 EDIT-KEYMSG 已经证明**按键确实到达了本窗口**
  264. // (focusIsSelf=1、WM_KEYDOWN/WM_CHAR 都有),可编辑框就是不进字。
  265. // 剩下的可能只有两类,必须一刀切开:
  266. // ① **本窗口拒绝这个字符** —— 只读 / 被禁用 / 文本长度上限已满;
  267. // ② **本窗口不可见或被摆到了屏幕外** —— 字其实进去了,但用户看不见,
  268. // 而 EN_CHANGE 反射回 CEditUI::m_sText 的那条链又没走通
  269. // ⇒ DuiLib 自绘的框当然是空的。
  270. // 字段分工:
  271. // ro / en / limit ⇒ 切开 ①; vis / rect ⇒ 切开 ②;
  272. // len 是基准值(和 EDIT-KEYAFTER 的 len 一比就知道字符有没有生效);
  273. // unicode / oldProc ⇒ 万一 CallWindowProc 的目标过程不对,这里能看出来。
  274. // ==================================================================
  275. {
  276. const LONG lStyleNow = ::GetWindowLong(m_hWnd, GWL_STYLE);
  277. RECT rcWndNow = { 0 };
  278. ::GetWindowRect(m_hWnd, &rcWndNow);
  279. HWND hParNow = ::GetParent(m_hWnd);
  280. CDuiString sOwnerName = m_pOwner->GetName();
  281. 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"),
  282. sOwnerName.GetData(), (void*)m_hWnd,
  283. (int)((lStyleNow & 0x0800L) ? 1 : 0), // 0x0800 = ES_READONLY
  284. (int)(::IsWindowEnabled(m_hWnd) ? 1 : 0),
  285. (int)(::IsWindowVisible(m_hWnd) ? 1 : 0),
  286. (unsigned)::SendMessage(m_hWnd, 0x00D5, 0, 0), // 0x00D5 = EM_GETLIMITTEXT(字面量以避开 SDK 版本宏)
  287. (int)::GetWindowTextLength(m_hWnd), (unsigned)lStyleNow,
  288. (int)rcWndNow.left, (int)rcWndNow.top, (int)rcWndNow.right, (int)rcWndNow.bottom,
  289. (void*)hParNow, DiagWndClass(hParNow),
  290. (int)(::IsWindowUnicode(m_hWnd) ? 1 : 0), (void*)m_OldWndProc);
  291. }
  292. }
  293. RECT CEditWnd::CalPos()
  294. {
  295. CDuiRect rcPos = m_pOwner->GetPos();
  296. RECT rcInset = m_pOwner->GetTextPadding();
  297. rcPos.left += rcInset.left;
  298. rcPos.top += rcInset.top;
  299. rcPos.right -= rcInset.right;
  300. rcPos.bottom -= rcInset.bottom;
  301. LONG lEditHeight = m_pOwner->GetManager()->GetFontInfo(m_pOwner->GetFont())->tm.tmHeight;
  302. if( lEditHeight < rcPos.GetHeight() ) {
  303. rcPos.top += (rcPos.GetHeight() - lEditHeight) / 2;
  304. rcPos.bottom = rcPos.top + lEditHeight;
  305. }
  306. CControlUI* pParent = m_pOwner;
  307. RECT rcParent;
  308. while( pParent = pParent->GetParent() ) {
  309. if( !pParent->IsVisible() ) {
  310. rcPos.left = rcPos.top = rcPos.right = rcPos.bottom = 0;
  311. break;
  312. }
  313. rcParent = pParent->GetClientPos();
  314. if( !::IntersectRect(&rcPos, &rcPos, &rcParent) ) {
  315. rcPos.left = rcPos.top = rcPos.right = rcPos.bottom = 0;
  316. break;
  317. }
  318. }
  319. // 【分层模式】本窗是独立顶层 WS_POPUP:SetWindowPos 要的是**屏幕坐标**,
  320. // 而上面算出来的 rcPos 是「相对宿主窗口客户区」的 —— 这里统一补宿主窗口屏幕原点。
  321. //
  322. // ⚠️ 必须集中在 CalPos 里,因为它的调用方有三个:
  323. // ① Init(建窗)② CEditUI::SetPos(控件重排)③ CEditUI::Move(控件移动)
  324. // 只有集中在此,才不会出现「一个调用方补了、另一个没补」的错位。
  325. // (y 方向那个 -1 是原作者调出来的视觉补偿,原值保持不动。)
  326. if (m_bTopLevelWnd)
  327. {
  328. HWND hHost = m_pOwner->GetManager()->GetPaintWindow();
  329. RECT rcHost = { 0 };
  330. ::GetWindowRect(hHost, &rcHost);
  331. rcPos.left += rcHost.left;
  332. rcPos.right += rcHost.left;
  333. rcPos.top += rcHost.top - 1;
  334. rcPos.bottom += rcHost.top - 1;
  335. }
  336. return rcPos;
  337. }
  338. LPCTSTR CEditWnd::GetWindowClassName() const
  339. {
  340. return _T("EditWnd");
  341. }
  342. LPCTSTR CEditWnd::GetSuperClassName() const
  343. {
  344. return WC_EDIT;
  345. }
  346. void CEditWnd::OnFinalMessage(HWND hWnd)
  347. {
  348. {
  349. CDuiString sOwnerName = (m_pOwner != NULL) ? m_pOwner->GetName() : CDuiString(_T("?"));
  350. LPCTSTR pszText = (m_pOwner != NULL) ? m_pOwner->GetText() : (LPCTSTR)NULL;
  351. // 同一个 owner 反复出现 FIN → INIT ⇒ 窗口在抖动重建;
  352. // 重建走的是 m_sText,而 m_sText 可能落后于 IME 尚未提交的组合串 ⇒ 字"没了"。
  353. DuiLibDiag::Log(_T("EDIT-FIN hwnd=%p owner=%s text='%s' nFocus=%p"),
  354. (void*)hWnd, sOwnerName.GetData(), pszText != NULL ? pszText : _T(""),
  355. (void*)::GetFocus());
  356. }
  357. m_pOwner->Invalidate();
  358. // Clear reference and die
  359. if( m_hBkBrush != NULL ) ::DeleteObject(m_hBkBrush);
  360. if (m_pOwner->GetManager()->IsLayered()) {
  361. m_pOwner->GetManager()->RemoveNativeWindow(hWnd);
  362. }
  363. m_pOwner->m_pWindow = NULL;
  364. delete this;
  365. }
  366. LRESULT CEditWnd::HandleMessage(UINT uMsg, WPARAM wParam, LPARAM lParam)
  367. {
  368. LRESULT lRes = 0;
  369. BOOL bHandled = TRUE;
  370. if( uMsg == WM_CREATE ) {
  371. bHandled = FALSE;
  372. }
  373. else if( uMsg == WM_KILLFOCUS ) lRes = OnKillFocus(uMsg, wParam, lParam, bHandled);
  374. else if( uMsg == WM_CLOSE ) lRes = OnClose(uMsg, wParam, lParam, bHandled);
  375. else if( uMsg == OCM_COMMAND ) {
  376. if( GET_WM_COMMAND_CMD(wParam, lParam) == EN_CHANGE ) lRes = OnEditChanged(uMsg, wParam, lParam, bHandled);
  377. else if( GET_WM_COMMAND_CMD(wParam, lParam) == EN_UPDATE ) {
  378. RECT rcClient;
  379. ::GetClientRect(m_hWnd, &rcClient);
  380. ::InvalidateRect(m_hWnd, &rcClient, FALSE);
  381. }
  382. }
  383. else if( uMsg == WM_KEYDOWN && TCHAR(wParam) == VK_RETURN ){
  384. m_pOwner->GetManager()->SendNotify(m_pOwner, DUI_MSGTYPE_RETURN);
  385. }
  386. else if( uMsg == WM_KEYDOWN && TCHAR(wParam) == VK_TAB ){
  387. if (m_pOwner->GetManager()->IsLayered()) {
  388. m_pOwner->GetManager()->SetNextTabControl();
  389. }
  390. }
  391. else if( uMsg == OCM__BASE + WM_CTLCOLOREDIT || uMsg == OCM__BASE + WM_CTLCOLORSTATIC ) {
  392. ::SetBkMode((HDC)wParam, TRANSPARENT);
  393. DWORD dwTextColor = m_pOwner->GetTextColor();
  394. ::SetTextColor((HDC)wParam, RGB(GetBValue(dwTextColor),GetGValue(dwTextColor),GetRValue(dwTextColor)));
  395. DWORD clrColor = m_pOwner->GetNativeEditBkColor();
  396. if (clrColor < 0xFF000000) {
  397. if (m_hBkBrush != NULL) ::DeleteObject(m_hBkBrush);
  398. RECT rcWnd = m_pOwner->GetManager()->GetNativeWindowRect(m_hWnd);
  399. HBITMAP hBmpEditBk = CRenderEngine::GenerateBitmap(m_pOwner->GetManager(), rcWnd, m_pOwner, clrColor);
  400. m_hBkBrush = ::CreatePatternBrush(hBmpEditBk);
  401. ::DeleteObject(hBmpEditBk);
  402. }
  403. else {
  404. if (m_hBkBrush == NULL) {
  405. m_hBkBrush = ::CreateSolidBrush(RGB(GetBValue(clrColor), GetGValue(clrColor), GetRValue(clrColor)));
  406. }
  407. }
  408. return (LRESULT)m_hBkBrush;
  409. }
  410. else if( uMsg == WM_PAINT) {
  411. bHandled = FALSE;
  412. }
  413. else if( uMsg == WM_PRINT ) {
  414. bHandled = FALSE;
  415. }
  416. else if( uMsg == WM_TIMER ) {
  417. if (wParam == CARET_TIMERID) {
  418. m_bDrawCaret = !m_bDrawCaret;
  419. RECT rcClient;
  420. ::GetClientRect(m_hWnd, &rcClient);
  421. ::InvalidateRect(m_hWnd, &rcClient, FALSE);
  422. return 0;
  423. }
  424. bHandled = FALSE;
  425. }
  426. else if( uMsg == WM_SETFOCUS || uMsg == WM_KEYDOWN || uMsg == WM_CHAR ) {
  427. // 【r39 取证 · 只写日志,行为**完全不变**】
  428. // 本分支放在 VK_RETURN / VK_TAB 两支**之后**,所以那两支的语义一字不动;
  429. // 末尾照旧 `bHandled = FALSE` ⇒ 与原来"落到 else"的路径完全一致。
  430. //
  431. // 目的:把"按键到底有没有进到这个窗口"这一件事一次判死 ——
  432. // · 抢回之后敲键盘,这三条**一条都不出现** ⇒ 按键根本没投进来
  433. // (前台/焦点没恢复对,"事后抢回"这条路走不通);
  434. // · 出现 WM_KEYDOWN 却没有 EN_CHANGE ⇒ 按键到了、被吞(输入法/业务过滤器层);
  435. // · 出现 WM_CHAR 或直接 EDIT-CHANGE ⇒ 落点正常。
  436. CDuiString sKeyOwnerName = (m_pOwner != NULL) ? m_pOwner->GetName() : CDuiString(_T("?"));
  437. HWND hKeyFocus = ::GetFocus();
  438. DuiLibDiag::Log(_T("EDIT-KEYMSG msg=%u owner=%s wParam=%u lParam=%u nFocus=%p focusIsSelf=%d"),
  439. (unsigned)uMsg, sKeyOwnerName.GetData(), (unsigned)wParam, (unsigned)lParam,
  440. (void*)hKeyFocus, (int)(hKeyFocus == m_hWnd ? 1 : 0));
  441. bHandled = FALSE;
  442. }
  443. else bHandled = FALSE;
  444. if( !bHandled )
  445. {
  446. // 【r40 取证 · 行为零改变】把默认处理的返回值**原样**带出去(与原写法完全等价),
  447. // 只在按键类消息之后补一行"事后状态"。
  448. //
  449. // 这一行是整个问题的裁判:
  450. // · len 变大 / 变小 ⇒ 编辑框**接受了**这个字符。那么"用户看不见字"就只能
  451. // 是显示或反射链的问题(m_sText 没被 EN_CHANGE 更新 ⇒ DuiLib 自绘为空);
  452. // · len 不变 ⇒ 编辑框**拒绝**了它(只读/禁用/上限),或者这条消息压根没走到
  453. // 真正 EDIT 的窗口过程。两者改法完全不同,所以必须先看这一行。
  454. const LRESULT lDefRes = CWindowWnd::HandleMessage(uMsg, wParam, lParam);
  455. if( uMsg == WM_CHAR || uMsg == WM_KEYDOWN )
  456. {
  457. TCHAR szTxtNow[512] = { 0 };
  458. ::GetWindowText(m_hWnd, szTxtNow, _countof(szTxtNow));
  459. const LONG lStyleAfter = ::GetWindowLong(m_hWnd, GWL_STYLE);
  460. CDuiString sOwnerAfter = (m_pOwner != NULL) ? m_pOwner->GetName() : CDuiString(_T("?"));
  461. DuiLibDiag::Log(_T("EDIT-KEYAFTER msg=%u wParam=%u owner=%s len=%d ro=%d en=%d vis=%d text='%s'"),
  462. (unsigned)uMsg, (unsigned)wParam, sOwnerAfter.GetData(),
  463. (int)::GetWindowTextLength(m_hWnd),
  464. (int)((lStyleAfter & 0x0800L) ? 1 : 0),
  465. (int)(::IsWindowEnabled(m_hWnd) ? 1 : 0),
  466. (int)(::IsWindowVisible(m_hWnd) ? 1 : 0),
  467. szTxtNow);
  468. }
  469. return lDefRes;
  470. }
  471. return lRes;
  472. }
  473. LRESULT CEditWnd::OnKillFocus(UINT uMsg, WPARAM wParam, LPARAM lParam, BOOL& bHandled)
  474. {
  475. LRESULT lRes = ::DefWindowProc(m_hWnd, uMsg, wParam, lParam);
  476. // ---- r37:把现场一次性记全(变量全部落局部,避免 DiagWndClass 的静态缓冲互相覆盖)----
  477. HWND hNewFocus = ::GetFocus();
  478. HWND hFg = ::GetForegroundWindow();
  479. const DWORD dwSelfTid = ::GetCurrentThreadId();
  480. const DWORD dwSelfPid = ::GetCurrentProcessId();
  481. const DWORD dwNewTid = DiagWndTid(hNewFocus);
  482. const DWORD dwFgPid = DiagWndPid(hFg);
  483. const DWORD dwFgTid = DiagWndTid(hFg);
  484. const bool bFgOurs = (hFg != NULL && dwFgPid == dwSelfPid);
  485. CPaintManagerUI* pManager = (m_pOwner != NULL) ? m_pOwner->GetManager() : NULL;
  486. const bool bOwnerKeepsFocus = (pManager != NULL && pManager->GetFocus() == m_pOwner);
  487. {
  488. CDuiString sOwnerName = (m_pOwner != NULL) ? m_pOwner->GetName() : CDuiString(_T("?"));
  489. TCHAR szNewCls[128] = { 0 };
  490. TCHAR szFgCls[128] = { 0 };
  491. if (hNewFocus != NULL && ::IsWindow(hNewFocus)) ::GetClassName(hNewFocus, szNewCls, _countof(szNewCls));
  492. if (hFg != NULL && ::IsWindow(hFg)) ::GetClassName(hFg, szFgCls, _countof(szFgCls));
  493. // r38:把"前台到底是哪个程序"也记下来 —— 类名只能说"像 UWP",进程名才定案。
  494. TCHAR szFgExe[128] = { 0 };
  495. DiagProcName(hFg, szFgExe, _countof(szFgExe));
  496. // ★ 这条就是"输入法正在拼的字会不会消失"的现场。
  497. // 失焦本身不致命 —— 致命的是"失焦即销毁"叠加"瞬态失焦"(见 OnClose)。
  498. // 判读:
  499. // · newFocus = 00000000 ⇒ 本线程此刻**没有**焦点窗口。
  500. // 此时 sameProc 无意义(下面记成 -1,别再把它读成"被别的进程抢走"
  501. // —— NULL 没有进程可言)。真实成因靠下面三个新字段分开。
  502. // · newFocus != NULL 且 sameProc=0 ⇒ 真被**别的进程/线程**抢走;
  503. // · newClass = EditWnd ⇒ 我们自己的另一个输入框接走了焦点(正常切框);
  504. // · newClass = #32770 ⇒ 被本线程的弹框本体拿走(窗口内部搬家)。
  505. //
  506. // ── r37 新增字段:专为把"两种焦点空了"分开(判读已按 r38 真机证据修订)──
  507. // · fg / fgClass / fgPid / fgTid / fgExe —— 此刻**前台窗口**是谁。
  508. // ★ r38 真机实证到的形态是:**前台被触摸键盘夺走**
  509. // (fgClass=Windows.UI.Core.CoreWindow、fgExe 应为 TextInputHost.exe)。
  510. // 前台一变,系统必然清空**旧**前台线程的键盘焦点 —— 所以同一条日志里
  511. // newFocus=00000000 且 killParam=00000000,两者是同一件事的两面。
  512. // 触摸键盘是按"哪个应用持有输入焦点"投递按键/候选词/退格的;我们此刻
  513. // 既不是前台、焦点也被清空 ⇒ 每一次按键全部丢掉。**这就是本体。**
  514. // · fg=00000000 ⇒ 系统当前没有前台窗口(少见,见下面的 EDIT-FGLOST 分支)。
  515. // · fgOurs=1 ⇒ 前台仍是本进程的窗口,只是本线程键盘焦点被清空
  516. // ⇒ 属于"焦点被抢",多发生在窗口内部的焦点搬家途中,会自行恢复。
  517. // · killParam —— WM_KILLFOCUS 的 wParam(系统指定的接手窗口,比 GetFocus() 权威)。
  518. 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 复核)"),
  519. (void*)m_hWnd, sOwnerName.GetData(), (void*)hNewFocus, szNewCls,
  520. (unsigned)dwNewTid, (unsigned)dwSelfTid,
  521. (int)(hNewFocus == NULL ? -1 : ((dwNewTid == dwSelfTid) ? 1 : 0)),
  522. (void*)hFg, szFgCls, (unsigned)dwFgTid, (unsigned)dwFgPid, szFgExe,
  523. (int)(bFgOurs ? 1 : 0), (int)(bOwnerKeepsFocus ? 1 : 0), (void*)(HWND)wParam);
  524. }
  525. PostMessage(WM_CLOSE);
  526. // ==================================================================
  527. // ★★★【r37 根治 · 把被"莫名清空"的键盘焦点抢回来】(2026-09-24)
  528. //
  529. // 真机形态(用户原话):「用手指点触摸键盘的回退按钮无法删除输入框的文字,
  530. // 但是用鼠标点同一个按钮就可以正常删除」。
  531. //
  532. // 触摸键盘是**另一个进程**的窗口(TabTip),手指点它时本进程一行代码都不会执行
  533. // —— 所以"点键盘没反应"绝不可能是我们窗口内部逻辑写错。差别只可能在一个地方:
  534. // 触摸键盘把按键投给「**前台窗口所在线程的焦点窗口**」。
  535. // · 鼠标点它:TabTip 是 NOACTIVATE 窗口,点它不改变激活 ⇒ 本进程仍是前台
  536. // ⇒ 焦点窗口是编辑窗 ⇒ 字符/退格/候选词全部正常落点。
  537. // · 手指点它:触摸键盘自己**把前台拿走了**(r38 真机日志实证:
  538. // `fgClass=Windows.UI.Core.CoreWindow fgPid=7012 fgOurs=0`)⇒
  539. // ① 前台一变,系统必然清空旧前台线程的键盘焦点(见下方日志
  540. // newFocus=00000000,killParam 同样是 00000000);
  541. // ② 触摸键盘是靠"有输入焦点的那个应用"投递按键/候选词/退格的,
  542. // 我们这个应用此刻既非前台、焦点也被清空 ⇒ 按键没有落点。
  543. // ⇒ 用户看到的正是"打字母不进、退格删不掉、候选词点了没反应"。
  544. //
  545. // 注意:OnClose 那处修复(延后判 owner 是否仍是 DuiLib 焦点)只解决了
  546. // "窗口被销毁",**并没有解决"焦点不见了"** —— 窗口活着但没有键盘焦点,
  547. // 按键依旧无处可去。这就是用户反馈"还是不行"的那一半。
  548. //
  549. // ── r38 / r39 两轮真机证据的合并结论(**两种形态,同一个根**)──
  550. //
  551. // r37 版日志看到的形态(形态 A):
  552. // EDIT-KILLFOCUS ... newFocus=00000000
  553. // fg=0001020C fgClass=Windows.UI.Core.CoreWindow fgTid=7080 fgPid=7012
  554. // fgOurs=0 ownerKeepsFocus=1 killParam=00000000
  555. // ⇒ 前台被**触摸键盘本人**拿走(Win10 现代触摸键盘 = TextInputHost.exe,
  556. // 窗口类正是 Windows.UI.Core.CoreWindow)。
  557. //
  558. // r38 版日志看到的形态(形态 B):
  559. // EDIT-KILLFOCUS ... newFocus=00000000 fg=00000000 fgExe= fgOurs=0 ownerKeepsFocus=1
  560. // ⇒ 前台**落空**(系统当前没有任何前台窗口)。
  561. //
  562. // 两者本质相同:**本进程既不是前台、本线程键盘焦点又被清空** ⇒ 触摸键盘
  563. // 投递按键/候选词/退格时找不到落点。所以判据必须把两种都收进来(见下方 bKbTookFg)。
  564. // r38 只收了形态 A,结果正好把真机上实际出现的那次排除在外 —— 这就是
  565. // "还是不行"的直接原因。
  566. //
  567. // 同时这也解释了 r37 两个抢回钩子为什么一行日志都没打 —— 它们**都是死代码**:
  568. // · UIManager 的 ACT-RECLAIM 挂在 WM_ACTIVATE 上:弹框在那一刻**早已是
  569. // INACTIVE**(最后一次 WND-ACTIVATE 是 531080953 的 wParam=0),
  570. // 系统不会为它再补发一次 ⇒ 钩子永不触发;
  571. // · 本文件的 EDIT-REFOCUS 判据要求 fgOurs=1,而真实形态恰恰是 fgOurs=0
  572. // ⇒ 条件恒假。
  573. //
  574. // 唯一**确实到达**的信号就是这条 WM_KILLFOCUS(r37 日志里它准时出现)。
  575. // 所以抢回逻辑必须挂在这里。
  576. //
  577. // 判据(纯状态、零时间戳,三条缺一不可):
  578. // ① newFocus = NULL —— 不是"切到我们自己的另一个框"(那会是另一个 EditWnd);
  579. // ② ownerKeepsFocus —— DuiLib 侧焦点仍在这个 EDIT 上 ⇒ 用户还在这个框里打字
  580. // (弹框正在销毁/失焦时该值为假 ⇒ 不抢);
  581. // ③ 前台是触摸键盘**或落空** —— 两种都算(形态 A / 形态 B,见上)。
  582. // **用户主动 Alt+Tab、点别的应用或点桌面时,前台是那个应用的窗口
  583. // (既不是 NULL、也不命中键盘类名/进程名)⇒ ③ 为假 ⇒ 绝不抢。**
  584. //
  585. // 动作顺序**不能反**:先把前台要回来,再复位焦点 ——
  586. // 前台变更本身会重置线程的键盘焦点,先 SetFocus 再抢前台等于白设。
  587. // ① SetForegroundWindow(画布窗口):常规路径(前台本就是被键盘临时拿走的);
  588. // ② 若被系统"后台进程不得抢前台"的闸门挡住 ⇒ 挂到当前前台线程上重试一次
  589. // (经典配方,用完**立刻脱开**,不常驻、不放钩子);
  590. // ③ 前台回来后再 ::SetFocus(本编辑窗)。
  591. // 全程无定时器、无时间戳、无事后清洗;不改几何、不改命中、不碰业务侧键盘显隐。
  592. // ==================================================================
  593. if (hNewFocus == NULL && bOwnerKeepsFocus)
  594. {
  595. CDuiString sOwnerName = (m_pOwner != NULL) ? m_pOwner->GetName() : CDuiString(_T("?"));
  596. TCHAR szFgClsR[128] = { 0 };
  597. TCHAR szFgExe[128] = { 0 };
  598. if (hFg != NULL && ::IsWindow(hFg)) ::GetClassName(hFg, szFgClsR, _countof(szFgClsR));
  599. DiagProcName(hFg, szFgExe, _countof(szFgExe));
  600. // ★★★ r39 关键修正:把「前台落空(fg = NULL)」也算作触摸键盘夺前台 ★★★
  601. //
  602. // r38 这里写的是 `hFg != NULL && ...` —— 恰好把**真机唯一出现过的那种形态**
  603. // 排除在外。r38 真机日志里那两行就是铁证:
  604. // EDIT-KILLFOCUS ... newFocus=00000000 fg=00000000 fgExe= fgOurs=0 ownerKeepsFocus=1
  605. // EDIT-FGLOST ... -> 非触摸键盘夺前台,不动作 ← 于是我们一个字都没做
  606. //
  607. // 「触摸键盘夺走前台」在真机上有**两种形态**,本质完全相同(本进程既不是
  608. // 前台、本线程键盘焦点又被清空 ⇒ 按键注入没有落点),必须一起收:
  609. // · 形态 A(r37 日志实证):
  610. // fg = Windows.UI.Core.CoreWindow / fgExe = TextInputHost.exe
  611. // —— 键盘**本人持有前台**;
  612. // · 形态 B(r38 日志实证):
  613. // fg = 00000000 —— **前台落空**(当前没有任何前台窗口)。
  614. // 成因:触摸键盘的窗口是 WS_EX_NOACTIVATE 的,手指触摸它引发的
  615. // 激活交接**没有落点**,系统索性把前台置空。
  616. // ⇔ 对照组正是"鼠标点同一个键盘一切正常":鼠标点 NOACTIVATE 窗口
  617. // 根本不产生激活交接,前台一直留在我们身上。
  618. //
  619. // 安全性完全不变:用户主动 Alt+Tab / 点别的应用 / 点桌面时,fg 是
  620. // **那个应用的窗口**(既不是 NULL,也不命中键盘类名)⇒ 判据为假 ⇒ 绝不抢。
  621. const bool bFgIsKb = (hFg != NULL && DiagFgIsTouchKb(hFg));
  622. const bool bKbTookFg = (hFg == NULL) || (!bFgOurs && bFgIsKb);
  623. if (!bKbTookFg)
  624. {
  625. // 焦点确实空了,但不是触摸键盘的账(例如用户切应用/切盘途中)⇒ 一个字都不动。
  626. // 留痕是为了万一还要再查:把"谁拿了前台"直接写进日志。
  627. DuiLibDiag::Log(_T("EDIT-FGLOST hwnd=%p owner=%s newFocus=NULL fg=%p(%s) fgExe=%s fgOurs=%d -> 前台属他方(既非键盘也非落空),不动作"),
  628. (void*)m_hWnd, sOwnerName.GetData(), (void*)hFg, szFgClsR, szFgExe, (int)(bFgOurs ? 1 : 0));
  629. }
  630. else
  631. {
  632. HWND hCanvas = (pManager != NULL) ? pManager->GetPaintWindow() : NULL;
  633. const bool bCanvasOk = (hCanvas != NULL && ::IsWindow(hCanvas) && ::IsWindowVisible(hCanvas));
  634. // ① 把前台要回来
  635. BOOL bOkFg = FALSE;
  636. if (bCanvasOk) bOkFg = ::SetForegroundWindow(hCanvas);
  637. HWND hFg1 = ::GetForegroundWindow();
  638. const bool bOurFg1 = (hFg1 != NULL && DiagWndPid(hFg1) == dwSelfPid);
  639. // ② 被闸门挡住时,挂到当前前台线程上重试一次(用完立刻脱开)
  640. BOOL bAttached = FALSE;
  641. BOOL bOkFg2 = FALSE;
  642. if (!bOurFg1 && bCanvasOk)
  643. {
  644. const DWORD dwFgTid2 = DiagWndTid(hFg1);
  645. if (dwFgTid2 != 0 && dwFgTid2 != dwSelfTid)
  646. {
  647. bAttached = ::AttachThreadInput(dwFgTid2, dwSelfTid, TRUE);
  648. if (bAttached)
  649. {
  650. bOkFg2 = ::SetForegroundWindow(hCanvas);
  651. ::AttachThreadInput(dwFgTid2, dwSelfTid, FALSE);
  652. }
  653. }
  654. }
  655. // ③ 前台回来之后再复位焦点(顺序不能反)
  656. ::SetFocus(m_hWnd);
  657. HWND hFg2 = ::GetForegroundWindow();
  658. TCHAR szFg2Cls[128] = { 0 };
  659. if (hFg2 != NULL && ::IsWindow(hFg2)) ::GetClassName(hFg2, szFg2Cls, _countof(szFg2Cls));
  660. // hit = 本进程内第几次执行抢回。判读(这是本轮最关键的一栏):
  661. // · hit 只涨到 1、之后再没出现 ⇒ 抢回后状态**稳住了**,打字应当恢复;
  662. // · hit 每敲一下键盘就 +1 ⇒ 触摸键盘在**每一次点按**时都重新夺走前台,
  663. // 抢在投递之前 ⇒ "事后抢回"这条路走不通,必须改成"不让它夺"。
  664. static int s_nReclaimHit = 0;
  665. ++s_nReclaimHit;
  666. HWND hFocusNow = ::GetFocus();
  667. 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"),
  668. (int)s_nReclaimHit, (hFg == NULL ? _T("fgNULL") : _T("fgKB")),
  669. (void*)m_hWnd, sOwnerName.GetData(), (void*)hFg, szFgClsR, szFgExe,
  670. (int)bOkFg, (int)bAttached, (int)bOkFg2,
  671. (void*)hFg2, szFg2Cls,
  672. (int)((hFg2 != NULL && DiagWndPid(hFg2) == dwSelfPid) ? 1 : 0),
  673. (void*)hFocusNow, (int)(hFocusNow == m_hWnd ? 1 : 0));
  674. }
  675. }
  676. return lRes;
  677. }
  678. // ★ 修复(2026-09-23 · 「手指点触摸键盘输入不进去」)
  679. //
  680. // 背景(真机日志实证,一次"手指点输入框"就够看清楚):
  681. //
  682. // PTR-DOWN stealFocus ::SetFocus(m_hWndPaint) ← UIManager.cpp 抢走键盘焦点
  683. // EDIT-KILLFOCUS PostMessage(WM_CLOSE) ← 销毁请求**排队**(异步)
  684. // EDITUI-SETFOCUS 复用编辑窗 ::SetFocus(hEdit) ← 焦点已经还给 EDIT
  685. // …… 几十毫秒后 ……
  686. // EDIT-FIN 窗口还是被那条排队的 WM_CLOSE 销毁了
  687. //
  688. // 也就是说:**焦点刚被抢回来,窗口照旧死掉**。而 CPaintManagerUI::SetFocus 有
  689. // `pControl == m_pFocus` 早退,不会再发 UIEVENT_SETFOCUS ⇒ 窗口不再重建。
  690. // 触摸键盘敲出的字符、退格、输入法候选词的提交,就全都失去了落点
  691. // (真机日志里对应"9.7 秒零条 EDIT-CHANGE")。
  692. //
  693. // "手指点虚拟键盘"之所以必现、鼠标不犯,是因为手指点它的那一刻本窗口会短暂失活
  694. // (WND-ACTIVATE wParam=0(INACTIVE) fg=00000000 + EDIT-KILLFOCUS newFocus=00000000),
  695. // 于是又排上一条 WM_CLOSE;鼠标那一路不会走到这里。
  696. //
  697. // ⇒ 判据(纯状态、零时间戳):**延后到 WM_CLOSE 真正被取出来处理时**,再看一眼
  698. // 「我的 owner 现在还是不是 DuiLib 的焦点控件」:
  699. //
  700. // · 还是 ⇒ 这次失焦只是**瞬态**(窗口内部焦点搬家 / 本窗口短暂失活),
  701. // owner 还要接着输入 ⇒ 取消销毁,窗口活着等 SETFOCUS 回来;
  702. // · 不是 ⇒ 焦点已被别的控件/别的窗口拿走,或者控件被隐藏/移除、弹框正在销毁
  703. // (那些路径都会先 SetFocus(NULL) 或把 m_pFocus 置 NULL)
  704. // ⇒ 按原设计销毁。
  705. //
  706. // 为什么必须延后:CPaintManagerUI::SetFocus 的顺序是
  707. // 「先 ::SetFocus(画布窗口) 让旧编辑窗失焦 → 再派发 KILLFOCUS → 最后才把 m_pFocus 换成新控件」。
  708. // 若在原生 WM_KILLFOCUS 里当场判断,读到的一定还是**旧值** ⇒ 区分不了
  709. // "瞬态失焦"和"真的切到另一个控件"。
  710. //
  711. // 这条判据对"窗口失活"同样成立 —— 而窗口失活恰恰是手指点触摸键盘时唯一会发生的变数。
  712. LRESULT CEditWnd::OnClose(UINT /*uMsg*/, WPARAM /*wParam*/, LPARAM /*lParam*/, BOOL& bHandled)
  713. {
  714. CPaintManagerUI* pManager = (m_pOwner != NULL) ? m_pOwner->GetManager() : NULL;
  715. CControlUI* pMgrFocus = (pManager != NULL) ? pManager->GetFocus() : (CControlUI*)NULL;
  716. const bool bOwnerKeepsFocus = (pManager != NULL && pMgrFocus == m_pOwner);
  717. {
  718. CDuiString sOwnerName = (m_pOwner != NULL) ? m_pOwner->GetName() : CDuiString(_T("?"));
  719. DuiLibDiag::Log(_T("EDIT-CLOSE hwnd=%p owner=%s mgrFocus=%p ownerKeepsFocus=%d -> %s"),
  720. (void*)m_hWnd, sOwnerName.GetData(), (void*)pMgrFocus,
  721. (int)(bOwnerKeepsFocus ? 1 : 0),
  722. bOwnerKeepsFocus ? _T("取消销毁(瞬态失焦,窗口保留)") : _T("销毁"));
  723. }
  724. bHandled = TRUE;
  725. if (bOwnerKeepsFocus)
  726. {
  727. // 不销毁。焦点会由下面两条既有路径送回来,不需要在这里造新机制:
  728. // · 我们自己的 ::SetFocus(m_hWndPaint) → WM_SETFOCUS → 重发 UIEVENT_SETFOCUS
  729. // → CEditUI::DoEvent 的"复用编辑窗"分支 Show+SetFocus;
  730. // · 整个窗口重新被激活 → MessageHandler 的 WM_ACTIVATE 同理。
  731. return 0;
  732. }
  733. ::DestroyWindow(m_hWnd);
  734. return 0;
  735. }
  736. LRESULT CEditWnd::OnEditChanged(UINT /*uMsg*/, WPARAM /*wParam*/, LPARAM /*lParam*/, BOOL& /*bHandled*/)
  737. {
  738. // 【r40 取证】EN_CHANGE 到了、却被"Init 还没走完"挡掉的情形必须留痕。
  739. // 否则在日志里,"反射链根本没通"和"反射通了但窗口尚未初始化完"长得一模一样
  740. // —— 而这两件事的结论完全相反(前者要查 OCM 反射,后者是正常路径)。
  741. if( !m_bInit )
  742. {
  743. CDuiString sOwnerName = (m_pOwner != NULL) ? m_pOwner->GetName() : CDuiString(_T("?"));
  744. DuiLibDiag::Log(_T("EDIT-CHANGE 忽略(Init未完成) hwnd=%p owner=%s len=%d"),
  745. (void*)m_hWnd, sOwnerName.GetData(), (int)::GetWindowTextLength(m_hWnd));
  746. return 0;
  747. }
  748. if( m_pOwner == NULL ) return 0;
  749. // Copy text back
  750. int cchLen = ::GetWindowTextLength(m_hWnd) + 1;
  751. LPTSTR pstr = static_cast<LPTSTR>(_alloca(cchLen * sizeof(TCHAR)));
  752. ASSERT(pstr);
  753. if( pstr == NULL ) return 0;
  754. ::GetWindowText(m_hWnd, pstr, cchLen);
  755. m_pOwner->m_sText = pstr;
  756. {
  757. CDuiString sOwnerName = m_pOwner->GetName();
  758. // 每一次内容变化都留痕 —— "字符进来"与"被清空"都能看到。
  759. // 判读:
  760. // · append 之后突然出现 len=0 ⇒ 有代码把它清掉了。可能是业务侧的
  761. // 数字过滤(首个字符非法时 SetText("")),也可能是 CEditUI::SetText
  762. // 被调用 —— 后者会在 EDITUI-SETTEXT 行留下记录;
  763. // · 只看到 append、从没出现 len=0,用户却说"字没了"
  764. // ⇒ 字符根本没进到这个窗口 ⇒ 焦点已经不在它身上(查 EDIT-KILLFOCUS)。
  765. DuiLibDiag::Log(_T("EDIT-CHANGE hwnd=%p owner=%s len=%d text='%s'"),
  766. (void*)m_hWnd, sOwnerName.GetData(), cchLen - 1, pstr);
  767. }
  768. m_pOwner->GetManager()->SendNotify(m_pOwner, DUI_MSGTYPE_TEXTCHANGED);
  769. if( m_pOwner->GetManager()->IsLayered() ) m_pOwner->Invalidate();
  770. return 0;
  771. }
  772. /////////////////////////////////////////////////////////////////////////////////////
  773. //
  774. //
  775. IMPLEMENT_DUICONTROL(CEditUI)
  776. CEditUI::CEditUI() : m_pWindow(NULL), m_uMaxChar(255), m_bReadOnly(false),
  777. m_bPasswordMode(false), m_cPasswordChar(_T('*')), m_bAutoSelAll(false), m_uButtonState(0),
  778. m_dwEditbkColor(0xFFFFFFFF), m_dwEditTextColor(0x00000000), m_iWindowStyls(0),m_dwTipValueColor(0xFFBAC0C5)
  779. {
  780. SetTextPadding(CDuiRect(4, 3, 4, 3));
  781. SetBkColor(0xFFFFFFFF);
  782. }
  783. LPCTSTR CEditUI::GetClass() const
  784. {
  785. return _T("EditUI");
  786. }
  787. LPVOID CEditUI::GetInterface(LPCTSTR pstrName)
  788. {
  789. if( _tcsicmp(pstrName, DUI_CTR_EDIT) == 0 ) return static_cast<CEditUI*>(this);
  790. return CLabelUI::GetInterface(pstrName);
  791. }
  792. UINT CEditUI::GetControlFlags() const
  793. {
  794. if( !IsEnabled() ) return CControlUI::GetControlFlags();
  795. return UIFLAG_SETCURSOR | UIFLAG_TABSTOP;
  796. }
  797. void CEditUI::DoEvent(TEventUI& event)
  798. {
  799. // 【诊断 · 新建商品弹框「输入文字消失」】只对焦点/点击类事件留痕,避免鼠标移动刷屏。
  800. //
  801. // 为什么这条日志值得存在:本控件后面那个 `if( m_pWindow ) return;` 会让
  802. // UIEVENT_SETFOCUS **提前返回**,从而走不到 CLabelUI::DoEvent 里的
  803. // `m_bFocused = true` —— 于是 IsFocused() 与真实焦点可能脱节。
  804. // 而下面 UIEVENT_BUTTONDOWN 的三岔口正是**用 IsFocused() 分流**的:
  805. // · IsFocused() && m_pWindow == NULL ⇒ 路 A:新建窗口 + **全选整框**
  806. // · m_pWindow != NULL ⇒ 路 B:光标落到点击处(正常点击)
  807. // · 两者都不满足 ⇒ 什么都不做(点击被静默丢弃)
  808. // m_bFocused / m_pWindow 这两个值必须能在日志里直接看到,否则无从判断走了哪条。
  809. if (event.Type == UIEVENT_SETFOCUS
  810. || event.Type == UIEVENT_KILLFOCUS
  811. || event.Type == UIEVENT_BUTTONDOWN
  812. || event.Type == UIEVENT_BUTTONUP
  813. || event.Type == UIEVENT_TOUCHDOWN
  814. || event.Type == UIEVENT_TOUCHUP
  815. || event.Type == UIEVENT_DBLCLICK
  816. || event.Type == UIEVENT_RBUTTONDOWN)
  817. {
  818. CDuiString sName = GetName();
  819. DuiLibDiag::Log(_T("EDITUI-EVENT type=%d owner=%s m_bFocused=%d m_pWindow=%p editHwnd=%p pt=%d,%d nFocus=%p mgrHwnd=%p"),
  820. event.Type, sName.GetData(), (int)(m_bFocused ? 1 : 0), (void*)m_pWindow,
  821. (void*)GetHWND(), event.ptMouse.x, event.ptMouse.y,
  822. (void*)::GetFocus(), (void*)(m_pManager != NULL ? m_pManager->GetPaintWindow() : (HWND)NULL));
  823. }
  824. if( !IsMouseEnabled() && event.Type > UIEVENT__MOUSEBEGIN && event.Type < UIEVENT__MOUSEEND ) {
  825. if( m_pParent != NULL ) m_pParent->DoEvent(event);
  826. else CLabelUI::DoEvent(event);
  827. return;
  828. }
  829. if( event.Type == UIEVENT_SETCURSOR && IsEnabled() )
  830. {
  831. ::SetCursor(::LoadCursor(NULL, MAKEINTRESOURCE(IDC_IBEAM)));
  832. return;
  833. }
  834. if( event.Type == UIEVENT_WINDOWSIZE )
  835. {
  836. //if( m_pWindow != NULL ) m_pManager->SetFocusNeeded(this);
  837. }
  838. if( event.Type == UIEVENT_SCROLLWHEEL )
  839. {
  840. if( m_pWindow != NULL ) return;
  841. }
  842. if( event.Type == UIEVENT_SETFOCUS && IsEnabled() )
  843. {
  844. if( m_pWindow != NULL )
  845. {
  846. // ★ 修复(见本文件顶部「输入文字消失」说明)
  847. //
  848. // 这里是原来的 `if( m_pWindow ) return;`。它有两个后果:
  849. // (1) 走不到 CLabelUI::DoEvent,`m_bFocused = true` 永不执行
  850. // ⇒ IsFocused() 与真实焦点长期脱节(下面 BUTTONDOWN 曾用它分流);
  851. // (2) 编辑窗口**还在**、但键盘焦点已被系统/输入法/触摸键盘挪走时,
  852. // 没有任何代码把焦点还回去 —— 而所有按键(含退格)与输入法
  853. // 候选词的提交都必须依赖它。这正是"切换过一次输入框后
  854. // 连退格都输不进去"的那一格。
  855. //
  856. // 既然 DuiLib 的焦点此刻仍指向本控件(否则收不到 SETFOCUS),
  857. // 就把显示与键盘焦点一并还给它。
  858. HWND hEdit = *m_pWindow;
  859. if (hEdit != NULL && ::IsWindow(hEdit))
  860. {
  861. if (!::IsWindowVisible(hEdit)) ::ShowWindow(hEdit, SW_SHOWNOACTIVATE);
  862. if (::GetFocus() != hEdit) ::SetFocus(hEdit);
  863. }
  864. {
  865. CDuiString sName = GetName();
  866. DuiLibDiag::Log(_T("EDITUI-SETFOCUS 复用编辑窗 owner=%s editHwnd=%p nFocus=%p -> Show+SetFocus"),
  867. sName.GetData(), (void*)hEdit, (void*)::GetFocus());
  868. }
  869. CLabelUI::DoEvent(event); // 让基类补上 m_bFocused = true
  870. Invalidate();
  871. return;
  872. }
  873. m_pWindow = new CEditWnd();
  874. ASSERT(m_pWindow);
  875. m_pWindow->Init(this);
  876. Invalidate();
  877. }
  878. if( event.Type == UIEVENT_KILLFOCUS && IsEnabled() )
  879. {
  880. Invalidate();
  881. }
  882. if( event.Type == UIEVENT_BUTTONDOWN || event.Type == UIEVENT_DBLCLICK || event.Type == UIEVENT_RBUTTONDOWN)
  883. {
  884. if( IsEnabled() ) {
  885. GetManager()->ReleaseCapture();
  886. // ★ 修复:判据原来还多要求一个 `IsFocused()`。但那个标志由
  887. // CLabelUI::DoEvent 维护,而上面的 SETFOCUS 分支在"编辑窗已存在"
  888. // 时会早退、根本走不到那里 ⇒ IsFocused() 可能长期为假。
  889. // 于是"点击一个还没有编辑窗的输入框"时**两条分支都不走**:
  890. // 点击被静默丢弃(体感就是"点了没反应")。
  891. // 现在只看"有没有编辑窗" —— 点了输入框就一定拿得到输入能力。
  892. if( m_pWindow == NULL )
  893. {
  894. CDuiString sName = GetName();
  895. DuiLibDiag::Log(_T("EDITUI-BUTTONDOWN 无编辑窗 -> 新建并全选 owner=%s m_bFocused=%d"),
  896. sName.GetData(), (int)(m_bFocused ? 1 : 0));
  897. m_pWindow = new CEditWnd();
  898. ASSERT(m_pWindow);
  899. m_pWindow->Init(this);
  900. if( PtInRect(&m_rcItem, event.ptMouse) )
  901. {
  902. int nSize = GetWindowTextLength(*m_pWindow);
  903. if( nSize == 0 ) nSize = 1;
  904. Edit_SetSel(*m_pWindow, 0, nSize);
  905. }
  906. }
  907. else
  908. {
  909. CDuiString sName = GetName();
  910. DuiLibDiag::Log(_T("EDITUI-BUTTONDOWN 有编辑窗 -> 光标落到点击处 owner=%s pt=%d,%d autoSelAll=%d"),
  911. sName.GetData(), event.ptMouse.x, event.ptMouse.y, (int)(m_bAutoSelAll ? 1 : 0));
  912. if (!m_bAutoSelAll) {
  913. RECT rcTextPadding = GetTextPadding();
  914. POINT pt = event.ptMouse;
  915. pt.x -= m_rcItem.left + rcTextPadding.left;
  916. pt.y -= m_rcItem.top + rcTextPadding.top;
  917. Edit_SetSel(*m_pWindow, 0, 0);
  918. ::SendMessage(*m_pWindow, WM_LBUTTONDOWN, event.wParam, MAKELPARAM(pt.x, pt.y));
  919. // ==========================================================
  920. // ★ r40 修复:**这条鼠标按下消息必须配平**(补发配对的抬起)。
  921. //
  922. // 原生 EDIT 收到 WM_LBUTTONDOWN 后会进入"按下拖选"状态
  923. // (内部 bCaptureState = TRUE,并 SetCapture 自己),
  924. // 只有收到**配对的 WM_LBUTTONUP** 才会退出这个状态。
  925. //
  926. // 而历史上这里只发了按下:本控件的 UIEVENT_BUTTONUP 在下面被
  927. // `return;` 直接丢掉、从不下传。于是编辑框长期停在"鼠标按着"
  928. // 的状态里 —— 而这正是**触摸点击专有**的路径:
  929. // · 鼠标点击:落点在原生编辑窗自己身上,DOWN/UP 由系统成对
  930. // 投递 ⇒ 状态干净 ⇒ 一切正常;
  931. // · 手指点击:指针消息只到画布窗口,由 DuiLib 合成这条 DOWN
  932. // ⇒ 半截手势 ⇒ 之后编辑框行为异常(真机表现就是"手指点
  933. // 键盘打不进字、退格也删不掉,鼠标点同一个键盘却正常")。
  934. // 坐标完全相同 ⇒ 光标仍精确落在点击处,与一次普通点击等价。
  935. // ==========================================================
  936. ::SendMessage(*m_pWindow, WM_LBUTTONUP, 0, MAKELPARAM(pt.x, pt.y));
  937. }
  938. }
  939. }
  940. return;
  941. }
  942. if( event.Type == UIEVENT_MOUSEMOVE )
  943. {
  944. return;
  945. }
  946. if( event.Type == UIEVENT_BUTTONUP )
  947. {
  948. return;
  949. }
  950. if( event.Type == UIEVENT_CONTEXTMENU )
  951. {
  952. return;
  953. }
  954. if( event.Type == UIEVENT_MOUSEENTER )
  955. {
  956. if( ::PtInRect(&m_rcItem, event.ptMouse ) ) {
  957. if( IsEnabled() ) {
  958. if( (m_uButtonState & UISTATE_HOT) == 0 ) {
  959. m_uButtonState |= UISTATE_HOT;
  960. Invalidate();
  961. }
  962. }
  963. }
  964. }
  965. if( event.Type == UIEVENT_MOUSELEAVE )
  966. {
  967. if( IsEnabled() ) {
  968. m_uButtonState &= ~UISTATE_HOT;
  969. Invalidate();
  970. }
  971. return;
  972. }
  973. CLabelUI::DoEvent(event);
  974. }
  975. void CEditUI::SetEnabled(bool bEnable)
  976. {
  977. CControlUI::SetEnabled(bEnable);
  978. if( !IsEnabled() ) {
  979. m_uButtonState = 0;
  980. }
  981. }
  982. void CEditUI::SetText(LPCTSTR pstrText)
  983. {
  984. {
  985. CDuiString sName = GetName();
  986. // 谁在改文本、改成什么。业务侧的数字过滤在"首个字符非法"时会
  987. // SetText("") —— 那会把整框内容清空(正是"输入完被自动删除"的一种表现)。
  988. // 注意:本函数无论编辑窗在不在都会执行,m_pWindow=NULL 时只改 m_sText,
  989. // 那个新值要等下次 CEditWnd::Init 才会真正写进 EDIT。
  990. DuiLibDiag::Log(_T("EDITUI-SETTEXT owner=%s m_pWindow=%p old='%s' new='%s'"),
  991. sName.GetData(), (void*)m_pWindow,
  992. m_sText.GetData(), pstrText != NULL ? pstrText : _T(""));
  993. }
  994. m_sText = pstrText;
  995. if( m_pWindow != NULL ) Edit_SetText(*m_pWindow, m_sText);
  996. Invalidate();
  997. }
  998. void CEditUI::SetMaxChar(UINT uMax)
  999. {
  1000. m_uMaxChar = uMax;
  1001. if( m_pWindow != NULL ) Edit_LimitText(*m_pWindow, m_uMaxChar);
  1002. }
  1003. UINT CEditUI::GetMaxChar()
  1004. {
  1005. return m_uMaxChar;
  1006. }
  1007. void CEditUI::SetReadOnly(bool bReadOnly)
  1008. {
  1009. if( m_bReadOnly == bReadOnly ) return;
  1010. m_bReadOnly = bReadOnly;
  1011. if( m_pWindow != NULL ) Edit_SetReadOnly(*m_pWindow, m_bReadOnly);
  1012. Invalidate();
  1013. }
  1014. bool CEditUI::IsReadOnly() const
  1015. {
  1016. return m_bReadOnly;
  1017. }
  1018. void CEditUI::SetNumberOnly(bool bNumberOnly)
  1019. {
  1020. if( bNumberOnly )
  1021. {
  1022. m_iWindowStyls |= ES_NUMBER;
  1023. }
  1024. else
  1025. {
  1026. m_iWindowStyls &= ~ES_NUMBER;
  1027. }
  1028. }
  1029. bool CEditUI::IsNumberOnly() const
  1030. {
  1031. return (m_iWindowStyls & ES_NUMBER) ? true:false;
  1032. }
  1033. int CEditUI::GetWindowStyls() const
  1034. {
  1035. return m_iWindowStyls;
  1036. }
  1037. void CEditUI::SetPasswordMode(bool bPasswordMode)
  1038. {
  1039. if( m_bPasswordMode == bPasswordMode ) return;
  1040. m_bPasswordMode = bPasswordMode;
  1041. Invalidate();
  1042. if( m_pWindow != NULL ) {
  1043. LONG styleValue = ::GetWindowLong(*m_pWindow, GWL_STYLE);
  1044. bPasswordMode ? styleValue |= ES_PASSWORD : styleValue &= ~ES_PASSWORD;
  1045. ::SetWindowLong(*m_pWindow, GWL_STYLE, styleValue);
  1046. }
  1047. }
  1048. bool CEditUI::IsPasswordMode() const
  1049. {
  1050. return m_bPasswordMode;
  1051. }
  1052. void CEditUI::SetPasswordChar(TCHAR cPasswordChar)
  1053. {
  1054. if( m_cPasswordChar == cPasswordChar ) return;
  1055. m_cPasswordChar = cPasswordChar;
  1056. if( m_pWindow != NULL ) Edit_SetPasswordChar(*m_pWindow, m_cPasswordChar);
  1057. Invalidate();
  1058. }
  1059. TCHAR CEditUI::GetPasswordChar() const
  1060. {
  1061. return m_cPasswordChar;
  1062. }
  1063. LPCTSTR CEditUI::GetNormalImage()
  1064. {
  1065. return m_sNormalImage;
  1066. }
  1067. void CEditUI::SetNormalImage(LPCTSTR pStrImage)
  1068. {
  1069. m_sNormalImage = pStrImage;
  1070. Invalidate();
  1071. }
  1072. LPCTSTR CEditUI::GetHotImage()
  1073. {
  1074. return m_sHotImage;
  1075. }
  1076. void CEditUI::SetHotImage(LPCTSTR pStrImage)
  1077. {
  1078. m_sHotImage = pStrImage;
  1079. Invalidate();
  1080. }
  1081. LPCTSTR CEditUI::GetFocusedImage()
  1082. {
  1083. return m_sFocusedImage;
  1084. }
  1085. void CEditUI::SetFocusedImage(LPCTSTR pStrImage)
  1086. {
  1087. m_sFocusedImage = pStrImage;
  1088. Invalidate();
  1089. }
  1090. LPCTSTR CEditUI::GetDisabledImage()
  1091. {
  1092. return m_sDisabledImage;
  1093. }
  1094. void CEditUI::SetDisabledImage(LPCTSTR pStrImage)
  1095. {
  1096. m_sDisabledImage = pStrImage;
  1097. Invalidate();
  1098. }
  1099. void CEditUI::SetNativeEditBkColor(DWORD dwBkColor)
  1100. {
  1101. m_dwEditbkColor = dwBkColor;
  1102. }
  1103. DWORD CEditUI::GetNativeEditBkColor() const
  1104. {
  1105. return m_dwEditbkColor;
  1106. }
  1107. void CEditUI::SetNativeEditTextColor( LPCTSTR pStrColor )
  1108. {
  1109. if( *pStrColor == _T('#')) pStrColor = ::CharNext(pStrColor);
  1110. LPTSTR pstr = NULL;
  1111. DWORD clrColor = _tcstoul(pStrColor, &pstr, 16);
  1112. m_dwEditTextColor = clrColor;
  1113. }
  1114. DWORD CEditUI::GetNativeEditTextColor() const
  1115. {
  1116. return m_dwEditTextColor;
  1117. }
  1118. bool CEditUI::IsAutoSelAll()
  1119. {
  1120. return m_bAutoSelAll;
  1121. }
  1122. void CEditUI::SetAutoSelAll(bool bAutoSelAll)
  1123. {
  1124. m_bAutoSelAll = bAutoSelAll;
  1125. }
  1126. void CEditUI::SetSel(long nStartChar, long nEndChar)
  1127. {
  1128. if( m_pWindow != NULL ) Edit_SetSel(*m_pWindow, nStartChar,nEndChar);
  1129. }
  1130. void CEditUI::SetSelAll()
  1131. {
  1132. SetSel(0,-1);
  1133. }
  1134. void CEditUI::SetReplaceSel(LPCTSTR lpszReplace)
  1135. {
  1136. if( m_pWindow != NULL ) Edit_ReplaceSel(*m_pWindow, lpszReplace);
  1137. }
  1138. void CEditUI::SetTipValue( LPCTSTR pStrTipValue )
  1139. {
  1140. m_sTipValue = pStrTipValue;
  1141. }
  1142. LPCTSTR CEditUI::GetTipValue()
  1143. {
  1144. if (!IsResourceText()) return m_sTipValue;
  1145. return CResourceManager::GetInstance()->GetText(m_sTipValue);
  1146. }
  1147. void CEditUI::SetTipValueColor( LPCTSTR pStrColor )
  1148. {
  1149. if( *pStrColor == _T('#')) pStrColor = ::CharNext(pStrColor);
  1150. LPTSTR pstr = NULL;
  1151. DWORD clrColor = _tcstoul(pStrColor, &pstr, 16);
  1152. m_dwTipValueColor = clrColor;
  1153. }
  1154. DWORD CEditUI::GetTipValueColor()
  1155. {
  1156. return m_dwTipValueColor;
  1157. }
  1158. HWND CEditUI::GetHWND()
  1159. {
  1160. if(m_pWindow != NULL) {
  1161. return m_pWindow->GetHWND();
  1162. }
  1163. return NULL;
  1164. }
  1165. // 【分层模式】把浮层原生编辑窗重新对齐到本控件所在位置。
  1166. //
  1167. // 顶层浮窗(WS_POPUP)**不会**随宿主窗口移动 —— owner 关系只保证 z 序。
  1168. // 所以宿主窗口每次 WM_MOVE / WM_WINDOWPOSCHANGED 都要来这里补位,
  1169. // 由 CPaintManagerUI::MessageHandler 遍历所有 EditUI 调用(见 UIManager.cpp)。
  1170. // 非分层模式本函数是空操作(那时 EditWnd 是 WS_CHILD,跟着父窗口走)。
  1171. void CEditUI::SyncNativeWindowPos()
  1172. {
  1173. if( m_pWindow == NULL || m_pWindow->GetHWND() == NULL ) return;
  1174. RECT rcPos = m_pWindow->CalPos();
  1175. // 已在位就不要 SetWindowPos —— 否则每次 WM_WINDOWPOSCHANGED 都白刷一次,
  1176. // 拖动时会看到输入框闪。
  1177. RECT rcCur = { 0 };
  1178. ::GetWindowRect(m_pWindow->GetHWND(), &rcCur);
  1179. if( rcPos.left == rcCur.left && rcPos.top == rcCur.top &&
  1180. rcPos.right == rcCur.right && rcPos.bottom == rcCur.bottom ) return;
  1181. ::SetWindowPos(m_pWindow->GetHWND(), NULL, rcPos.left, rcPos.top,
  1182. rcPos.right - rcPos.left, rcPos.bottom - rcPos.top,
  1183. SWP_NOZORDER | SWP_NOACTIVATE);
  1184. }
  1185. void CEditUI::SetPos(RECT rc, bool bNeedInvalidate)
  1186. {
  1187. CControlUI::SetPos(rc, bNeedInvalidate);
  1188. if( m_pWindow != NULL ) {
  1189. RECT rcPos = m_pWindow->CalPos();
  1190. ::SetWindowPos(m_pWindow->GetHWND(), NULL, rcPos.left, rcPos.top, rcPos.right - rcPos.left,
  1191. rcPos.bottom - rcPos.top, SWP_NOZORDER | SWP_NOACTIVATE);
  1192. }
  1193. }
  1194. void CEditUI::Move(SIZE szOffset, bool bNeedInvalidate)
  1195. {
  1196. CControlUI::Move(szOffset, bNeedInvalidate);
  1197. if( m_pWindow != NULL ) {
  1198. RECT rcPos = m_pWindow->CalPos();
  1199. ::SetWindowPos(m_pWindow->GetHWND(), NULL, rcPos.left, rcPos.top, rcPos.right - rcPos.left,
  1200. rcPos.bottom - rcPos.top, SWP_NOSIZE | SWP_NOZORDER | SWP_NOACTIVATE);
  1201. }
  1202. }
  1203. void CEditUI::SetVisible(bool bVisible)
  1204. {
  1205. CControlUI::SetVisible(bVisible);
  1206. if( !IsVisible() && m_pWindow != NULL ) m_pManager->SetFocus(NULL);
  1207. }
  1208. void CEditUI::SetInternVisible(bool bVisible)
  1209. {
  1210. if( !IsVisible() && m_pWindow != NULL ) m_pManager->SetFocus(NULL);
  1211. }
  1212. SIZE CEditUI::EstimateSize(SIZE szAvailable)
  1213. {
  1214. if( m_cxyFixed.cy == 0 ) return CDuiSize(m_cxyFixed.cx, m_pManager->GetFontInfo(GetFont())->tm.tmHeight + 6);
  1215. return CControlUI::EstimateSize(szAvailable);
  1216. }
  1217. void CEditUI::SetAttribute(LPCTSTR pstrName, LPCTSTR pstrValue)
  1218. {
  1219. if( _tcsicmp(pstrName, _T("readonly")) == 0 ) SetReadOnly(_tcsicmp(pstrValue, _T("true")) == 0);
  1220. else if( _tcsicmp(pstrName, _T("numberonly")) == 0 ) SetNumberOnly(_tcsicmp(pstrValue, _T("true")) == 0);
  1221. else if( _tcscmp(pstrName, _T("autoselall")) == 0 ) SetAutoSelAll(_tcscmp(pstrValue, _T("true")) == 0);
  1222. else if( _tcsicmp(pstrName, _T("password")) == 0 ) SetPasswordMode(_tcsicmp(pstrValue, _T("true")) == 0);
  1223. else if( _tcsicmp(pstrName, _T("passwordchar")) == 0 ) SetPasswordChar(*pstrValue);
  1224. else if( _tcsicmp(pstrName, _T("maxchar")) == 0 ) SetMaxChar(_ttoi(pstrValue));
  1225. else if( _tcsicmp(pstrName, _T("normalimage")) == 0 ) SetNormalImage(pstrValue);
  1226. else if( _tcsicmp(pstrName, _T("hotimage")) == 0 ) SetHotImage(pstrValue);
  1227. else if( _tcsicmp(pstrName, _T("focusedimage")) == 0 ) SetFocusedImage(pstrValue);
  1228. else if( _tcsicmp(pstrName, _T("disabledimage")) == 0 ) SetDisabledImage(pstrValue);
  1229. else if( _tcsicmp(pstrName, _T("tipvalue")) == 0 ) SetTipValue(pstrValue);
  1230. else if( _tcsicmp(pstrName, _T("tipvaluecolor")) == 0 ) SetTipValueColor(pstrValue);
  1231. else if( _tcsicmp(pstrName, _T("nativetextcolor")) == 0 ) SetNativeEditTextColor(pstrValue);
  1232. else if( _tcsicmp(pstrName, _T("nativebkcolor")) == 0 ) {
  1233. if( *pstrValue == _T('#')) pstrValue = ::CharNext(pstrValue);
  1234. LPTSTR pstr = NULL;
  1235. DWORD clrColor = _tcstoul(pstrValue, &pstr, 16);
  1236. SetNativeEditBkColor(clrColor);
  1237. }
  1238. else CLabelUI::SetAttribute(pstrName, pstrValue);
  1239. }
  1240. void CEditUI::PaintStatusImage(HDC hDC)
  1241. {
  1242. if( IsFocused() ) m_uButtonState |= UISTATE_FOCUSED;
  1243. else m_uButtonState &= ~ UISTATE_FOCUSED;
  1244. if( !IsEnabled() ) m_uButtonState |= UISTATE_DISABLED;
  1245. else m_uButtonState &= ~ UISTATE_DISABLED;
  1246. if( (m_uButtonState & UISTATE_DISABLED) != 0 ) {
  1247. if( !m_sDisabledImage.IsEmpty() ) {
  1248. if( !DrawImage(hDC, (LPCTSTR)m_sDisabledImage) ) {}
  1249. else return;
  1250. }
  1251. }
  1252. else if( (m_uButtonState & UISTATE_FOCUSED) != 0 ) {
  1253. if( !m_sFocusedImage.IsEmpty() ) {
  1254. if( !DrawImage(hDC, (LPCTSTR)m_sFocusedImage) ) {}
  1255. else return;
  1256. }
  1257. }
  1258. else if( (m_uButtonState & UISTATE_HOT) != 0 ) {
  1259. if( !m_sHotImage.IsEmpty() ) {
  1260. if( !DrawImage(hDC, (LPCTSTR)m_sHotImage) ) {}
  1261. else return;
  1262. }
  1263. }
  1264. if( !m_sNormalImage.IsEmpty() ) {
  1265. if( !DrawImage(hDC, (LPCTSTR)m_sNormalImage) ) {}
  1266. else return;
  1267. }
  1268. }
  1269. void CEditUI::PaintText(HDC hDC)
  1270. {
  1271. DWORD mCurTextColor = m_dwTextColor;
  1272. if( m_dwTextColor == 0 ) mCurTextColor = m_dwTextColor = m_pManager->GetDefaultFontColor();
  1273. if( m_dwDisabledTextColor == 0 ) m_dwDisabledTextColor = m_pManager->GetDefaultDisabledColor();
  1274. CDuiString sDrawText = GetText();
  1275. CDuiString sTipValue = GetTipValue();
  1276. if(sDrawText == sTipValue || sDrawText == _T("")) {
  1277. mCurTextColor = m_dwTipValueColor;
  1278. sDrawText = sTipValue;
  1279. }
  1280. else {
  1281. CDuiString sTemp = sDrawText;
  1282. if( m_bPasswordMode ) {
  1283. sDrawText.Empty();
  1284. LPCTSTR pStr = sTemp.GetData();
  1285. while( *pStr != _T('\0') ) {
  1286. sDrawText += m_cPasswordChar;
  1287. pStr = ::CharNext(pStr);
  1288. }
  1289. }
  1290. }
  1291. RECT rcTextPadding = GetTextPadding();
  1292. RECT rc = m_rcItem;
  1293. rc.left += rcTextPadding.left;
  1294. rc.right -= rcTextPadding.right;
  1295. rc.top += rcTextPadding.top;
  1296. rc.bottom -= rcTextPadding.bottom;
  1297. if( IsEnabled() ) {
  1298. CRenderEngine::DrawText(hDC, m_pManager, rc, sDrawText, mCurTextColor, \
  1299. m_iFont, DT_SINGLELINE | m_uTextStyle);
  1300. }
  1301. else {
  1302. CRenderEngine::DrawText(hDC, m_pManager, rc, sDrawText, m_dwDisabledTextColor, \
  1303. m_iFont, DT_SINGLELINE | m_uTextStyle);
  1304. }
  1305. }
  1306. }