Win32 GDI 非矩形区域剪裁,双缓冲技术
传统的Win32通过GDI提供图形显示的功能,包括了基本的绘图功能,如画线、方块、椭圆等等,高级功能包括了多边形和Bezier的绘制。这样app就不用关心那些图形学的细节了,有点类似于UNIX上的X-window协议。你信或者不信,那些看上去很花哨的控件,其实就是一笔一划画上去的而已。GDI提供了画笔(用于线条)、画刷(用于填充)、调色板(用于支持256色显示)、字体(用于文字)。如果简单的图形不足以表达,你可以使用位图和画布(DC,设备上下文)直接将图像绘制到屏幕上去。此外,GDI还支持一些简单的坐标变换,以方便app缩放、平移绘制的内容,特别是支持打印机操作。在打印设备上,程序使用英寸而不是像素来绘画。不得不说,GDI的变换和位图部分不是很好用,DIB和DDB简直是折磨人,以至于经典的《Windows程序设计》大部分篇幅都在教你如何写字和画画。
GDI+内部调用了GDI,提供了一些更高级的新功能,如半透明(Alpha channel)、路径合并(Path Combination)、抗锯齿(Anti-alias)和渐变色(Gradient)。另外还包括了比GDI更全面的颜色/坐标变换方法,以及方便的图像显示。除了支持BMP以外,添加了GIF、PNG、JPG等编码解码器,方便app导入导出图像文件。从Windows Vista开始,图形显示领域有弱化、代替GDI的趋势。XP的普通窗口运行于GDI窗口管理器中,而从Vista开始所有窗口都运行于D3D的窗口管理器中。也就是说以前你往显示器上点颜色,而你现在只不过在往一个3D游戏中帖平面图而已。新的WPF程序的图形呈现完全依赖于D3D而跳过了GDI、GDI+。Win 7新出现的Direct 2D就致力于用显卡的加速功能代替GDI进行二维图形和文字的显示。
不知道Win8的Metro App是不是也是基于D3D的呢~~~
充分利用硬件加速好像是一个趋势
例子1
====
如何画出如“唐老鸭”这样一个造型的窗口。
窗口风格为 WS_POPUP,所以创建的窗口没有标题栏。但是当窗口没有标题栏后,我们就无法用拖动标题栏的办法来移动窗口,如果让窗口一动不动呆在屏幕中间显然是不行的,这里有一个替代办法,我们可以响应按下鼠标左键的消息,在 WM_LBUTTONDOWN 消息中想窗口发送 WM_NCLBUTTONDOWN (非客户区鼠标按下消息) 位置在 HTCAPTION 来模拟鼠标按在标题栏中来实现移动的功能。
Windows 里有专门的 API 来实现特殊形状的窗口,步骤是首先建立区域(Region),Region 可以合并,这样一来就可以用几个简单的区域合并出一个复杂的区域,建立、合并区域和设置窗口的 API 主要有以下几条:
CreateRectRgn(Left,Top,Right,Bottom) - 建立矩型区域
CreateEllipticRgn(Left,Top,Right,Bottom) - 建立椭圆区域
CreatePolygonRgn(lpPoints,NumberOfPoints,Mode) - 建立多边形区域,这些API返回区域句柄
CombineRgn(hDest,hSource1,hSource2,CombineMode) - 合并区域
SetWindowRgn(hWnd,hRgn,bRedraw) - 根据区域设置窗口形状
本程序的方法是扫描位图的点,按行设置区域,然后合并到总的区域中。
例子2
====
双缓冲技术
用VC做的画图程序,当所画的图形大于屏幕时,在拖动滚动条时屏幕就会出现严重的闪烁,为了解决这一问题,就得使用双缓冲来解决。程序产生严重的闪烁问题是因为画图过程中前后两次的画面反差很大造成的人的视觉的闪烁。因为在VC中每次在调用OnDraw时系统都是先用背景画刷将画布清除再执行画图命令,这样在你每次移动滚动条时每执行一次OnDraw就会有一个空白页,这样和你的最终结果图象之间有一个很大的反差,因而看起来闪烁,而且滚动条滚动越快闪烁越严重。当然,你可以将背景画刷设为NULL,这样可以解决闪烁问题,但是不能将先前的图象擦除,这样整个屏幕就显得很乱。
设备描述符抽象了不同的硬件环境为标准环境,用户编写时使用的是这个虚拟的标准环境,而不是真实的硬件,与真实硬件打交道的工作一般交给系统和驱动程序去完成(这同样解释了为什么我们需要经常更新驱动程序的问题)。使用在windows图形系统(gdi,而不包括direct x)上面,就体现在一系列的图形DC上面,我们如果要在gdi上面绘图,就必须先得到图形DC的句柄(handle),然后在指定句柄的基础上进行图形操作。
但是WM_PAINT消息响应的频度太高了,比如最小化最大化,移动窗体,覆盖等等都引起重绘,经常的这样画图,很是消耗性能;在有些场合,比如随机作图的场合,每一次就改变,还导致了程序的无法实现。怎么解决后一种问题呢。
ms在msdn的例子里面交给我们document/view的经典解决办法,将图形的数据存储在document类里面,view类只是根据这些数据绘图。比如你要画个圆,只是将圆心和半径存在document里面,view类根据这个里面的数据在屏幕上面重新绘制。那么,我们只需要随机产生一次数据就可以了。
这样还是存在性能的问题,于是我们开始考虑另外的解决方法。我们知道,将内存中的图片原样输出到屏幕是很快的,这也是我们在dos时代经常做的事情,能不能在windows也重新利用呢?答案就是内存缓冲绘图。
CRect rc; // 定义一个矩形区域变量
GetClientRect(rc);
int nWidth = rc.Width();
int nHeight = rc.Height();
CDC *pDC = GetDC(); // 定义设备上下文
CDC MemDC; // 定义一个内存显示设备对象
CBitmap MemBitmap; // 定义一个位图对象
//建立与屏幕显示兼容的内存显示设备
MemDC.CreateCompatibleDC(pDC);
//建立一个与屏幕显示兼容的位图,位图的大小可选用窗口客户区的大小
MemBitmap.CreateCompatibleBitmap(pDC,nWidth,nHeight);
//将位图选入到内存显示设备中,只有选入了位图的内存显示设备才有地方绘图,画到指定的位图上
CBitmap *pOldBit = MemDC.SelectObject(&MemBitmap);
//先用背景色将位图清除干净,否则是黑色。这里用的是白色作为背景
MemDC.FillSolidRect(0,0,nWidth,nHeight,RGB(255,255,255));
//绘图操作等在这里实现
MemDC.MoveTo(……);
MemDC.LineTo(……);
MemDC.Ellipse(……);
//将内存中的图拷贝到屏幕上进行显示
pDC->BitBlt(0,0,nWidth,nHeight,&MemDC,0,0,SRCCOPY);
//绘图完成后的清理
MemDC.SelectObject(pOldbitmap);
MemBitmap.DeleteObject();
在缓冲区还可以实现很多高级的图形操作,比如透明,合成等等,取决于具体的算法,需要对内存直接操作(其实就是当年dos怎么做,现在还怎么做)。
为什么不能用全局变量保存DC?
DC需要占用一定的内存,那么在频繁的页面调度中,位置难免改变,于是用来标志指针的句柄也就不同了。
为什么动画的重画频率高,而看起来却不闪?
闪烁是什么?闪烁就是反差,反差越大,闪烁越厉害。因为动画的连续两个帧之间的差异很小所以看起来不闪。如果不信,可以在动画的每一帧中间加一张纯白的帧,不闪才怪呢。
电力系统的网络图形的CAD软件,在一个窗口中往往要显示成千上万个电力元件,而每个元件又是由点、线、圆等基本图形构成。如果真要在一次重绘过程重画这么多元件,可想而知这个过程是非常漫长的。如果加上了图形的浏览功能,鼠标拖动图形滚动时需要进行大量的重绘,速度会慢得让用户将无法忍受。怎么办?只有再研究研究MFC的绘图过程了。
实际上,在OnDraw(CDC *pDC)中绘制的图并不是所有都显示了的,例如:你在OnDraw中画了两个矩形,在一次重绘中虽然两个矩形的绘制函数都有执行,但是很有可能只有一个显示了,这是因为MFC本身为了提高重绘的效率设置了裁剪区。裁剪区的作用就是:只有在这个区内的绘图过程才会真正有效,在区外的是无效的,即使在区外执行了绘图函数也是不会显示的。因为多数情况下窗口重绘的产生大多是因为窗口部分被遮挡或者窗口有滚动发生,改变的区域并不是整个图形而只有一小部分,这一部分需要改变的就是pDC中的裁剪区了。因为显示(往内存或者显存都叫显示)比绘图过程的计算要费时得多,有了裁剪区后显示的就只是应该显示的部分,大大提高了显示效率。但是这个裁剪区是MFC设置的,它已经为我们提高了显示效率,在进行复杂图形的绘制时如何进一步提高效率呢?那就只有去掉在裁剪区外的绘图过程了。可以先用pDC->GetClipBox()得到裁剪区,然后在绘图时判断你的图形是否在这个区内,如果在就画,不在就不画。但如果你的绘图过程不复杂,这样做可能对你的绘图效率不会有提高。
例子3
====
在窗口中显示出按正弦曲线起伏排列的“龙腾虎跃”五个楷体大字。窗口背景为灰色,文字前景则为一幅256色位图,就好象是把彩图剪成文字粘贴在窗口上一样。
调用BeginPath()函数开始路径定义
定义路径的GDI绘图函数包括:
AngleArc Arc ArcTo Chord *CloseFigure
Ellipse *ExtTextOut *LineTo *MoveToEx Pie
*PolyBezier *PolyBezierTo PolyDraw *Polygon *Polyline
*PolyLineTo *PolyPolygon *PolyPolylin Rectangl RoundRect
*TextOut
调用EndPath()函数结束路径定义。
绘制路径轮廓StrokePath(),填充路径FillPath(),绘制轮廓并填充StrokeAndFillPath(),把路径转换成区域PathToRegion(),把路径直线化FlattenPath(),提取路径数据GetPath(),加宽路径WidenPath()和设置裁剪路径SelectClipPath()等。
m_fontKaiTi.CreateFont(200 , 0 , 0 , 0 , FW_BLACK ,
FALSE , FALSE , FALSE ,
GB2312_CHARSET ,
OUT_DEFAULT_PRECIS ,
CLIP_DEFAULT_PRECIS ,
DEFAULT_QUALITY ,
FIXED_PITCH | FF_MODERN,
"楷体_GB2312");
RECT rect;
GetClientRect(&rect);
CFont* pOldFont=(CFont*)pDC->SelectObject(&m_fontKaiTi);
pDC->SetBkMode(TRANSPARENT);
//定义路径
pDC->BeginPath();{
pDC->TextOut(0,10,"龙",2);
pDC->TextOut(200,10,"腾",2);
pDC->TextOut(400,10,"虎",2);
pDC->TextOut(600,10,"跃",2); }
pDC->EndPath();
pDC->SelectObject(pOldFont);
//检取路径数据
int nCount=pDC->GetPath(NULL,NULL,0);
CPoint* points=new CPoint[nCount];
BYTE* bytes=new BYTE[nCount];
pDC->GetPath(points,bytes,nCount);
//对路径定义点按正弦曲线进行变换
int i;
for(i=0;i< nCount;i++)>/p>
points[i].y=points[i].y+(int)(80*sin(points[i].x
/300.*3.1415926)+100);
//重建一个新的路径
CPoint ptStart;
pDC->BeginPath();{
for(i=0;i< nCount;i++){>/p>
switch(bytes[i]){
//移动当前点位置
case PT_MOVETO:
pDC->MoveTo(points[i]);
ptStart=points[i];
break;
//画直线
case PT_LINETO:
pDC->LineTo(points[i]);
break;
//画贝塞尔曲线
case PT_BEZIERTO:
pDC->PolyBezierTo(points+i,3);
i=i+2;
break;
//画贝塞尔曲线并封闭图形
case PT_BEZIERTO|PT_CLOSEFIGURE:
points[i+2]=ptStart;
pDC->PolyBezierTo(points+i,3);
i=i+2;
break;
//画直线并封闭图形
case PT_LINETO|PT_CLOSEFIGURE:
pDC->LineTo(ptStart);
break;
}
pDC->EndPath();
//绘制窗口灰色背景
CBrush* pOldBrush=(CBrush*)(pDC->SelectStockObject(GRAY_BRUSH));
pDC->Rectangle(&rect);
pDC->SelectObject(pOldBrush);
//设置裁剪路径
pDC->SetPolyFillMode(WINDING);
pDC->SelectClipPath(RGN_COPY);
//用位图填充裁剪区域
CBitmap bmp;
CBitmap* pBmpOld;
bmp.LoadBitmap(IDB_BMP);
CDC dcMem;
dcMem.CreateCompatibleDC(pDC);
pBmpOld=dcMem.SelectObject(&bmp);
pDC->StretchBlt(0,0,rect.right,rect.bottom,
&dcMem,0,0,600,100,SRCCOPY);
dcMem.SelectObject(pBmpOld);
dcMem.DeleteDC();
bmp.DeleteObject();