为GDI函数增加透明度处理
用户对客户端的UI的要求越来越高,采用alpha通道对前景背景做混合是提高UI质量的重要手段。
UI开发离不开GDI,然后要用传统的GDI函数来处理alpha通道通常是一个恶梦:虽然有AlphaBlend这个API可以做alpha混合,但是前提必须是操作的DC中的位图有alpha通道的数据,问题的关键在于GDI函数在操作的地方会把原来的alpha通道清空。
使用GDI做alpha混合还要增加透明度关键要解决2个问题:
1、需要把内容画到一个临时位图上,同时保护好alpha通道。
2、在于把临时位图的数据和原位图做混合,而且不能改变镂空部分原位图的alpha通道的值。
在SOUI的render-gdi中我采用下面的类来实现GDI的半透明。
class DCBuffer { public: DCBuffer(HDC hdc,LPCRECT pRect,BYTE byAlpha,BOOL bCopyBits=TRUE) :m_hdc(hdc) ,m_byAlpha(byAlpha) ,m_pRc(pRect) ,m_bCopyBits(bCopyBits) { m_nWid = pRect->right-pRect->left; m_nHei = pRect->bottom-pRect->top; m_hBmp = SBitmap_GDI::CreateGDIBitmap(m_nWid,m_nHei,(void**)&m_pBits); m_hMemDC = ::CreateCompatibleDC(hdc); ::SetBkMode(m_hMemDC,TRANSPARENT); ::SelectObject(m_hMemDC,m_hBmp); ::SetViewportOrgEx(m_hMemDC,-pRect->left,-pRect->top,NULL); //从原DC中获得画笔,画刷,字体,颜色等 m_hCurPen = ::SelectObject(hdc,GetStockObject(BLACK_PEN)); m_hCurBrush = ::SelectObject(hdc,GetStockObject(BLACK_BRUSH)); m_hCurFont = ::SelectObject(hdc,GetStockObject(DEFAULT_GUI_FONT)); COLORREF crCur = ::GetTextColor(hdc); //将画笔,画刷,字体设置到memdc里 ::SelectObject(m_hMemDC,m_hCurPen); ::SelectObject(m_hMemDC,m_hCurBrush); ::SelectObject(m_hMemDC,m_hCurFont); ::SetTextColor(m_hMemDC,crCur); if(m_bCopyBits) ::BitBlt(m_hMemDC,pRect->left,pRect->top,m_nWid,m_nHei,m_hdc,pRect->left,pRect->top,SRCCOPY); //将alpha全部强制修改为0xFF。 BYTE * p= m_pBits+3; for(int i=0;i<m_nHei;i++)for(int j=0;j<m_nWid;j++,p+=4) *p=0xFF; } ~DCBuffer() { //将alpha为0xFF的改为0,为0的改为0xFF BYTE * p= m_pBits+3; for(int i=0;i<m_nHei;i++)for(int j=0;j<m_nWid;j++,p+=4) *p=~(*p); BLENDFUNCTION bf={AC_SRC_OVER,0,m_byAlpha,AC_SRC_ALPHA }; BOOL bRet=::AlphaBlend(m_hdc,m_pRc->left,m_pRc->top,m_nWid,m_nHei,m_hMemDC,m_pRc->left,m_pRc->top,m_nWid,m_nHei,bf); ::DeleteDC(m_hMemDC); ::DeleteObject(m_hBmp); //恢复原DC的画笔,画刷,字体 ::SelectObject(m_hdc,m_hCurPen); ::SelectObject(m_hdc,m_hCurBrush); ::SelectObject(m_hdc,m_hCurFont); } operator HDC() { return m_hMemDC; } protected: HDC m_hdc; HDC m_hMemDC; HBITMAP m_hBmp; LPBYTE m_pBits; BYTE m_byAlpha; LPCRECT m_pRc; int m_nWid,m_nHei; BOOL m_bCopyBits; HGDIOBJ m_hCurPen; HGDIOBJ m_hCurBrush; HGDIOBJ m_hCurFont; };
下面以实现DrawText的半透明为例来分析如何实现GDI函数的半透明。
HRESULT SRenderTarget_GDI::DrawText( LPCTSTR pszText,int cchLen,LPRECT pRc,UINT uFormat) { if(uFormat & DT_CALCRECT) { ::DrawText(m_hdc,pszText,cchLen,pRc,uFormat); return S_OK; } if(cchLen == 0) return S_OK; { DCBuffer dcBuf(m_hdc,pRc,m_curColor.a); ::DrawText(dcBuf,pszText,cchLen,pRc,uFormat); } return S_OK; }
首先来看如何解决alpha通道的保护问题。
为了在目标HDC上调用DrawText绘制文字,先声明一个DCBuffer对象:dcBuf。
DCBuffer的构造函数中,我们会创建一个临时的32位位图。
再将原DC中的数据复制到临时位图中(注意,原位图也是32位的)。
一个非常重要的工作在于在调用GDI的DrawText之前,DCBuffer会先把临时位图中alpha通道置为255。这样做的目的在于标识哪些像素被DrawText修改过。
在调用了::DrawText后,SRenderTarget_GDI::DrawText会进入DCBuffer的析构函数。
在析构函数中,首先对alpha通道中的值取反,经过这一步操作,被::DrawText清空的点的alpha通道值被修改成255,而那些需要透明的点的alpha值则变成了0。
到这里已经实现了对alpha通道的保护。
有了前面的基础,做第二步的alphablend就简单了,这里只需要直接调用API:AlphaBlend,注意BLENDFUNCTION中几个参数的设置。
下面解释一下为什么需要做上面的处理就可以实现GDI函数的半透明。
首先如DrawText这样的GDI函数通常会产生透明效果:即矩形中的一部分点变色,而其它点不变色。
GDI函数只会将那些变色的点的alpha通道清0。我们的目标则是将变色的点的RGB值与目标做alpha混合。
通过将临时位图中的alpha值做取反处理,被GDI函数修改过的点的alpha变为255,而需要镂空的点的alpha则变为了0。
此时再调用用AlphaBlend做混合,对于那些需要镂空的点,由于临时位图的alpha为0,混合后根据AlphaBlend的公式,即不会改变原来的RGB值,也不会改变原来的alpha值。
对于那些被GDI函数改变过的点,由于其alpha值都变成了255,其RGB部分,AlphaBlend会根据BLENDFUNCTION中指定的alpha值来和原值混合,而alpha部分则被修改为255。
最终达到半透明效果。
注:DCBuffer中CopyBits这一步有时候不是必须的。不过很多函数如DrawText需要做反锯齿处理,反锯齿处理的关键也是和背景色做混合,因此从原位图复制出数据也是很有必要的。
如果用GDI+也可以达到相同的效果,但是GDI+出了名的效率低,不知道GDI函数经过如此处理后效率会不会比GDI+慢,从我目前简单的测试来看,效果还是很好的,效率也很高,有兴趣的朋友可以比较一下。