DircetDraw c/c++ 使用指导

时间:2022-01-09 05:34:45
注意:这些指南中的例子是用c++写的.如果你使用的是c编译器,请进行适当的改变,以能进行成功的编译.你需要把vtable和this指针添加到接口方法中.

 

1.DirectDraw基础用法

要使用DirectDraw,你必须先创建一个代表计算机显示接口的DirectDraw实例.然后,你就可以通过接口的方法来操纵这个对象.你可能会需要创建一个或更多的DirectDraw平面对象(DirectDraw surface object)的实例
来在图形设备上显示你的应用程序.

为了演示这个,例子DDEx1 sample((SDK root)/Samples/Multimedia/DDraw/Src/Ddex1)演示了以下几步:

step 1:创建一个DirectDraw对象
想创建一个DirectDraw对象的实例,你的应用程序必须在InitApp函数中使用DirectDrawCreateEx函数,就像例程ddex1中一样.DirectDrawCreateEx函数包括4个参数.第一个是:显示设备全局唯一标识(GUID).GUID大部分情况下是设为NULL,选择系统默认的显示设备.第二个参数是:包含的是标示DirectDraw对象是否建立的指针的地址.第三个参数是IDirectDraw7接口的参考标示.必须设为IID_IDirectDraw7.第四个参数是设置为NULL的,是为了将来的扩展做准备的.
以下的例子展示了怎样创建DirectDraw对象,并判断创建是否成功.

hRet = DirectDrawCreateEx(NULL, (VOID**)&g_pDD, IID_IDirectDraw7, NULL);
if(hRet == DD_OK)
{
    // g_pDD is a valid DirectDraw object.
}
else
{
    // The DirectDraw object could not be created.
}

 

step2:决定程序的行为
在你改变显示方法前,你必须至少在IDirectDraw7::SetCooperativeLevel函数中指定dwFlags参数(DDSCL_EXCLUSIVE 和 DDSCL_FULLSCREEN).这样,你的应用程序就得到了显示设备的全部控制权,其他的程序不能共享.DDSCL_FULLSCREEN让你的应用程序在全屏幕模式下运行.你的程序覆盖了桌面,并且只有你的程序能在屏幕上输出.但是,桌面仍然是有效的.(按ALT+TAB切换到桌面)

下面的例子演示了SetCooperativeLevel函数的用法.

HRESULT       hRet;
LPDIRECTDRAW7 g_pDD;   // already created by DirectDrawCreateEx

hRet = g_pDD->SetCooperativeLevel(hWnd, DDSCL_EXCLUSIVE | DDSCL_FULLSCREEN);
if (hRet != DD_OK)
{
    // Exclusive mode was successful.
}
else
{
    // Exclusive mode was not successful.
    // The application can still run, however.
}

如果 SetCooperativeLevel不返回DD_OK,你可以指定为DDSCL_NORMAL继续运行你的程序.但是,你的程序不再是独占模式,而且,可能会无法完成你的一部分要求.在这种情况下,你最好显示一个对话框让用户决定是否要继续.
如果你设置的是全屏幕独占合作级别,你必须把程序的窗口句柄传递给SetCooperativeLevel,这样可以让Windows有能力决定你的程序是否异常终止.

 

step3 :改变显示模式
接下来,你可以用IDirectDraw7::SetDisplayMode函数来改变显示模式.下面的例子将演示怎样把显示模式设置为640 × 480 × 8 bits per pixel (bpp).

HRESULT       hRet;
LPDIRECTDRAW7 g_pDD;   // already created by DirectDrawCreateEx

hRet = g_pDD->SetDisplayMode(640, 480, 8, 0, 0);
if (hRet != DD_OK)
{
    // The display mode changed successfully.
}
else
{
    // The display mode cannot be changed.
    // The mode is either not supported or
    // another application has exclusive mode.
}

