| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092 |
- #ifndef __UIMANAGER_H__
- #define __UIMANAGER_H__
- #pragma once
- // 本头文件原本不含任何 include(符号由 StdAfx.h/UIlib.h 提供),
- // 但触屏相关的成员与状态里直接使用了 std::int64_t 与 std::vector,
- // 显式补齐以保证在 DuiLib 库工程(_WIN32_WINNT 兜底为 XP)里也能独立通过编译。
- #include <cstdint>
- #include <vector>
- 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() 在**进入** DialogBoxIndirectParam 之前调用。
- // 用途有两个(同一根因、同一拦截点):
- // ① Win7:消除"点控件 → 回调里弹模态框 → 咚"那一声(MessageBeep(0));
- // ② Win7:消除"弹框刚出来就被同一次手势自动点掉"(残留消息落在弹框上)。
- // 成因与边界说明见 UIBase.cpp 文件头;执行点在 MessageHandler 的
- // WM_LBUTTONDOWN/UP 分支里(连同 IsPromotedTouchMouseMessage 与
- // IsModalInputTarget 两道筛选)。
- //
- // 生命周期 = **手势配对**,不是固定超时:
- // DOWN 记账(m_nPromotedMousePending++)、UP 销账(--),归零即解除。
- // m_dwSuppressPromotedMouseForModalTick 只是**僵尸兜底**(配对消息若永久丢失,
- // 10 秒后强制清),正常路径永远走不到。
- void SetSuppressPromotedMouseForModal(bool bSuppress)
- {
- m_bSuppressPromotedMouseForModal = bSuppress;
- m_dwSuppressPromotedMouseForModalTick = bSuppress ? ::GetTickCount() : 0;
- if (!bSuppress) m_nPromotedMousePending = 0;
- }
- bool IsSuppressPromotedMouseForModal() const { return m_bSuppressPromotedMouseForModal; }
- // DOWN 到达时记账:还欠一条 UP 来配对。
- void AddPromotedMousePending() { ++m_nPromotedMousePending; }
- // UP 到达时销账。归零即自动解除抑制(手势配对完成)。
- void ReleasePromotedMousePending()
- {
- if (m_nPromotedMousePending > 0) --m_nPromotedMousePending;
- if (m_nPromotedMousePending == 0)
- {
- m_bSuppressPromotedMouseForModal = false;
- m_dwSuppressPromotedMouseForModalTick = 0;
- }
- }
- // 当前处于模态状态的弹框句柄(由 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);
- // ------------------------------------------------------------------
- // [第十一轮诊断] 只读快照访问器:给 UIBase.cpp 的模态迁移点打日志用。
- //
- // 存在的理由:真模态把**同一个** manager 从旧窗口搬到弹框,而 AttachPaintWindow
- // 只换 paint window,**不清理**下面这些输入状态。于是它们会跟着一起"住进"弹框 ——
- // 这正是"商品控件悬浮态要等弹框关闭才消失"的嫌疑点。这些访问器只为读值,
- // 不改变任何行为;问题定位后可随日志一起删除。
- //
- // ⚠️ 实现必须放在 .cpp:CControlUI 在本头文件里只有前向声明(见文件顶部),
- // 在类内直接写 m_pEventHover->GetName() 会因"不完整类型"而编译失败。
- // ------------------------------------------------------------------
- LPCTSTR DbgHoverName() const;
- LPCTSTR DbgRClickName() const;
- int DbgNeedMouseLeaveCount() const { return m_aNeedMouseLeaveNeeded.GetSize(); }
- int DbgIsDragMode() const { return m_bDragMode ? 1 : 0; }
- 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;
- // ------------------------------------------------------------------
- // [模态提示音] "点控件 → 回调里 ShowModal() → 咚一声" 的消音窗口。
- //
- // 成因(真机日志实证,详见 UIBase.cpp 文件头那段说明):
- // 真模态把 owner 置为 EnableWindow(FALSE)。Win32 默认处理规定「点击被禁用的
- // 窗口要 MessageBeep(0)」。而触摸手势的消息**归属在按下那一刻就定死了**,
- // 整个序列都投给按下时命中的窗口 —— 于是"按下时 owner 还启用、抬起前 owner
- // 被 ShowModal 禁用了"这一交错,会让提升出来的鼠标消息撞上禁用瞬间 ⇒ 咚。
- //
- // 为什么需要这个抑制位(而不是别处解决):
- // · 咚声由**系统默认处理**发出,不是窗口消息、窗口过程拦不到;
- // · 它只在"消息被投递到禁用窗口的那一瞬"触发,而那一刻发生在
- // ShowModal() 内部(DialogBoxIndirectParam 的模态循环启动期间)——
- // 调用方还没拿回控制权,唯一能提前布置的状态就是本 manager 的成员。
- //
- // 语义:为真时,在 MessageHandler 里丢弃"触摸提升出来的"点击类鼠标消息
- // (WM_LBUTTONDOWN/UP/DBLCLK,且带 MOUSEEVENTF_FROMTOUCH 标记)。
- // **只丢触摸提升的** —— 真实鼠标点击照常放行。
- //
- // ⚠️⚠️ 【第二轮:拦截目标从"仅 owner"扩展到"owner + 当前弹框"】⚠️⚠️
- //
- // 第一版只保护 owner(用 IsCurrentModalOwner 判定),真机验证后暴露出两个漏网:
- // (a) 弹框弹出后,本次手势残留的提升鼠标消息有可能**投给弹框**(弹框成了
- // 前台/启用窗口,系统按当前状态投递)。若手指原位置恰好压着弹框里某个
- // 按钮(用户实测:正好是"选好了"),就会**自动点击它 ⇒ 弹框刚出来就关闭**。
- // (b) 手势残留消息的投递时机可能 > 1 秒(回调里要建窗口、布局、绘制),
- // 于是固定 1 秒窗口过期 ⇒ 漏网 ⇒ 咚声。
- // ⇒ 判定改为 **目标窗口 ∈ {当前模态 owner, 当前模态弹框}**,且**不再按时间过期**,
- // 而是按"手势配对"过期(见下)。
- //
- // ── 生命周期:手势级,不是时间级 ───────────────────────────────────
- // 第一版用「置位后 1 秒自动失效」,已被证明是错的:
- // · 太短 ⇒ 稍长一点的手势残留漏网(咚声 / 自动点弹框);
- // · 且它是个**定时炸弹**:超时一到抑制位被清,残留的提升鼠标消息立刻被放行
- // ⇒ Win10 上"弹框关闭后 3 秒突然透点"就是这么来的(3 秒 = 另一处
- // m_bSuppressPromotedTouchMouse 的自愈超时,抑制位被清后露出的破洞)。
- //
- // 现在改为:抑制位由**手势配对计数**决定何时结束 ——
- // m_nPromotedMousePending 记录"还欠几条提升鼠标消息没配对完"。
- // DOWN 到手 ⇒ 记下(等下一条 UP 来收);UP 到手 ⇒ 计数减一。
- // 计数归零 ⇒ 抑制位自动解除。
- // 同时保留一个**兜底超时** m_dwSuppressPromotedMouseForModalTick,
- // 但它是"清理僵尸状态"用的(防配对消息永久丢失),不是"业务到期"用的,
- // 因此取值放宽到 10 秒 —— 正常手势几十毫秒就配完对,根本不会走到兜底。
- // ------------------------------------------------------------------
- bool m_bSuppressPromotedMouseForModal = false;
- DWORD m_dwSuppressPromotedMouseForModalTick = 0;
- // 还欠几条"触摸提升出来的点击类鼠标消息"没配对完(DOWN 记账、UP 销账)。
- int m_nPromotedMousePending = 0;
- // ==================================================================
- // ★★【透传根治 · 代数(generation)机制 —— 已改为**全局**实现】★★
- //
- // 第一版把这套状态做成了 CPaintManagerUI 的**成员**,真机一验就废:
- // 本项目属性弹框 CDiandanNatureWnd 自带独立的 CPaintManagerUI m_pm,
- // 与主窗口的 manager **不是同一个对象**。日志铁证:
- // MODAL-BEEP arm manager=0DCE0960 <- ShowModal 动的是弹框的
- // MANAGER-MSG hwnd=00D20604 manager=05796840 <- 泄漏消息落在这里
- // 在弹框 manager 上 +gen,主窗口那份看不见 ⇒ 水位永远持平 ⇒ 拦不住。
- //
- // 而“泄漏”本质是**线程消息队列**现象,与 manager 归属无关
- // ⇒ 代数必须是线程级全局量。
- //
- // 现实现位置:
- // · 状态与消费逻辑 —— UIManager.cpp 里 s_nPromotedMouseGeneration 一带
- // · 推进点 —— CWindowWnd::ShowModal() 里 BumpPromotedMouseGenerationGlobal()
- // · 拦截点 —— CPaintManagerUI::MessageHandler() 开头的 PROMOTED-GEN 块
- // · 对外声明 —— UIBase.h(DuiLib 命名空间)
- //
- // 语义与不变式(**全程无时间戳**,符合用户要求):
- // 。代数在“进入模态循环之前” +1;
- // 。提升鼠标消息若其水位落后于当前代数 ⇒ 产生于边界之前 ⇒ 吞掉;
- // 。配对消息一并吞(解作废点在抬起或真实鼠标点击)。
- // ==================================================================
- // 手指抬起后,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)。
- // 验证要点(看 touch_trace.log):
- // 开启后弹框里应出现 `DEFPROC-RELAY call` 与随后的 `WNDPROC-TOUCH`;
- // 若出现但点击异常,把环境变量去掉即可立刻回到当前已可用的行为。
- bool m_bRelayDefProcForTouchMove = false;
- // 诊断计数:补调发生了多少次、对应的窗口。便于在日志里确认机制是否生效。
- unsigned m_uDefProcRelayCount = 0;
- //
- 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__
|