DirectUI框架GUIFW

最近抽出点时间学习了DuiLib,也总结了Qt库的一些可借鉴的思想。这两个库有许多学习的地方,于是就抽丝剥茧,写了一个简单的DirectUI框架guifw皮肤库,可以去http://download.csdn.net/detail/sllins/7707771 下载看看效果,先看看下面的效果
DirectUI框架GUIFW_第1张图片
DirectUI框架GUIFW_第2张图片DirectUI框架GUIFW_第3张图片DirectUI框架GUIFW_第4张图片
guifw实现了xml动态创建,实现了UI和逻辑的分离,支持换肤等,基于guifw所搭建的框架可以很容易派生自定义控件,目前实现了常用的button,image,label等控件,由于时间关系,没有对其他控件进行完善。下面对guifw的整个框架进行大概的总结。

所谓DirectUI(Paint on parent dc directly)就是直接在父窗口上绘图,也即直接在DC上面画一个窗口里面的所有控件,最终整个窗口是一张图。所有的DirectUI最基本框架的无外乎就是动态创建,事件派发,绘制引擎和UI布局这四部分组成,guifw也是如此,对于guifw来说,所有的窗口都从GuiFrameWindow派生,而所有的控件都是从GuiWidget派生,窗口里面的内容都由GuiWidget组成。GuiFrameWindow和GuiWidget都派生自GuiObject,一个继承自GuiObject的对象只要有Parent,那么会在Parent删除的时候一并被删除。下面对整个框架进行介绍

 一、框架简介
DirectUI框架GUIFW_第5张图片

上图是guifw的框架图,其中底层是core模块,用于为上层和外部提供最基本的支持,基于core模块可以扩展各种各样的控件,对话框等,最终形成一个完整的皮肤库。
1、 xmlui主要实现了动态创建功能
2、 layout实现了对GuiFrameWindow里面所有的GuiWidget的逻辑排版,支持HBox和VBox
3、 application是整个程序的入口,程序所有消息都经过这里,再派发给相应的窗口
4、 skinmanager实现换肤功能以及换肤广播工作
5、 window是所有窗口的基类,也即GuiFrameWindow,GuiframeWindow管理着所有的GuiWidget,负责将绘制以及事件派发给相应的GuiWidget等工作
6、 widget是每个窗口里面最基本的元素,也即GuiWidget,一个窗口的界面由若干个widget组成,所有的控件都由它派生,整个界面数据结构是由一颗每个节点是widget组成的树。
7、 painter跟窗口有关,每个窗口都有一个painter,在需要绘制的时候将painter传给需要重绘区域里面的所有widget,guifw里面采用的绘图引擎是gdi+。实际上painter就是封装了对一个窗口相关的Bitmap的绘图操作,所有的widget都在这张bitmap上面画图,最终绘制出来一个窗口就是一张图。
8、 eventdispatcher是事件派发者,包括application将事件派发给所有的窗口以及窗口派发事件给需要响应的widget两部分。

下面对guifw进行更深入的介绍。

二、动态创建

动态创建要解决的就是如何根据类名创建一个类,我们很容易想到switch/case语句,DuiLib就是这么做的。但这么做是最简单扩展性极差的办法,如果新增加多一种类型就必须在case语句加上这种类型并且创建相应的实例,如果我们开发的皮肤库是以dll的方式提供给第三方使用者而不是源码,那就完蛋了,根本无法自定义控件。

其实只要想到将类名和类似getInstance这样创建该类对象的函数注册进一个map里面,当我们需要创建某个类的实例的时候就去这个map里面找,找到了就调用相应的getInstance创建实例即可,这个问题就解决了,这个事情就是xmlui所做的一部分工作,xmlui还将创建整颗界面树,解析和设置widget的属性等等

//用于动态创建的函数定义
typedef GuiWidget* (* pGuiWidgetCreateFunc)(GuiWidget *parent);

//动态创建的宏
#define GUI_DECLARE_DYNAMIC_CREATE_SELF(classname,parenttype,functype) \
public:\
 static parenttype* dynamicCreateObject##classname(parenttype* parent) \
 { \
  classname* p = new classname(parent); \
  return static_cast(p);\
 } \
private: \
 static TypeNode s_register##classname;

#define GUI_IMPLEMENT_DYNAMIC_CREATE_SELF( classname, functype ) \
 TypeNode classname::s_register##classname=TypeNode( L#classname, classname::dynamicCreateObject##classname );