当你设置显示模式的时候,你应该确定用户的硬件是否支持这样的模式.你可以设置一个能被大部分显示适配器支持的标准的模式.例如:你的程序可以以640 × 480 × 8作为备选模式,设计成为可以在所有系统上运行.
注意:IDirectDraw7::SetDisplayMode返回一个DDERR INVALIDMODE错误值,如果显示适配器无法切换到想要的模式时.你可以在试图改变显示模式前,用IDirectDraw7::EnumDisplayModes函数看一下用户显示适配器的能力.


step4:创建交换页(flipping surface)
设置完显示模式以后,你应该创建一个平面.例程:DDEx1用IDirectDraw7::SetCooperativeLevel 设置为独占模式.所以,你可以创建交换页(flipping surfaces).如果你设置的是DDSCL_NORMAL模式,你可以创建一个可以块移动(blit)的平面.创建交换页(flipping surfaces)需要以下步骤:
(4.1)定义需求的平面:
第一步是以DDSURFACEDESC2结构定义一个需求的平面.下面的例子演示了结构的定义和标志位的设定:

// Create the primary surface with one back buffer.
ZeroMemory(&ddsd, sizeof(ddsd));
ddsd.dwSize = sizeof(ddsd);
ddsd.dwFlags = DDSD_CAPS | DDSD_BACKBUFFERCOUNT;
ddsd.ddsCaps.dwCaps = DDSCAPS_PRIMARYSURFACE | DDSCAPS_FLIP |
                      DDSCAPS_COMPLEX;
ddsd.dwBackBufferCount = 1;

在这个例子中,dwSize成员是DDSURFACEDESC2结构的大小.这是防止你用到的DirectDraw方法返回无效成员的错误.(dwSize是准备给将来的DDSURFACEDESC2结构的扩展用的)
dwFlags成员决定的DDSURFACEDESC2结构中那些成员将被填充有效的信息.例如在DDEx1中,dwFlags被设为你想要用DDSCAPS结构(DDSD_CAPS)和你想创建一个后台缓冲(back buffer)(DDSD_BACKBUFFERCOUNT)
dwCaps成员在例子中标示一个将要在DDSCAPS结构中使用的标志位.在这种情况下,他指定一个主平面(primary surface DDSCAPS_PRIMARYSURFACE),一个交换页(flipping surface DDSCAPS_FLIP),一个合成表面(complex surface DDSCAPS_COMPLEX).
最后,例子指定了一个后台缓冲.后台缓冲就是实际的绘图操作先在那里完成,然后,再快速的翻动(flip)到主平面(primary surface)上.在DDEx1中,后台缓冲的数目是1.其实,你要你的显存允许,你想建几个就建几个.你想知道更多的关于创建大于1块缓冲的信息,可以去看  "triple buffering".
创建的"平面"占用的存储空间,可以是系统内存也可以是显存.如果应用程序使用的空间超出了显存,DirectDraw就会使用系统内存.(例如你指定多块缓存在一个仅有1MB显存的是配器上).你也可以这样设置DDSCAPS结构的dwCaps成员,设成DDSCAPS_VIDEOMEMEORY或DDCAPS_SYSTEMMEMORY以达到只用显存或只用内存.(如指定用显存,而显存不够,IDirectDraw7::CreateSurface返回一个DDERR_OUTOFVIDEOMEMORY错误)
(4.2)创建平面
在填充完DDSURFACEDESC2结构后,你就可以用他和g_pDD指针(DirectDrawCreateEx函数创建的DirectDraw对象的指针)调用IDirectDraw7::CreateSurface方法,就像下面 :

hRet = g_pDD->CreateSurface(&ddsd, &g_pDDSPrimary, NULL);
if (hRet != DD_OK)
{
    // g_pDDSPrimary points to the new surface.
}
else
{
    // The surface was not created.
    return FALSE;
}


g_pDDSPrimary参数将指向由CreateSurface函数返回的主平面(primary surface)的地址,如果调用成功的话.
指向主平面(primary surface)的指针有效后,就可以调用IDirectDrawSurface7::GetAttachedSurface方法去得到一个指向缓冲的指针.如下:

