金山界面库分析解析bkwin.docx
《金山界面库分析解析bkwin.docx》由会员分享,可在线阅读,更多相关《金山界面库分析解析bkwin.docx(29页珍藏版)》请在冰豆网上搜索。
金山界面库分析解析bkwin
通过XML创建界面---对象的动态创建以及属性的设置
为了界面可配置化和换肤,需要界面元素可以根据XML动态创建和设置属性。
在BKLib中,CBkObject类就完成了这样的功能,主要负责类的创建和属性的设置。
因为对象都是从XML动态创建的,动态的创建是一个类最基本的属性,所以其他类都从CBkObject派生。
来看看这个类的四个方法:
BOOLIsClass(LPCSTRlpszName):
判断是不是这个类的对象。
纯虚方法,也就是从CBkObject继承来的类都要实现这个方法才行,同时,这个方法在不同的类上面会有不同的表现。
所以上层定义接口,下层提供实现。
这个方法可以在运行时检测类实例的实际类型。
LPCSTRGetObjectClass():
同上一个方法,用于获取类的名字。
BOOLLoad(TiXmlElement*pXmlElem):
从XML中获取属性并将属性设置到对象中,在基类中仅仅是将一个XML元素的属性设置到对象中。
当然,如果子类对象有更复杂的实现,比如一个对象对应的XML元素还有子节点,就需要Load其子节点,这些都可以在子类中通过覆盖父类方法来实现。
HRESULTSetAttribute(CStringAstrAttribName,CStringAstrValue,BOOLbLoading):
设置属性的方法,CBkObject是纯虚类,在XML中不会有对应的节点,自然也没有相应的属性,所以其实现仅仅放回了一个E_FAIL,没有其他操作。
接着我们就看到了在bkobject.h里面一大堆的宏定义:
宏定义一般是为了简洁,而这些宏的用途多为子类使用。
BKOBJ_DECLARE_CLASS_NAME:
获取类名,判断是否是某个类的对象,还有CheckAndNew,用于动态创建
以下的宏定义主要用于属性的设置和映射(XML节点属性和对象属性的对应)
BKWIN_DECLARE_ATTRIBUTES_BEGIN
BKWIN_DECLARE_ATTRIBUTES_END
BKWIN_CHAIN_ATTRIBUTE
BKWIN_CUSTOM_ATTRIBUTE
BKWIN_INT_ATTRIBUTE
BKWIN_UINT_ATTRIBUTE
BKWIN_DWORD_ATTRIBUTE
BKWIN_STRING_ATTRIBUTE
BKWIN_TSTRING_ATTRIBUTE
BKWIN_HEX_ATTRIBUTE
BKWIN_COLOR_ATTRIBUTE
BKWIN_FONT_ATTRIBUTE
BKWIN_ENUM_ATTRIBUTE
BKWIN_ENUM_VALUE
BKWIN_ENUM_END
BKWIN_STYLE_ATTRIBUTE
BKWIN_SKIN_ATTRIBUTE
现在我们看一个例子,继承自CBkObject的控件对象CBkProgress是如何完成从XML动态创建的。
首先,在类的定义中包含宏BKOBJ_DECLARE_CLASS_NAME(CBkProgress,"progress"),将宏展开如下:
public:
staticCBkProgress*CheckAndNew(LPCSTRlpszName)
{
if(strcmp(GetClassName(),lpszName)==0)
returnnewCBkProgress;
else
returnNULL;
}//通过传入名称创建对应的类,在解析XML中按照节点名字创建对应类的实例
staticLPCSTRGetClassName()
{
return“progress”;
}
virtualLPCSTRGetObjectClass()
{
return“progress”;
}//覆盖父类方法,返回类实例对应的XML节点名字
virtualBOOLIsClass(LPCSTRlpszName)
{
returnstrcmp(GetClassName(),lpszName)==0;
}//覆盖父类方法,根据XML节点名字检查类实例是否是此节点
另外,包含设置节点属性的宏,如下
BKWIN_DECLARE_ATTRIBUTES_BEGIN()
BKWIN_SKIN_ATTRIBUTE("bgskin",m_pSkinBg,TRUE)
BKWIN_DWORD_ATTRIBUTE("min",m_dwMinValue,FALSE)
BKWIN_UINT_ATTRIBUTE("showpercent",m_bShowPercent,FALSE)
BKWIN_DECLARE_ATTRIBUTES_END()
将宏展开如下:
public:
virtualHRESULTSetAttribute(
CStringAstrAttribName,
CStringAstrValue,
BOOLbLoading)//添加SetAttribute方法,在Load中循环调用设置属性
{
HRESULThRet=__super:
:
SetAttribute(//首先设置父类定义的属性
strAttribName,
strValue,
bLoading
);
if(SUCCEEDED(hRet))
returnhRet;
if("bgskin"==strAttribName)//属性名称是strAttribName
{
m_pSkinBg=BkSkin:
:
GetSkin(strValue);//属性的值是strValue
hRet=TRUE?
S_OK:
S_FALSE;//是否全部重绘
}
else
if("min"==strAttribName)
{
m_dwMinValue=(DWORD):
:
StrToIntA(strValue);
hRet=FALSE?
S_OK:
S_FALSE;
}
else
if("showpercent"==strAttribName)
{
m_bShowPercent=(UINT):
:
StrToIntA(strValue);
hRet=FALSE?
S_OK:
S_FALSE;
}
else
returnE_FAIL;
returnhRet;
}
现在我们来看看CheckAndNew和SetAttribute这两个方法是如何被调用的,看调用栈:
1.在实窗口的Create方法中(DoModal中调用Create)调用实窗口的Load和SetXml方法装载XML,在SetXML方法中查找XML中存在的"header"、"body"、"footer"节点调用各自的Load方法并设置相应属性。
这三个节点开始就是虚窗口了,调用其Load方法就进入了虚窗口的创建体系。
2.在这三者的调用中会调用CBkPanel:
:
Load方法
virtualBOOLLoad(TiXmlElement*pTiXmlElem)
{
if(!
CBkWindow:
:
Load(pTiXmlElem))//调用父类load方法,主要进行自身属性设置
returnFALSE;
returnLoadChilds(pTiXmlElem->FirstChildElement());//load子节点
}
在CBkPanel:
:
LoadChilds方法中,顺序调用每个子节点的创建方法并调用Load方法。
3.在CBkPanel:
:
LoadChilds方法中进行了如下调用:
CBkWindow*pNewChildWindow=_CreateBkWindowByName(pXmlChild->Value());
在_CreateBkWindowByName函数根据从XML解析出的节点名称调用
pNewWindow=CBkDialog:
:
CheckAndNew(lpszName);//CBkDialog为需要动态创建类的名称
创建出对应节点对象。
安装我们展开的CheckAndNew方法,如果节点名称相同,创建类对象并返回,否则返回空。
至此,按照XML节点名称动态创建类对象的过程就完成了。
统一的资源管理
为了对界面资源进行管理,同时也为了方便替换,需要对使用的资源进行统一的管理。
在BKLib中,资源管理主要由以下几种:
BkBmpPool:
HBITMAP资源池,用于统一管理HBITMAP,单例。
BkFontPool:
FONT资源池,用于统一管理FONT,单例。
BkPngPool:
PNG图片资源池,用于统一管理PNG图片,单例,使用GDI+。
BkString:
CString资源池,用于统一管理String,单例,从XML中获取。
BkColor:
HLS&RGB颜色处理工具类
CBkImage:
图像处理类
BkResManager:
资源处理器,单例,用于获取资源
BkSkin:
Skin资源池,单例,统一管理Skin(CBkSkinBase),从XML中获取
BkStyle:
Style资源池,单例,统一管理Style,从XML中获取
在窗口创建之前需要加载相应的资源
BkString:
:
Load(IDR_BK_STRING_DEF);//加载字符串
BkFontPool:
:
SetDefaultFont(BkString:
:
Get(IDS_APP_FONT),-12);//设置字体
BkSkin:
:
LoadSkins(IDR_BK_SKIN_DEF);//加载皮肤
BkStyle:
:
LoadStyles(IDR_BK_STYLE_DEF);//加载风格
//其中输入参数为XML文件的名称
在程序关闭后应释放对应资源。
使用时只要按照ID在资源池中查找对应资源即可,如
if("bgskin"==strAttribName)//属性名称是strAttribName
{
m_pSkinBg=BkSkin:
:
GetSkin(strValue);//属性的值是strValue
hRet=TRUE?
S_OK:
S_FALSE;//是否全部重绘
}
根据ID(strValue)就可在BkSkin中获取相应的Skin。
真实窗口的封装以及实窗口到虚窗口的转化
所谓的DUI库,windowless都是在一个窗口体系内虚拟出来虚窗口概念,并且通过接管界面布局、消息传递和分发以及界面绘制来完成更优秀的界面效果。
不过这些的根基却又都要落到真实的窗口上,所以在界面库中需要对真实窗口进行封装,并将真实窗口纳入到我们创建的控件体系当中,并在这个过程中完成windows消息的传递,鼠标键盘事件的分发处理,实窗口上的虚窗口的布局排版和绘制操作。
首先我们看看BKLib中的实窗口体系:
BKLib中并没有自己对windows窗口进行封装,而是使用了WTL的CWindowImpl类,这个类对于windows窗口,以及消息分发、窗口属性设置等进行了封装。
对于windows消息分发中有一个重要的地方就是注册的窗口过程是按照窗口句柄进行处理,但是在我们的程序中是窗口类的一个成员方法,如何将窗口句柄和C++类实例之间进行映射就成了一个重要的话题,WTL中主要使用trunk技术,而MFC则使用链表进行查找,具体的细节大家可以在网上查询,这里就不赘述了。
这里我们看一下上面类图中其他几个类的作用:
CBkDialog:
无窗口控件,它是我们虚窗口体系的一部分
CBkViewImpl:
这个算是一个接口类,不过它提供了我们需要包含虚窗口体系的实窗口所必须具备的一些方法(这句话真绕)
CBkDialogViewImpl:
它是一个实窗口了,因为他从封装了实窗口的CWindowImpl类继承而来,同时他也具有包含虚窗口所必须实现的方法(从CBkViewImpl继承而来),同时他还聚合了我们虚窗口体系的一部分,也就是CBkDialog,但是我们实际使用的类并不是它。
CBkDialogView:
这个类在我们的程序中会创建一个真正的窗口的,从CBkDialogViewImpl继承而来,具有了所有的能力,算是这里的中坚力量了。
CBkDialogImpl:
它也是一个实窗口,我们自定义窗口就是从此继承创建,同时它还聚合了上面的包含容器窗口(CBkDialogView),所以在BKLib中创建的窗口是有两层窗口的,上面的用于承载控件体系,下面的则是我们需要自定义的窗口。
为什么要创建两个窗口我们之后再研究,这里就先不解释了。
了解了上面的窗口体系后,我们就来看看在一个窗口创建过程中,各种消息、排版、绘制是如何从我们封装的实窗口想我们的虚窗口——控件体系上转化的吧。
创建过程:
布局过程(WM_SIZE消息处理)
绘制过程(WM_PAINT消息处理)
在实窗口到虚窗口的转化过程中,主要在于CBkDialogViewImpl类中包含三个成员变量:
m_bkHeader,m_bkBody,m_bkFooter,这三者的类型均是CBkDialog,也就是我们控件体系的一部分。
BkLib中将一个实窗口划分为三个虚窗口:
head、body、foot,对于界面的创建、布局、绘制等操作也由这三者传递到虚窗口体系中,并通过递归调用来对所有成员进行处理。
如何创建一个模态对话框
我们创建的窗口类从CBkDialogImpl继承而来,这个窗口就是一个模态的窗口,我们需要调用其DoModal方法,但是在界面库里面是如何实现的一个模态的对话框呢。
核心就在这个类中的_ModalMessageLoop方法,我们来研究一下。
void_ModalMessageLoop()
{
BOOLbRet;
MSGmsg;
for(;;)
{
if(:
:
PeekMessage(&msg,NULL,0,0,PM_NOREMOVE))
{
if(WM_QUIT==msg.message)
break;
}
if(m_bExitModalLoop||NULL==m_hWnd||!
:
:
IsWindow(m_hWnd))
break;
bRet=:
:
GetMessage(&msg,NULL,0,0);
if(bRet==-1)
{
continue;//error,don'tprocess
}
elseif(!
bRet)
{
ATLTRACE(L"WhyReceiveWM_QUIThere?
\r\n");
break;//WM_QUIT,exitmessageloop
}
:
:
TranslateMessage(&msg);
:
:
DispatchMessage(&msg);
}
}
在这个函数里面建立了一个消息处理循环。
首先进行WM_QUIT消息的检测,并且采用PM_NOREMOVE的方式,如果获得了这个消息,那么退出消息循环,在之后的DoModal函数中模态窗口就被销毁了。
然后检测bExitModalLoop标志,如果这个标志位为TRUE的话那么也退出处理。
我们来看一下退出模态的函数:
voidEndDialog(UINTuRetCode)
{
m_uRetCode=uRetCode;
m_bExitModalLoop=TRUE;
//DestroyWindow里面直接Send了WM_DESTROY,所以不会跑到DoModal的消息循环里,所以有了下面那行代码
//DestroyWindow();
//这句非常重要,可以让DoModal消息循环再跑一次,防止卡死在GetMessage,泪奔
:
:
PostThreadMessage(:
:
GetCurrentThreadId(),WM_NULL,0,0);
}
可见就是设置m_bExitModalLoop这个标志位,使窗口退出。
之后就是GetMessage,TranslateMessage和DispatchMessage了,就完成了常见的消息处理。
当我们创建窗口并运行在ModalMessageLoop函数当中时就形成了模态窗口的效果,也就是接管了消息的分发和处理,其他的窗口就被模住了。
无窗口模式---逻辑树结构的建立
对于无窗口的模式,各种控件之间的关系需要我们自己来维护,因为我们要进行消息传递,排版布局,创建等操作时都需要沿着各种包含关系来逐个调用,这样对于窗口中的控件就形成了一个逻辑上的树形结构。
我们看一下CBkPanel类,这个类的名字就表示它是一个有包含功能的类,可以有自己的子节点。
这个类有一个成员CAtlListm_lstWndChild,也就是每个CBkPanel类都有一个链表,在这个链表中存储了它的子节点控件。
那么这个链表是何时被填充的呢,我们看一下LoadChilds方法,就是在这个函数中进行的填充操作,从前面我们知道,这个函数是在用xml初始化界面是调用的。
其中对于当前节点下的所有xml子节点进行下面的处理:
CBkWindow*pNewChildWindow=_CreateBkWindowByName(pXmlChild->Value());//创建控件
if(!
pNewChildWindow)
continue;//创建失败,不进行后续设置
pNewChildWindow->SetParent(m_hBkWnd);//设置父节点HBKWND
pNewChildWindow->SetContainer(m_hWndContainer);//设置容器窗口的HWND
pNewChildWindow->Load(pXmlChild);//Load这个节点的子节点
m_lstWndChild.AddTail(pNewChildWindow);//将自己加入到父节点的链表中
在CBkPanel的OnDestroy函数中进行子节点的销毁操作
POSITIONpos=m_lstWndChild.GetHeadPosition();
while(pos!
=NULL)
{
CBkWindow*pBkWndChild=m_lstWndChild.GetNext(pos);
pBkWndChild->BkSendMessage(WM_DESTROY);
deletepBkWndChild;
}
m_lstWndChild.RemoveAll();
之后进行消息传递,绘制,排版操作都可以使用这个树来进行处理,循环调用所有的节点。
如在OnPaint函数中:
POSITIONpos=m_lstWndChild.GetHeadPosition();
BOOLbDisabled=IsDisabled(),bIsChildDisabled=FALSE;
while(pos!
=NULL)
{
//……此处略去
pBkWndChild->BkSendMessage(WM_PAINT,(WPARAM)(HDC)dc);
//……此处略去
}
可见也是循环向每个子节点发送WM_PAINT消息。
同时,在界面库中,还使用了BkWnds来记录所有的控件,以便可以便捷地进行获取操作:
CBkWindow类有HBKWND类型的成员变量m_hBkWnd,这是一个虚拟的窗口句柄。
BkWnds是一个控件池,创建的控件会在这里注册,并通过一个HBKWND类型变量标示,以后就可以通过控件的m_hBkWnd来获取这个控件。
消息和事件的传递、分发、相应
既然没有真实的窗口,那么也就不能使用windows的根据句柄来分发消息的方式了,我们需要创建自己的消息和事件分发体系。
主要应该包括这几个方面:
1.接收真实窗口的消息,并将其转化虚窗口体系中的处理
2.虚窗口体系内有一套独立的消息分发机制,可以讲系统消息发至该接收的控件
3.虚窗口需要能够抛出事件的能力,因为虚窗口之间也需要有相互的通知和相应的能力,那么就需要虚窗口可以对于指定窗口抛出事件
4.对于3中所抛出的事件,可以传递到指定的控件,并且控件内部应该有一套指定额相应体系
在CBkDialogViewImpl中使用WTL的消息分发方法用于系统消息的分发:
BEGIN_MSG_MAP_EX(CBkDialogViewImpl)
MESSAGE_RANGE_HANDLER_EX(WM_MOUSEFIRST,WM_MOUSELAST,OnToolTipEvent)
MSG_WM_SIZE(OnSize)
MSG_WM_PRINT(OnPrint)
MSG_WM_PAINT(OnPaint)
//……从略
NOTIFY_CODE_HANDLER_EX(BKINM_INVALIDATERECT,OnBKINMInvalidateRect)
REFLECT_NOTIFY_CODE(NM_CUSTOMDRAW)
MESSAGE_HANDLER_EX(WM_NOTIFY,OnChildNotify)
MESSAGE_HANDLER_EX(WM_COMMAND,OnChildNotify)
MESSAGE_HANDLER_EX(WM_VSCROLL,OnChildNotify)
MESSAGE_HANDLER_EX(WM_HSCROLL,OnChildNotify)
REFLECT_NOTIFICATIONS_EX()
END_MSG_MAP()
下面的是对于WM_SIZE消息的分发过程:
在CBkWindow类中创建了用于发送通知的函数:
//SendamessagetoBkWindow
LRESULTBkSendMessage(UINTMsg,WPARAMwParam=0,LPARAMlParam=0)
{
LRESULTlResult=0;
if(Msg&&Msg!
=WM_DESTROY
&&Msg!
=WM_CLOSE
)
{
TestMainThread();
}
SetMsgHandled(FALSE);
ProcessWindowMessage(NULL,Msg,wParam,lParam,lResult);
returnlResult;
}
这个函数很像windows的消息发送函数SendMessage,
LRESULTSendMessage(HWNDhWnd,UINTMsg,WPARAMwParam,LPARAMIParam)
需要向那个对象发送消息,那么首先需要获取这个对象,然后调用这个对象的BkSendMessage函数,用消息类型和附属参数填充,就可以完成事件通知的过程了。
在这个函数中调用了ProcessW