所谓DirectUI(Paint on parent dc directly)就是直接在父窗口上绘图,也即直接在DC上面画一个窗口里面的所有控件,最终整个窗口是一张图。所有的DirectUI最基本框架的无外乎就是动态创建,事件派发,绘制引擎和UI布局这四部分组成,guifw也是如此,对于guifw来说,所有的窗口都从GuiFrameWindow派生,而所有的控件都是从GuiWidget派生,窗口里面的内容都由GuiWidget组成。GuiFrameWindow和GuiWidget都派生自GuiObject,一个继承自GuiObject的对象只要有Parent,那么会在Parent删除的时候一并被删除。下面对整个框架进行介绍
一、框架简介上图是guifw的框架图,其中底层是core模块,用于为上层和外部提供最基本的支持,基于core模块可以扩展各种各样的控件,对话框等,最终形成一个完整的皮肤库。
下面对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然后进行相关设置。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都是这么做的。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函数进行绘图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等也就类似的操作了。//完整的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布局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+做到彻底隔离。
八、写在后面的话