Duilib源码浅析<一>之UI渲染流程

1. 流程简图

2. 自己的窗口类

以360浏览器demo为例, 这个窗口类是C360SafeFrameWnd,它继承了两个类,一个是窗口类CWindowWnd,另一个是消息类INotifyUI。在UI这一块我们主要关注CWindowWnd类。CWindowWnd类:这个类可以叫它窗口类,它封装了窗口的基本属性和接口,便于理解,可以把它类比为MFC中的CWnd类。

 

3. 创建窗口

WindMain中的创建流程如下:

int APIENTRY WinMain(HINSTANCE hInstance, HINSTANCE /*hPrevInstance*/, LPSTR /*lpCmdLine*/, int nCmdShow)
{
	CPaintManagerUI::SetInstance(hInstance);
	CPaintManagerUI::SetResourcePath(CPaintManagerUI::GetInstancePath() + _T("skin"));
	CPaintManagerUI::SetResourceZip(_T("360SafeRes.zip"));

	HRESULT Hr = ::CoInitialize(NULL);
	if( FAILED(Hr) ) return 0;

	C360SafeFrameWnd* pFrame = new C360SafeFrameWnd();
	if( pFrame == NULL ) return 0;
	pFrame->Create(NULL, _T("360安全卫士"), UI_WNDSTYLE_FRAME, 0L, 0, 0, 800, 572);
	pFrame->CenterWindow();
	::ShowWindow(*pFrame, SW_SHOW);

	CPaintManagerUI::MessageLoop();

	::CoUninitialize();
	return 0;
}

这里主要在做三件事:

(1) 设置资源

SetResourcePath函数设置资源路径,并将这个值保存在成员变量"m_pStrResourcePath"中,SetResourceZip函数是设置资源包,这里是压缩成zip格式,文件名保存在"m_pStrResourceZip"中。这个zip压缩文件中包含了程序用到的资源,例如图片和xml文件,将360SafeRes.zip解压可以看到所有的资源文件,其中"skin.xml"是主窗口UI配置文件。

(2) 创建窗口

这一步做了很多事情,包括xml文件加载、解析,UI对象创建渲染,窗口显示等。

(3) 消息循环

消息相关的有机会再后续章节再介绍。

 

4. XML解析流程

上一步中说道了创建窗口,我们知道WM_CREATE是常规意义上窗口收到的第一个消息,在windows程序开发中我们一般在这条消息中做一些初始化的工作。为了处理消息,需要实现虚函数HandleMessage:

LRESULT HandleMessage(UINT uMsg, WPARAM wParam, LPARAM lParam)
	{
		LRESULT lRes = 0;
		BOOL bHandled = TRUE;
		switch( uMsg ) {
		case WM_CREATE:        lRes = OnCreate(uMsg, wParam, lParam, bHandled); break;
		case WM_CLOSE:         lRes = OnClose(uMsg, wParam, lParam, bHandled); break;
		case WM_DESTROY:       lRes = OnDestroy(uMsg, wParam, lParam, bHandled); break;
		case WM_NCACTIVATE:    lRes = OnNcActivate(uMsg, wParam, lParam, bHandled); break;
		case WM_NCCALCSIZE:    lRes = OnNcCalcSize(uMsg, wParam, lParam, bHandled); break;
		case WM_NCPAINT:       lRes = OnNcPaint(uMsg, wParam, lParam, bHandled); break;
		case WM_NCHITTEST:     lRes = OnNcHitTest(uMsg, wParam, lParam, bHandled); break;
		case WM_SIZE:          lRes = OnSize(uMsg, wParam, lParam, bHandled); break;
		case WM_GETMINMAXINFO: lRes = OnGetMinMaxInfo(uMsg, wParam, lParam, bHandled); break;
		case WM_SYSCOMMAND:    lRes = OnSysCommand(uMsg, wParam, lParam, bHandled); break;
		default:
		bHandled = FALSE;
		}
		if( bHandled ) return lRes;
		if( m_pm.MessageHandler(uMsg, wParam, lParam, lRes) ) return lRes;
		return CWindowWnd::HandleMessage(uMsg, wParam, lParam);
	}

这样就可以接收我们想要处理的消息了。再看OnCreate()函数,这里会进行一系列初始化工作:

LRESULT OnCreate(UINT uMsg, WPARAM wParam, LPARAM lParam, BOOL& bHandled)
{
	LONG styleValue = ::GetWindowLong(*this, GWL_STYLE);
	styleValue &= ~WS_CAPTION;
	::SetWindowLong(*this, GWL_STYLE, styleValue | WS_CLIPSIBLINGS | WS_CLIPCHILDREN);

	m_pm.Init(m_hWnd);
	CDialogBuilder builder;
	CDialogBuilderCallbackEx cb;
	CControlUI* pRoot = builder.Create(_T("skin.xml"), (UINT)0,  &cb, &m_pm);
	ASSERT(pRoot && "Failed to parse XML");
	m_pm.AttachDialog(pRoot);
	m_pm.AddNotifier(this);

	Init();
	return 0;
}