ZeroMemory(&ddscaps, sizeof(ddscaps));
ddscaps.dwCaps = DDSCAPS_BACKBUFFER;
hRet = g_pDDSPrimary->GetAttachedSurface(&ddscaps, &g_pDDSBack);
if (hRet != DD_OK)
{
    // g_pDDSBack points to the back buffer.
}
else
{
    return FALSE;
}


如果IDirectDrawSurface7::GetAttachedSurface调用成功,g_pDDSBack参数将指向缓存区.

 

step5:在平面上输出
在主平面(primary surface)和后台缓冲(back buffer)创建完成后,例子DDEx1用windows标准GDI函数输出了一些文本在缓冲上.如下:

if (g_pDDSBack->GetDC(&hdc) == DD_OK)
{
        SetBkColor(hdc, RGB(0, 0, 255));
        SetTextColor(hdc, RGB(255, 255, 0));
        if (phase)
        {
            GetClientRect(hWnd, &rc);
            GetTextExtentPoint(hdc, szMsg, lstrlen(szMsg), &size);
            TextOut(hdc, (rc.right - size.cx) / 2, (rc.bottom - size.cy) / 2,
                    szMsg, sizeof(szMsg) - 1);
            TextOut(hdc, 0, 0, szFrontMsg, lstrlen(szFrontMsg));
            phase = 0;
        }
        else
        {
            TextOut(hdc, 0, 0, szBackMsg, lstrlen(szBackMsg));
            phase = 1;
        }
        g_pDDSBack->ReleaseDC(hdc);
}

这个例子使用the IDirectDrawSurface7::GetDC方法获得设备上下文的句柄,并且,为了准备写入,将缓冲上锁.如果你不准备用需要设备上下文句柄的windows函数,你可以使用IDirectDrawSurface7::Lock  IDirectDrawSurface7::Unlock方法加解锁.
接下来,phase变量决定了应该输出到主缓存消息(primary buffer message)还是后台缓存消息(back buffer message).如果phase=1,输出
主缓存消息,且设phase=0.如果phase=0,输出后台缓存消息,且设phase=1.
当消息输出到缓存区后,用IDirectDrawSurface7::ReleaseDC 方法将缓存区解锁.
对创建平面的内存上锁,是保证你的程序和系统不能同时对此内存进行存取.这防止你写入"平面"内存事发生错误.另外,不对"平面"内存解锁,你的程序将无法翻转平面.
平面被加锁之后,例子中用了标准windowsGDI函数:SetBkColor设置背景色,SetTextColor设置在背景上显示的字的颜色,用TextOut在"表面"上输出文字和背景色.
当文字被输出到缓存之后,例子应用IDirectDrawSurface7::ReleaseDC方法解锁"表面"并且释放句柄.不论在什么时候你的程序完成了对缓存的写入,一定要调用IDirectDrawSurface7::ReleaseDC 或IDirectDrawSurface7::Unlock之一,具体用哪个,由你的程序而定.不解锁,你的程序将不能翻转平面.
注意:用IDirectDrawSurface7::Unlock对"平面"解锁后,指向"平面"的指针将会无效.你必须再用IDirectDrawSurface7::lock去获得一个有效的指针.

 

step:6 翻转平面
在DDEx1中,WM_TIMER消息引发从缓存到主平面的翻转.当"平面"内存解锁后,你就可以用IDirectDrawSurface7::Flip方法实现从缓存到主平面的翻转.如下:

case WM_TIMER:
        // Update and flip surfaces
        if (g_bActive && TIMER_ID == wParam)
        {
            UpdateFrame(hWnd);
            while (TRUE)
            {
                hRet = g_pDDSPrimary->Flip(NULL, 0);
                if (hRet == DD_OK)
                    break;
                if (hRet == DDERR_SURFACELOST)
                {
                    hRet = g_pDDSPrimary->Restore();
                    if (hRet != DD_OK)
                        break;
                }
                if (hRet != DDERR_WASSTILLDRAWING)
                    break;
            }
        }
        break;

