|
|
@@ -378,9 +378,14 @@ namespace DuiLib {
|
|
|
DuiLibDiag::Log(_T("COMBO-FIN hwnd=%p alive=%ums"),
|
|
|
hWnd, (unsigned)(s_dwComboOpenTick != 0 ? (::GetTickCount() - s_dwComboOpenTick) : 0));
|
|
|
s_dwComboOpenTick = 0;
|
|
|
- m_pOwner->m_pWindow = NULL;
|
|
|
- m_pOwner->m_uButtonState &= ~ UISTATE_PUSHED;
|
|
|
- m_pOwner->Invalidate();
|
|
|
+ // 【第 42 轮】owner 可能已经不在了(控件被移除/销毁后才轮到我们收尾)。
|
|
|
+ // 判空只防"指针为 NULL",防不了"内存已释放" —— 另一半由 CComboUI::~CComboUI()
|
|
|
+ // 同步收掉本窗口来保证(见那里的长注释)。两者互补,缺一不可。
|
|
|
+ if( m_pOwner != NULL ) {
|
|
|
+ m_pOwner->m_pWindow = NULL;
|
|
|
+ m_pOwner->m_uButtonState &= ~ UISTATE_PUSHED;
|
|
|
+ m_pOwner->Invalidate();
|
|
|
+ }
|
|
|
delete this;
|
|
|
}
|
|
|
|
|
|
@@ -468,10 +473,15 @@ namespace DuiLib {
|
|
|
// 【诊断】关闭现场:走到这里说明"关下拉框"的收尾已经开始。
|
|
|
DuiLibDiag::Log(_T("COMBO-CLOSE reason=WM_CLOSE alive=%ums"),
|
|
|
(unsigned)(s_dwComboOpenTick != 0 ? (::GetTickCount() - s_dwComboOpenTick) : 0));
|
|
|
- m_pOwner->SetManager(m_pOwner->GetManager(), m_pOwner->GetParent(), false);
|
|
|
- RECT rcNull = { 0 };
|
|
|
- for( int i = 0; i < m_pOwner->GetCount(); i++ ) static_cast<CControlUI*>(m_pOwner->GetItemAt(i))->SetPos(rcNull);
|
|
|
- m_pOwner->SetFocus();
|
|
|
+ // 【第 42 轮】owner 可能已被移出控件树(此时它的 manager 是 NULL)或已销毁。
|
|
|
+ // 这段收尾是"把 items 归还给 owner 的 manager",owner 不在了就没有归还对象,
|
|
|
+ // 直接跳过即可 —— 但**绝不能**不判空就解引用。
|
|
|
+ if( m_pOwner != NULL ) {
|
|
|
+ m_pOwner->SetManager(m_pOwner->GetManager(), m_pOwner->GetParent(), false);
|
|
|
+ RECT rcNull = { 0 };
|
|
|
+ for( int i = 0; i < m_pOwner->GetCount(); i++ ) static_cast<CControlUI*>(m_pOwner->GetItemAt(i))->SetPos(rcNull);
|
|
|
+ m_pOwner->SetFocus();
|
|
|
+ }
|
|
|
}
|
|
|
else if( uMsg == WM_LBUTTONDOWN ) {
|
|
|
POINT pt = { 0 };
|
|
|
@@ -534,8 +544,18 @@ namespace DuiLib {
|
|
|
// 【诊断】谁把焦点抢走了 —— 这一行的 wParam 就是"接任焦点的窗口"。
|
|
|
// 若它与 owner 相同,说明是"主窗口被重新激活"把我们挤掉,而不是你点了别处
|
|
|
//(鼠标激活发生在**进入窗口过程之前**,所以这里拦不住)。
|
|
|
+ // ⚠️⚠️【第 42 轮 · "手指快速触摸点 Combo 直接崩"的修点】这一行的实参以前是
|
|
|
+ // `m_pOwner->GetManager()->GetPaintWindow()` —— **裸连环解引用**。两个理由使它必崩:
|
|
|
+ // ① `Log(...)` 的**实参在调用之前就求值**,与日志开关无关
|
|
|
+ // (把 `Enabled()` 改成 false 也拦不住 —— `Log` 第一行 return 时表达式早算完了);
|
|
|
+ // ② owner 被移出控件树 / 正在销毁时 `GetManager()` 返回 NULL
|
|
|
+ // ⇒ `NULL->GetPaintWindow()` ⇒ 访问违例 ⇒ 进程直接崩。
|
|
|
+ // ⇒ 铁律:**探针里的每一次指针解引用都必须先判空 —— 探针的代价不能高于它探测的东西。**
|
|
|
+ // (本项目第二次栽在同一件事上:第一次是把 CDuiString 对象塞进变参。)
|
|
|
+ CPaintManagerUI* pOwnerMgr = (m_pOwner != NULL) ? m_pOwner->GetManager() : NULL;
|
|
|
+ const HWND hOwnerWnd = (pOwnerMgr != NULL) ? pOwnerMgr->GetPaintWindow() : NULL;
|
|
|
DuiLibDiag::Log(_T("COMBO-DECIDE KILLFOCUS newFocus=%p self=%p ownerHwnd=%p -> %s alive=%ums"),
|
|
|
- (HWND)wParam, m_hWnd, m_pOwner->GetManager()->GetPaintWindow(),
|
|
|
+ (HWND)wParam, m_hWnd, hOwnerWnd,
|
|
|
(m_hWnd != (HWND)wParam) ? _T("延迟一帧判定") : _T("忽略(焦点回到自己)"),
|
|
|
(unsigned)(s_dwComboOpenTick != 0 ? (::GetTickCount() - s_dwComboOpenTick) : 0));
|
|
|
// ⚠️ 不能在这里当场 PostMessage(WM_CLOSE):
|
|
|
@@ -788,6 +808,51 @@ namespace DuiLib {
|
|
|
////////////////////////////////////////////////////////
|
|
|
IMPLEMENT_DUICONTROL(CComboUI)
|
|
|
|
|
|
+ // ======================================================================
|
|
|
+ // ★★【第 42 轮 · "手指快速触摸点击 Combo 程序直接崩"】owner 死了必须同步收掉下拉窗 ★★
|
|
|
+ //
|
|
|
+ // 【为什么必须有这个析构】下拉窗(CComboWnd)是一个**独立顶层窗口**,它的生命周期
|
|
|
+ // 跟 owner 控件**没有**任何自动绑定关系。而它收尾时(OnFinalMessage)会回头写 owner:
|
|
|
+ // m_pOwner->m_pWindow = NULL;
|
|
|
+ // m_pOwner->m_uButtonState &= ~UISTATE_PUSHED;
|
|
|
+ // m_pOwner->Invalidate();
|
|
|
+ // ⇒ 只要 owner 先被销毁(设置页重建 / 控件被 Remove+delete),这三行就是
|
|
|
+ // **写已释放内存** ⇒ 访问违例 ⇒ 进程直接崩。
|
|
|
+ //
|
|
|
+ // 【为什么触摸下特别容易撞上(鼠标不犯的原因)】
|
|
|
+ // 鼠标按在 Combo/item 上时,系统会**先激活 owner**(激活发生在进入窗口过程之前),
|
|
|
+ // 下拉窗随即因失焦而关掉 —— 这个"关闭"通常发生在 owner 被销毁**之前**。
|
|
|
+ // 而触摸的点击是本项目触摸链**合成**的(CPaintManagerUI::TouchUp),
|
|
|
+ // **不改变窗口激活** ⇒ 下拉窗的关闭只能靠显式收尾(itemclick / Activate 复用分支),
|
|
|
+ // 于是它**可能晚于** owner 的销毁(业务在 itemselect 回调里重建设置页时尤其如此)
|
|
|
+ // ⇒ 撞上悬垂写的概率远高于鼠标。这就是"手指点崩、鼠标点不崩"的分界线。
|
|
|
+ //
|
|
|
+ // ⚠️⚠️ 为什么**不能**照抄 DuiLib 原版的 `if (m_pWindow) m_pWindow->Close();`
|
|
|
+ // 本项目 `CWindowWnd::Close()` 末尾是 `PostMessage(WM_CLOSE, ...)` —— **异步**!
|
|
|
+ // 在析构里 Post 一条消息,真正的销毁与 OnFinalMessage 会推迟到本对象
|
|
|
+ // **析构完成之后**才跑 ⇒ 悬垂照旧,等于没修。
|
|
|
+ //
|
|
|
+ // ⇒ 修法:**同步**走完同一条收尾链,让 OnFinalMessage 在本对象仍然有效时跑完。
|
|
|
+ // SendMessage(WM_CLOSE) 会进 CComboWnd::HandleMessage 的 WM_CLOSE 分支
|
|
|
+ // (把 items 归还给 owner 的 manager、复位 owner 状态 —— 这段收尾不能跳过),
|
|
|
+ // 随后由基类/DefWindowProc 销毁窗口 ⇒ WM_NCDESTROY ⇒ OnFinalMessage(同步)。
|
|
|
+ // 仍不放心就再 DestroyWindow 兜一次;两处都用线程 ID 校验,避免句柄被系统
|
|
|
+ // 复用到别的窗口时误杀。
|
|
|
+ // ======================================================================
|
|
|
+ CComboUI::~CComboUI()
|
|
|
+ {
|
|
|
+ if( m_pWindow == NULL ) return;
|
|
|
+
|
|
|
+ HWND hDropWnd = m_pWindow->GetHWND();
|
|
|
+ if( hDropWnd != NULL && ::IsWindow(hDropWnd)
|
|
|
+ && ::GetWindowThreadProcessId(hDropWnd, NULL) == ::GetCurrentThreadId() )
|
|
|
+ {
|
|
|
+ ::SendMessage(hDropWnd, WM_CLOSE, 0L, 0L);
|
|
|
+ if( ::IsWindow(hDropWnd) ) ::DestroyWindow(hDropWnd);
|
|
|
+ }
|
|
|
+ m_pWindow = NULL; // OnFinalMessage 里已置 NULL;这里兜底,防它没跑到
|
|
|
+ }
|
|
|
+
|
|
|
CComboUI::CComboUI() : m_uTextStyle(DT_VCENTER | DT_SINGLELINE)
|
|
|
, m_dwTextColor(0)
|
|
|
, m_dwDisabledTextColor(0)
|