这里又出现了一个新的类"CDialogBuilder",主要功能是什么呢?看它的名字大概就是对话框创建类的意思。首先通过GetWindowLong和SetWindowLong两个函数改变了窗口风格,去掉了"WS_CAPTION"风格,即去掉了标题栏和边框,然后加上了风格"WS_CLIPSIBLINGS"和"WS_CLIPCHILDREN",简单来说作用是减少绘制。

接下来进入到CDialogBuilder::Create()函数,这里面会加载xml文件,这里又引出一个新类"CMarkup",这个类完成xml的加载与解析等工作,具体怎么解析的我也就大概看了下,不好发表看法,以后有机会补上。回到CDialogBuilder::Create(),最开始解析出所有的顶层节点属性,包括"Font"、"Default"、"Window"等,这里主要是看"Window",即主窗口。

(1) 解析主窗口属性

pstrClass = root.GetName();
if( _tcsicmp(pstrClass, _T("Window")) == 0 ) {
    if( pManager->GetPaintWindow() ) {
        int nAttributes = root.GetAttributeCount();
        for( int i = 0; i < nAttributes; i++ ) {
            pstrName = root.GetAttributeName(i);
            pstrValue = root.GetAttributeValue(i);
            pManager->SetWindowAttribute(pstrName, pstrValue);
        }
    }
}

进入函数CPaintManagerUI::SetWindowAttribute(),这里会设置主窗口的所有属性,像size(大小),sizebox(边框),caption(标题栏)等。

(2) 创建UI控件对象

解析完主窗口属性后,就开始解析所有的控件,进入函数CDialogBuilder::_Parse():