在例子中,g_pDDSPrimary参数指示主平面和与他连接的缓存.当IDirectDrawSurface7::Flip被调用,前后平面将会交换(只是交换指针,没有实际的数据交换).如果翻转成功,返回DD_OK,跳出循环.
如果翻转返回的是DDERR_SURFACELOST值,就试图用IDirectDrawSurface7::Restore 方法保存平面.如果保存成功,程序循环到调用IDirectDrawSurface7::Flip,再试一次.如果保存不成功,程序跳出循环,返回一个错误.
注意:当你调用IDirectDrawSurface7::Flip 时,翻转动作并不能马上完成.如果,前一个翻转动作还没有完成,IDirectDrawSurface7::Flip 会返回DDERR_WASSTILLDRAWING.在这个例子中,IDirectDrawSurface7::Flip会一直调用直到返回DD_OK.


step7:释放DirectDraw对象
当你按下F12键时,例子DDEx1将在退出程序前处理WM_DESTROY消息.这个消息将调用ReleaseAllObjects函数,这个函数包括了多个Release调用.像下面一样:

在缓存中载入位图

这个例子讨论在ddex1的基础上进行扩展.ddex2(例程在msdn上就有,搜索...)讲包含载入位图文件的函数.新的功能靠以下的步骤实现:
step1:创建调色板
step2:设置调色板
step3:在缓存中载入位图
step4:翻转平面.
像在ddex1中一样,在初始化函数中初始化了ddex2.
不同的代码如下:
lpDDPal = DDLoadPalette(lpDD, szBackground);
 
if (lpDDPal == NULL)
    goto error;
 
ddrval = lpDDSPrimary->SetPalette(lpDDPal);
 
if(ddrval != DD_OK)
    goto error;
 
// Load a bitmap into the back buffer.
ddrval = DDReLoadBitmap(lpDDSBack, szBackground);
 
if(ddrval != DD_OK)
    goto error;


step1:建立调色板
在ddex2中,首先用如下代码建立调色板.
lpDDPal = DDLoadPalette(lpDD, szBackground);
 
if (lpDDPal == NULL)
    goto error;
DDLoadPalette这个函数是在/Dxsdk/sdk/samples/misc/ddutil.cpp中的公共directdraw函数.很多directdraw的例子(sdk包中的)都用到这个文件.重要的是,它包含了载入调色板和位图的函数,无论是从文件还是资源.为了不重复的写代码,就把他放在了一个可以重复使用的文件中.确信你在编译ddexn是包含了这文件.
(以下内容在ddutial.cpp中)ddex2中,DDLoadPalette函数从back.bmp文件创建了DirectDrawPalette对象.DDLoadPalette函数判断创建调色板的文件或资源是否存在.如果不是的话,就创建一个默认的调色板.在ddex2中,他从位图文件提取调色板信息并储存在一个ape指向的结构中.
DDEx2随后创建了DirectDrawPalette对象,如下:
pdd->CreatePalette(DDPCAPS_8BIT, ape, &ddpal, NULL);
return ddpal;
当IDirectDraw7::CreatePalette方法返回,ddpal参数指向从DDLoadPalette函数返回的DirectDrawPalette对象.
ape参数是一个指针,指向一个能包含或者2或4或16或256个线性组织的纪录的结构.记录的数目依靠于CreatePalette方法中的dwFlags参数.在上面的情况下,dwFlags参数设为DDPCAPS_8BIT.这表示结构中有256个记录.每一条记录包含4字节(分别是红,绿,兰的通道和一个标志位).


 



step2:设置调色板
创建完调色板以后,通过指针调用主平面的IDirectDrawSurface7::SetPalette方法,如下:
ddrval = lpDDSPrimary->SetPalette(lpDDPal);
 
if(ddrval != DD_OK)
    goto error;// SetPalette failed.
在你调用完IDirectDrawSurface7::SetPalette方法之后,DirectDrawPalette对象就与DirectDrawSurface对象联系起来了.什么时候你想改变调色板了,可以简单的创建一个新的调色板,然后设置一下就可以了.(虽然这篇指导用了这些步骤,其实还有其他的方法改变调色板,以后的例子中将会演示)