//widget动态创建宏
#define GUI_DECLARE_WIDGET_DYNAMIC_CREATE_SELF( classname ) \
 GUI_DECLARE_DYNAMIC_CREATE_SELF( classname, GuiWidget, pGuiWidgetCreateFunc );

#define GUI_IMPLEMENT_WIDGET_DYNAMIC_CREATE_SELF( classname ) \
 GUI_IMPLEMENT_DYNAMIC_CREATE_SELF( classname, pGuiWidgetCreateFunc );
这里省略了列表定义以及将classname,functype这个对应关系插入map的操作。这样定义之后只要在需要支持动态创建的类声明里面添加GUI_DECLARE_WIDGET_DYNAMIC_CREATE_SELF这个声明,再在类实现添加GUI_IMPLEMENT_WIDGET_DYNAMIC_CREATE_SELF定义就可以了,这样动态创建功能就实现了,关于属性方面就没什么好说的了,无非就是解析xml然后进行相关设置。
三、事件派发
程序消息由application接收,再将事件转给相应窗口的窗口过程,窗口过程将消息细分后转给对应的窗口,窗口再将消息转给需要响应的widget。

1、 Application转发消息给相应窗口,其中eventFilterFunc是给外部一个机会过滤消息

MSG msg = { 0 };
while( ::GetMessage(&msg, NULL, 0, 0) ) 
{
	bool bMsgFlitered = false;
	if ( d_ptr->eventFilterFunc )
	{
		bMsgFlitered = (*(d_ptr->eventFilterFunc))( & msg ) ;
	}
	if( !bMsgFlitered ) 
	{
		::TranslateMessage(&msg);
		::DispatchMessage(&msg);
	}
}

2、 窗口过程将消息细分后转给对应的窗口,windoweventdispatcher就是干这个事情的
LRESULT CALLBACK GuiWindowEventDispatcher::WinProc( HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam )
{
	GuiFrameWindow* pData = (GuiFrameWindow*)::GetWindowLong( hWnd, GWL_USERDATA );
	if ( pData == NULL || pData->hwnd() != hWnd )
	{
		return ::DefWindowProc( hWnd, uMsg, wParam, lParam );
	}

	pData->winEvent( &ev );
	switch( uMsg ) 
	{
		case WM_NCHITTEST:
			{
				LRESULT lrs = pData->windowNcHitTest( &ev );
				return lrs;
			}
			break;
		case WM_SIZE: 
			{
				pData->windowResizeEvent( &ev ); 
				return FALSE ;
			}
			break;
		case WM_LBUTTONDOWN: 
			{
				pData->windowMouseLButtonPressEvent( &ev ); 
				return FALSE;
			}
			break;
		case WM_LBUTTONUP: 
			{
				pData->windowMouseLButtonReleaseEvent( &ev ); 
				return FALSE;
			}
			break;
		case GuiPaint:
			{
				pData->windowPaintEvent();
				return FALSE;
			}
			break;
		//…
		default:
			break;
	}

	return ::DefWindowProc( hWnd, uMsg, wParam, lParam );
}
3、 窗口再将消息转给需要响应的widge,通过widgeteventdispatcher完成,也就是说,每个窗口都有一个widgeteventdispatcher成员,负责将窗口消息转给widget。
//eg01
void GuiFrameWindow::windowMouseLButtonPressEvent(GuiEvent* msg)
{
      mouseLButtonPressEvent( msg);
      d_ptr->wigetEventDispatcher->mouseLButtonPressEvent( msg);
}
 
void GuiWidgetEventDispatcher::mouseLButtonPressEvent(GuiEvent* msg)
{
      Point pt = d_ptr->frameWindow->clientPointFromEvent( msg);
      GuiWidget* newClickWidget= d_ptr->frameWindow->findWidgetByPoint( pt);
      if ( newClickWidget!= NULL )
      {
           newClickWidget->mouseLButtonPressEvent(msg );
      }
      d_ptr->oldClickWidget= newClickWidget;
}
 