SIZE_T cchLen = _tcslen(pstrClass);
switch( cchLen ) {
case 4:
    if( _tcsicmp(pstrClass, DUI_CTR_EDIT) == 0 )                  pControl = new CEditUI;
    else if( _tcsicmp(pstrClass, DUI_CTR_LIST) == 0 )             pControl = new CListUI;
    else if( _tcsicmp(pstrClass, DUI_CTR_TEXT) == 0 )             pControl = new CTextUI;
    else if( _tcsicmp(pstrClass, DUI_CTR_TREE) == 0 )             pControl = new CTreeViewUI;
	else if( _tcsicmp(pstrClass, DUI_CTR_HBOX) == 0 )             pControl = new CHorizontalLayoutUI;
	else if( _tcsicmp(pstrClass, DUI_CTR_VBOX) == 0 )             pControl = new CVerticalLayoutUI;
    break;
case 5:

感兴趣的可以自己看下源码,这里只截取片段。功能就是根据节点名创建对应的对象,例如:

xml中配置的节点名是"Text",c++中对应宏定义为:

字符创长度为4,进入分支"case 4",然后根据节点名创建对应的对象"CTextUI"。如果有子节点,那么嵌套调用函数_Parse()自己:

(3) 解析控件属性

创建了UI对象后,接着就是设置UI属性:

每个控件都有自己的属性,且每个控件对属性可能都有自己独特的理解,所以每个控件都重写了虚函数SetAttribute(),例如按钮的SetAttribute()如下:

// Init default attributes
if( pManager ) {
    pControl->SetManager(pManager, NULL, false);
    LPCTSTR pDefaultAttributes = pManager->GetDefaultAttributeList(pstrClass);
    if( pDefaultAttributes ) {
        pControl->SetAttributeList(pDefaultAttributes);
    }
}
// Process attributes
if( node.HasAttributes() ) {
    // Set ordinary attributes
    int nAttributes = node.GetAttributeCount();
    for( int i = 0; i < nAttributes; i++ ) {
        pControl->SetAttribute(node.GetAttributeName(i), node.GetAttributeValue(i));
    }
}

到这里就已经完成了xml的解析和UI对象的创建等一系列功能了。

void CButtonUI::SetAttribute(LPCTSTR pstrName, LPCTSTR pstrValue)
{
	if( _tcscmp(pstrName, _T("normalimage")) == 0 ) SetNormalImage(pstrValue);
	else if( _tcscmp(pstrName, _T("hotimage")) == 0 ) SetHotImage(pstrValue);
	else if( _tcscmp(pstrName, _T("pushedimage")) == 0 ) SetPushedImage(pstrValue);
	else if( _tcscmp(pstrName, _T("focusedimage")) == 0 ) SetFocusedImage(pstrValue);
	else if( _tcscmp(pstrName, _T("disabledimage")) == 0 ) SetDisabledImage(pstrValue);
	else if( _tcscmp(pstrName, _T("foreimage")) == 0 ) SetForeImage(pstrValue);
	else if( _tcscmp(pstrName, _T("hotforeimage")) == 0 ) SetHotForeImage(pstrValue);
	else if( _tcscmp(pstrName, _T("fivestatusimage")) == 0 ) SetFiveStatusImage(pstrValue);
	else if( _tcscmp(pstrName, _T("fadedelta")) == 0 ) SetFadeAlphaDelta((BYTE)_ttoi(pstrValue));
	else if( _tcscmp(pstrName, _T("hotbkcolor")) == 0 )
	{
		if( *pstrValue == _T('#')) pstrValue = ::CharNext(pstrValue);
		LPTSTR pstr = NULL;
		DWORD clrColor = _tcstoul(pstrValue, &pstr, 16);
		SetHotBkColor(clrColor);
	}
	else if( _tcscmp(pstrName, _T("hottextcolor")) == 0 )
	{
		if( *pstrValue == _T('#')) pstrValue = ::CharNext(pstrValue);
		LPTSTR pstr = NULL;
		DWORD clrColor = _tcstoul(pstrValue, &pstr, 16);
		SetHotTextColor(clrColor);
	}
	else if( _tcscmp(pstrName, _T("pushedtextcolor")) == 0 )
	{
		if( *pstrValue == _T('#')) pstrValue = ::CharNext(pstrValue);
		LPTSTR pstr = NULL;
		DWORD clrColor = _tcstoul(pstrValue, &pstr, 16);
		SetPushedTextColor(clrColor);
	}
	else if( _tcscmp(pstrName, _T("focusedtextcolor")) == 0 )
	{
		if( *pstrValue == _T('#')) pstrValue = ::CharNext(pstrValue);
		LPTSTR pstr = NULL;
		DWORD clrColor = _tcstoul(pstrValue, &pstr, 16);
		SetFocusedTextColor(clrColor);
	}
	else CLabelUI::SetAttribute(pstrName, pstrValue);
}

 

5. 渲染

经过上面几个步骤,完成了xml文件的解析和UI对象的创建,接下来要做的就是把控件绘制到窗口上。Duilib中的控件分两种,一是容器,二是普通控件,且都是从类"CControlUI"继承而来,下面配一张简图说明:

什么是容器呢?从百度百科把定义拷贝过来:容器用来包装或装载物品的贮存器。Duilib里的容器也有类似的功能,就是可以包含其他的控件或容器。最常用的有两个类,"CVerticalLayoutUI"和"CHorizontalLayoutUI",即纵向布局和横向布局。其实我觉得Duilib里的这种布局的概念还是有点儿蹩脚的,它的好处是简化了xml的编写,只有在布局中才能包含子控件,而一般的控件的属性全部是以元素的形式编写在同一行,这样做的话xml编写会变得简单,弊端就是表现形式不够灵活,例如不能在Button中包含一个子Button或Text等(也有可能是我不会用),这样的话表达能力就很弱了。如果控件能随便包含子控件,想想是不是很灵活,像按钮上放置图片和文字,这是最常见的功能了吧。当然如果这样做的话,xml解析显然不会像现在这么简单。另外这个布局也显得很不灵活,不能随心所欲,例如我想让一个子控件相对于父控件(或兄弟控件)的九个顶点(left, right, top, bottom, center, topleft, topright, bottomleft, bottomright)中的任意一个布局,抱歉Duilib做不到。当然Duilib的定位是轻量级,在有限的代码内做力所能及的事,我们就不要要求那么多了,每个项目都有自己的优缺点,我们取其所长就好。

回过头来说绘制吧。windows的绘制一般都放在WM_PAINT消息中进行,Duilib也一样,进入CPaintManagerUI类的消息处理函数MessageHandler,然后看WM_PAINT分支,在这里会绘制整个窗口的内容。找到下面这一行代码:

m_pRoot要么是一个"CVerticalLayoutUI"对象,要么就是一个"CHorizontalLayoutUI"对象,为什么呢,因为顶层肯定是一个容器,不然怎么包含子控件。跟进到Paint函数看一下:

bool CControlUI::Paint(HDC hDC, const RECT& rcPaint, CControlUI* pStopControl)
{
	if (pStopControl == this) return false;
	if( !::IntersectRect(&m_rcPaint, &rcPaint, &m_rcItem) ) return true;
	if( OnPaint ) {
		if( !OnPaint(this) ) return true;
	}
	if (!DoPaint(hDC, rcPaint, pStopControl))
		return false;
    if( m_pCover != NULL ) return m_pCover->Paint(hDC, rcPaint);
    return true;
}

看到一个函数DoPaint(),有前面知道,此时的this就是上面的两种布局对象中的一个,所以这个DoPaint会调到CContainerUI的DoPaint,截取部分代码:

bool CContainerUI::DoPaint(HDC hDC, const RECT& rcPaint, CControlUI* pStopControl)
	{
		RECT rcTemp = { 0 };
		if( !::IntersectRect(&rcTemp, &rcPaint, &m_rcItem) ) return true;

		CRenderClip clip;
		CRenderClip::GenerateClip(hDC, rcTemp, clip);
		CControlUI::DoPaint(hDC, rcPaint, pStopControl);

		if( m_items.GetSize() > 0 ) {
			RECT rc = m_rcItem;
			rc.left += m_rcInset.left;
			rc.top += m_rcInset.top;
			rc.right -= m_rcInset.right;
			rc.bottom -= m_rcInset.bottom;
			if( m_pVerticalScrollBar && m_pVerticalScrollBar->IsVisible() ) rc.right -= m_pVerticalScrollBar->GetFixedWidth();
			if( m_pHorizontalScrollBar && m_pHorizontalScrollBar->IsVisible() ) rc.bottom -= m_pHorizontalScrollBar->GetFixedHeight();

			if( !::IntersectRect(&rcTemp, &rcPaint, &rc) ) {
				for( int it = 0; it < m_items.GetSize(); it++ ) {
					CControlUI* pControl = static_cast<CControlUI*>(m_items[it]);
					if( pControl == pStopControl ) return false;
					if( !pControl->IsVisible() ) continue;
					if( !::IntersectRect(&rcTemp, &rcPaint, &pControl->GetPos()) ) continue;
					if( pControl->IsFloat() ) {
						if( !::IntersectRect(&rcTemp, &m_rcItem, &pControl->GetPos()) ) continue;
                        if( !pControl->Paint(hDC, rcPaint, pStopControl) ) return false;
					}
				}
			}

主要是做两件事。一是绘制自己,二是绘制子控件,如果子控件也是布局,那么重复此过程,这样就可以对所用控件进行绘制。

控件的具体绘制动作在CControlUI::DoPaint()中:

bool CControlUI::DoPaint(HDC hDC, const RECT& rcPaint, CControlUI* pStopControl)
{
    // 绘制循序:背景颜色->背景图->状态图->文本->边框
    if( m_cxyBorderRound.cx > 0 || m_cxyBorderRound.cy > 0 ) {
        CRenderClip roundClip;
        CRenderClip::GenerateRoundClip(hDC, m_rcPaint,  m_rcItem, m_cxyBorderRound.cx, m_cxyBorderRound.cy, roundClip);
        PaintBkColor(hDC);
        PaintBkImage(hDC);
        PaintStatusImage(hDC);
        PaintText(hDC);
        PaintBorder(hDC);
    }
    else {
        PaintBkColor(hDC);
        PaintBkImage(hDC);
        PaintStatusImage(hDC);
        PaintText(hDC);
        PaintBorder(hDC);
    }
    return true;
}

这里是具体的绘制操作,再跟进到PaintBkColor,PaintBkImage这些函数,又会引出一个新类"CRenderEngine",顾名思义就是渲染引擎。这个类封装了所有的绘图功能,像绘制矩形,绘制椭圆等。到这里整个渲染过程就已经完成了,写的比较简洁,对照着代码看相信还是很容易理清楚的。

最后还有一个小细节双缓冲绘图,在windows编程中经常会提到这个概念。双缓冲是用来解决闪烁问题的一种方式,当然仅仅只是众多方式中的一种,因为屏幕闪烁跟很多因素相关,双缓冲解决的是由屏幕刷新频率造成的闪烁,即绘制动作不是在同一个刷新周期内完成,给人的感觉有可能就是"闪烁"。双缓冲简单来说就是先将屏幕内容绘制到内存DC中,也就是先在内存中把要绘制的内容准备好,然后再一次全部贴到屏幕上,而不是每一个控件各自直接在屏幕上绘制。

多年前做过一个项目,客户端用的是Duilib,后来很长时间都没用过,直到最近才重新看了看代码,顺手就做下记录,以后有机会再把消息路由这一块也整理下,希望对新手有所帮助。这里并没有教你如何使用Duilib,因为相关教程网上有很多,也很详细,对照着做几天就会用的很熟练;如果你想看Duilib源码,那么这儿或许能帮你简单的理理思路。

20180606 by LL.

转载于:https://my.oschina.net/u/3443876/blog/1825586

  • 3
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值