Duilib源码分析(四)绘制管理器—CPaintManagerUI—(前期准备一)

时间:2023-03-08 16:16:47

  上节中提到在遍历创建控件树后,执行了以下操作:
       1. CDialogBuilder构建各控件对象并形成控件树,并返回第一个控件对象pRoot;
       2. m_pm.AttachDialog(pRoot);m_pm.AddNotifier(this);将控件attach到CPaintManagerUI  m_pm中管理,此外INotifyUI子类对象被加入到m_pm;
           这样m_pm不仅管理控件对象也对消息监听接口Notify回调,这样用户可以在Notify中实现自己的感兴趣的消息映射处理,其Notify(TNotifyUI& msg)中的参数为通告消息msg;
  
    在进行分析前,我觉得有必要先对一些细节事先分析,包括必要的结构定义、宏定义、辅助定义等;一方面提高可以继续深入分析下去的信心另外也有助于减少后期细节上的影响;姑且称本节为前期准备;

  那如何进行呢?先进入文件UIlib.h中,可以看到差不多此文件包含了所有可能用到的头文件、包含顺序,那我们就按照顺序来学习;此外UIlib.cpp中的函数DisableThreadLibraryCalls,该函数主要

  用以优化本duilib库被多次加载或卸载时的优化作用,因许多工程对该DLL频繁创建和删除线程,并且DLL不需要线程级消息如DLL_THREAD_ATTACH and DLL_THREAD_DETACH时的多线程应用中

  是很有效的优化;

  如下:

    1. UILIB_API:宏开关,标准的做法,根据当前设定环境,指定为导入或导出接口;

    2. UILIB_COMDAT:__declspec是一个Microsoft Visual C++特定的编译器属性开关;此处的selectany表示在头文件定义全局变量,并且这个头文件被include多次时可以用这个开关剔除

    由于多次include而产生的重定义,一般情况会将全局变量放到CPP中定义,所以此情况会用得比较少,查找整个duilib项目,可以看到只有UIDefine.h文件中定义消息映射用到;

    3. #pragma comment(linker, "..."):表示设置当前链接器选项,包括类型、公共控件版本、处理器架构等,一般用来设置控件属性、美化控件界面显示而不是旧风格的;

    前面部分以<>包含的必要的头文件,后面为本项目相关头文件;再次说明:注意留意一下头文件包含顺序,可以看到其他文件中包含的结构、宏居然可以使用,原理就在这里,

    统一包含必要的头文件,便于管理;

    4. Utils.h:工具相关

      STRINGorID:从名字可以看出表示为资源字符串或是资源ID的一个类;两个构造函数重载版本,分别以资源字符串为参数、以资源ID为参数;但最终还是以m_lpstr资源字符串保存;

      CPoint:定义点坐标类(X,Y),此继承于tagPOINT(同POINT);其重载四个构造函数的版本,注意第四个版本参数为LPARAM,可以看到取其低16位为X坐标,高16位为Y坐标,

      该版本一般用于获取鼠标点击时的坐标位置;

      CSize:定义大小类(长、宽),此继承于tagSIZE(同SIZE);同样重载了四个构造函数的版本,注意其第三个版本参数为RECT(矩形类(定义了左、上、右、下) )来初始化cx,cy;

      CDuiRect:定义的矩形类(左、上、右、下),此继承于tagRECT(同RECT);重载了三个版本构造函数;此外增加了几个操作函数,GetWidth:获取矩形宽度,GetHeight:获取

      矩形的高度,Empty:使矩形是否为空,即均为0,IsNull:判断矩形是否为空(个人建议命名应该统一为IsEmpty),Join:与参数矩形rc求并集,类似于集合的并集,ResetOffset:

      将指定的矩形移动到指定的位置,内部调用OffsetRect(this, -left, -top),很显然得到的结果矩形为(0, 0, right - left, buttom - top),所以可以认为该函数为重置矩形到"原点"

      位置,此时CDuiRect的Offset函数含义同理,Normalize:将矩形正规化,因为一般情况下矩形应该是left < right, top < buttom,Inflate:增加或减少矩形的高度和宽度

      (根据cx、cy正负确定增加还是减少),内部调用InflateRect(this, cx, cy)(事实上增加矩形的高度和宽度的操作为left - cx, top - cy, right + cx, buttom + cy),Deflate:根据

      内部实现,应该是减少矩形,个人感觉CDuiRect的Deflate和Inflate有功能重复之嫌,可以合并为一个,根据参数正负自己去确定增加或减少,Union:矩形的并集操作,可以看出

      该函数同Join,功能重复;

      CStdPtrArray:一个用以容纳对象指针、地址的“数组"容器,先看数据属性:m_ppVoid,容纳指针对象的地址的容器;m_nCount,当前容纳的实际数目;m_nAllocated,

      预留的容器大小;再看成员函数:构造函数以iPreallocSize为参数设置预先分配的预留容器大小,复制构造函数以CStdPtrArray为参数构造,Empty:清空容器和预留空间,

      Resize:清空并释放容器资源,重新分配iSize大小的容器空间,IsEmpty:容器实际容纳是否为空,Find:查找对象保存在当前容器中(事实上只是比较地址)的位置,失败返回-1,

      Add:向容器中增加存储对象,可以看到若容器为空且第一次调用Add时将分配11个指针对象的空间,若不为空且达到了容器的预留空间时,重新分配m_nAllocated * 2的大小,

      内部调用realloc重新分配,不用担心重新分配可能导致内容无效的情况,因保存的为指针地址(可以认为是一个整数值),重分配时直接拷贝值(等同于拷贝保存的指针值),

      若还有预留,不用重新分配则直接在末尾处直接保存新添加的数据即可,SetAt:重新设置保存于iIndex索引处位置的值pData,InsertAt:于iIndex处插入值pData,事实上

      内部也要处理预留空间可能不够的情况,该操作同Add,此外因从iIndex处插入,需要将后面部分均向后移动一个位置,内部调用memmove实现内存拷贝比较高效且可以保证

      目标区域和源区域有重叠的话也可以移动到正确的位置,Remove:移除iIndex位置的值,需要将后半部分向前移动一个位置,内部调用CopyMemory(事实上是memcpy)

      并使得m_nCount - 1即可,GetSize:获取当前存储的实际容量大小m_nCount,GetData:获取当前容器的起始地址,GetAt:获取索引iIndex位置的数据,此外也实现了

      运算符operator[],这样就可以使用[index]方式获取数据(不过内部没有保护措施,在非调试版本下,可能导致访问越界);

      CStdValArray:同CStdPtrArray相似,不过不同点在于其内部缓冲区保存的不再数据内容的索引下的地址,而是具体的数据内容;重点关注一下其构造函数CStdValArray中

      的参数iElementSize,其表示为单个数据内容元素的大小,也就意味着保存的是分别为固定大小尺度为iElementSize的元素数据;此外好像duilib库未用到该类;

      CDuiString:字符串类,模仿string、wstring;先看数据成员,m_pstr:缓存空间指针对象,当字符串小于MAX_LOCAL_STRING_LEN(63)长度时其指向m_szBuffer首地址,

      当大于MAX_LOCAL_STRING_LEN时,将重新指向新申请分配的缓存区,m_szBuffer:栈上的缓存区,主要用来存储长度小于MAX_LOCAL_STRING_LEN的字符串;

      接下来我们继续看成员函数,其有四个重载版本的构造函数,Empty:清空当前字符串,若m_pstr指向的不是栈上的缓冲区m_szBuffer,则释放堆上分配的,并重新设置其指向

      为栈上缓存区m_szBuffer,IsEmpty:判断当前字符串是否为空,GetLength:获取存储字符串实际长度;GetAt:获取索引nIndex处的字符,此外重载运算符operator[]可以

      实现下标取值,Append:追加字符串,对于内部数据的追加需要对当前存储状况进行调整,最终调用_tcscat连接新旧字符串,Assign:根据传入的字符串pstr和截断长度cchMax

      来实现初始化m_pstr内容,GetData:获取缓存区m_pstr指向的数据内容,SetAt:设置指定索引nIndex处的值为ch,此外重载了运算符operator LPCTSTR()和多个不同版本的

      operator=,operator+=,operator+,还有比较操作符operator ==,operator !=,operator <=,operator <,operator >=,operator >,而比较操作符实现中均使用

      Compare函数实现比较(实际采用_tcscmp实现),CompareNoCase:忽略大小写的字符串比较(实际采用_tcsicmp实现),类似于CStrig的MakeUpper:使得保存的字符串变为

      大写(实际采用_tcsupr实现),MakeLower:使得保存的字符串变为小写(实际采用_tcslwr实现),Left:获取字符串左边长度iLength的字符串,不足iLength的则获取全部字符串,

      Mid:获取指定位置iPos开始长度为iLength的字符串,Right:获取字符串右边长度为iLength的字符串,不足iLength的,获取到全部字符串,Find:查找从iPos位置处开始的第一

      次出现ch字符的位置(内部通过_tcschr实现),该函数的另一个重载版本(内部通过_tcsstr实现)则为查找从iPos开始处第一个出现匹配pstrSub字符串的位置,若未查找到则返回-1,

      ReverseFind:反向查找ch字符出现的位置(内部通过_tcsrchr实现),Replace:将字符串内部所有的pstrFrom字符串替换为字符串pstrTo,Format:通过格式化字符串初始化该

      对象,并返回格式化后的字符串长度,SmallFormat:微型格式化字符串,内部通过局部栈空间约64字节长作为临时缓存,故可以方便用于短字符串的初始化避免在堆上操作,

      而长字符串则应该用Format(内部临时缓存在堆上申请并释放);

      TITEM:用以保存key-value键值对的一个节点,此外还有指向上一个和下一个ITEM节点的指针;

      CStdStringPtrMap:如其名,保存的是一个TITEM的key-value键值对的容器,key为字符串,value则为指针对象地址;先看成员数据:m_aT,一个指向类型为TITEM的指针

      的指针,事实上,由它管理着整个容器,m_nBuckets:桶的长度,可以认为即m_aT所指向的指针数组的长度;m_nCount:容器实际存储的键值对TITEM的数量;接下来看成员

      函数,构造函数CStdStringPtrMap(int nSize):参数nSize用以初始化m_nBuckets,并分配长度为m_nBuckets、存储TITEM指针的桶(m_aT = new TITEM*[nSize]),

      并将m_aT指向该桶容器的首位值,此外观察析构函数和Insert可以看出当前的整个容器的存储结构布局为:m_aT指向一个大小为m_nBuckets个指向TITEM指针的桶,每个桶元素

      指向一个双向链表,此外Insert函数执行的方式为首插法,这样就可以管理需要TITEM结构的指针容器,Resize:重置容器(事实上操作为释放早期所有存储内容,并重新分配大小为

      nSize长度的桶),发现这些代码与析构函数一致代码重复了,个人认为可以增加一个clear的接口用以释放存储内容,虽然其提供了一个RemoveAll函数,不过该函数内部调用的还是

      Resize,的确达到了移除所有的目的,不过没有修改桶的大小(即没有释放桶),Find:查找指定的key并获取其value,在查找中使用了一个称作HashKey的哈希散列函数返回计算

      后的桶位置,内部实现采用如:UINT i = 0;SIZE_T len = _tcslen(Key); while( len-- > 0 ) i = (i << 5) + i + Key[len];最终得到的i % m_nBuckets值即为桶位置,参数

      optimize:是否需要优化,这种优化主要用来实现类似于最近查找算法的特点,内部将查找到的key置于桶对应的双向链表首,当下一次查找到同一key时可以提高查找效率,Insert:

      插入指定key-value键值对,同样通过哈希散列函数找到一个桶,然后执行链表的首插法,快速完成数据插入,Set:设置某键key的值替换为value,若key不存在,则执行Insert操作,

      Remove:移除指定的key键的键值对,RemoveAll:移除所有桶下的数据,但是桶并没有"释放"(事实上是先释放再申请之前那么多的桶),GetSize:获取当前存储的键值对数,GetAt:

      获取指定索引的键值对(内部采用依次遍历桶以及桶下的链表节点,累积到iIndex数时返回该键值对,感觉该功能比较鸡肋),此外重载的版本operator[],可以以下标的方式访问;

      CWaitCursor:等待光标类,比较简单,只是加载了一个Win32预定义的IDC_WAIT撒漏的等待光标并保存之前的光标句柄;

      CVariant:继承于VARIANT的变体类型,并提供了几个已经实现的版本的构造函数,这类东西一般用在activex、COM组件中,需要和CString、BYTE等之间类型转换用到,在使用转化

      时需要先设置类型vt再对其值intVal赋值,此外一般VARIANT变体类型需要先初始化内部值调用VariantInit,初始化为VT_EMPTY,此后需要释放资源调用VariantClear;

    5. UIDelegate.h:委托基类、事件源相关

      委托:通俗地说,就是将一件事情交给他人代办,编程的说法:可以将方法当作另一个方法的参数来进行传递,将方法动态地赋给参数,使得程序具有更好的可扩展性,C++没有委托模型,

      所以通过模拟实现委托,一般情况下效率都是比较低下的。

      CDelegateBase:委托基类,提供必要的委托相关操作实现接口;先看一下数据成员:m_pObject:委托人,用在类对象作为委托时使用,若是一般函数(全局、静态)的则可为NULL,

      m_pFn:委托的事件(执行函数的地址);成员函数:提供了以委托人和委托事件为参数的构造函数,此外提供了赋值构造函数,Equals:比较两个委托对象是否相同(内部均是地址比较),

      operator():重载了以param为参数的函数,这样在使用委托对象的时候传递必要的参数即可当作函数来使用就很方便了,Copy:允许复制本委托对象的纯虚函数,受保护的GetFn、

      GetObject、Invoke分别对应为获取委托事件、获取委托人以及执行调用操作(纯虚函数),子类自己可实现相应的Copy、Invoke操作,因参数均为void,所以子类可以非常灵活的定制;

      CDelegateStatic:继承于CDelegateBase基类,实现了基类必要的实现接口Copy、Invoke,注意构造函数传参(NULL, pFn),另外pFn类型为bool (*Fn)(void*),也就意味着该委托

      类是一个代理处理全局、静态函数的委托类,委托事件是一个参数为void *,返回值为bool类型的函数,个人认为这个类做成模板将会有更大的扩展,模板参数分别为返回值、委托事件

      参数;

      CDelegate:模板类,继承于CDelegateBase基类,不过它不是对委托事件的模板化,而是对m_pObject委托人类型的模板,此外留意Invoke的实现(调用委托人的委托事件进行处理)

      ,其他同CDelegateStatic;

      MakeDelegate:委托工具类,提供了重载版本,一个针对模板,一个专为CDelegateStatic类;

      CEventSource:事件源,一个容纳多个委托对象的容器;先看数据成员:m_aDelegates:CStdPtrArray类型,为委托集合、容纳各种委托操作的容器,成员函数:析构函数释放

      保存的委托对象,运算符operator bool():判断当前事件源是否为空,重载的版本operator+=,-=:实现委托对象的添加和移除并通过MakeDelegate统一委托类实现添加或移除;

      bool operator() (void* param):运算符重载,实现对容器内所有的委托对象均执行各自的委托事件,参数均统一相同为param;

    6. UIDefine.h:消息、通告消息、消息映射、控件名称宏的定义

      DuiSig:枚举类型的消息映射中用到的消息类型,主要是用来区分消息处理函数是哪一种(DuiSig_lwl——>LRESULT (WPARAM, LPARAM)或DuiSig_vn——>void (TNotifyUI) )

      以及消息映射的结束标识DuiSig_end;

      TNotifyUI:通告UI消息结构体,sType:控件名称,sVirtualWnd:目前未用到的虚拟窗口,pSender:发送该消息的控件对象,dwTimestamp:发送消息时的时间戳(GetTickCount),

      ptMouse:鼠标的位置,wParam,lParam:对应msg的wParam,lParam;

      DUI_PMSG:一个声明类型为CNotifyPump类、返回值为void,参数类型为TNotifyUI类型的通告消息;

      DuiMessageMapFunctions:一个消息映射函数的union联合类型,在消息分发的时候会用到,可以看到内部通用函数指针pfn、pfn_Notify_lwl、pfn_Notify_vn共享同一个地址结构,

      事实上pfn与pfn_Notify_vn具有相同的函数类型,此外内部的CNotifyPump类将后面介绍;

      各种以DUI_MSGTYPE_XXX开头的各种控件名称宏;

      DUI_MSGMAP_ENTRY:消息映射入口结构定义:存放消息的结构;sMsgType:消息类型,sCtrlName:控件名称,nSig:标记函数指针类型(DuiSig_lwl、DuiSig_vn、

      DuiSig_end),pfn:DUI_PMSG类型,指向函数的指针;

      DUI_MSGMAP:消息映射,pfnGetBaseMap:获取基类消息映射的指针对象,返回值为消息映射,lpEntries:指向DUI_MSGMAP_ENTRY结构类型的指针;

      DUI_DECLARE_MESSAGE_MAP:声明消息映射的宏定义,此宏主要用在需要处理消息的类中;类似于MFC的声明消息映射宏;仔细看一下该宏内部:_messageEntries:存放一系列

      消息映射的一个入口(集合),DUI_MSGMAP_ENTRY类型,_GetBaseMessageMap:获取基类消息映射地址, GetMessageMap:获取当前消息映射,messageMap:DUI_MSGMAP

      类型的消息映射,它主要的目的就是给自己的pfnGetBaseMap、lpEntries赋值,即直接得到_GetBaseMessageMap、GetMessageMap,具体请继续看下面分析;

      DUI_BASE_BEGIN_MESSAGE_MAP:基类声明的开始,theClass即为当前类,可以看到分别对_GetBaseMessageMap、GetMessageMap、messageMap赋值;

      DUI_BEGIN_MESSAGE_MAP:子类声明的开始,仔细看一下“参数”theClass, baseClass,分别表示当前类与当前类的基类,可以看到这次给_GetBaseMessageMap、GetMessageMap、      

      messageMap赋值与DUI_BASE_BEGIN_MESSAGE_MAP间的关系;

      DUI_END_MESSAGE_MAP:声明的结束,含基类声明和子类声明;

      接下来就是一些以DUI_ON_XXX开头的消息类型宏,可以方便的使用宏填充DUI_MSGMAP_ENTRY结构,添加到_messageEntries中;

      以DUI_CTR_XXX开头的一系列的控件名称宏,这样可以方便的直接使用该宏表示对应的控件类型名称,这些一般用在控件构建器和获取控件对象的接口中;

   因考虑到需要分析的文件众多,故下一节将继续分析后面的文件。