step3:在缓存区载入位图
DirectDrawPalette对象与DirectDrawSurface对象联系起来之后,DDEx2用下面的代码在缓存中载入位图back.bmp
// Load a bitmap into the back buffer.
ddrval = DDReLoadBitmap(lpDDSBack, szBackground);
 
if(ddrval != DD_OK)
    // Load failed.
DDReLoadBitmap是另一个在Ddutil.cpp中的函数.他载入一个位图文件或资源到一个已经存在的DirectDraw平面.(你也可以使用DDLoadBitmap函数创建一个平面然后载入.函数也在ddutil.cpp中.)在ddex2中,他载入由szBackground(ID)指向的back.bmp文件到由lpDDSBack(指针)指向的后台缓存.DDReLoadBitmap函数调用DDCopyBitmap函数将文件拷贝到缓存,并拉伸到适当的大小.
DDCopyBitmap函数将位图拷贝到内存中,用GetObject函数恢复位图的大小.然后是由下面的代码将位图调整到将要放位图的缓存的大小.
// Get the size of the surface.
ddsd.dwSize = sizeof(ddsd);
ddsd.dwFlags = DDSD_HEIGHT | DDSD_WIDTH;
pdds->GetSurfaceDesc(&ddsd);
ddsd是一个DDSURFACEDESC2结构的指针.这个结构保存了现在的DirectDraw平面的描述.在这种情况下,DDSURFACEDESC2的成员描述了平面的高和宽,由DDSD_HEIIGHT和DDSD_WIDTH标示的.IDirectDrawSurface7::GetSurfaceDesc方法的调用把属性值装入了这个结构.在DDEX2中,值将被设为高480,宽640.
DDCopyBitmap函数给平面加锁然后把位图拷贝到缓存中,用StretchBlt函数拉伸或压缩.如下:
if ((hr = pdds->GetDC(&hdc)) == DD_OK)
{
    StretchBlt(hdc, 0, 0, ddsd.dwWidth, ddsd.dwHeight, hdcImage, x, y,
        dx, dy, SRCCOPY);
    pdds->ReleaseDC(hdc);
}



step4:翻转页面
ddex2中翻转页面的部分与ddex1中的十分的相像.所不同的是:当平面丢失时(DDERR_SURFACELOST),在平面储存后,必须用DDReLoadBitmap函数将位图重新载入缓存.

色彩键码和位图动画
第三篇指导的例子ddex3演示了一个简单的在翻转页面前把位图放到离屏缓存的行为.这篇指导的例子将利用前面描述的技术,装载一个背景图片和一系列的精灵(译者注:一般放置小幅的不断改变的图片)到离屏表面中.然后,使用IDirectDrawSurface7::BltFast方法拷贝离屏表面的一部分到后台缓冲,以次,产生一个简单的位图动画.
ddex4用到的位图文件是all.bmp,它包含了背景和60个连续的黑背景的旋转的红圈.DDEx4包含了新的函数,用来为旋转红圈精灵设置色彩键码.然后,例子从离屏表面中拷贝适当的精灵到后台缓冲.
DDEx4中新添的函数演示如下:
step1:设置色彩键码
step2:建立一个简单的动画
(译者注:色彩键码就是设置了的,在从离屏表面向缓冲拷贝的时候不拷贝的色彩,从而形成透明的样子)

step1:设置色彩键码
在其他例子的基础上,ddex4的doInit函数包含了设置精灵色彩键码的代码.色彩键码是用来设置一个色彩值,这个色彩值是用于透明的.当系统使用硬件块移动支持时,所有的像素中除了设为色彩键码的颜色的,都将被块移动(blit)到缓存.这样就创建了一个不是矩形样子的精灵.下面的代码演示了DDEx4中如何设置色彩键码的.

// Set the color key for this bitmap (black).
DDSetColorKey(lpDDSOne, RGB(0,0,0));
 