//eg02
void GuiFrameWindow::windowResizeEvent(GuiEvent* msg)
{
      resizeEvent( msg);
      d_ptr->wigetEventDispatcher->resizeEvent( msg);
}
void GuiWidgetEventDispatcher::resizeEvent(GuiEvent* msg)
{
      // DispatchEventToWidgets是一个宏定义,他会分层遍历整颗界面树然后调用每个widget的resizeEvent函数
      DispatchEventToWidgets( resizeEvent,msg );
}
4、 走到了第三步,widget事件处理的问题就很容易了,例如一个按钮,有多种状态,我们只需要在各个事件发生的时候设置正确的状态,然后调用update即可。目前guifw已经实现了大部分事件的派发。
protected:
      friend class GuiWidgetEventDispatcher;
      virtual void construct();
      virtual void paintEvent( GuiPainter*painter );
      virtual void closeEvent( GuiEvent*msg );
      virtual void resizeEvent( GuiEvent*msg );
      virtual void hoverMoveEvent( GuiEvent*msg );
      virtual void hoverEnterEvent( GuiEvent*msg );
      virtual void hoverLeaveEvent( GuiEvent*msg );
      virtual void mouseLButtonPressEvent( GuiEvent*msg );
      virtual void mouseLButtonReleaseEvent( GuiEvent*msg );
      //...

事件派发的比较麻烦的地方在于怎么合理处理某些事件的过渡,例如HoverEnter和HoverLeave和HoverMove,再例如先在其他地方按下鼠标,再将鼠标移到按钮上面放开,这个时候按钮不应该收到release消息的,这些问题如需要妥当处理否则可能导致消息派发错

四、绘图引擎

根据上面搭建的widget结构树,绘图基本上已经不成问题了,最简单的做法是在需要绘制的时候分层遍历整棵树,然后一次性把这张图贴到窗口上,但很显然,我们不可能为了更新一个按钮区域而刷新整个窗口,这个效率太差了,所以需要对此进行改进,两个办法:设置“脏区域”(也即无效区域)以及设置裁剪区域,,在绘制的时候设置裁剪区域并且只绘制“脏区域”内的widget,Qt和DuiLib都是这么做的。
可能在事件派发的时候你已经留意到guifw在绘制的时候使用的是GuiPaint 自定义消息然后调用window的windowPaintEvent去进行绘制,这是因为guifw最终使用的是UpdateLayeredWindow去绘制窗口,目前guifw没有实现用BitBlt去绘制,更高效的做法应该是支持两种情况,当没有透明度的时候我们就用BitBlt,当需要支持透明的时候就用UpdateLayeredWindow,Qt就是这样实现的,DuiLib就只用了BitBlt,所以这个就意味着DuiLib不支持背景半透明这个局限。回到刚才的问题,因为使用了UpdateLayeredWindow所以就没有WM_PAINT消息了,也就是说guifw自己接管了绘制工作,而不依赖于系统去绘制。这会带来另外一个问题是我们刚才说设置脏区域,UpdateLayeredWindow不能像BitBlt那样可以局部刷新,窗口无效区域只对WM_PAINT有效,所以我们需要自己管理无效区域,这个并不麻烦,只需要在窗口内保存一个Rect标识无效区域,在绘制的时候只绘制无效区域内的widget即可。
具体操作如下:

1、在绘制前已经设置好了无效区域,需要注意的是无效区域必须合并

