#ifndef __UIMANAGER_H__ #define __UIMANAGER_H__ #pragma once // 本头文件原本不含任何 include(符号由 StdAfx.h/UIlib.h 提供), // 但触屏相关的成员与状态里直接使用了 std::int64_t 与 std::vector, // 显式补齐以保证在 DuiLib 库工程(_WIN32_WINNT 兜底为 XP)里也能独立通过编译。 #include #include namespace DuiLib { ///////////////////////////////////////////////////////////////////////////////////// // class CControlUI; class CContainerUI; class CRichEditUI; class CIDropTarget; ///////////////////////////////////////////////////////////////////////////////////// // enum UILIB_RESTYPE { UILIB_FILE=1, // 来自磁盘文件 UILIB_ZIP, // 来自磁盘zip压缩包 UILIB_RESOURCE, // 来自资源 UILIB_ZIPRESOURCE, // 来自资源的zip压缩包 }; ///////////////////////////////////////////////////////////////////////////////////// // typedef enum EVENTTYPE_UI { UIEVENT__FIRST = 1, UIEVENT__KEYBEGIN, UIEVENT_KEYDOWN, UIEVENT_KEYUP, UIEVENT_CHAR, UIEVENT_SYSKEY, UIEVENT__KEYEND, UIEVENT__MOUSEBEGIN, UIEVENT_MOUSEMOVE, UIEVENT_MOUSELEAVE, UIEVENT_MOUSEENTER, UIEVENT_MOUSEHOVER, UIEVENT_BUTTONDOWN, UIEVENT_BUTTONUP, UIEVENT_RBUTTONDOWN, UIEVENT_RBUTTONUP, UIEVENT_MBUTTONDOWN, UIEVENT_MBUTTONUP, UIEVENT_DBLCLICK, UIEVENT_CONTEXTMENU, UIEVENT_SCROLLWHEEL, UIEVENT__MOUSEEND, // 触摸事件(NIM Duilib 模型)。必须排在 __MOUSEEND 之后: // CContainerUI::DoEvent 开头对 UIEVENT__MOUSEBEGIN~__MOUSEEND 区间做了 IsMouseEnabled 早退分支, // 排在区间之外才能保证触摸事件始终走正常派发与冒泡。 UIEVENT__TOUCHBEGIN, UIEVENT_TOUCHDOWN, UIEVENT_TOUCHMOVE, UIEVENT_TOUCHUP, UIEVENT__TOUCHEND, UIEVENT_KILLFOCUS, UIEVENT_SETFOCUS, UIEVENT_WINDOWSIZE, UIEVENT_SETCURSOR, UIEVENT_TIMER, UIEVENT__LAST, }; ///////////////////////////////////////////////////////////////////////////////////// // // 内部保留的消息 typedef enum MSGTYPE_UI { UIMSG_TRAYICON = WM_USER + 1,// 托盘消息 UIMSG_SET_DPI, // DPI WM_MENUCLICK, // 菜单消息 UIMSG_USER = WM_USER + 100, // 程序自定义消息 }; ///////////////////////////////////////////////////////////////////////////////////// // // Flags for CControlUI::GetControlFlags() #define UIFLAG_TABSTOP 0x00000001 #define UIFLAG_SETCURSOR 0x00000002 #define UIFLAG_WANTRETURN 0x00000004 // Flags for FindControl() #define UIFIND_ALL 0x00000000 #define UIFIND_VISIBLE 0x00000001 #define UIFIND_ENABLED 0x00000002 #define UIFIND_HITTEST 0x00000004 #define UIFIND_UPDATETEST 0x00000008 #define UIFIND_TOP_FIRST 0x00000010 #define UIFIND_ME_FIRST 0x80000000 // Flags used for controlling the paint #define UISTATE_FOCUSED 0x00000001 #define UISTATE_SELECTED 0x00000002 #define UISTATE_DISABLED 0x00000004 #define UISTATE_HOT 0x00000008 #define UISTATE_PUSHED 0x00000010 #define UISTATE_READONLY 0x00000020 #define UISTATE_CAPTURED 0x00000040 ///////////////////////////////////////////////////////////////////////////////////// // typedef struct UILIB_API tagTFontInfo { HFONT hFont; CDuiString sFontName; int iSize; bool bBold; bool bUnderline; bool bItalic; bool bStrikeout; TEXTMETRIC tm; } TFontInfo; typedef struct UILIB_API tagTImageInfo { tagTImageInfo(); Gdiplus::Image* pImage; HBITMAP hBitmap; LPBYTE pBits; LPBYTE pSrcBits; int nX; int nY; bool bAlpha; bool bUseHSL; CDuiString sResType; DWORD dwMask; } TImageInfo; typedef struct UILIB_API tagTDrawInfo { tagTDrawInfo(); void Parse(LPCTSTR pStrImage, LPCTSTR pStrModify, CPaintManagerUI *pManager); void Clear(); CDuiString sDrawString; CDuiString sDrawModify; CDuiString sImageName; CDuiString sResType; RECT rcDest; RECT rcSource; RECT rcCorner; DWORD dwMask; UINT uFade; UINT uRotate; bool bHole; bool bTiledX; bool bTiledY; bool bHSL; bool bGdiplus; CDuiSize szImage; RECT rcPadding; CDuiString sAlign; } TDrawInfo; typedef struct UILIB_API tagTPercentInfo { double left; double top; double right; double bottom; } TPercentInfo; typedef struct UILIB_API tagTResInfo { DWORD m_dwDefaultDisabledColor; DWORD m_dwDefaultFontColor; DWORD m_dwDefaultLinkFontColor; DWORD m_dwDefaultLinkHoverFontColor; DWORD m_dwDefaultSelectedBkColor; TFontInfo m_DefaultFontInfo; CStdStringPtrMap m_CustomFonts; CStdStringPtrMap m_ImageHash; CStdStringPtrMap m_AttrHash; CStdStringPtrMap m_StyleHash; CStdStringPtrMap m_DrawInfoHash; } TResInfo; // Structure for notifications from the system // to the control implementation. typedef struct UILIB_API tagTEventUI { int Type; CControlUI* pSender; DWORD dwTimestamp; POINT ptMouse; TCHAR chKey; WORD wKeyState; WPARAM wParam; LPARAM lParam; } TEventUI; // Drag&Drop control const TCHAR* const CF_MOVECONTROL = _T("CF_MOVECONTROL"); typedef struct UILIB_API tagTCFMoveUI { CControlUI* pControl; } TCFMoveUI; // Listener interface class INotifyUI { public: virtual void Notify(TNotifyUI& msg) = 0; }; // MessageFilter interface class IMessageFilterUI { public: virtual LRESULT MessageHandler(UINT uMsg, WPARAM wParam, LPARAM lParam, bool& bHandled) = 0; }; class ITranslateAccelerator { public: virtual LRESULT TranslateAccelerator(MSG *pMsg) = 0; }; class IDragDropUI { public: virtual bool OnDragDrop(CControlUI* pControl) { return false; } }; ///////////////////////////////////////////////////////////////////////////////////// // typedef CControlUI* (*LPCREATECONTROL)(LPCTSTR pstrType); class UILIB_API CPaintManagerUI : public CIDropTarget { public: CPaintManagerUI(); ~CPaintManagerUI(); public: void Init(HWND hWnd, LPCTSTR pstrName = NULL); // ------------------------------------------------------------------ // 真模态(DialogBox)支持:把本 manager 从原 paint window 上摘下来 / 挂到新窗口上。 // // 背景:Windows 的 DialogBox 机制不允许调用方指定 HWND,只能由系统创建对话框窗口。 // 而调用方是「先 Create 再 ShowModal」,所以 ShowModal 里必须把已经 Init 好的 manager // 从旧 HWND 迁移到对话框的新 HWND —— 但**不能**重新走 Init(Init 会清空字体/图片/样式 // 等已加载资源,且会重复 Add 到 m_aPreMessages)。因此这里提供一对轻量的挂载/摘除。 // // DetachPaintWindow:释放旧 DC、从 m_aPreMessages 摘除,但**保留** m_hWndPaint 之外的 // 全部状态(控件树、资源、定时器、字体…)。 // AttachPaintWindow:绑定新 HWND 与 DC,重新加入 m_aPreMessages。 // ------------------------------------------------------------------ void DetachPaintWindow(); void AttachPaintWindow(HWND hWnd); bool IsUpdateNeeded() const; void NeedUpdate(); void Invalidate(); void Invalidate(RECT& rcItem); LPCTSTR GetName() const; HDC GetPaintDC() const; HWND GetPaintWindow() const; HWND GetTooltipWindow() const; int GetHoverTime() const; void SetHoverTime(int iTime); POINT GetMousePos() const; SIZE GetClientSize() const; SIZE GetInitSize(); void SetInitSize(int cx, int cy); RECT GetSizeBox(); void SetSizeBox(RECT& rcSizeBox); RECT GetCaptionRect(); void SetCaptionRect(RECT& rcCaption); SIZE GetRoundCorner(); void SetRoundCorner(int cx, int cy); SIZE GetMinInfo(); void SetMinInfo(int cx, int cy); SIZE GetMaxInfo(); void SetMaxInfo(int cx, int cy); bool IsShowUpdateRect(); void SetShowUpdateRect(bool show); bool IsNoActivate(); void SetNoActivate(bool bNoActivate); BYTE GetOpacity() const; void SetOpacity(BYTE nOpacity); bool IsLayered(); void SetLayered(bool bLayered); RECT& GetLayeredInset(); void SetLayeredInset(RECT& rcLayeredInset); BYTE GetLayeredOpacity(); void SetLayeredOpacity(BYTE nOpacity); LPCTSTR GetLayeredImage(); void SetLayeredImage(LPCTSTR pstrImage); CShadowUI* GetShadow(); void SetUseGdiplusText(bool bUse); bool IsUseGdiplusText() const; void SetGdiplusTextRenderingHint(int trh); int GetGdiplusTextRenderingHint() const; static HINSTANCE GetInstance(); static CDuiString GetInstancePath(); static CDuiString GetCurrentPath(); static HINSTANCE GetResourceDll(); static const CDuiString& GetResourcePath(); static const CDuiString& GetResourceZip(); static const CDuiString& GetResourceZipPwd(); static bool IsCachedResourceZip(); static HANDLE GetResourceZipHandle(); static void SetInstance(HINSTANCE hInst); static void SetCurrentPath(LPCTSTR pStrPath); static void SetResourceDll(HINSTANCE hInst); static void SetResourcePath(LPCTSTR pStrPath); static void SetResourceZip(LPVOID pVoid, unsigned int len, LPCTSTR password = NULL); static void SetResourceZip(LPCTSTR pstrZip, bool bCachedResourceZip = false, LPCTSTR password = NULL); static void SetResourceType(int nType); static int GetResourceType(); static bool GetHSL(short* H, short* S, short* L); static void SetHSL(bool bUseHSL, short H, short S, short L); // H:0~360, S:0~200, L:0~200 static void ReloadSkin(); static CPaintManagerUI* GetPaintManager(LPCTSTR pstrName); static CStdPtrArray* GetPaintManagers(); static bool LoadPlugin(LPCTSTR pstrModuleName); static CStdPtrArray* GetPlugins(); bool IsForceUseSharedRes() const; void SetForceUseSharedRes(bool bForce); // 注意:只支持简单类型指针,因为只释放内存,不会调用类对象的析构函数 void DeletePtr(void* ptr); DWORD GetDefaultDisabledColor() const; void SetDefaultDisabledColor(DWORD dwColor, bool bShared = false); DWORD GetDefaultFontColor() const; void SetDefaultFontColor(DWORD dwColor, bool bShared = false); DWORD GetDefaultLinkFontColor() const; void SetDefaultLinkFontColor(DWORD dwColor, bool bShared = false); DWORD GetDefaultLinkHoverFontColor() const; void SetDefaultLinkHoverFontColor(DWORD dwColor, bool bShared = false); DWORD GetDefaultSelectedBkColor() const; void SetDefaultSelectedBkColor(DWORD dwColor, bool bShared = false); TFontInfo* GetDefaultFontInfo(); void SetDefaultFont(LPCTSTR pStrFontName, int nSize, bool bBold, bool bUnderline, bool bItalic, bool bStrikeout, bool bShared = false); DWORD GetCustomFontCount(bool bShared = false) const; void AddFontArray(LPCTSTR pstrPath); HFONT AddFont(int id, LPCTSTR pStrFontName, int nSize, bool bBold, bool bUnderline, bool bItalic, bool bStrikeout, bool bShared = false); HFONT GetFont(int id); HFONT GetFont(LPCTSTR pStrFontName, int nSize, bool bBold, bool bUnderline, bool bItalic, bool bStrikeout); int GetFontIndex(HFONT hFont, bool bShared = false); int GetFontIndex(LPCTSTR pStrFontName, int nSize, bool bBold, bool bUnderline, bool bItalic, bool bStrikeout, bool bShared = false); void RemoveFont(HFONT hFont, bool bShared = false); void RemoveFont(int id, bool bShared = false); void RemoveAllFonts(bool bShared = false); TFontInfo* GetFontInfo(int id); TFontInfo* GetFontInfo(HFONT hFont); const TImageInfo* GetImage(LPCTSTR bitmap); const TImageInfo* GetImageEx(LPCTSTR bitmap, LPCTSTR type = NULL, DWORD mask = 0, bool bUseHSL = false, bool bGdiplus = false, HINSTANCE instance = NULL); const TImageInfo* AddImage(LPCTSTR bitmap, LPCTSTR type = NULL, DWORD mask = 0, bool bUseHSL = false, bool bGdiplus = false, bool bShared = false, HINSTANCE instance = NULL); const TImageInfo* AddImage(LPCTSTR bitmap, HBITMAP hBitmap, int iWidth, int iHeight, bool bAlpha, bool bShared = false); void RemoveImage(LPCTSTR bitmap, bool bShared = false); void RemoveAllImages(bool bShared = false); static void ReloadSharedImages(); void ReloadImages(); const TDrawInfo* GetDrawInfo(LPCTSTR pStrImage, LPCTSTR pStrModify); void RemoveDrawInfo(LPCTSTR pStrImage, LPCTSTR pStrModify); void RemoveAllDrawInfos(); void AddDefaultAttributeList(LPCTSTR pStrControlName, LPCTSTR pStrControlAttrList, bool bShared = false); LPCTSTR GetDefaultAttributeList(LPCTSTR pStrControlName) const; bool RemoveDefaultAttributeList(LPCTSTR pStrControlName, bool bShared = false); void RemoveAllDefaultAttributeList(bool bShared = false); void AddWindowCustomAttribute(LPCTSTR pstrName, LPCTSTR pstrAttr); LPCTSTR GetWindowCustomAttribute(LPCTSTR pstrName) const; bool RemoveWindowCustomAttribute(LPCTSTR pstrName); void RemoveAllWindowCustomAttribute(); // 样式管理 void AddStyle(LPCTSTR pName, LPCTSTR pStyle, bool bShared = false); LPCTSTR GetStyle(LPCTSTR pName) const; BOOL RemoveStyle(LPCTSTR pName, bool bShared = false); const CStdStringPtrMap& GetStyles(bool bShared = false) const; void RemoveAllStyle(bool bShared = false); const TImageInfo* GetImageString(LPCTSTR pStrImage, LPCTSTR pStrModify = NULL); // 初始化拖拽 bool EnableDragDrop(bool bEnable); void SetDragDrop(IDragDropUI* pDragDrop); virtual bool OnDrop(FORMATETC* pFmtEtc, STGMEDIUM& medium,DWORD *pdwEffect); bool IsValid(); bool AttachDialog(CControlUI* pControl); bool InitControls(CControlUI* pControl, CControlUI* pParent = NULL); void ReapObjects(CControlUI* pControl); bool AddOptionGroup(LPCTSTR pStrGroupName, CControlUI* pControl); CStdPtrArray* GetOptionGroup(LPCTSTR pStrGroupName); void RemoveOptionGroup(LPCTSTR pStrGroupName, CControlUI* pControl); void RemoveAllOptionGroups(); CControlUI* GetFocus() const; void SetFocus(CControlUI* pControl); void SetFocusNeeded(CControlUI* pControl); bool SetNextTabControl(bool bForward = true); bool SetTimer(CControlUI* pControl, UINT nTimerID, UINT uElapse); bool KillTimer(CControlUI* pControl, UINT nTimerID); void KillTimer(CControlUI* pControl); void RemoveAllTimers(); void SetCapture(); void ReleaseCapture(); bool IsCaptured(); bool IsPainting(); void SetPainting(bool bIsPainting); bool AddNotifier(INotifyUI* pControl); bool RemoveNotifier(INotifyUI* pControl); void SendNotify(TNotifyUI& Msg, bool bAsync = false); void SendNotify(CControlUI* pControl, LPCTSTR pstrMessage, WPARAM wParam = 0, LPARAM lParam = 0, bool bAsync = false); bool AddPreMessageFilter(IMessageFilterUI* pFilter); bool RemovePreMessageFilter(IMessageFilterUI* pFilter); bool AddMessageFilter(IMessageFilterUI* pFilter); bool RemoveMessageFilter(IMessageFilterUI* pFilter); int GetPostPaintCount() const; bool IsPostPaint(CControlUI* pControl); bool AddPostPaint(CControlUI* pControl); bool RemovePostPaint(CControlUI* pControl); bool SetPostPaintIndex(CControlUI* pControl, int iIndex); int GetNativeWindowCount() const; RECT GetNativeWindowRect(HWND hChildWnd); bool AddNativeWindow(CControlUI* pControl, HWND hChildWnd); bool RemoveNativeWindow(HWND hChildWnd); void AddDelayedCleanup(CControlUI* pControl); void AddMouseLeaveNeeded(CControlUI* pControl); bool RemoveMouseLeaveNeeded(CControlUI* pControl); bool AddTranslateAccelerator(ITranslateAccelerator *pTranslateAccelerator); bool RemoveTranslateAccelerator(ITranslateAccelerator *pTranslateAccelerator); bool TranslateAccelerator(LPMSG pMsg); CControlUI* GetRoot() const; CControlUI* FindControl(POINT pt) const; CControlUI* FindControl(LPCTSTR pstrName) const; CControlUI* FindSubControlByPoint(CControlUI* pParent, POINT pt) const; CControlUI* FindSubControlByName(CControlUI* pParent, LPCTSTR pstrName) const; CControlUI* FindSubControlByClass(CControlUI* pParent, LPCTSTR pstrClass, int iIndex = 0); CStdPtrArray* FindSubControlsByClass(CControlUI* pParent, LPCTSTR pstrClass); static void MessageLoop(); static bool TranslateMessage(const LPMSG pMsg); static void Term(); CDPI* GetDPIObj(); void ResetDPIAssets(); void RebuildFont(TFontInfo* pFontInfo); void SetDPI(int iDPI); static void SetAllDPI(int iDPI); bool MessageHandler(UINT uMsg, WPARAM wParam, LPARAM lParam, LRESULT& lRes); bool PreMessageHandler(UINT uMsg, WPARAM wParam, LPARAM lParam, LRESULT& lRes); void UsedVirtualWnd(bool bUsed); // 触屏支持(参考 NIM Duilib 模型,库级统一接管:主窗口 + 所有弹框通用)。 // // 【重要】滑动逻辑与输入通道解耦(Begin/Update/End 三个共享函数): // Win10 上同一次触摸会在**两条互相独立**的通道上产生消息 —— // WM_TOUCH 与 WM_POINTER*(Win8+ 指针栈)。哪条通道驱动本次手势是运行时决定的 // (本台收银机:主窗口常常 WM_TOUCH 先到;弹框 / 被指针抢走的手势只有指针)。 // // 早期版本把"滑动"的全部实现(1:1 跟手平移 / 拖滚动条滑块 / Δy→滚轮)只写在 // WM_TOUCH 通道里。于是指针通道一旦抢到手势,就出现两个症状: // · 「滑不动」—— 没有任何滑动逻辑被执行; // · 「滑动反倒触发点击」—— 指针把 UP 归一化成 WM_LBUTTONUP 走原生点击路径, // 而 tap/drag 判定所需的 m_bTouchMoved 从未被置真。 // 现在抽成通道无关的 Begin/Update/End,两条通道都调用同一套 —— 行为必然一致。 // // 派发给命中控件的事件语义(两通道统一): // DOWN → UIEVENT_TOUCHDOWN 滚动条据此抓滑块;容器据此记手势起点(其余控件忽略) // MOVE → UIEVENT_TOUCHMOVE CScrollBarUI 消费则拖滑块;否则冒泡给 CContainerUI 做 1:1 跟手平移 // UP → UIEVENT_TOUCHUP 滚动条据此松开滑块;容器收尾(其余控件忽略) // // 按钮"点击"由 **End 的返回值(tap 判定)** 统一决定是否合成 BUTTONDOWN/BUTTONUP: // 位移超过 8px 阈值 ⇒ 判为拖拽 ⇒ **不**触发点击(这是"滑动误点"的防线); // 例外:位移虽大但抬起时仍落在同一控件上、且该控件不在可滚动容器内 ⇒ 仍算轻点 // (吸收手指按下时的轻微抖动,否则弹框里的小按钮会点不动)。 // // 注册在 Init 中完成,无需逐窗口样板。 void InitTouchSupport(); void OnTouchInput(WPARAM wParam, LPARAM lParam); // ------------------------------------------------------------------ // [手势滑动] 与输入通道无关的"手指拖动"核心逻辑。 // // 为什么需要把它单独抽出来:WM_TOUCH 与 WM_POINTER* 是两条**互相独立**的通道 // (本台收银机上弹框只拿得到指针通道,主窗口有时也被指针抢先),而"滑动"的全部 // 实现(1:1 跟手平移 / 拖滚动条滑块 / Δy→滚轮)原先只写在 WM_TOUCH 的 // OnTouchInput 里。于是指针通道一旦抢到本次手势: // · WM_TOUCH 整条被 OnTouchInput 忽略 ⇒ 没有任何滑动逻辑被执行 ⇒ 「滑不动」; // · 指针通道把 UP 归一化成 WM_LBUTTONUP 走原生点击路径, // tap/drag 判定(m_bTouchMoved)从未被触摸链置真 ⇒「滑动反倒触发点击」。 // 现在把这段逻辑抽成通道无关的公共函数,两条通道都调用它,行为立刻一致。 // // 调用约定(两条通道都必须按这个顺序调): // BeginSlideGesture(pt) —— DOWN 时调一次:记起点、命中控件、找可滚动容器; // UpdateSlideGesture(pt) —— MOVE/UPDATE 时调:按 Δ 派发 UIEVENT_TOUCHMOVE(+合成滚轮); // EndSlideGesture(pt) —— UP 时调:派发 UIEVENT_TOUCHUP 并返回"是否算一次轻点"。 // 详见各函数实现处的长注释。 // ------------------------------------------------------------------ void BeginTouchSlideGesture(POINT pt); void UpdateTouchSlideGesture(POINT pt); // 返回 true 表示"本次手势应当按轻点处理"(未越过拖拽阈值)。 bool EndTouchSlideGesture(POINT pt); // [对标 nim_duilib] WM_POINTER*(Win8+ 指针栈)处理:把指针消息归一化成鼠标消息, // 与 WM_TOUCH 归一化形成互补通道。返回 true 表示已消费该消息。 // 说明:本项目 _WIN32_WINNT=0x0601,WM_POINTER* 宏在编译期不可见, // 故 uMsg 用字面量常量比较(见 UIManager.cpp 的 PointerMsg 命名空间)。 bool OnPointerInput(UINT uMsg, WPARAM wParam, LPARAM lParam); void ClearTouchHoverState(POINT pt); // [触摸点击 · 兜底] 清一个控件的"按下/悬浮"视觉(PUSHED | CAPTURED | HOT)。 // // ⚠️ 这是**兜底**,不是主要修复手段 —— 主要修复在控件侧 // (`CDiandanFoodItemUI::DoEvent` 已按 NIM 的顺序改为"先复位、后 SendNotify")。 // // 只在三种"没人会替我们收尾"的路径上调用(见 TouchUp): // (a) 手势被判为滑动(不派发点击); // (b) 轻点但没有候选点击目标; // (c) 抬手点已被上层窗口(弹框)覆盖(`TOUCHUP ABORT covered-by-overlay`)。 // 另外鼠标路径的 WM_LBUTTONUP 也用它。 // // ⚠️⚠️ 绝不能在"派发 BUTTONDOWN 之前"调用! ⚠️⚠️ // 曾有一版在 TouchUp 派发前先调它,结果**所有商品卡的按压态都不显示了**。 // 原因:触摸链的 BUTTONDOWN/BUTTONUP 是**同一次调用里背靠背合成**的, // 中间没有消息循环、没有重绘机会。事前清一次 ⇒ 等于"先擦、再置(pressed)、 // 再复位(normal)"三步连跑,pressed 帧一帧都画不出来。 // 鼠标路径能看见 pressed,是因为它的 DOWN/UP 是**两条独立消息**,中间会重绘。 // ⇒ 正确做法是让**控件自己**在 `UIEVENT_TOUCHDOWN` 时置 pressed // (按下瞬间就能重绘),抬起时再复位。 // // 参照 NIM_Duilib:`Control::ButtonUp` 里 `m_uButtonState = kControlStateNormal` // 出现在 `Activate()`(= 发 CLICK)**之前** —— 复位必须早于业务回调。 // 本函数把这个"复位"独立出来,供兜底路径使用。 // // ⚠️ 只处理传入的那一个控件(不向上遍历祖先链): // 会挂按下态的只有被点击的控件本身(BUTTONDOWN 不冒泡),祖先从未被置位。 // pControl 传 NULL 时按 pt 命中查找。 // // ⚠️ 只清显示位:不发通知、不 Activate。 // ⚠️ 只对"库内按钮"(`DUI_CTR_BUTTON`)生效 —— 自定义控件(如商品卡) // 不吃 UISTATE_*,复位必须由它自己的 DoEvent 负责。 void ClearPushedStateAt(CControlUI* pControl, POINT pt); // ------------------------------------------------------------------ // [触摸点击 · 顺序修复的核心] 触摸链的"抬起"处理:收尾 + 按正确顺序派发点击。 // // ⚠️⚠️ 前提事实(真机日志实测,务必记住,别再假设)⚠️⚠️ // 本机 Win10 的触摸**只产生 WM_POINTER\***,**不会**由系统提升出 // WM_LBUTTONDOWN / WM_LBUTTONUP(`fromtouch=1` 的 LBUTTON 消息数为 **0**)。 // ⇒ 触摸链**必须自己派发**点击事件,绝不能指望"放行原生鼠标消息"。 // (曾经按"放行原生消息"改过一版,结果整机点不动 —— 因为压根没有那条消息。) // // 背景(用户真机 Win10 bug · 两个): // ① 弹框里的按钮会"透传" —— 点弹框的「选好了」,弹框关闭后, // 屏幕同位置的**主窗口商品卡**也被点了一次; // ② 所有商品卡的**按压态都不显示**了。 // // 根因(参照 NIM_Duilib 的真实实现得出): // ② 曾有一版在派发点击**之前**先 `ClearPushedStateAt`。触摸链的 // BUTTONDOWN/BUTTONUP 是同一次调用背靠背合成的,中间无重绘 // ⇒ "先擦、再置 pressed、再复位 normal"三段连跑,pressed 帧画不出来。 // ① `m_pEventClick` 是 **DOWN 时刻**记的(那时弹框还不存在); // 抬手时弹框已盖在手指上,但指针仍指向背后的主窗口卡片 // ⇒ 我们把点击派发给了它。鼠标路径没这问题是因为真模态弹框 // (独立 `#32770` HWND + 独立 manager)独占捕获与消息投递。 // // 修法(顺序修复 + 死线复核,全程无时间戳): // 0) 复核心脏判据 —— `pClick->GetManager() == this` // 且 `FindControl(pt)` 沿祖先链仍命中 `pClick`; // 不满足 ⇒ `TOUCHUP ABORT ...`,只收尾、**绝不 SendNotify**(治 ①)。 // 1) 收掉 hover 登记(`m_pEventHover` + `m_aNeedMouseLeaveNeeded`), // **不碰按下态**; // 2) 派发 BUTTONDOWN / BUTTONUP —— 让业务回调(ShowModal)跑。 // 按下态的置/复完全由控件自己的 DoEvent 负责: // `UIEVENT_TOUCHDOWN` → pressed(按下瞬间就画出来,治 ②) // `UIEVENT_BUTTONUP` → **先** SetCardState("normal") **再** SendNotify // (详见 CDiandanFoodItemUI::DoEvent 的注释,与 NIM `Control::ButtonUp` // 的 `先复位 → Activate()` 同构。) // 3) 末尾 `ClearPushedStateAt(NULL, pt)` 只对 `DUI_CTR_BUTTON` 窄类兜底。 // // 全程**无时间戳**:只由"本次手势是否算轻点"决定,无任何超时。 // ------------------------------------------------------------------ bool TouchUp(const POINT& pt, bool bTapLike); // 记录"跨窗口通用的抑制参考点":把本窗口客户区点换算成屏幕坐标后写进 // **线程级**参考点 `s_ptLastTouchScreen`(定义在 UIManager.cpp 文件头, // 那里有"为什么不能用 manager 成员"的完整说明)。 // 约定:凡是写了 m_ptLastTouchPos 的**触摸**路径,都应同步调一次本函数 —— // 二者是同一个物理点的两种坐标表达,缺了屏幕坐标那份,"窗口一换判据就失准"。 void SetLastTouchScreenPoint(POINT ptClient); // ------------------------------------------------------------------ // [WM_TOUCH 归属权] 未被文档化的关键机制与"补调 DefWindowProc"钩子。 // // 机制(来源:The Witness 作者逆向结论 + MSDN Architectural Overview): // `WM_TOUCH` **不会**由系统直接投递。系统先发一条"特殊标记的" // `WM_MOUSEMOVE`(开了 raw input 时是 `WM_INPUT`); // **只有这条消息一路走到 `DefWindowProc` 里**,DefWindowProc 才会 // 把驻留在窗口上的 `WM_TOUCH` "翻译"出来并派发。 // // 推论:任何一层**吞掉**这条被标记的 `WM_MOUSEMOVE` 且不回落到 // `DefWindowProc`,就会**永久掐断该窗口后续 WM_TOUCH 的派发链**。 // // 本项目的两个真实断点(都在 MessageHandler 的 WM_MOUSEMOVE 分支): // · `if (bPromotedTouchMove && m_pEventTouch != NULL) { ... return true; }` // —— 触摸手势进行中,丢弃"提升出来的鼠标移动"; // · `if (s_bSuppressHoverAfterTouch) { ... return true; }` // —— 触摸抬手后抑制悬浮态复活,丢弃合成移动(该状态是**线程级**的, // 定义在 UIManager.cpp 文件头;不是本类的成员)。 // 两者都 `return true`,顺着 MessageHandler → HandleMessage 直接返回, // **跳过了 DefWindowProc** ⇒ 弹框(#32770 对话框类就是靠 DefDlgProc→DefWindowProc // 这条链来"翻译"WM_TOUCH 的)永远收不到 WM_TOUCH。 // 这与实测完全吻合:主窗口有 `WNDPROC-TOUCH`、弹框零条。 // // 钩子用法:窗口过程(__WndProc / __DialogWndProc)在构造时把自己的 // "原窗口过程调用入口"注册进来;MessageHandler 在**吞掉那条被标记的 // WM_MOUSEMOVE 之前**调用一次 `RelayDefWindowProc(...)`, // 让系统的"翻译"链继续跑 —— 我们仍然吃掉消息、不破坏 DuiLib 状态机, // 只是补一次 DefWindowProc 的副作用。 // // 默认**关闭**(见 UIManager.cpp 的 m_bRelayDefProcForTouchMove):这是一个 // 尚未在本机验证的机制假设,先做成可开关 + 带日志,验证通过再考虑默认开启。 // ------------------------------------------------------------------ // 钩子签名:就是一个"把消息喂给原窗口过程"的调用入口。 // 参数与普通 WNDPROC 一致;返回值同样是 LRESULT(本场景只关心副作用)。 typedef LRESULT (CALLBACK* PFN_RELAY_DEFPROC)(HWND, UINT, WPARAM, LPARAM); void SetDefProcRelay(PFN_RELAY_DEFPROC pfnRelay, HWND hWndTarget); void ClearDefProcRelay(HWND hWndTarget); PFN_RELAY_DEFPROC GetDefProcRelay() const { return m_pfnDefProcRelay; } // 当前钩子绑定的窗口。窗口过程用它判断"是否需要为本窗口(重新)绑定"—— // 真模态弹框复用的是同一个 manager 对象,必须靠这个判据在换 HWND 时重新绑定。 HWND GetDefProcRelayBoundWindow() const { return m_hWndDefProcRelay; } // 让"被吞掉的那条触摸提升鼠标消息"补走一次原窗口过程(仅当开关开启)。 void RelayDefWindowProc(UINT uMsg, WPARAM wParam, LPARAM lParam, LPCTSTR pszWhy); bool IsDefProcRelayEnabled() const { return m_bRelayDefProcForTouchMove; } void SetDefProcRelayEnabled(bool bEnable) { m_bRelayDefProcForTouchMove = bEnable; } // 当前处于模态状态的弹框句柄(由 CWindowWnd::ShowModal 设置/清除)。 // 目的是:万一 Windows 把 WM_TOUCH 错投给了被弹框挡住的 owner(主窗口), // 非模态窗口一律忽略触摸,避免“弹框还开着、主窗口却在响应手指”的穿透。 static void SetModalWindow(HWND hWnd); static HWND GetModalWindow(); // ------------------------------------------------------------------ // ★【Win7"咚"声 · 根治】"提升副本在途"标志的只读快照。 // // 真 ⇒ 触摸通道确实收到了本次手势,它的提升鼠标副本正排在队列里等着被投递。 // 用途:`CWindowWnd::ShowModal` 进模态循环之前用它决定"要不要把队列里那份 // 在途副本摘掉" —— 因为系统对"投给**禁用**窗口的鼠标消息"的默认处理是 // `MessageBeep(0)` + 退回队列,那就是 Win7 上那一声"咚"。 // // 判据只依赖"触摸通道收到了这次手势"这一件事(与 PROMOTED-DUP 同源、同一处置位), // 与模态边界 / 代数水位 / extra 指纹全都无关。详见 UIManager.cpp 里 // `IsPromotedMouseDuplicatePending` / `DropQueuedPromotedMouseMessages` 的定义处。 // ------------------------------------------------------------------ static bool IsPromotedMouseDuplicatePending(); // ★【Win7"咚"声 · 根治】把"本次手势在途的提升鼠标副本"从本线程消息队列里 // **就地摘掉**,返回摘掉的条数(0 = 队列里本来就没有)。 // // 调用点:`CWindowWnd::ShowModal` 进模态循环之前 —— 那一刻副本必然已排在队列里, // 而"摘除"与"创建弹框"之间没有消息泵 ⇒ 摘除对投递而言是原子的。 // 原理与"为什么这样不会误伤",见 UIManager.cpp 里本函数的定义处。 static int DropQueuedPromotedMouseMessages(HWND hWndTarget); // ------------------------------------------------------------------ // ★【Win7 · Combo 下拉框"点了不出来"】"刚刚真的投递过一条提升鼠标点击"的一次性受理口。 // // 真机现象:Win7 上点 Combo 有时"点了没反应",要连点 2~4 下才弹出 // (日志实证:下拉框建好之后 15~46ms 就被 WM_KILLFOCUS 关掉,10 次失败 10 次如此)。 // // 根因不在下拉框自己:Win7 会把**同一次触摸**提升成鼠标消息,而这份副本是在 // **触摸抬起之后**才投递的。系统对"把鼠标点击投给非前台窗口"的处理是先激活该窗口, // 而且**激活发生在进入窗口过程之前**(任何 WndProc 里的闸门都拦不住)。 // 于是:owner 被激活 ⇒ 刚 ShowWindow 出来的下拉框收到 WM_KILLFOCUS ⇒ 自己关掉。 // // 判定分两步(都只看状态,无时间戳),见 UICombo.cpp 的 DecideAfterFocusLoss: // ① 焦点丢失之后**紧跟着**真的投递了一条提升鼠标点击(本函数); // ② 该次点击的落点仍在这个 Combo 自己身上。 // ①+② 同时成立 ⇒ 这次焦点丢失是"打开我那一出手势的回声",不是用户点了别处。 // // 一次性语义:读完即清(调用方每次判定只关心"自上次判定以来")。 // // ⚠️【2026-09-22 真机日志修正】原来这里写着"Win10 不产生提升副本 ⇒ 恒返回 false // ⇒ 调用方行为零变化"。**那个前提是错的**:这台 Win10 收银机**确实**会产生提升 // 副本(60 秒日志里 54 对 0x0201/0x0202),只是它们的归属窗口全是主窗口、 // 而非下拉框所在窗口。48 次判定里 47 次返回真 —— 因为它按"自上次判定以来" // 结算,而每次手势的副本都落在那次手势自己的判定**之后**,于是恒被**下一次** // 判定收走 ⇒ 这个标志在稳态下**恒为真、不携带任何信息**。 // ⇒ 下拉框的判定**不得**再以它作为必要条件(见 CComboWnd::DecideAfterFocusLoss)。 // ------------------------------------------------------------------ static bool ConsumePromotedMouseMsgJustSeen(); // ------------------------------------------------------------------ // ★【Win10 Combo 下拉框"再点一下收不起来 / item 点一下没反应"· 根治的判据源】 // // 返回"**最近一次点击类输入**发生在屏幕上的哪个点": // · 最近一次是**触摸** ⇒ 返回触摸链记录的真实触点(BeginTouchSlideGesture 里 // 由 ClientToScreen 换算的屏幕坐标,两通道共用同一处); // · 最近一次是**鼠标** ⇒ 返回 `::GetCursorPos()`。 // // 消费点:`CComboWnd::DecideAfterFocusLoss` —— 它要回答"这一下焦点丢失,是不是 // 用户正在点我这个 owner Combo"。**必须用触摸触点,不能用 GetCursorPos()**: // 系统的触摸提升才会挪光标,而本项目的 Combo 开关是触摸链**合成**的点击、 // 发生在提升之前 ⇒ 触摸下 GetCursorPos() 读到的是**上一手**的位置。 // 真机实证:`COMBO-INIT begin cursor=768,251` 而本手触点是 (789,153), // 768,251 正是上一手点在下拉列表里的位置。详见本函数在 UIManager.cpp 的定义处。 // // 【自限】从未有过触摸(纯鼠标机器)⇒ 恒走 GetCursorPos() ⇒ 与改动前逐字一致。 // ------------------------------------------------------------------ static POINT GetLastInputScreenPoint(); private: CStdPtrArray* GetFoundControls(); static CControlUI* CALLBACK __FindControlFromNameHash(CControlUI* pThis, LPVOID pData); static CControlUI* CALLBACK __FindControlFromCount(CControlUI* pThis, LPVOID pData); static CControlUI* CALLBACK __FindControlFromPoint(CControlUI* pThis, LPVOID pData); static CControlUI* CALLBACK __FindControlFromTab(CControlUI* pThis, LPVOID pData); static CControlUI* CALLBACK __FindControlFromShortcut(CControlUI* pThis, LPVOID pData); static CControlUI* CALLBACK __FindControlFromName(CControlUI* pThis, LPVOID pData); static CControlUI* CALLBACK __FindControlFromClass(CControlUI* pThis, LPVOID pData); static CControlUI* CALLBACK __FindControlsFromClass(CControlUI* pThis, LPVOID pData); static CControlUI* CALLBACK __FindControlsFromUpdate(CControlUI* pThis, LPVOID pData); static void AdjustSharedImagesHSL(); void AdjustImagesHSL(); void PostAsyncNotify(); private: CDuiString m_sName; HWND m_hWndPaint; //所附加的窗体的句柄 HDC m_hDcPaint; HDC m_hDcOffscreen; HDC m_hDcBackground; HBITMAP m_hbmpOffscreen; BYTE* m_pOffscreenBits; HBITMAP m_hbmpBackground; COLORREF* m_pBackgroundBits; // 提示信息 HWND m_hwndTooltip; TOOLINFO m_ToolTip; int m_iHoverTime; bool m_bNoActivate; bool m_bShowUpdateRect; // 触屏状态(库级统一接管后,每个窗口的 CPaintManagerUI 各持一份) // 鼠标点击仍由 WM_LBUTTONDOWN/UP 维护 m_pEventClick;触摸期间则由 // m_pEventTouch 和 m_bTouchMoved 独立维护,避免合成鼠标消息提前激活控件。 CControlUI* m_pEventTouch = NULL; // 本次触摸命中的控件 CContainerUI* m_pTouchScrollContainer = NULL; // 本次触摸对应的最近可滚动容器 POINT m_ptLastTouchPos = { 0, 0 }; // 上一包触点:用于去重 + 计算跟手滚动的位移增量 // 【已上移为线程级】原先这里有一个 `POINT m_ptLastTouchScreen` 成员,以及 // 下方一个 `bool m_bSuppressHoverAfterTouch` 成员。二者现在都是 // UIManager.cpp **文件级静态变量**(`s_ptLastTouchScreen` / // `s_bSuppressHoverAfterTouch`)。 // // 为什么不能用成员:属性弹框**自带独立的 CPaintManagerUI m_pm**,与主窗口的 // manager 不是同一个对象。在弹框上点「选好了」那次触摸记在**弹框**那份状态上; // 而弹框一销毁,系统补投的 `extra=0` WM_MOUSEMOVE 发给**主窗口**,落在主窗口的 // manager 上 —— 那份状态不知道刚发生过触摸,±8px 判据必然失败 ⇒ 解除抑制 ⇒ // 背后商品卡悬浮态残留。详见 UIManager.cpp 文件头的完整说明。 POINT m_ptTouchStart = { 0, 0 }; // 本次手势起点:用于判断是否发生位移(拖拽 vs 轻点) bool m_bTouchMoved = false; // 本次触摸是否发生位移(>阈值视为拖拽),松开时抑制点击 bool m_bSuppressPromotedTouchMouse = false; // 触摸点击已由 TOUCHUP 处理,丢弃随后提升的鼠标点击 // 本次 TOUCHDOWN 的时间戳。若 TOUCHUP 丢失(激活/前台被抢、手势被系统中断、窗口被遮挡等), // m_pEventTouch / m_bSuppressPromotedTouchMouse 会永久残留 → 之后所有提升的鼠标消息都被吞, // 该窗口表现为“突然完全点不动”。用这个时间戳做超时自愈(见 MessageHandler 开头)。 DWORD m_dwTouchDownTick = 0; // m_bSuppressPromotedTouchMouse 被置真时的时刻。 // 用途:该抑制位有可能在【没有 TOUCHDOWN】的情况下被置真(两条输入流交叉时), // 此时 m_dwTouchDownTick 仍为 0,若只按它计时则自愈永远不触发 → 点击永久失效。 // 所以单独记一个时间戳,保证"只要有抑制位,就一定会被超时复位"。 DWORD m_dwSuppressTick = 0; // 手指抬起后,Windows 仍会补发合成的 WM_MOUSEMOVE;且真实 WM_MOUSELEAVE 在 // m_bMouseTracking 为真时会【自己给自己重发 WM_MOUSEMOVE】(UIManager.cpp WM_MOUSELEAVE 分支), // 导致刚清掉的悬浮态立刻被重新点亮 → “悬浮态不消失 / 多个控件同时悬浮”。 // 抬起后置 true,丢弃随后这批“非真实”的 MOUSEMOVE 悬浮逻辑;一旦收到用户真实移动鼠标 // (这里用“距上次触摸位置明显移动”判定)即解除。 // 注:这是【第二道】保险。第一道(根治)在 WM_MOUSEMOVE 里:凡带 MOUSEEVENTF_FROMTOUCH // 标记的“触摸提升出来的鼠标消息”一律不参与悬浮 enter/leave —— 触摸没有“光标停留”语义, // 让这类消息参与必然出现“点过的控件一直高亮 / 多点几个后多个同时高亮”。 // // 【已上移为线程级】该标志原为成员 `m_bSuppressHoverAfterTouch`,现改为 // UIManager.cpp 文件级静态变量 `s_bSuppressHoverAfterTouch`。 // 原因:补投的 `extra=0` WM_MOUSEMOVE 可能落在**另一个 manager** 上 // (属性弹框自带独立 m_pm),成员级状态会“看不见刚刚的触摸”。 // 完整说明见 UIManager.cpp 文件头,以及本头文件 `m_ptLastTouchPos` 上方的注释。 // [对标 nim_duilib] 竖向滑动 → WM_MOUSEWHEEL 的累加器。 // 触摸只有位移增量,没有滚轮事件;把 TOUCHEVENTF_MOVE 的 Δy 累加,每满一个 // WHEEL_DELTA(120) 就合成一次 UIEVENT_SCROLLWHEEL,让只认滚轮的控件在触摸下也能滚动。 // 手势开始时(TOUCHDOWN)与手势结束时(TOUCHUP)都应清零,避免余量串到下一次手势。 int m_nTouchWheelAccum = 0; // [通道无关滑动] 本次手势的滑动状态是否已由 BeginTouchSlideGesture 建立。 // // 用途:指针通道(WM_POINTER*)在 DOWN 时也要走一遍 BeginTouchSlideGesture, // 才能在 UPDATE 里正确算出 Δ 并驱动容器/滚动条;UP 时也要走 // EndTouchSlideGesture 来收尾并拿到"是否算轻点"。 // 这个标志把"滑动状态机是否活着"与"本次手势由哪条通道驱动"解耦 —— // 两条通道都只是驱动者,滑动逻辑本身是共享的。 bool m_bTouchSlideActive = false; // [通道混合] 是否启用 WM_POINTER*(Win8+ 指针栈)归一化。 // // 为什么需要这个开关:Win10 上同一次触摸可能同时产生 // ① 提升出来的鼠标消息 WM_LBUTTONDOWN/UP // ② 指针消息 WM_POINTERDOWN/UP // ③ legacy 触摸消息 WM_TOUCH // 如果把这些一起归一化成鼠标消息,同一次点击会驱动多套点击状态机 ⇒ 互相吞 → // 【所有窗口点击全失效】(含主窗口、含真实鼠标)。实测踩过这个坑。 // // ⚠️ 历史教训(务必别再改回"二选一"): // 第一版策略是"默认关闭,收到真实 WM_TOUCH 就【永久】关闭本窗口的指针通道"。 // 结果主窗口修好了,真模态弹框却彻底没救: // 本台收银机上 WM_TOUCH 的"贪婪归属权"(MSDN:收到第一条触摸消息后, // 后续所有触摸消息都发给该窗口,直到【另一个窗口获得焦点】)卡在主窗口身上, // 弹框(被程序 SetFocus,不是触摸点出来的焦点)永远拿不到 WM_TOUCH, // 只拿得到 WM_POINTER*(指针消息按窗口命中投递,不受贪婪归属权约束); // 而指针通道被永久关掉 ⇒ 弹框既无 WM_TOUCH 又无指针 ⇒ 完全点不动。 // // 现在的策略:【始终开启 + 手势级让路(handover)】 // · 指针通道永远开着(本开关默认 true),保证弹框这条"只有指针"的窗口有活路; // · 谁先驱动本次手势,谁就赢:指针先 DOWN ⇒ 本次手势内吃掉 WM_TOUCH; // WM_TOUCH 先 DOWN ⇒ 本次手势内拒绝指针; // · 手势结束(UP/超时/被夺)后立刻解禁 —— 下一个手势可以换通道。 // 让路只加在**开始**(DOWN)上,因为同一手势两条通道的 DOWN 才是一对重复; // UP/MOVE 侧不做让路,避免某个通道的 UP 被误丢导致点一直悬着、成对状态卡死。 // ⚠️ 这条规则是踩过坑之后补上的、不可省:曾经让路连 WM_POINTERUP 一起吞, // 而 UP 是唯一能解除 m_bPointerGestureActive 的消息 ⇒ 状态永久为真 // ⇒ 之后每个 WM_POINTERDOWN 都被当"重复按下"挡掉 ⇒【整个窗口触摸点击全灭】。 // 现在 UP 一律放行到 OnPointerInput,由它决定"只清状态"(让路中)还是"派发点击"。 // 状态的"自愈"由 3 秒超时兜底(见下面的 m_dwPointerDownTick)。 bool m_bPointerNormalizeEnabled = true; // ---- [NIM 风格] 通道归属:两个标记 + 一个计时器 ---- // // 这套结构对照 nim_duilib Core/Window.cpp 的 OnPointerMsgs / HandleMessage: // · nim 用 m_pEventTouch(WM_TOUCH 占用)与 m_pEventPointer(指针占用)两个 // **控件指针**做归属标记,谁先建立谁赢,另一条通道看到标记即退让; // · 本项目的等价物:m_pEventTouch(触摸占用)与 m_bPointerGestureActive(指针占用)。 // // ⚠️⚠️ 最关键的教训:【不要让同一个字段同时表示"归属"和"计时"】⚠️⚠️ // 曾经用一个 m_dwSuppressPointerUntilUpTick 时间戳身兼两职,结果 // (GetTickCount() - 该值) < 3000 恒成立 ⇒ 指针通道自己驱动的手势被自己判定为 // "让路中" ⇒ UP 走"只清状态、不合成点击"的早退路 ⇒ 点击全灭(真机日志可复现)。 // 现在严格分离:归属用 bool 标记,计时用专用 tick 字段,互不干扰。 // // m_bPointerGestureActive: // 本窗口当前正在由【指针通道】驱动一次手势(已 DOWN、还没 UP)。 // 为真期间,进入 OnTouchInput 的 WM_TOUCH 一律忽略(含其 TOUCHDOWN/TOUCHUP)。 bool m_bPointerGestureActive = false; // // m_dwActivePointerId: // m_bPointerGestureActive 为真时,对应的是哪个 pointerId(wParam 低 16 位)。 // 多指时不相关的第二、三指消息直接放行/忽略,不会把状态机搅乱。 DWORD m_dwActivePointerId = 0; // // m_dwPointerDownTick: // 指针手势的开始时刻,**仅**用于 3 秒超时自愈。 // ⚠️ 与 m_dwActivePointerId 配套,只在指针 DOWN 时置、在 UP/CAPTURECHANGED/超时时清。 // 绝不参与任何"归属判定"——归属只看 m_bPointerGestureActive。 DWORD m_dwPointerDownTick = 0; // ---- "补调 DefWindowProc" 钩子(修复弹框收不到 WM_TOUCH)---- // 见上方 SetDefProcRelay / RelayDefWindowProc 的长注释: // 被吞掉的触摸提升 WM_MOUSEMOVE 需要补走一次原窗口过程, // 否则系统的 WM_TOUCH "翻译"链会断掉,该窗口永远收不到 WM_TOUCH。 PFN_RELAY_DEFPROC m_pfnDefProcRelay = NULL; // 由窗口过程注册(一般为 m_OldWndProc 的包装) HWND m_hWndDefProcRelay = NULL; // 该钩子绑定的窗口(换窗口时校验用) // 开关:**默认关闭**。开启后 MessageHandler 在吞掉那两条触摸提升 // WM_MOUSEMOVE 前会补调一次原窗口过程,把系统的 WM_TOUCH 翻译链接上。 // // 为什么默认关:这是一个"未经本机验证"的机制假设,贸然默认开启风险不可控 // (最坏情况是主窗口本来正常的 WM_TOUCH 反而被干扰)。 // // 如何开启(无需重新编译): // 设环境变量 `DUI_RELAY_DEFPROC=1` 再启动程序即可(见构造函数里的读取)。 // 也可以在代码里调 SetDefProcRelayEnabled(true)。 bool m_bRelayDefProcForTouchMove = false; // CControlUI* m_pRoot; CControlUI* m_pFocus; CControlUI* m_pEventHover; CControlUI* m_pEventClick; CControlUI* m_pEventRClick; CControlUI* m_pEventKey; CControlUI* m_pLastToolTip; // POINT m_ptLastMousePos; SIZE m_szMinWindow; SIZE m_szMaxWindow; SIZE m_szInitWindowSize; RECT m_rcSizeBox; SIZE m_szRoundCorner; RECT m_rcCaption; UINT m_uTimerID; bool m_bFirstLayout; bool m_bUpdateNeeded; bool m_bFocusNeeded; bool m_bOffscreenPaint; BYTE m_nOpacity; bool m_bLayered; RECT m_rcLayeredInset; bool m_bLayeredChanged; RECT m_rcLayeredUpdate; TDrawInfo m_diLayered; bool m_bMouseTracking; bool m_bMouseCapture; bool m_bIsPainting; bool m_bUsedVirtualWnd; bool m_bAsyncNotifyPosted; // CStdPtrArray m_aNotifiers; CStdPtrArray m_aTimers; CStdPtrArray m_aTranslateAccelerator; CStdPtrArray m_aPreMessageFilters; CStdPtrArray m_aMessageFilters; CStdPtrArray m_aPostPaintControls; CStdPtrArray m_aNativeWindow; CStdPtrArray m_aNativeWindowControl; CStdPtrArray m_aDelayedCleanup; CStdPtrArray m_aAsyncNotify; CStdPtrArray m_aFoundControls; CStdPtrArray m_aFonts; CStdPtrArray m_aNeedMouseLeaveNeeded; CStdStringPtrMap m_mNameHash; CStdStringPtrMap m_mWindowCustomAttrHash; CStdStringPtrMap m_mOptionGroup; bool m_bForceUseSharedRes; TResInfo m_ResInfo; // 窗口阴影 CShadowUI m_shadow; // DPI管理器 CDPI* m_pDPI; // 是否开启Gdiplus bool m_bUseGdiplusText; int m_trh; ULONG_PTR m_gdiplusToken; Gdiplus::GdiplusStartupInput *m_pGdiplusStartupInput; // 拖拽 bool m_bDragDrop; bool m_bDragMode; HBITMAP m_hDragBitmap; IDragDropUI *m_pDragDrop; // static HINSTANCE m_hInstance; static HINSTANCE m_hResourceInstance; static CDuiString m_pStrResourcePath; static CDuiString m_pStrResourceZip; static CDuiString m_pStrResourceZipPwd; static HANDLE m_hResourceZip; static BYTE* m_cbZipBuf; static bool m_bCachedResourceZip; static int m_nResType; static TResInfo m_SharedResInfo; static bool m_bUseHSL; static short m_H; static short m_S; static short m_L; static CStdPtrArray m_aPreMessages; static CStdPtrArray m_aPlugins; }; } // namespace DuiLib #endif // __UIMANAGER_H__