return TRUE;
在DDSetColorKey函数的调用的时候,你可以设置你想设的颜色的RGB值来设置色彩键码.黑色的RGB值是(0,0,0).DDSetColorKey函数调用了DDColorMatch函数.(两个函数都在ddutil.cpp中) DDColorMatch函数储存了在lpDDSOne表面中的位图的(0,0)位置的像素的颜色值.然后,他把位图的(0,0)位置的像素设为你提供的那个颜色.最后,它求了颜色值和每个像素色彩位数的掩码(异或).使(0,0)位置变回了原来的颜色,当这些都做完后,函数调用完返回到DDSetColorKey中.返回了色彩键码的值(dw)值.放在了DDCOLORKEY结构的成员dwColorSpaceLowValue中,并被拷贝到dwColorSpaceHighValue成员中.随后IDirectDrawSurface7::SetColorKey的调用设置了色彩键码.
你可能已经注意到了DDSetColorKey和DDColorMatch函数中的CLR_INVALID了.如果在DDEx4中你把CLR_INVALID当作色彩键码传递给DDSetColorKey函数,位图中的左上角(0,0)像素将被当作色彩键码.DDEx4中这个意义不大,因为位图的(0,0)位置是灰色的.你要是想看看怎样在ddex4中让(0,0)位置作为色彩键码,你可以用绘图软件打开all.bmp文件.然后把(0,0)这个点改为黑色.一定要保证你把修改保存好了(这个不好看出来).然后,你可以把DDSetColorKey的调用改成下面这样:
DDSetColorKey(lpDDSOne, CLR_INVALID);

重新编译ddex4,保证包含了新位图的资源文件也重新编译过了.这样,ddex4将用那个现在已经被改为黑色的(0,0)点作为色彩键码了.


step2:创建一个简单的动画
ddex4用到了updateFrame例子函数创建了一个简单的动画,用的是all.bmp中包含的那些红圈.这个动画由三个呈三角形位置的以不同速度转动的红圈组成.例子中通过比较win32 GetTickCount函数和上一次调用GetTickCount函数到现在的毫秒数来决定是否重绘精灵.然后,使用IDirectDrawSurface7::BltFast方法把背景从离屏表面(lpDDSOne)中块移动(blit)到后台缓存,并且把精灵们也块移动到后台缓存,这是要用到你刚才设置的色彩键码决定哪些像素是透明的.当精灵们移动到后台缓存后,就调用IDirectDrawSurface7::Flip方法翻转页面.
注意当你用IDirectDrawSurface7::BltFast方法块移动背景的时候,dwTrans参数定义了传送的参数是DDBLTFAST_NOCOLORKEY.这个标示了将是一个没有透明的普通块移动.后来,当红圈被块移动的时候,dwTrans参数设为DDBLTFAST_SRCCOLORKEY.这个标示了块移动将用到定义了的色彩键码,以实现透明的效果.在这个例子中,是对lpDDSOne缓存.
在这个例子中,每次调用updateFrame函数都将重绘全部背景.优化这个例子的一个方法是,只重绘红圈旋转所引起的背景发生变化的那一小部分.因为放置红圈精灵的位置和大小都是不变得,所以,你可以很简单的修改ddex4以达到这个优化.





 

static void
ReleaseAllObjects(void)
{
    if (g_pDD != NULL)
    {
        if (g_pDDSPrimary != NULL)
        {
            g_pDDSPrimary->Release();
            g_pDDSPrimary = NULL;
        }
        g_pDD->Release();
        g_pDD = NULL;
    }
}


这个程序检测DirectDraw对象的指针g_pDD和DirectDraw平面对象的指针g_pDDSPrimary是否为NULL.然后,调用IDirectDrawSurface7::Release方法令DirectDraw平面对象的reference count(可以认为是创建的对象的数目)数减1,这使得reference count减为0,DirectDraw平面对象就释放了.然后,还需将他的指针值设为NULL.接下来,程序调用IDirectDraw7::Release,同样是令DirectDraw的reference count减1,然后然后....全销毁.