第三章:离屏技术(也就是我们常说的屏幕缓冲区)
导读
在我们学习离屏(OFF SCREEN)技术之前,让我们看看ON SCREEN是什么?为什么没有任何游戏程序使用这种方法?此外,还有另外一个基础问题,我们怎么在窗口的客户区绘制文字、图片或者是图画?
通常,在屏幕上显示任何东西的方法是使用GDIs(图形设备接口)和APIs(应用程序接口)。Windows的窗口区域被划分成两个重要部分:客户区和非客户区(比如菜单、标题栏和边界框)。大多数窗口是可以移动的。因此它所显示的内容也跟着窗口自己的左上角一起关联移动。GDIs和APIs帮助我们管理这种关联。
Windows的GDI是用来对所有硬件设备提供一种硬件无关支持的程序。因为各个厂家的硬件技术是不同的,所以用这些相同的代码是无法获得硬件的最大性能。实现它们的目的只是保证支持。然而,很多有些开发者想要获得硬件设备的最大性能,他们不用GDIs和APIs,而是直接访问硬件。当然这些方法也可以工作,但却依赖于使用的硬件,它们可能无法在全部的设备上工作。
Windows CE的显示技术又如何呢?有人都能改变显示适配器吗?当然不能,因为它是一种嵌入式系统。通常硬件厂商不会在系统中包含优化代码。然而,这种显示速度已经能够应付某些游戏类型了。
Windows GDIs
设备正文的句柄,通常表示为hDC,是一个连接GDI程序的32位的整数值。通常,大多数窗口有它自己的DC(设备文本),并且你可以通过它的hWnd(窗口句柄)获得它的DC。方法如下: HDC hDC; hDC = GetDC(hWnd);
要调用GDIs和APIs,例如画一条线,你需要将hDC作为这些GDI函数的第一个参数。
MoveToEx(hDC,0,0,NULL); //将作图点移动到(0,0) LineTo(hDC,240,300); //从作图点画线到(240,300) 做完这些,你还必须要从内存中释放hDC
ReleaseDC(hWnd,hDC);
这里还有一个特殊的绘图工作,那就是在收到WM_PAINT消息时,这意味着系统要求你画出你的客户区域,这些代码通常在MainWndProc中完成。下面的代码就的功能就是在窗口的正中央显示文字"hello"。另外,在WM_PAINT消息处理中,除了GetDC,还有另一种更方便的从窗口获取hDC的方法,那就是BeginPaint和EndPaint。
LRESULT MainWndProc(HWND hWnd, UINT message, WPARAM uParam, LPARAM lParam) { PAINTSTRUCT ps; HDC hDC; RECT rcClient; switch(message) { case WM_PAINT: hDC = BeginPaint(hWnd,&ps); GetClientRect(hWnd,&rcClent); DrawText(hDC,L"Hello",5, &rcClient,DT_CENTER|DT_VCENTER); EndPaint(hWnd,&ps); break; case WM_LBUTTONDOWN: DestroyWindow(hWnd); break; default: DefWindowProc(hWnd, message, uParam, lParam); } return 0; } 这些代码中GetClientRect用来获得整个客户区域的矩形范围,DrawText用来在屏幕上这个举行范围的中心画出文字"hello"。
当然我们也可以在别的地方进行绘制,你可以再试试下面这些代码。
LRESULT MainWndProc(HWND hWnd, UINT message, WPARAM uParam, LPARAM lParam) { HDC hDC; int nXpos; int nYpos;
switch(message) {
case WM_LBUTTONDOWN: nXpos = LOWORD(lParam); nYpos = HIWORD(lParam); hDC = GetDC(hWnd); MoveToEx(hDC,nXpos-4,nYpos-4,NULL); LineTo(hDC,nXpos+4,nYpos+4); MoveToEx(hDC,nXpos+4,nYpos-4,NULL); LineTo(hDC,nXPos-4,nYpos+4); ReleaseDC(hWnd,hDC); break;
case WM_KEYDOWN: DestroyWindow(hWnd); break;
default: DefWindowProc(hWnd, message, uParam, lParam); }
return 0; } 上面这种直接在屏幕(设备文本)上绘图的方法,就叫做ON SCREEN。在下面,我将给你演示为着这种方法不适合运用在游戏程序中。让我们看看下面这些长一些的代码。
static HBRUSH hbrRed; static HPEN hpeBlue;
static void _drw_object(HDC hDC, int nX, int nY) { HGDIOBJ hOldPen, hOldBrush;
hOldPen = SelectObject(hDC,hpeBlue); hOldBrush = SelectObject(hDC,hbrRed); Ellipse(hDC,nX-20,nY-20,nX+20,nY+20); SelectObject(hDC,hOldBrush); SelectObject(hDC,hOldPen); }
LRESULT MainWndProc(HWND hWnd, UINT message, WPARAM uParam, LPARAM lParam) { HDC hDC;
switch(message) { case WM_LBUTTONDOWN: hDC = GetDC(hWnd); _drw_object(hDC,LOWORD(lParam),HIWORD(lParam)); ReleaseDC(hWnd,hDC); break;
case WM_MOUSEMOVE; hDC = GetDC(hWnd); PatBlt(hDC,0,0,240,320,WHITENESS); _drw_object(hDC,LOWORD(lParam),HIWORD(lParam)); ReleaseDC(hWnd,hDC); break;
case WM_LBUTTONUP: hDC = GetDC(hWnd); PatBlt(hDC,0,0,240,320,WHITENESS); ReleaseDC(hWnd,hDC); break;
case WM_CREATE: hbrRed = CreateSolidBrush(RGB(255,0,0)); hpeBlue = CreatePen(0,0,RGB(0,0,255); break;
case WM_DESTROY: DeleteObject(hbrRed); DeleteObject(hpeBlue); PostQuitMessage(0); break;
case WM_KEYDOWN: DestroyWindow(hWnd); break;
default: DefWindowProc(hWnd, message, uParam, lParam); }
return 0; } 现在,试试用笔点击屏幕并在拖动看看。
计算机将首先将屏幕填充为白色,将原本的圆形物体擦除,然后在新的位置画上。因为填充屏幕要画掉很多的时间,因此我们将看到旧有图形被长时间的擦除,然后新图形在别的位置出现。这个过程就在我们的眼睛中形成了闪烁。如果有多个需要绘制的物体,这种闪烁将更加明显。
离屏技术
离屏技术的优点就恰恰是避免屏幕的闪烁。它的方法就是:创建一个隐藏的屏幕,一个虚拟的屏幕,或者是在可显区域外的屏幕。然后,我们将所要画的任何东西都先画在这个屏幕上。在这个期间,真正的屏幕是不会变化的。当绘制结束后,在再将整个虚拟屏幕上的内容拷贝到真正的屏幕,因为这个时间很短,内容变化不大时,几乎看不到任何闪烁。
现在,就让我们看看它是如何在Pocket PC上实现的?有三个重要的步骤:
1. 创建离屏的虚拟屏幕。 2. 在离屏虚拟屏幕上绘图。 3. 将离屏虚拟屏幕的内容拷贝到真正的屏幕。
static HBRUSH hbrRed; static HPEN hpeBlue;
static HDC hOffscreenDC; static HBITMAP hOffscreenBuffer; static int nOffscreenCX, nOffscreenCY;
//创建离屏表面 void InitOffscreen(int nWidht, int nHeight) { HDC hDesktopDC;
//获取桌面的设备文本 hDesktopDC = GetDC(0); //创建和桌面相同的设备文本,也就是虚拟屏幕的设备文本 hOffscreenDC = CreateCompatibleDC(hDesktopDC); //创建和桌面相同的位图(缓冲内存) hOffscreenBuffer = CreateCompatibleBitmap(hDesktopDC,nWidth,nHeight); //将内存缓冲选入虚拟屏幕的设备文本 SelectObject(hOffscreenDC,hOffscreenBuffer); nOffscreenCX = nWidth; nOffscreenCY = nHeight; //释放桌面的设备文本 ReleaseDC(0,hDesktopDC); //将整个屏幕画为白色 PatBlt(hOffscreenDC,0,0,nWidth,nHeight,WHITENESS); }
//释放离屏表面 void DeinitOffsceen(void) { //释放虚拟屏幕的设备文本 DeleteDC(hOffscreenDC); //删除虚拟屏幕的内存对象 DeleteObject(hOffscreenBuffer); }
//更新屏幕(将离屏虚拟屏幕的内容拷贝到真正的屏幕) void UpdateDisplay(HWND hWnd) { HDC hDC;
hDC = GetDC(hWnd); //将虚拟屏幕位块传送到窗口hDC的屏幕上 BitBlt(hDC,0,0,nOffscreenCX, nOffscreenCY,h0ffscreenDC,0,0, SCRCPY); ReleaseDC(hWnd,hDC); }
//绘制圆形物体 static void _drw_object(HDC hDC, int nX, int nY) { HGDIOBJ hOldPen, hOldBrush;
hOldPen = SelectObject(hDC,hpeBlue); hOldBrush = SelectObject(hDC,hbrRed); Ellipse(hDC,nX-20,nY-20,nX+20,nY+20); SelectObject(hDC,hOldBrush); SelectObject(hDC,hOldPen); }
//窗口过程 LRESULT MainWndProc(HWND hWnd, UINT message, WPARAM uParam, LPARAM lParam) { switch(message) { case WM_LBUTTONDOWN: _drw_object(hOffscreenDC,LOWORD(lParam),HIWORD(lParam)); UpdateDisplay(hWnd); break;
case WM_MOUSEMOVE; PatBlt(hOffscreenDC,0,0,240,320,WHITENESS); _drw_object(hOffscreenDC,LOWORD(lParam),HIWORD(lParam)); UpdateDisplay(hWnd); break;
case WM_LBUTTONUP: PatBlt(hOffscreenDC,0,0,240,320,WHITENESS); UpdateDisplay(hWnd); break;
case WM_CREATE: hbrRed = CreateSolidBrush(RGB(255,0,0)); hpeBlue = CreatePen(0,0,RGB(0,0,255); InitOffscreen(240,320); break;
case WM_DESTROY: DeleteObject(hbrRed); DeleteObject(hpeBlue); DeinitOffscreen(); PostQuitMessage(0); break;
case WM_KEYDOWN: DestroyWindow(hWnd); break;
default: DefWindowProc(hWnd, message, uParam, lParam); }
return 0; }
这些代码显示了一个以GDI为基础的离屏表面,它是怎么工作的呢?
让我们解释这些代码,InitOffscreen函数中,使用简单的GDIs和APIs——CreateCompatibleDC和CreateCompatibleBitmap创建一个虚拟屏幕。
DC,也就是设备文本,是一种调用设备例程(或者是设备驱动例程)的方法。因为我已经在前面的Windows的GDIs部分大概介绍了它,这里我们来学习更多有关它的其他方面。设备文本也有三个主要的类型:它们是显示设备文本、打印设备文本、和内存设备文本。如果我们在打印文本上画线,设备文本将会调用打印机驱动程序中的画线程序来完成这一过程。如果是在显示文本上画线,调用的自然也就是显示驱动程序中的例程。如:
hDC = CreateDC("Printer",0,0,0); // 创建缺省的打印文本 hDC = CreateDC("Display",0,0,0); // 创建的显示文本 hDC = GetDC(hWnd); // 获得窗口客户区的显示文本 hDC = GetWindowDC(hWnd); // 获得整个窗口的显示文本(包含非客户区)
换句话说,GDIs可以理解为是一个通过DC选择器来实现的硬件设备的通路。这个通路可以让你以共同的方法控制图形设备,包含一些非原始的设备。
要了解更多有关Windows图形设备接口(GDI)的和设备驱动的概念,请参考MSDN。
而什么是内存设备文本呢?可以这么说,它就是虚拟屏幕!CompatibleDC函数创建一个和源设备文本一致的新的设备文本,但它只存在于内存中,并不关联设备。下一步,我们需要分配一个内存块,也就是用来储存图象的屏幕缓冲区。Windows的位图对象就是实现这个的一种方法。我们可以用CreateCompatibleBitmap来创建一个和屏幕相同象素位数,相同颜色格式、相同调色板数目的位图,通过SelectObject将它选入内存设备文本。这样,一个离屏的虚拟屏幕就建成了。
之后,内存设备文本就可以象普通的显示设备文本一样使用了,我们可以将它用在各种GDIs和APIs中,在它上面做图,将它画到其他设备文本上,等等。如,在UpdateDisplay中,我们就用位块传送函数BitBlt将它拷贝到了窗口的客户区域。这个函数在各种设备文本上以相同的形式,非常快速的复制以矩形区域为单位的数据。
在例子程序中,WM_CREATE消息只是在我们的主窗口在内存中被创建时,发送到我们的窗口函数,在整个程序的生存周期内只有一次,因此适合做一些初始化的工作。而WM_DESTROY消息则在窗口别销毁时被发送,可以放入对应内存释放的工作。而WM_MOUSEMOVE 、WM_LBUTTONDOWN和WM_LBUTTONUP是鼠标事件消息。我们将在笔尖接触屏幕、笔尖移过屏幕和笔尖离开屏幕时分别收到它们。
(ixmy) |