|
|
@@ -42,6 +42,100 @@ namespace DuiLib
|
|
|
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
|
|
|
@@ -187,6 +281,39 @@ namespace DuiLib
|
|
|
(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()
|
|
|
@@ -329,36 +456,265 @@ namespace DuiLib
|
|
|
}
|
|
|
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 ) return CWindowWnd::HandleMessage(uMsg, wParam, lParam);
|
|
|
+ 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("?"));
|
|
|
- HWND hNewFocus = ::GetFocus();
|
|
|
- const DWORD dwSelfTid = ::GetCurrentThreadId();
|
|
|
- const DWORD dwNewTid = DiagWndTid(hNewFocus);
|
|
|
+ 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 没有进程可言)。
|
|
|
+ // 此时 sameProc 无意义(下面记成 -1,别再把它读成"被别的进程抢走"
|
|
|
+ // —— NULL 没有进程可言)。真实成因靠下面三个新字段分开。
|
|
|
// · newFocus != NULL 且 sameProc=0 ⇒ 真被**别的进程/线程**抢走;
|
|
|
// · newClass = EditWnd ⇒ 我们自己的另一个输入框接走了焦点(正常切框);
|
|
|
// · newClass = #32770 ⇒ 被本线程的弹框本体拿走(窗口内部搬家)。
|
|
|
- DuiLibDiag::Log(_T("EDIT-KILLFOCUS hwnd=%p owner=%s newFocus=%p newClass=%s newTid=%u selfTid=%u sameProc=%d -> 投递 WM_CLOSE(去留由 OnClose 复核)"),
|
|
|
- (void*)m_hWnd, sOwnerName.GetData(), (void*)hNewFocus, DiagWndClass(hNewFocus),
|
|
|
+ //
|
|
|
+ // ── 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)));
|
|
|
+ (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;
|
|
|
}
|
|
|
|
|
|
@@ -425,7 +781,16 @@ namespace DuiLib
|
|
|
|
|
|
LRESULT CEditWnd::OnEditChanged(UINT /*uMsg*/, WPARAM /*wParam*/, LPARAM /*lParam*/, BOOL& /*bHandled*/)
|
|
|
{
|
|
|
- if( !m_bInit ) return 0;
|
|
|
+ // 【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;
|
|
|
@@ -608,6 +973,24 @@ namespace DuiLib
|
|
|
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));
|
|
|
}
|
|
|
}
|
|
|
}
|