void GuiFrameWindow::update(Rect dirtyRect)
{
      if ( dirtyRect.IsEmptyArea() )
      {
           d_ptr->painter->updatePainter();
           d_ptr->dirtyRect= rect();
      }
      else
      {
           if ( d_ptr->dirtyRect.IsEmptyArea())
           {
                 d_ptr->dirtyRect= dirtyRect;
           
           else
           {
                 Rect::Union( d_ptr->dirtyRect,dirtyRect, d_ptr->dirtyRect );
           }
      }
 
      _postMessage( GuiPaint,0, 0 ); //通知窗口绘图
}
2、前面我们说到当程序收到GuiPaint的反应是调用窗口的windowPaintEvent函数进行绘图
3、绘图前先设置裁剪区域,之后先画背景(如果有)
4、紧跟着画所有在无效区域内可见的widget

d_ptr->wigetEventDispatcher->paintEvent(d_ptr->painter);
前面我们已经知道wigetEventDispatcher是干嘛的了,我们再看看wigetEventDispatcher里面的paintEvent实现。

void GuiWidgetEventDispatcher::paintEvent(GuiPainter* painter)
{
      if ( d_ptr->frameWindow == NULL)
      {
           return;
      }
 
      std::deque dequeNode;
      dequeNode.push_back(static_cast(d_ptr->frameWindow->rootWidget() ) );
      while( !dequeNode.empty() )
      {
           GuiObject *pItem=dequeNode.front();
           dequeNode.pop_front();
           GuiWidget* itemWidget= static_cast( pItem );
           if ( itemWidget&& itemWidget->isVisible() ) //如果widget可见
           {
                 bool widgetInDirtyRect= ( itemWidget->geometry().Intersect( d_ptr->frameWindow->dirtyRect()) == TRUE );
                 if ( widgetInDirtyRect)  //如果widget在无效区域内才画
                 {
                      itemWidget->paintEvent(painter );
                 }
           }
           for ( std::list::iterator it = pItem->getChildren().begin(); it != pItem->getChildren().end(); it++ )
           {
                 dequeNode.push_back(*it );
           }
      }
}

paintEvent实际上干的事情只有一个,那就是是分层遍历整颗widget树,将可见并且在无效区域内的widget进行绘制,分层遍历为的是先调用父亲的paint然后再调用孩子的paint,也可以理解为这个是widget之间的z轴关系。

前面提到painter说是封装了对一个窗口相关的Bitmap的绘图操作,所有的widget都在这张bitmap上面画图,最终绘制出来一个窗口就是一张图。下面看看painter的一些实现。
void GuiPainterPrivate::updatePainter()
{
       if ( frameWindow== NULL )
       {
              return;
       }
 
       clearPainter();
       memoryBitmap = newBitmap( frameWindow->width(), frameWindow->height() );
       graphics = Graphics::FromImage( memoryBitmap);
}
在窗口resize的时候就调用updatePainter,updatePainter干的事情是更新一张和window尺寸一样的Bitmap,在窗口大小没有发生改变的情况下,所有的绘图操作都在这张Bitmap上面完成,举个例子,例如绘制一张图,可以如下操作
void GuiPainter::drawImage(const std::wstring& imagePath,const Rect&rc )
{
       if ( d_ptr->graphics == NULL)
       {
              return;
       }
 
       Image image( imagePath.c_str());
       d_ptr->graphics->DrawImage( &image, rc );
}
这样就实现了在Bitmap上面绘制一张图的目的,其他DrawText等也就类似的操作了。
这就是为什么要把painter传给每个widget的原因,所有的widget都共用一个painter以及painter提供的drawImage,drawText函数,通过共用一个painter也就是实现了所有widget的绘图操作都在同一个Bitmap上面进行。
5、最后将整个内存位图一次性贴到窗口上

//完整的windowPaintEvent实现代码如下:
void GuiFrameWindow::windowPaintEvent()
{
      if ( !isVaildWindow())
      {
           return;
      }  
 
      //设置裁剪区域
      d_ptr->painter->setClipRect( d_ptr->dirtyRect, CombineModeReplace);
      if ( !d_ptr->bTransparentBackground )
      {
           drawBackground( d_ptr->painter );
      }
 
      paintEvent( d_ptr->painter );
      //画所有的widget
      d_ptr->wigetEventDispatcher->paintEvent( d_ptr->painter );
 
      //将整张图一次性贴到DC上
      PAINTSTRUCT ps= { 0 };
      HDC hdc = BeginPaint( hwnd(),&ps); 
      HDC hMemDc = CreateCompatibleDC( hdc);  // 创建与当前DC兼容的内存DC   
 
      BYTE * pBits ;
      BITMAPINFOHEADER bmih;
      ZeroMemory( &bmih,sizeof( BITMAPINFOHEADER) );
      bmih.biSize = sizeof (BITMAPINFOHEADER);
      bmih.biWidth = width() ;
      bmih.biHeight =height() ;
      bmih.biPlanes =1 ;
      bmih.biBitCount= 32;
      bmih.biCompression= BI_RGB ;
      bmih.biSizeImage= 0 ;
      bmih.biXPelsPerMeter= 0 ;
      bmih.biYPelsPerMeter= 0 ;
      bmih.biClrUsed= 0 ;
      bmih.biClrImportant= 0 ;
      HBITMAP hBmpBuffer= CreateDIBSection(NULL,(BITMAPINFO *) &bmih,0, (VOID**)&pBits,NULL, 0) ;// 创建一块指定大小的位图
      HGDIOBJ hPreBmp= SelectObject( hMemDc,hBmpBuffer );  // 将该位图选入到内存DC中,默认是全黑色的  
 
      Image* bitmap =d_ptr->painter->memoryBitmap();
      Graphics graph(hMemDc );
      graph.DrawImage(bitmap, 0, 0, 0, 0, (INT)bitmap->GetWidth(), (INT)bitmap->GetHeight(),UnitPixel );
      graph.ReleaseHDC(hMemDc );
 
      POINT ptWinPos={geometry().GetLeft(),geometry().GetTop()};
      SIZE sizeWindow={width(),height()};
      POINT ptSrc={0,0};
      DWORD dwExStyle= ::GetWindowLong( hwnd(),GWL_EXSTYLE );
      if ( (dwExStyle&0x80000)!= 0x80000 )
      {
           ::SetWindowLong( hwnd(),GWL_EXSTYLE,dwExStyle^0x80000);
      }
 
      ::UpdateLayeredWindow( hwnd(),hdc, &ptWinPos,&sizeWindow, hMemDc,&ptSrc, 0, &d_ptr->blend, ULW_ALPHA );
 
      SelectObject( hMemDc,hPreBmp ); 
      DeleteObject( hBmpBuffer); 
      DeleteDC( hMemDc);
      EndPaint( hwnd(),&ps );
     
      _setWindowCorner(); //设置窗口圆角
      d_ptr->dirtyRect= Rect(0,0,0,0); //更新无效区域
}

通过这样我们就完成了整个窗口的绘制,这样我们的界面就能顺利绘制出来了。

五、UI布局
guifw排版和绘制是分开的,并不像duilib一样把绘制和排版混在一起,在绘制之前先排版。目前支持两种基本的布局方式:HBox和VBox,每个widget如果有孩子那么一定要设置布局方式。

void GuiWidget::setLayout(const std::wstring& value)
{    
       if ( value == L"HBox")
       {
              setLayout( HBox);
       }
       else if ( value == L"VBox")
       {
              setLayout( VBox);
       }
}
然后widget通过在设置位置或者大小的时候widget会自动调用updateLayout进行排版
void GuiWidget::updateLayout()
{
       if ( d_ptr->layoutType == HBox)
       {
              GuiWidgetHBoxLayout::updateLayout(this );
       }
       else if ( d_ptr->layoutType== VBox )
       {
              GuiWidgetVBoxLayout::updateLayout(this );
       }
}

updateLayout函数实现布局目前只是进行了比较简单计算操作,如果要扩展布局方式也很简单,只需要只增加多一种类型,然后调用相应的排版函数进行排版,updateLayout里面要支持多种属性(例如padding等)都很方便,只不过是排版算法的问题。

六、二进制兼容

如果在开发皮肤库的时候是以dll方式提供给第三方使用者而不是源码,那么考虑二进制兼容很有必要,Qt的做法非常值得借鉴,guifw也是学习了Qt的这种做法,采用私有类指针,这样成员变量保存在私有类里面,成员变量的改动不会破坏二进制兼容,当然如果改动成员函数那就没办法了。

七、其他

在做guifw之前其实可以设计得更加灵活,例如绘图引擎可以定义一个接口类,再实现一个对gdi+进行封装的绘图引擎,这样如果以后有需要换其他方式绘图引擎只需要实现引擎接口然后把新引擎注册进去就可以,这是Qt的做法,但由于这样做需要自己封装Rect,Point,Image等等常用的类型,guifw里面使用的是gdi+的这些类型,所以无法将gdi+做到彻底隔离。

八、写在后面的话
guifw开发前前后后用了差不多一个月的时间,框架还是比较简陋,可优化的地方还有很多,通过实践这个项目,对自己的提升还是很有帮助,duilib估计大家比较熟悉,如果你没接触过Qt,我强烈建议你去看一下,因为里面可以学习的东西太多了,甚至你可以基于Qt去写一个皮肤库,绘图引擎Qt已经帮里做了,事件派发Qt也已经帮你做了,布局也帮你做了一些,你需要做的,只是组织一下布局以及动态创建,一个皮肤库就出来了。另外由于整个框架还是比较简陋就没打算开源,主要是思想,如果你有兴趣了解源码或者交流,请留言给我。

参考资料:
1:http://www.viksoe.dk/code/windowless1.htm
2:http://code.google.com/p/duilib/
3:http://qt-project.org/

你可能感兴趣的:(C++高级编程)