视频课:https://edu.csdn.net/course/play/7621
第一节 关于声音
声音是空气的一系列振荡,称为声波,一般可以用二维的波形图来表示。数字音频是指使用某种设备将声波记录下来并保存为一种数字化的文件。播放相应的文件就可以产生某种声音效果。数字音频的音质随着采样频率及所使用的位数不同而有很大的差异。因此,了解所使用音频文件格式的有关标准是很有必要的。例如,CD中的音频是16位,采样频率达到44.1MHz的立体声数字音频。
在所有声音文件的格式中,WAV是最普遍的。这是Windows平台上最常见的格式,由微软公司创造。支持8位和16位的音质、多样本、对立体声和单声道音频均可播放。它还支持多种音频压缩算法。
要在游戏中取得好的声音效果,例如,使用3D音效,可以有两种方法来实现:一是使用一定的工具软件对声音文件进行处理,生成播放效果足够好的文件,然后在游戏程序中直接将这样的文件播放。显然,这样比较简单,但是不灵活。如果需要音效随着游戏场景的变化而不断改变,且不受所具有声音文件数量的限制,就需要进行实时混音了。
第二节DirectSound结构
DirectSound的功能模块包括播放、声音缓冲区、三维音效、音频抓获、属性集等。
DirectSound playback建构于IDirectSound COM接口之上。IDirectSoundBuffer,IDirectSound3DBuffer和IDirectSound3DListener接口则用以实现对声音缓冲区和三维音效的操作。
DirectSound capture建构于IDirectSoundCapture和IDirectSoundCaptureBuffer COM接口之上。
其它的COM接口,如IKsPropertySet,使应用程序能够从声卡的扩展功能中最大地受益。
最后,IDirectSoundNotify接口用于在播放或音频抓获达到一定地方时向产生一个事件。
第三节 播放功能概述
DirectSound缓冲区对象表示一个包含声音数据的缓冲区,这些数据以PCM格式被存储。该对象不仅可以用于开始、停止或暂停声音的播放,还能够设置声音数据中诸如频率和格式等属性。
缓冲区分为主缓冲区和副缓冲区。主缓冲区中是听者将要听到的音频信号,一般是将副缓冲区中信号混音后的结果。而副缓冲区中存放着许多单独的声音信号,有的可以直接播放,有的要混音,有的循环播放。主缓冲区由DirectSound自动创建,而副缓冲区需由应用程序来创建。DirectSound将副缓冲区中的声音混合后,存入主缓冲区,再输出到相应播放设备。
DirectSound中没有解析声音文件的功能,需要您自己在应用程序中将不同格式的声音信号改变过来(PCM)。
缓冲区可以在主板的RAM、波表存储器、DMA通道或虚拟存储器中。
多个应用程序可以用同一声音设备来创建DirectSound对象。当输入焦点在应用程序中发生变化时,音频输出将自动在各个应用程序的流之间切换。于是,应用程序不用在输入焦点改变中反复地播放和停止它们的缓冲区。
通过IDirectSoundNotify接口,当播放到了一个用户指定的地方,或播放结束时,DirectSound将动态地通知拥护这一事件。
第四节 音频抓获概述
DirectSoundCapture对象可以查询音频抓获设备的性能,并为从输入源抓获音频而创建缓冲区。
其实,在Win32中早已经有了抓获音频的功能,而目前的(版本5)DirectSoundCapture与只比较并没有什么新的功能。不过,DirectSoundCapture API使您能够编写使用相同接口的播放和音频抓获程序,而且,这也为将来可能出现的API改进提供了原始模型,使您可以从中受益。
DirectSoundCapture还能够抓获压缩格式的音频。
DirectSoundCaptureBuffer对象表示一个用于抓获音频的缓冲区。它可以循环利用,也就是说,当输入指针达到缓冲区的最后时,它会回到开始的地方。
DirectSoundCaptureBuffer对象的各种方式使您能够设定缓冲区的属性、开始或停止操作、锁定某部分存储器(这样就可以安全地将这些数据保存或用于其它目的)。
与播放类似,IDirectSoundNotify接口使在输入指针到达一定地方时通知用户。
第五节 初始化
对于一些简单的操作,可以使用缺省的首选设备。不过,在游戏的制作中,我们可能还是需要知道一些特定的声音设备。于是,您应该先列举出可用的声音设备。
在此之前,您需要先设定一个回收函数,在每一次DirectSound发现新设备后调用该函数。函数中您可以做任何事情,但您必须将它定义得与DSEnumCallback形式相同。如果希望列举继续,函数应返回真,否则返回假。
下面的例程来自光盘Example目录下的Dsenum.c文件。它列举可用的设备并在一个列表框中增加一条相应的信息。首先是他的回收函数:
BOOL CALLBACK DSEnumProc(LPGUID lpGUID, LPCTSTR lpszDesc, LPCTSTR lpszDrvName, LPVOID lpContext ) { HWND hCombo = *(HWND *)lpContext; LPGUID lpTemp = NULL; if( lpGUID != NULL ) { if(( lpTemp = LocalAlloc( LPTR, sizeof(GUID))) == NULL ) return( TRUE ); memcpy( lpTemp, lpGUID, sizeof(GUID)); } ComboBox_AddString( hCombo, lpszDesc ); ComboBox_SetItemData( hCombo, ComboBox_FindString( hCombo, 0, lpszDesc ), lpTemp ); return( TRUE ); } 当包含了列表框的对话框被初始化后,列举开始: if (DirectSoundEnumerate((LPDSENUMCALLBACK)DSEnumProc, &hCombo) != DS_OK ) { EndDialog( hDlg, TRUE ); return( TRUE ); }
创建DirectSound对象最简单的方法是使用DirectSoundCreate函数。其中的第一个参数为相应设备的全局独有标志符(GUID)。您可以通过列举声音设备得到GUID,或使用NULL来为缺省设备创建对象。
LPDIRECTSOUND lpDirectSound;
HRESULT hr;
hr = DirectSoundCreate(NULL, &lpDirectSound, NULL));
创建DirectSound对象后,应设置合作层。这是为了确定各个DirectSound应用程序被允许操作声音设备的范围,防止它们在错误的时间或通过错误的方式操作设备。
所使用的方式为IDirectSound::SetCooperativeLevel。这里hwnd参数是应用程序窗口的句柄:
HRESULT hr = lpDirectSound->lpVtbl->SetCooperativeLevel(
lpDirectSound, hwnd, DSSCL_NORMAL);
这里确定的合作层为normal,这样使用声卡的应用程序可以顺序地进行切换。合作层包括
Normal、Priority、Exclusive和Write-primary,级别依次增加。
正如在前面提到过,DirectSound可以充分发挥硬件的增强功能,因此,它需要先设法了解设备的特性。我们可以通过IDirectSound::GetCaps方式来达到这个要求。如下所示:
DSCAPS dscaps;
dscaps.dwSize = sizeof(DSCAPS);
HRESULT hr = lpDirectSound->lpVtbl->GetCaps(lpDirectSound,
&dscaps);
DSCAPS结构接收关于声音设备性能和资源的信息。注意,初始化该结构中dwSize成员是调用它之前所必须的。
除此之外,您还可以查询和设定扬声器的设置,以及整理声音存储器使尽量获得最大的备用空间。
第六节 如何播放
初始化完成后,DirectSound将自动创建主缓冲区用于混音并传送至输出设备。而副缓冲区则需要您自己来创建了。
下面的例程演示了用IDirectSound::CreateSoundBuffer方式创建一个基本的副缓冲区:
BOOL AppCreateBasicBuffer( LPDIRECTSOUND lpDirectSound, LPDIRECTSOUNDBUFFER *lplpDsb) { PCMWAVEFORMAT pcmwf; DSBUFFERDESC dsbdesc; HRESULT hr; // 设定声波格式结构 memset(&pcmwf, 0, sizeof(PCMWAVEFORMAT)); pcmwf.wf.wFormatTag = WAVE_FORMAT_PCM; pcmwf.wf.nChannels = 2; pcmwf.wf.nSamplesPerSec = 22050; pcmwf.wf.nBlockAlign = 4; pcmwf.wf.nAvgBytesPerSec = pcmwf.wf.nSamplesPerSec * pcmwf.wf.nBlockAlign; pcmwf.wBitsPerSample = 16; // 设置DSBUFFERDESC结构,用以设定缓冲区控制选项 memset(&dsbdesc, 0, sizeof(DSBUFFERDESC)); dsbdesc.dwSize = sizeof(DSBUFFERDESC); // 要求缺省的控制 dsbdesc.dwFlags = DSBCAPS_CTRLDEFAULT; // 3秒的缓冲区 dsbdesc.dwBufferBytes = 3 * pcmwf.wf.nAvgBytesPerSec; dsbdesc.lpwfxFormat = (LPWAVEFORMATEX)&pcmwf; // 创建缓冲区 hr = lpDirectSound->lpVtbl->CreateSoundBuffer(lpDirectSound, &dsbdesc, lplpDsb, NULL); if(DS_OK == hr) { // 成功,获得的接口在*lplpDsb当中 return TRUE; } else { // 失败 *lplpDsb = NULL; return FALSE; } }
您必须设定缓冲区的控制选项。这是使用DSBUFFERDESC结构中的dwFlags成员,具体细节请参见DirectX 5的帮助。
副缓冲区不支持混音等特效,因此,您需要能够直接操作主缓冲区。不过,当您获权写主缓冲区时,其它特性将失去作用,从而硬件加速混音失效。所以,大部分应用程序几少直接操作主缓冲区。
如果要求操作主缓冲区,可以在调用IDirectSound::CreateSoundBuffer方式时设定DSBUFFERDESC结构中的DSBCAPS_PRIMARYBUFFER标志符,而且,合作层必须是Write-primary。
下面的例程演示了如何得到对主缓冲区的写操作能力:
BOOL AppCreateWritePrimaryBuffer( LPDIRECTSOUND lpDirectSound, LPDIRECTSOUNDBUFFER *lplpDsb, LPDWORD lpdwBufferSize, HWND hwnd) { DSBUFFERDESC dsbdesc; DSBCAPS dsbcaps; HRESULT hr; // 设置声波格式结构 memset(&pcmwf, 0, sizeof(PCMWAVEFORMAT)); pcmwf.wf.wFormatTag = WAVE_FORMAT_PCM; pcmwf.wf.nChannels = 2; pcmwf.wf.nSamplesPerSec = 22050; pcmwf.wf.nBlockAlign = 4; pcmwf.wf.nAvgBytesPerSec = pcmwf.wf.nSamplesPerSec * pcmwf.wf.nBlockAlign; pcmwf.wBitsPerSample = 16; // 设置DSBUFFERDESC结构 memset(&lplpDsb, 0, sizeof(DSBUFFERDESC)); dsbdesc.dwSize = sizeof(DSBUFFERDESC); dsbdesc.dwFlags = DSBCAPS_PRIMARYBUFFER; // 缓冲区大小由声音硬件决定 dsbdesc.dwBufferBytes = 0; dsbdesc.lpwfxFormat = NULL; // 对主缓冲区必须设为NULL // 获得write-primary合作层 hr = lpDirectSound->lpVtbl->SetCooperativeLevel(lpDirectSound, hwnd, DSSCL_WRITEPRIMARY); if (DS_OK == hr) { // 成功,试图创建缓冲区 hr = lpDirectSound->lpVtbl->CreateSoundBuffer(lpDirectSound, &dsbdesc, lplpDsb, NULL); if (DS_OK == hr) { // 成功,设定主缓冲区为desired格式 hr = (*lplpDsb)->lpVtbl->SetFormat(*lplpDsb, &pcmwf); if (DS_OK == hr) { // 如果希望得知缓冲区大小,调用GetCaps dsbcaps.dwSize = sizeof(DSBCAPS); (*lplpDsb)->lpVtbl->GetCaps(*lplpDsb, &dsbcaps); *lpdwBufferSize = dsbcaps.dwBufferBytes; return TRUE; } } } // 设定合作层失败 // 创建缓冲区,或设定结构 *lplpDsb = NULL; *lpdwBufferSize = 0; return FALSE; }
播放一段声音的过程包括以下四个步骤:
1 锁定(IDirectSoundBuffer::Lock)副缓冲区的一部分。由您设定的偏移量决定下一步写操作的起始点;
2 写数据;
3 解锁(IDirectSoundBuffer::Unlock);
4 将声音传送给主缓冲区,并由那里输出(IDirectSoundBuffer::Play)。
下面的C程序向缓冲区中写入数据,由dwOffset指定开始时的偏移量:
BOOL AppWriteDataToBuffer( LPDIRECTSOUNDBUFFER lpDsb, // DirectSound缓冲区 DWORD dwOffset, // 自己的写标记位置 LPBYTE lpbSoundData, // 数据的起点 DWORD dwSoundBytes) // 拷贝块的大小 { LPVOID lpvPtr1; DWORD dwBytes1; LPVOID lpvPtr2; DWORD dwBytes2; HRESULT hr; // 得到被写块的地址 hr = lpDsb->lpVtbl->Lock(lpDsb, dwOffset, dwSoundBytes, &lpvPtr1, &dwBytes1, &lpvPtr2, &dwBytes2, 0); // 如果返回DSERR_BUFFERLOST,释放并重试锁定 if(DSERR_BUFFERLOST == hr) { lpDsb->lpVtbl->Restore(lpDsb); hr = lpDsb->lpVtbl->Lock(lpDsb, dwOffset, dwSoundBytes, &lpvPtr1, &dwAudio1, &lpvPtr2, &dwAudio2, 0); } if(DS_OK == hr) { // 写到指针 CopyMemory(lpvPtr1, lpbSoundData, dwBytes1); if(NULL != lpvPtr2) { CopyMemory(lpvPtr2, lpbSoundData+dwBytes1, dwBytes2); } // 释放 hr = lpDsb->lpVtbl->Unlock(lpDsb, lpvPtr1, dwBytes1, lpvPtr2, dwBytes2); if(DS_OK == hr) { // 成功 return TRUE; } } // 某步骤失败 return FALSE; }
er
我们将讨论每一个接口,并随后讨论它们的成员函数。但我们并不讨论每个函数的细节,因为我们并不是向您提供一份参考手册。相反,我们将讨论每个函数是干什么的,为什么这样使用,以及你有可能如何去使用它。
当DirectX首次推出的时候(早先它被称作Games SDK),DirectDraw核心函数性以DirectDraw接口表示。当DirectX2推出的时候,DirectDraw也已经被升级了。DirectDraw遵守COM规格而未被改变。新的函数性可能通过DirectDraw2接口存取。
特别要注意的是,DirectDraw2接口是DirectDraw接口的超级设置。DirectDraw2接口可提供DirectDraw接口的所有函数,另外还增加了一些新的函数。如果你正在使用DirectX或更高版高,那么你可以随意选用DirectDraw接口或DirectDraw2接口。但是,由于DirectDraw2接口较DirectDraw接口的功能更强,所以没有必要使用DirectDraw接口。同样,Microsoft并不主张使用这些无组织的、网络可变的接口。因此,在本书以后的程序中我们只使用DirectDraw2接口。
DirectDraw和DirectDraw2接口提供的成员函数如下(按字母顺序排列):
●Compact()
●CreateClipper()
●CreatePalette()
●CreateSurface()
●DuplicateSurface()
●EnumDisplayModes()
●EnumSurfaces()
●FlipToGDISurface()
●GetAvailableVidMem()
●GetCaps()
●GetDisplayMode()
●GetFourCCCodes()
●GetGDISurface()
●GetMonitorFrequency()
●GetScanline()
●GetVerticalBlankStatus()
●RestoreDisplayMode()
●SetCooperativeLevel()
●SetDisplayMode()
●WaitForVerticalBlank()
接下来我们讨论DirectDraw接口函数。注意,在本章以后的内容中,DirectDraw接口既表示DirectDraw接口,也表示DirectDraw2接口。只有在区分DirectDraw接口和DirectDraw2接口的函数时,才加以区别。
1. 接口创建函数
DirectDraw接口表示DirectDraw本身。该接口在被用于创建其他DirectDraw接口实例时,是一个主接口。DirectDraw接口提供三个这样的接口实例创建函数:
●CreateClipper()
●CreatePalette()
●CreateSurface()
CreateClipper()函数用于创建DirectDrawClipper接口实例。并非所有的DirectDraw应用程序都用到剪裁器,所以该函数并不是在所有的程序中都出现。我们将很快讨论DirectDrawClipper的细节。
CreatePalette()函数用于创建DirectDrawPalette接口实例。同DirectDrawClipper一样,并非所有的DirectDraw应用程序都用到调色板。比如,应用程序使用16位显示模式时,就不用调色板。但是,当应用程序使用8位显示模式时,就必须创建至少一个DirectDrawPalette实例。
CreateSurface()函数用于创建DirectDrawSurface接口实例。任何一个DirectDraw应用程序都要用表面来生成图像数据,因此经常要用到这一函数。
DirectDraw接口自己的实例是由DirectDrawCreate()函数创建的。DirectDrawCreate()是DirectDraw函数中少有的几个常规函数之一,但并不是COM接口成员函数。
2. GetCaps()函数
DirectDraw接口允许准确确定软硬件都支持的特征。GetCaps()函数可以对两个DDCAP结构实例进行初始化。一个结构表明哪些特征由显示硬件直接支持,另一个结构表明哪些特征由软件仿真支持。最好是用GetCaps()函数来决定你将用到的特征是否被支持。
提示:DirectX浏览器
DirectX SKD是与DXVIEW程序同时推出的。DXVIEW说明了DirectX组件的功能,包括DirectDraw。大多数系统中,有两个DirectDraw项目:主显示驱动器和硬件仿真层。第一项说明了显示硬件的功能。第二项说明了在缺乏硬件支持的情况下,DirectDraw将要仿真的一些特征。在具有两个以上的DirectDraw支持的显示卡的计算机中,DXVIEW会显示卡的功能。
3. SetCooperativeLevel()函数
SetCooperativeLevel()函数用于指定应用程序所要求的对显示硬件的控制程度。比如,一个正常合作度意味着应用程序既改变不了当前显示模式,也不能指定整个系统调色板的内容。而一个专有的合作度允许显示模式切换,并能完全控制调色板。不管你决定使用哪种合作度,都必须调用SetCooperativeLevel()函数。
4. 显示模式函数
DirectDraw接口提供4种显示模式操作函数。它们是:
●EnumDisplayModes()
●GetDisplayMode()
●RestoreDisplayMode()
●SetDisplayMode()
EnumDisplayModes()函数可用于查询DirectDraw使用何种显示模式。通过设置EnumDisplayModes()函数默认值可以得到所有的显示模式,而且可以通过显示模式描述消除那些不感兴趣的模式。进行显示模式切换的过程中最好使用EnumDisplayModes()函数。现在市场上有各种各样的显示设备,每种显示设备都有自己的特征和局限。除了默认的640×480×8窗口显示模式,最好不要依靠任何给定的显示模式的支持。
GetDisplayMode()函数可以检索到有关当前显示模式的信息,并在DDSURFACEDESC结构实例中显示当前显示模式的宽度、高度、像素深度以及像素格式等信息。还有别的途径可以检索到同样的信息(比如检索主表面描述),因此该函数并不出现在所有的程序中。
SetDisplayMode()函数用于激活所支持的显示模式。SetDisplayMode()函数的DirectDraw2版本还允许设定显示模式的刷新率。而DirectDraw接口版本的SetDisplayMode()函数只能进行显示模式宽度、高度和像素深度的设置。任何一个要进行显示模式切换的程序都要用到SetDisplayMode()函数。
RestoreDisplayMode()函数用于存储调用SetDisplayMode()函数之前的显示模式。SetDisplayMode()和RestoreDisplayMode()函数都要求优先使用SetCooperativeLevel()函数得到的专有合作存取。
5. 表面支持函数
除了CreateSurface()函数之外,DirectDraw接口还提供了以下向个表面相关函数:
●DuplicateSurface()
●EnumSurfaces()
●FlipToGDISurface()
●GetGDISurface()
●GetAvailableVidMem()
●Compact()
DuplicateSurface()函数用于考贝当前表面。该函数只复制表面接口,不复制内存。被复制的表面与源表面共享内存,因此改变内存的内容就同时改变了两个表面的图像。
EnumSurfaces()函数可用于迭代所有满足指定标准的表面。如果没有指定标准,那么所有当前表面都被枚举。
FlipToGDISurface()函数的作用是在终止页面翻转应用程序前确保主表面得以正确存储。取消页面翻转时,有两个表面交替显示。这就是说,在终止应用程序之前有可能没有保存最初的可见表面。这种情况下,Windows通过绘制一个不可见表面来恢复。利用FlipToGDISurface()函数就可以轻而易举地避免发生这种情况。
GetGDISurface()函数可以向只被GDI认可的表面返回一个提针。GDI表面是Windows用于输出的表面。在进行屏幕捕捉时,这个函数非常有用,DirectDraw可以捕捉到Windows桌面的任一部分。
GetAvailableVidMem()函数用于检索正在使用中的视频存储器(显示RAM)的数量。这一函数由DirectDraw2接口提供,而不是由DirectDraw接口提供。该函数用于确定应用程序利用显示RAM可创建表面的数量。
Compact()函数不是通过DirectX5实现的,但它可以为视频存储器提供碎片整理技巧。在基于显示RAM的表面被不断创建或受到破坏的时候,可以释放大量内存。
6. 监视器刷新函数
DirectDraw接口提供了4种适于计算机显示设备或监视器的函数,但这些函数不适于显示卡,它们是:
●GetMonitorFrequency()
●GetScanLine()
●GetVerticalBlankStatus()
●WaitForVerticalBlank()
这些函数尤其与监视器的刷新机制紧密机连。这在确保生成动画时尽可能不产生闪烁和图像撕裂现象时是至关重要的。但必须注意,并非所有的显示卡/监视器组合都支持这些函数。
GetMonitorFrequency()函数用于检索监视器当前的刷新率。刷新率通常用赫兹表示,缩写为Hz。例如,60Hz的刷新率表示屏幕每秒更新60次。
GetScanLine()函数用于向监视器返回当前正在被刷新的扫描行(水平像素行)。不是所有的显示设备/监视器组合都支持该函数。如果这一功能得不到支持,该函数将返回DDERR-UNSUPPORTED。
对于高性能图形应用程序来说,通常要求利用垂直刷新同步地更新屏幕。尤其是,当显示器刚完成屏幕刷新时,最好能够更新主表面。否则,屏幕的一部分显示新的图像数据,而另一部分仍显示旧的图像数据,这种现象就是所谓的图像撕裂。DirectDraw默认利用垂直刷新同步更新屏幕。如果不是这样还可以利用GetVerticalBlankStatus()和WaitForVerticalBlank()函数实现同步刷新。
7. GetFourCCCodes()函数
DirectDraw接口提供的最后一个函数是GetFourCCCodes()函数。该函数用于返回显示卡所支持的FourCC代码。FourCC代码用于描述非RGB或YUV表面。我们不在此讨论YOV表面,它们已超出本书的范围。
第五节 DirectDrawSurface接口函数
同DirectDraw接口一样,DirectDrawSurface接口也遵守COM规格.最初,表面支持是由DirectDrawSurface接口提供的。DirectX2介绍了DirectDrawSurface2接口的新的函数性,DirectX5介绍了DirectDrawSurface3接口。
尽管本软件中讨论的是DirectDraw2接口,而不是DirectDraw接口,但我们仍忠于最初的DirectDrawSurface接口,因为DirectDrawSurface2和DirectDrawSurface3接口新增的函数并不十分重要。在以后的内容里,我们将用DirectDrawSurface接口表示这3种接口,除非特别注明。
DirectDrawSurface是最大的DirectDraw接口,它允许表面内容的拷贝、清除以及被调用程序直接存取。DirectDrawSurawSurface接口总共提供36个成员函数,按字母顺序排列如下:
●AddAttachedSurface()
●AddOverlayDirtyRect()
●Blt()
●BltBatch()
●BltFast()
●DeleteAttachedSurface()
●EnumAttachedSurfaces()
●EnumOverlayZOrders()
●Flip()
●GetAttachedSurface()
●GetBltstatus()
●GetCaps()
●GetClipper()
●GetColorKey()
●GetDC()
●GetDDInterface()
●GetFlipStatus()
●GetOverlayPosition()
●GetPalette()
●GetPixelFormat()
●GetSurfaceDesc()
●IsLost()
●Lock()
●PageLock()
●PageUnlock()
●ReleaseDC()
●Restore()
●SetClipper()
●SetColorKey()
●SetOverlayPosition()
●SetPalette()
●SetSurfaceDesc()
●Unlock()
●UpdateOverlay()
●UpdateOverlayDisplay()
●UpdateOverlayZOrder()
1. 表面描述函数
我们首先讨论的个可用于检索表面自身信息的函数,它们是:
●GetCaps()
●GetPixelFormat()
●GetSurfaceDesc()
●SetSurfaceDesc()
同DirectDraw接口提供的GetCaps()函数一样,DirectDrawSurface接口提供的GetCaps()函数用于输出表征哪些特征可被表面支持的数据。该信息包括:表面是主表面还是离屏表面;表面使用的存储器定位于显示RAM还是系统RAM。
GetPixelFormat()函数在用于高彩和真彩表面时是非常重要的,这是由于像素格式因显示卡的不同而不同。该函数返回表征码,这些表征码表明每一种颜色部件是如何存储的。
GetSurfaceDesc()函数返回一个表面描述。该信息包括表面的宽度、高度和深度。表面像素格式(同样被GetPixelFormat()函数检索)也包含在其中。
SetSurfaceDesc()函数(对于DirectX5)来讲是新增的,只由DirectDrawSurface3接口提供)允许设定某些表面属性。该函数可用于指定表面使用的内存。这一点在设计定制表面存储器管理器策略时非常有用。
2。 表面Blt函数
DirectDrawSurface接口提供3个支持blt操作的函数:
●Blt()
●BltBatch()
●BltFast()
Blt()函数是一个主要函数。Blt()函数能够进行常规的blting(无特殊影响的简单的表面到表面的blt),同时支持延伸、旋转、镜像和颜色填充的操作。当用于同剪裁器关联的表面时,Blt()可进行剪裁blt操作。
BltBatch()函数不是在DirectX3下实现的(你可以调用该函数,但什么也不会发生)。执行BltBatch()函数时,如果可能,它可同时进行多blt操作。
BltFast()函数是Blt()函数的优化版本。BltFast()函数的效率提高了,但性能却下降了。BltFast()函数不能进行一些特殊的blt操作,而Blt()函数可以。而且,BltFast()函数不能用于剪裁。但是BltFast()函数支持源和目标色彩键码blt的操作。在遵循定制剪裁例程的情况下,BltFast()函数可进行DirectDraw能够提供的最快捷、灵活的blt操作。在下章中我们将执行一个定制剪裁例程。
以上3个blt函数均将源表面和目标表面作为变量。其他的数据,例如blt在目标表面上的理想定位,是通过指定理想blt操作的确切属性来提供的。一旦可能,这3个函数将进行硬件加速blt。
3. Flip()函数
Flip()函数用于页面翻转操作。调用Flip()函数可隐藏屏幕上先前可见的表面,并使一个后备缓冲区显现。只有被明确地创建为翻转表面的表面,才响应该函数的调用。
必须牢记,真正的翻转操作不可能总是成功。页面翻转要求有足够的显示RAM容纳两整屏有效数据。如果满足不了这一要求,系统RAM中将创建一个后备缓冲区。这时调用Flip()函数进行的是blt操作而不是页面翻转。基于系统RAM的后备缓冲区中的内容被拷贝到主表面上。这样会严重影响性能,但是,在真正的页面翻转中如果没有足够的显示RAM,又不退出程序,也只能如此了。如果你的应用程序要求最佳性能,就得设法避免激活不能进行真正页面翻转的显示模式。
4. 表面状态函数
下面讨论两个能检索有关操作和翻转操作信息的函数,它们是:
●GetBltStatus()
●GetFlipStatus()
GetBltStatus()函数用于确定当前是否进行blt操作。这一点很重要,因为正被blt的表面不能进行其他操作。该函数表明,给定的表面是否正是一个进行blt操作的源表面或目标表面。
同样地,GetBltStatus()函数表明是否正在进行翻转操作。即使DirectDraw通过blt操作仿真页面翻转,该函数而不GetBltStatus()也必须用于由Flip()函数初始化的监视器页面翻转。
5. 色彩键码函数
DirectDrawSurface接口提供了以下两个函数,来设置和检查表面色彩键码或色彩键码的范围,它们是:
●GetColorKey()
●SetColoKey()
默认状态下,表面没有色彩键码。一个色彩键码只对应一种颜色,但某些硬件支持色彩键码范围。色彩键码和色彩键码范围是DDCOLORKEY结构定义的。GetColorKey()和SetColoKey()函数都将该结构的指针作为变量。在要求表面的一部分透明时或需要进行目标色彩键码操作时,可以使用这两个函数。
6. Lock和Unlock()函数
DirectDraw的一个主要特点,就是能够提供对图像数据的直接存取。直接存取可以提供最佳性能和更好的灵活性,因为没有中间API影响运行速度,并且开发人员呆任意使用图像数据。对表面存储器的中间存取通过以下出众个函数实现:
●Unlock()
●Lock()
Lock()函数向组成表面的存储器返回一个指针,不管表面存储器位于显示RAM还是系统RAM。存储器一般按线性风格排列,以便能简单地进行图像 数据存取。Unolock()函数在完成表面存储器的存取之后指定给DirectDraw。
对图像数据的直接存取必须付出代价。为了支持这种存取方式,DirectDraw在表面锁定的时候必须关闭基本的Windows机构。在Windows95状态下,如果忘记解锁表面,必定会损坏机器。
因此,表面锁定的时间应尽量缩短。测试前应仔细检查Lock()和Unlock()函数之间的程序调用。因为这一程序无法用传统的调试程序进行调试。
锁定表面不能被blt和翻转,因此试图保持表面处于锁定状态没有任何有益之处。而且,一旦表面解锁,由Lock()函数检索的指针就失效了。
表面锁定后不能再次被锁定。在表面锁定时也就无法调用Lock()函数。
7. GetDC()ReleaseDC()函数
对表面的直接存取占用很大内存,有时候把表面作为一个常规的Windows设备会更好。在此,DirectDrawSurface接口提供以下两个函数:
●GetDC()
●ReleaseDC()
GetDC()函数提供了一个DC(设备环境),可以用常规的Win32函数写到表面上。例如,DC可以用Win32的TextOut()函数在表面上绘制文本。用完DC后必须马上调用ReleaseDC()函数。
同Lock()和Unlock()函数使用一样,在调用完GetDC()函数后必须马上调用ReleaseDC()函数。这是因为GetDC()函数内部调用Lock函数,而ReleaseDC()函数内部调用Unlock()函数。
8. PageLock()和PageUnlock()函数
接下来,我们讨论另外两个与Lock()函数和Unlock()函数看上去非常相像的函数:
●PageLock()
●PageUnlock()
尽管这两个函数看上去很像Lock()和Unlock()函数,但它们却有完全不同的作用。PageLock()和PageUnlock()函数用于控制Windows对基于系统RAM的表面的处理方式。这两个函数由DirectDrawSurface2接口提供,而不是由DirectDrawSurface接口提供。
当Windows认为当前正在运行的其他应用程序或进程更适于使用内存时,Windows会向硬盘释放部分内存。这种缓冲对于整个系统内存都起作用,因此驻留在系统内存中的DirectDraw表面有可能被存到硬盘上。如果要用到这样的表面,Windows需要花费一定的时间从硬盘上读取表面数据。
PageLock()函数提示Windows哪些给定的表面不应该释放到硬盘上。这样,在使用表面时就不用耗费时间进行硬盘存取了。相反地,PageUnlock()函数用于告知Windows哪些表面内存可被释放。
过程调用PageLock()函数会减少缓冲内存的总量,从而导致Windows的速度大大降低。至于这种情况何时发生,取决于页面锁定系统内存量及机器提供的系统内存量。
PageLock()和PageUnlock()函数主要是由DirectDraw提供而非DirectDraw应用程序。举个例子来说,DirectDraw自动使用PageLock()函数,以确保运行blt操作时,基于系统RAM的表面不被释放到硬盘。
PageLock()函数可以被同一个表面多次调用。DirectDraw用参考计数法记录PageLock()函数被调用的次数,因此多次调用PageUnlock()函数就必须避免多次调用PageLock()函数。
PageLock()和PageUnlock()函数对于驻留在显示RAM中的表面不起作用。
9. IsLost()的Restore()函数
现在讨论两个与使用驻留在显示RAM中的表面有关的函数:
●IsLost()
●Restore()
让我们来看一看下面这种情况。应用程序正在运行时,尽量把表面分配到显示RAM中,剩下的创建到系统RAM中。应用程序在运行一段时间之后,用户执行或切换到另一个应用程序。该应用程序是任意的,可以是一个常规的Windows程序,如Windows开发程序或记事本。它也可以是另外的DirectDraw应用程序,该程序也试图将尽可能多地分配显示RAM。如果DirectDraw不接受显示RAM,那么新的应用程序就很可能根本运行不了。相反的情况就意味着,应用程序不允许分配到任何显示RAM中。
因此,DirectDraw可以随意将任何一个或者所有的基于显示RAM的表面从非激活应用程序中移走。这种情况就是所谓的表面丢失。从技术上讲,程序仍具有表面,但它们不再同任何内存相关。要使用丢失的表面会导致DDERR-SURFACELOST错误。IsLost()函数可用于确定一个表面是否丢失了内存。
表面内存丢失后可通过Restore()函数恢复,但只能在应用程序被重新激活之后才可恢复。这会导致应用程序无法将处于最小化状态的所有表面复原。
Restore()函数可恢复附属于表面的任一内存,但并不恢复内存的内容。表面被复原后,应用程序就可以恢复表面内容了。
注意,这种用法不适合利用系统RAM创建的表面。如果需要用到基于系统RAM的表面所占内存,那么Windows会立即将这些表面释放到硬盘上。Windows自动地处理存储和恢复,包括恢复表面的内容。
10. GetDDInterface()函数
GetDDInterface()函数可检索用于创建给定表面的DirectDraw接口的指针。由于程序中大多数情况下只有一个DirectDraw接口实例,所以GetDDInterface()函数并不常用。但是一个应用程序中有可能使用多个DirectDraw接口,在这种情况下,GetDDInterface()函数会起到重要作用。
11. 表面连接函数
DirectDrawSurface接口提供以下4个函数,用来维持表面间的连接:
●AddAttachedSurface()
●DeleteAttachedSurface()
●EnumAttachedSurfaces()
●GetAttachedSurface()
DirectDraw支持大量的用于表面间的连接情况。最常见的情况就是页面翻转。进行页面翻转时,两个或两个以上的表面连接成环状,每次调用Flip()函数时,都会使连成环状的表面中的下一个表面显现。
表面连接函数用于创建、检查或消除表面间的连接,但这些函数并非必不可少的。DirectDraw往往是自动创建连接表面。比如,当创建一个主翻转表面时,可以指定用于连接表面的后备缓冲区的数量。DirectDraw就会创建这些表面,并将它们连接起来。
12. 重叠函数
DirectDrawSurface接口用于支持重叠的函数如下:
●AddOverlayDirtyRect()
●EnumOverlayZOrders()
●GetOverlayPosition()
●SetOverlayPosition()
●UpdateOverlay()
●UpdateOverlayDisplay()
●UpdateOverlayZOrder()
GetOverlayPosition()和SetOverlayPosition()函数用于控制重叠的位置。UpdateOverlay()函数用于更新大量的重叠设置,包括重叠是否可见,以及重叠是以色彩键码还是用alpha混合到背景表面上。
UpdateOverlayDisplay()函数用于更新显示设置。该函数用于更新整个重叠显示,或者只更新由AddOverlayDirtyRect()函数指定的矩形重叠部分。EnumOverlayZOrders()函数可根据重叠的Z值(Z值控制哪一个重叠位于最上面)重复重叠。重叠可按从前到后或从后到前的顺序枚举。
13. 剪裁器函数
DirectDraw支持的剪裁是将DirectDrawClipper接口(该接口我们尚未讨论)的一个实例连接到表面。一旦连接完毕,剪裁器对象就会有规律地blt到表面。剪裁器/表面的连接由以下两个DirectDrawSurface函数控制:
●GetClipper()
●SetClipper()
SetClipper()函数用来将剪裁器对象连接到表面。GetClipper()函数用于向前一个连接的剪裁器对象返回一个指针。SetClipper()函数还可用于解除表面同剪裁器的连接,具体的做法是通过指定NULL来替代DirctDrawClipper接口指针。
14。 调色板函数
像剪裁器一样,调色板也可连接到表面。DirctDrawSurface接口为此提供以下两个函数:
●GetPalette()
●SetPalette()
SetPalette()函数用来将DirctDrawPalette接口(该接口我们接下来就要讨论)的一个实例连接到表面。GetPalette()函数用于检索前一个连接调色板的指针。
调色板可被连接到任何表面,但只有连接到主表面时,调色板才起作用。当与主表面连接时,调色板决定显示硬件调色板的设置。
第六节 DirectDrawPlette接口函数
DirctDraw提供DirctDrawPalette接口用于调色板显示模式和表面。尽管Windows支持几种低于8位像素深度的显示模式,但DirctDraw所支持的唯一的调色板显示模式是8位模式。
DirctDrawPalette接口实例由DirctDraw CreatePalette()函数创建。CreatePalette()函数用大量的标志来定义调色板属性。
DirctDrawPalette接口只提供以下3个函数:
●GetCaps()
●GetEntries()
●SetEntries()
GetCaps()函数用于检索有关调色板的信息,包括调色板项目数量,调色板是否支持同步垂直刷新,以及在8位调色板状态下是否所有的256个调色板项目都能被设定。
SetEntries()函数允许在程序中设置调色板的色彩值。该数据从文件中读取。而这些项目在运行过程中可被计算和设定。GetEntries()函数用于检索先前设定的调色板项目。
DirctDrawPalette()接口实例可利用DirctDrawSurface SetPalette()函数连接到表面。将不同调色板连接到主表面或利用SetEntries()函数改变调色板项目都可激活调色板。
第七节 DirectDrawClipper接口函数
DirctDrawClipper接口支持剪裁。将剪裁器对象连接到表面并在blt操作中将其当作目标表面就可以进行剪裁。
directDrawClipper实例由DirectDraw CreateClipper()函数创建。DirectDrawClipper接口支持以下5个函数:
●SetHWnd()
●GetHWnd()
●IsClipListChanged()
●SetClipList()
●GetClipList()
剪裁器对象一般用于出现在窗口中的DirctDraw应用程序必需的剪裁。要求剪裁器必须确保在blt操作过程中考虑到桌面上其他的窗口。比如,当应用程序的全部或一部分被另一个窗口遮蔽,剪裁就必须确保被遮蔽的窗口不被DirctDraw应用程序破坏。
桌面剪裁由SetWnd()函数完成。SetHWnd()函数将剪裁器对象连接到一个窗口句柄。这样就初始化了Windows和剪裁器对象之间的通讯。当桌面上的任何一个窗口发生变化时,剪裁器对象就会得到通知,并作出反应。GetHWnd()函数用于决定剪裁器同哪一个窗口句柄连接。IsClipListChanged()函数用于决定内部剪裁清单是否因桌面的改变而被更新。
SetClipList()和GetClipList()函数为DirectDrawClipper接口提供便利的定制使用。SetClipList()函数用于定义一些矩形区域,这些矩形区域用来定义blt操作的合法区域。GetClipList()函数用于检索剪裁器的内部剪裁数据。
一旦被连接到表面,Blt(),BltBatch()以及UpdateOverlay()函数所进行的blt操作将根据DirctDrawCliper接口中包含的数据被自动地剪裁。注意,Blt Fast()函数在此被忽略。BltFast()函数不支持剪裁。
第八节 附加DirectDraw接口
DirctDraw还提供了另外个我们没有讨论的接口,它们是:
●DDVideoPortContainer
●DirectDrawColorControl
●DirectDrawVideoport
这些接口由DirectX5介绍,它们提供低水平视频端口控制。这些接口还向DirctDraw表面提供流活动视频的方法。尽管利用这些接口可以为DirctDraw应用程序增加视频支持,但除非高水平视频APIs不能满足需要,否则最好不用这一方法。
第九节 DirectDraw结构
我们已讨论过DirctDraw接口及其成员函数了,接下来再看看DirctDraw定义的结构。DirctDraw总共定义了8全结构:
●DDBLTFX
●DDCAPS
●DDOVERLAYFX
●DDPIXELFORMAT
●DDSURFACEDESC
●DDSCAPS
●DDBLTBATCH
●DDCOLORKEY
我们已经见过其中的一些结构了,比如在讨论DirctDrawSurface SetColorKey()函数时我们就接触过DDCOLORKEY结构。在此,我们不详细讨论每一个结构的细节,但必须指出,DirctDraw quirk被忘记时会导致失败。
以上所列结构中的前5个有一个称作dwSize的字段。该字段用于存储结构的大小,设定该字段的工作由你来做。另外,该字段如果没有被正确设定的话,那么任何一个将这5个结构作为变量的DirctDraw函数都会失效。
以DDSURFACEDESC结构为例,使用结构的代码如下:
DDSURFACEDESC surfdesc; surfdesc.dwSize=sizeof(surfdesc); surf->GetSurfaceDesc(&surfdesc);
首先声明结构,然后用sizeof()关键字设定dwSize字段。最后结构传递给DirctDrawSurface GetSurfaceDesc()函数。忘记设定dwSize字段将导致这段代码失效。
到底为什么DirctDraw坚持要求给出它所定义的结构的大小?原因在于这5个包含dwSize字段的结构将来有可能会改变。DirctDraw会检查结构的大小,以便确定正在使用的版本。DirctDraw坚持要求给出一个有效的大小值,是为了让开发者提供有效的结构大小。这样做是有好处的,因为DirctDraw的新版本可以正确运用旧版本的DirctDraw程序。
在使用结构之前,最好将结构初始化为零。这样,前面的代码就变成:
DDSURFACEDESC surfdesc; ZeroMemory (&surfdesc,sizeof(surfdesc)); surfdesc.dwSize=sizeof(surfdesc); surf->GetSurfaceDesc(&surfdesc);
ZeroMemory()函数是一个Win32函数,它将作为第一个参数的存储器设定为零.ZeroMemory()函数的第二个参数表明有多少存储器应被初始化。这一做法的好处是,通过GetSurfaceDesc()函数调用可以知道结构的哪些字段被更新了。如果没有对结构进行初始化,就有可能将结构字段中不可预测的值当作DirectDraw的设定值。
第十节 窗口应用程序
DirctDraw应用程序主要有两种型式:窗口的和全屏的。窗口DirctDraw应用程序看起来就像一个常规的Windows程序。我们很快将讨论到全屏应用程序。
窗口应用程序包括窗口边界、标题框以及菜单,这些都是传统的Windows应用程序中常见的部分。由于窗口应用程序同其他窗口一起出现在桌面上,因此它们被迫使用Windows当前所使用的分辨率和比特深度。
窗口程序有一个主表面,但只在进行真实页面翻转时才显现。而且,主表面并不代表窗口的客户区域(该区域在窗口边界内)。主表面还代表整个桌面。这就是说,你的程序必须追踪窗口的位置和大小,以便在窗口内正确显示可见的输出。换言之,利用窗口化的应用程序中可以在整个桌面上进行绘图。
如果不允许页面翻转,那么图像就必须从离屏缓冲区blt到主表面上。这就增加了图像撕裂的可能性,因为blt比页面翻转速度慢。为了避免图像撕裂,blt操作可以与监视的刷新率保持同步。
如果与窗口客户区域同样大小的离屏缓冲区被创建到显示RAM中,窗口应用程序就可以很好地运行。这样,窗口的内容可利用离屏表面合成。然后离屏表面可以通过硬件加速很快地到主表面上。
由于显示存储器的缺乏而不得不将离屏缓冲区创建到系统RAM中时,会严重影响性能。不幸的是,这种情况常常发生,尤其是在只有2MB显示卡的时候,这是因为人们总希望为自己Windows的桌面设置高分辨的显示模式。例如,采用1024×768×16显示模式的主表面自己就要占用2MB的RAM。在一个2MB显示卡上,几乎没有显示RAM留给离屏表面。
窗口应用程序的另一个问题是剪裁。一个性能良好的应用程序必须有一个连接到主表面的剪裁对象。这是有损性能的,原因在于为了检查剪裁器的剪裁清单内容,blt操作只能在一个小的矩形部分内进行。而且,不能使用优化的BltFast()函数。Bltting必须用较慢的,(而且更笨重的)Blt()函数。
最后要讲的是,窗口应用程序不允许全调色板控制。由于Windows保留了20个调色板项,所以在256种颜色只有236种颜色可被设定。被Windows保留的颜色只用系统调色板的前10个项和后10个项。因此在给图像上色时,只能使用中间236个调色板项。
第十一节全屏应用程序
包含对显示设备进行专有存取的DirctDraw应用程序就是全屏应用程序。这种应用程序可以任意选取显示卡所支持的显示模式,并享有全调色板控制。另外,全屏应用程序还可进行页面翻转。因此同窗口应用程序相比,全屏应用程序速度更快,灵活性更好。
典型的全屏应用程序首先检查所支持的显示模式,并激活其中一个。然后创建具有一个或更多后备缓冲区的可翻转主表面,剩下的显示RAM用于创建离屏表面。当显示RAM耗尽时,就启用系统RAM。屏幕被在后备缓冲区中合成的第一个场景更新,然后进行页面翻转。即使主表面占用了所有的可用的显示RAM,全屏应用程序还可输出执行其窗口化的副本,这是因为全屏应用程序可进行真实页面翻转。
由于全屏应用程序不一定非得使用Windows的当前显示模式,所以显示RAM的可用与否不存在多少问题。如果只检测到2MB的显示RAM,就可使用低分辨率显示模式来保留内存。如果检测到4MB的显示RAM,应用程序就可使用要求的显示模式并仍有保持良好性能。
全调色板控制也是个有利因素。这样可以使用所有256个调色板项而无需根据Windows保留的20中颜色重新分配位图。
第十二节混合应用程序
混合应用程序既可以在全屏方式下运行也可在窗口方式下运行。混合应用程序内部非常复杂,但却可以提供良好的性能。用户可在窗口方式下运行,如果太慢,则可切换到全屏方式下运行。
编写混合应用程序最好的方法是编写一个定制库,该库包括那些与应用程序使用全屏方式还是窗口方式无关的函数。dows下的消息系统负责在多任务环境中分解信息。从应用程序的角度来看,消息是关于发生的事件的通知。用户可以通过按下或移动鼠标来产生这些事件,也可以是通过改变窗口大小或选择一个菜单项等。这些事件也可以由应用程序本身产生。Windows本身也能产生消息。如“关闭Windows”消息,Windows通过这个消息来通知所有的应用程序,Windows将被关闭。
内存管理
在Windows系统中系统内存是最重要的共享资源之一。当同一时刻有多个应用程序在运行时,为了不耗尽系统资源,每个应用程序必须合作以共享内存。同时,当启动新的程序和关闭老的程序时,内存会变得碎片化。通过移动内存中的代码和数据块,Windows能够使内存空闲空间连起来。在Windows下也有可能超量使用内存。例如,应用程序可以比内存容量大。Windows能够废弃当前不使用的代码,在以后需要时再从应用程序中将之读入内存。Windows应用程序可以共享可执行文件中的例程。包含可共享的例程的文件称为动态链接库(DLL)。Windows包括了运行时将DLL例程链入程序的机制。
硬件无关性
Windows同时提供了硬件或设备无关性,使你免于在生成程序的时候不得不考虑所有可能使用的显示器、打印机或输入设备。在Windows下面,每种硬件设备的驱动程序只编写一次。硬件无关性使编程对应用程序开发者来说更为简单。应用程序与Windows而不是各种设备打交道。
动态键接库
动态键接库提供了更多的Windows功能。它们通过一个有力而灵活的图形用户界面增强了基本的操作系统。动态键接库包括一些预定义的函数,它们可以在一个应用程序被调入时与之键接(动态地),而不是在应用程序被创建时(静态地)。动态键接库使用DLL后缀。函数库将每一个程序员从重复开发诸如读取字符或格式化输出之类的通用例程中解放出来。程序员可以方便地构造它们自己的库以包含更多的功能,比如改变字体或检验文本。把函数变为通用工具减少了冗余设计,这是OOP的一个关键特性。
Windows的库是被动态地键接的。或者说,键接器并不把函数拷贝到程序的可执行文件中去。相反,当程序运行时,它产生对库函数的调用。自然,这样做节约了内存。不管有多少应用程序在运行,在RAM中总是只有库的一份考贝,而这个库可以被共享。
Windows的可执行文件格式
Windows具有一种新的可执行文件的格式,称为New Excutable格式。它包括新型的文件头,能够保存有关DLL函数的信息。
第四节 windows的窗口
Windows的窗口
窗口看起来就是显示设备中的一个矩形区域,它的外观与特定的应用程序无关,可是,对于一个应用程序来说,窗口是屏幕上应用程序能够直接控制的矩形区域。应用程序能够创建并控制主窗口的一切,如大小和形状。当用户启动一个程序时,一个窗口就被创建了。用户每次单击窗口,应用程序作出响应。关闭一个窗口会使应用程序结束。多窗口带给用户Windows的多任务能力。通过将屏幕分为不同的窗口,用户能够使用键盘或鼠标选择一个并行运行的应用程序,以此对多任务环境中的一个特定程序进行输入,Windows截取了用户的输入并分配必要的资源(例如微处理器)。
Windows的布局
所有的Windows应用程序都具有诸如边框、控制菜单、About对话框之类的共同特征。这些特征使得各个Windows应用程序非常类似。
边框
Windows的窗口被边框所包围。边框由围出窗口的线条组成。对于新手而言,边框看起来仅仅是为了将一个应用程序的屏幕视口与其它的区别开。但是,对于熟练者,边框有着不同的作用。例如,如果将鼠标指针放在边框上并按下鼠标的左键,用户就可以改变窗口的大小。
标题条
应用程序的名字显示在窗口顶部的标题条中。标题条总是在相关窗口顶部的中央。标题条非常有用,它可以帮助你记住正在运行哪个应用程序。活动应用的标题条以不同于非活动应用程序的颜色显示。
控制图标
控制图标是每个窗口左上方的小图片,每个应用程序都使用它。在控制图标上单击鼠标键会使Windows显示系统菜单。
系统菜单
当用鼠标单击控制图标时就打开了控制菜单。它提供了诸如Restore,Move,Size,Minimize,Maximize以及Close这样的标准操作。
最小化图标
每个Windows 95或Windows NT应用程序都在窗口的右上角显示三个图标。最左边的图标是一段短下划线,这就是最小化图标。它可以使用程序被最小化。
最大化图标
最大化图标是三个图标中中间的那一个,看起来象两个小窗口。使用最大化图标可以使用应用程序占满整个屏幕。如果选择了这个图标,其它应用程序窗口都会被盖住。
垂直滚动条
如果有必要,应用程序可以显示一个垂直滚动条。垂直流动条显示在应用程序窗口的右边,在两端有两个方向相反的箭头。它还有一个着色的棒和一个透明的窗口块。后者被用于显示当前显示内容与整个文档(着色的棒)的关系。你可以用滚动条来选择显示哪一页。一般在任何一个箭头上单击一下会使显示内容移动一行。单击向上箭头下方的窗口块并拖动它会使屏幕输出快速更新到应用程序屏幕输出的任意位置。
水平滚动条
也可以显示一个水平滚动条。水平滚动条显示在窗口的底部,具有与垂直滚动条类似的功能。你用它来选择要显示哪些列。一般在任何一个箭头上单击一个会使显示内容移动一列。单击向左箭头右边的窗口块并拖动它会使屏幕输出快速更新到应用程序屏幕输出的任意位置。
菜单条
一个可选择的菜单条可以显示在标题条的下方。通过菜单条来选择菜单和子菜单。这种选择可以通过用鼠标单击,也可以用热键组合来实现。热键组合经常是ALT与命令中带下划线的字母的组合,比如File命令中的“F”。
用户区
通常用户区占据了窗口最大的部分。这是应用程序的基本输出区域。应当由应用程序来复杂管理用户区。另外,应用程序可以输出到用户区。
第五节 windows的类
窗口的基本组件有助于说明应用程序的外观。有的时候应用程序需要创建两个外观和表现都相似的窗口。Windows的Paint就是一个例子。借助于同时运行Paint的两个实例(或拷贝),Paint允许用户剪贴或拷贝图片的一部分。然后信息就可以从一个实例拷贝到另一个实例。Paint的每个运行实例的外观和表现都与其他的相同。这就需要每个实例创建自己的外观和功能类似的窗口。
在这种情况下被创建的外观和功能都很类似的窗口被称为是属于同一个窗口类的。但是,你创建的窗口可以有不同的特征。它们可以有不同的大小,不同的位置,不同的颜色或不同的标题,也可以使用不同的光标。
每个被创建的窗都基于一个窗口类。在用C语言开发撕于的基于传统的函数调用方式的应用程序中,一些窗口为在Windows应用程序初始化的进修注册。你的应用程序可以注册属于自己的窗口类。为了能够使几个窗口在同一个窗口类的基础上创建,Windows定义了一些窗口特征,如CreateWindows()的参数,另一些定义的窗口类的结构。当你注册一个窗口类的时候,这个类可以被Windows下运行着的任何程序所使用。对于使用MFC的应用程序来说,多数注册工作已经由预定义的对象完成了。
具有相似的外观和表现的窗口可以被组合成一个类,以此来减少需要维护的信息。因为每个窗口类都有自己的可共享的类结构,不需要复制不必要的窗口类参数。同时,同类的两个窗口使用相同的函数以及相关的例程。这样可以节省时间和空间,因为不存在代码复制。
第六节 windows中的面向对象编程
在Windows下传统的C程序吸收了一些面向对象编程的特性。对象是一种包含数据结构和对这些数据结构进行操作的函数的抽象数据类型。而且,对象接收会引起它们不同动作的消息。
比如,一个Windows的图形对象是可以被作为一个实体来操纵的一些数据的集合,对于用户它是可视界面的一部分。特别地,一个对象意味这数据和数据的功能。菜单、标题条、控制块以及滚动条等都是图形对象的例子。下一部分描述一些影响应用程序外观的新的图形对象。
图标
图标是用来使用记住特定操作、想法或产品的小图形对象。比如,一个电子表格程序被最小化时可以显示一个很小的柱状图以提醒用户这个程序还在运行之中。在柱状图上双击鼠标会使Windows激活这个应用程序。图标是非常有力的工具。它很适合用来引起用户的注意,比如在发出错误警告或者是向用户提供选择时。
光标
光标是Windows用来跟踪指点设备的运动的图形符号。这种图形符号可以改变形状以指明特定的Windows操作。比如,当标准的箭头光标变为沙漏光标时说明Windows正在执行一个命令,需要暂停。
编辑光标
应用程序在窗口中显示编辑光标以告诉用户在哪儿输入。编辑光标与其他屏幕符号显然不同,因为它是闪烁的。多数时候,鼠标输入与光标相连,而键盘输入与编辑光标相连。但是,可以用鼠标来改变编辑光标的输入点。
消息框
消息框是另一类Windows图形对象。消息框是一种包含标题、图标和消息的弹出式窗口。图(?)是关闭Windows Notepad程序时出现的一个标准的消息框。
-------------------------------------------------------------------------
| |
------------------------------------------------------------------------
Windows的对话框
对话框与消息框相似的地方在于它也是一种弹出式窗口。但是对话框主要用于接受用户输入而不仅仅是显示一些输出。对话框允许应用程序接受输入,每次一个域或是一个框的内容,而不是每次一个字符。图(?)显示了一个典型的Windows对话框。对知框的图形设计由Windows为你自动完成。对话框的布局通常用编译器中的资源编辑器完成。
-----------------------------------------------------------------------
| |
-----------------------------------------------------------------------
字体
字体是一种图形对象或资源,它定义了完整的字符集合的字样。这些字符都有一个特定的大小和风格,可以使文本具有不同的外观。字样是字符的一种基本属性,它定义了字符的衬线和笔画宽度。
位图
位图是一种显示图片(按像素组织),存储于内存。当应用程序需要快速显示图片时可以使用位图。因为位图直接从内存中传送,所以它比用程序重新画出图片要快得多。位图有两个基本用途。首先,它可以在屏幕上显示图片。其次位图也用于创建刷子。刷子使你可以在屏幕上画出并填充对象。
使用位图有两个缺点。首先,与其尺寸有关,位图会占据难以预估的大量内存。每个被显示的像素都要在内存中占据相应的空间。在彩色显示器上显示一个像素会比在单色显示器上占据更多的内存。在单色显示器上,只需一位(bit)就可以表示出像素的状态。可是在可以显示16种颜色的彩色显示器上,需要四位才能表示一个像素的特征。同样地,随着显示设备分辨率的增加,位图对内存的需求也增加了。位图的另一个缺点是它只包括静态的图片。比如,如果用位图来代表一辆汽车,就没有办法来访问图片的不同部分,如轮踏、顶盖、窗等。但是,如果汽车是有一系列基本绘图例程来生成的,应用程序就可以改变向这些例程传送的数据从而改变图片的不同部分。例如,应用程序可以修饰顶蓬线并把一辆轿车变为敞蓬车。
画笔
当Windows在屏幕上显示一个图形时,它使用当前画笔的信息。画笔用于画出线条或轮廊。画笔具有三个基本特征:线宽、线型(虚线、短线、实线)以及颜色。Windows永远保留着用于画白线和黑线的画笔,任何应用程序可以使用它。你也可以创建自己的画笔。
刷子
Windows用刷子来画出颜色并以预定义的样式来填充一个区域。刷子至少有8×8个像素大小。刷子有三个基本特征:样式和颜色。由于它们至少有8×8的大小,刷子被称作具有样式而不象画笔,称为线型。样式可以是纯的颜色,也可以是阴影线、斜线或其它用户自定义的组合
第七节 windows的消息
Windows的消息
在Windows中,应用程序并不直接写屏幕、处理硬件中断或直接对打印机输出。相反,应用程序使用合适的Windows函数或者等待一个适当的消息被发出。
Windows消息系统负责在多任务环境中分派消息。从应用程序的角度来看,消息可以看作是发生的事件的通知,有些需要作出特定的反应,有些就不需要。这些事件可能由用户产生,比如按下了鼠标或移动了鼠标,改变了窗口的大小或者选择了一个菜单。同时,这些事件也可能由应用程序本身所产生。
这个过程使你的应用程序必须完全面向消息处理。当接收到消息时,应用程序必须能激活并决定正确的动作,完成这个动作之后回到等待状态。
通过检查消息的格式和来源,下一部分将更仔细地讨论消息系统。
消息的格式
消息通知一个应用程序发生了一个事件。从技术上来讲,消息不仅仅是与应用程序相关,而且是与应用程序的某一特定窗口有关。因此,所有的消息都被发往窗口。
在Windows下只有一个消息系统-即系统消息队列。但是,每个正在Windows下运行的应用程序都有它自己的消息队列。系统消息队列中的每个消息最终都要被USER模块传送到应用程序的消息队列中去。应用程序的消息队列中存储了程序的所有窗口的全部消息。
不管消息具有什么类型,它们都有四个参数:一个窗口句柄,一个消息类型,两个附加的32位参数。窗口消息中定义的第一个参数是消息所关联的窗口句柄。
在编写Windows应用程序的时候经常使用句柄。句柄是一个唯一的数字,它被用于标识许多类型的对象,如菜单、图标、画笔和刷子、内存分配、输出设备甚至窗口实例。在Windows 95和Windows NT下面,程序的每个运行着的拷贝叫做实例。
因为Windows 95和Windows NT允许你同时运行一个程序的多个实例,操作系统就有必要保持对这些实例的追踪。这是通过赋予每个运行实例一个唯一的实例句柄来实现的。
实例句柄通常被用作一个内部维护着的表的索引。通过引用表中的元素而不是实际的内存地址,Windows 95和Windows NT可以动态地调整所有的资源,而只需在此资源所对应的表格位置中插入一个新的地址。
根据一个应用程序的多个实例被处理的方式,内存资源由Windows 95和Windows NT保存。
应用程序的实例具有很重要的作用。应用程序的实例定义了程序的函数所需的所有对象。这包括控件、菜单、对话框以及更多的新Windows类。
消息中的第二个参数是消息类型。这是在Windows独有的一些头文件中定义的标识符。这些头文件可以通过WINDOWS.H来使用。在Windows下,每个消息由两个字符的助记符开始,跟着是下划线,最后是一个描述符。
最后的两个参数提供了解释消息所需的附加信息。因此最后两个参数的内容依赖于消息的类型。
产生消息
消息传送概念使Windows能够实现多任务。消息有四个基本来源。应用程序可以从用户那儿接受消息,也可以是Windows本身,应用程序本身或者是其它应用程序。
用户消息包括按键消息、鼠标移动、鼠标指点或单击、菜单选择、滚动条的定位等。应用程序必须花费大量的时间来处理用户消息。用户产生的消息表明运行程序的人希望改变应用程序的表现方式。
无论何时,如果状态发生改变,将会有一个消息被发往应用程序。一个例子是用户单击了应用程序的图标,表明他们想要将此应用程序变为活动的应用程序。在这种情况下,Windows告诉应用程序它的主窗口被打开了,它的大小和位置被改变了等等Windows产生的消息可以被处理,也可以被忽略,这跟应用程序当前的状态有关。
相应消息
在传统的面向过程的C语言Windows应用程序中,对于遇到的每一种消息,它都有一个相应的过程来处理这消息。不同的窗口对相同的消息会产生不同的响应。Windows把每个消息发送到应用程序的不同窗口,而不同的窗口对相同的消息会有不同解释。不令应用程序需要不同的过程来处理每一种消息,每一个窗口也应有不同的过程来处理不同的消息。窗口过程集合了应用程序的所有消息处理过程。
消息循环
所有Windows应用程序的一个基本组成就是消息处理循环。每一个C应用程序都在内部执行这个操作。C应用程序包含了创建并初始化窗口的过程,随后是消息处理循环,最后是结束应用程序所需的一些代码。消息循环负责处理Windows发给主程序的消息。在这儿,程序知道有了消息,并且要求Windows将消息发送到合适的窗口过程以供处理。当消息被接受时,窗口过程就执行希望的动作。
第八节 windows的函数
Windows向应用程序开发人员提供了数以百计的函数。这些函数的例子包括DispatchMes-sage(),PostMessage(),RegisterWindowMessage()以及SetActiveWindow()。对于使用基础类库的C++程序员,许多函数自动被运行。
在16位的Windows 3.x下的函数声明包括一个pascal修饰符,这在DOS下更为有效Windows95和Windows NT下的32位应用程序不再使用这个修饰符。如你所知,所有Windows函数的参数是通过系统来传递的。函数的参数从最右边的参数开始向左压入栈,这是标准的C方式。在从函数返回之前,调用过程必须按原来压入栈的字节数调整栈指针。
第九节 windows应用程序框架
Windows头文件:WINDOWS.H
WINWS.H头文件(以及其它相关文件)是所有程序的内在部分。传统上,WINDOWS.H是所有C语言编写的Windows应用程序必需的一部分。当在C++中使用基础类库时,WINDOWS.H包括在AFXWIN.H头文件中。
Windows应用程序的组成
在开发Windows应用程序的过程中有一些重要的步骤:
*用C语言编写WinMain()函数和相关的窗口函数,或者在C++中使用基础类,比如CWinApp等。
*创建菜单、对话框和其它资源并把它们放入资源描述文件。
*(可选)使用Vinsual C++编译器中的企业编辑器来创建对话框。
*(可选)使用Vinsual C++编译器中的企业编辑器来创建对话框。
*用项目文件来编译并链接所有的C/C++源程序和资源文件
Windows应用程序中的组成部分
1. WinMain()函数
Windows 95和Windows NT需要一个WinMain()函数。这是应用程序开始执行和结束的地方。
从Windows向WinMain()传递四个参数。下面的代码段演示了这些参数的使用:
int WINAPI WinMain(HINSTANCE hInst, HINSTANCE hPreInst, LPSTR 1pszCmdLine, int nCmdShow)
第一个参数hInst包含了应用程序的实例句柄。当应用程序在Windows下运行时,这个数字唯一标识了应用程序。
第二个参数hPreInst将始终是一个NULL值,表明没有这个应用程序的其它实例正在运行,因为在Windows 95和Windows NT下每个应用程序都在它自己单独的地址空间中运行。
第三个参数1pszCmdLine是指向一个以'/0'结尾的字符串的长指针,这个字符串代表了应用程序的命令行参数。
WinMain()的第四个参数是nCmdShow。在nCmdShow中存储的整数代表了Windows预定义的许多常量中的一个,它决定了窗口显示的方式。
2. WNDCLASS
WinMain()负责注册应用程序的主窗口类。每个窗口类都建立在一些用户选择的风格、字体、标题字、图标、大小、位置等的基础上。窗口类实际上是定义这些属性的一个模板。
基本上,所有的Windows类定义都使用相同的标准C/C++结构。下面的例子是一个说明WNDCLASSW结构的typedef语句,WNDCLASS是从这儿继承的:
typedef struct tagWNDCLASSW
UINT style;
WNDPROC 1pfnWndProc;
int cbClsExtra;
int cbWndExtra;
HANDLE hInstance;
HICON hIcon;
HCURSOR hCursor;
HBR8USH hbrBackground;
LPCWSTR 1pszMenuName;
LPCWSTR 1pszClassName;
WNDCLASSW,*PWNDCLASSW,NEAR*NPWNDCLASSW, FAR*LPWNDCLASSW;
下面的部分讨论了WNDCLASS结构中的不同的域。其中有些域可以被赋予NULL,告诉Windows使用缺省的预定义值。
style:style域指明了类风格。
1pfnWndProc:接受一个指向窗口函数的指针,它将执行所有的窗口任务。
cbClsExtra:指定了必须在窗口类结构后面分配的字节数,它可以是NULL。
cbWndExtra:指定了必须在窗口实例后面分配的字节数,它可以是NULL。
hInstance:定义了注册窗口类的应用程序实例。它必须是一个实例句柄,不得是NULL。
hIconhIcon:划定利用窗口最小化时显示的图标。它可以是NULL。
hCursorhCursor:定义了应用程序使用的光标。这个句柄可以是NULL。
hbrBackground:提供了背景刷子的标识符。
1pszMenuName:是指向一个以空字符结尾的字符串的指针。这个字符串是菜单的资源名。这一项可以为NULL。
1pszClassName:是指向一个以空字符结尾的字符串的指针。这个字符串是窗口类的名字。
3.WNDCLASSEX
Windows提供了一种扩展的WNDCLASS定义,名为WNDCLASSEX,它允许应用程序使用小图标。下面是WNDCLASSEX结构的定义:
typedef struct WNDCLASSEX
UINT style;
WNDPROC 1pfnWndProc;
int cbClsExtra;
int cbWndExtra;
HANDLE hInstance;
HICON hIcon;
HCURSOR hbrBackground;
LPCTSTR 1pszMenuName;
LPCTSTR 1pszClassName;
HICON hIconSm;
WNDCLASSEX;
你可以看到这两个结构是相同的,除了WNDCLASSEX包括了hIconSm成员,这是与窗口类有关的小图标的句柄。
4.定义窗口类
应用程序可以定义它们自己的窗口类,只要先定义一个合适类型的结构,然后用窗口类的信息来填充结构的域。
下面的代码示范了如何定义并初始化一个WNDCLASS结构。
char szProgName[]="ProgName"; . . . WNDCLASS wcApp; . . . wcApp.1pszClassName=szProgName; wcApp.hInstance=hInst; wcApp.1pfnWndProc=WndProc; wcApp.hCursor=LoadCursor(NULL,IDC-ARROW); wcApp.hIcon=NULL; wcApp.1pszMenuName=szAppIName; wcApp.hbrBackground=GetStockObject(WHITE-BRUSH); wcApp.style=CS-HREDRAW| CS-VREDRAW; wcApp.cbClsExtra=0; wcApp.cbWndExtra=0; if(!RegisterClass (&wcApp)) return 0;
WNDCLASS结构中的第二个域是wcApp.hInstance,它被赋予了WinMain()被激活后返回的hInst的值。这指明了应用程序的当前实例。1pfnWndProc被赋予执行所有窗口任务的窗口函数的指针地址。对于大部分应用程序,这个函数叫做WndProc()。
注意:WndProc()是一个用户定义而不是预定义的函数名。在赋值语句之前必须给出函数原型。
wcApp.hCursor域被赋予实例的光标句柄。
当wcApp.1pszMenuName被赋予NULL值的时候,Windows就认为这个窗口类没有菜单。如果有,菜单必须有一个名字,它必须出现在引号里面。GetStockOject()函数返回一个刷子句柄,用于在这个类创建的窗口用户区中画出背景色。
wcApp.style窗口类风格被设为CS-HREDRAW或CS-VREDRAW。
最后的两个域,weApp.cbClsExtra以及wcApp.cbWndExtra经常被设为0。这些域可以被选用以指明窗口结构和窗口数据结构后面应该保留的附加字节数。
下面这段代码用于注册窗口类:
if(!hpreInst) . . . if(! RegisterClass(&wcApp)) return FALSE;
Windows 95和Windows NT通过检查hPreInst的值来确定多少个实例,而hPreInst总是NULL,所以就注册窗口类.
5.创建窗口
窗口通过调用CreateWindow()函数来创建。这个过程对所有版本的Windows都是一样的。窗口类定义了窗口的一般特征,允许同一个窗口类被用于多个不同的窗口,CreateWin-dow()函数的参数指明了关于窗口的更详细的信息。
CreateWindow()函数的参数信息包括以下内容:窗口类、窗口标题、窗口风格、屏幕位置、窗口的父句柄、菜单句柄、实例句柄以及32位的附加信息。在大部分应用程序中 ,这个函数会是下面这个样子:
hWnd=CreateWindow(szProgName,"Simple Windows Program",
WS-OVERLAPPEDWINDOW,CW-USEDEFAULT,
CW-USEDEFAULT,CW-USEDEFAULT,
CW-USEDEFAULT,(HWND)NULL,(HMENU)NULL,
(HANDLE)hInst,(LPSTR)NULL);
第一个域szProgName(已赋过值)定义了窗口的类,后面是窗口标题条上使用的标题。窗口的风格是第三个参数
下面的六个参数代表了窗口的x、y坐标和x、y方向的大小,然后是父窗口句柄和窗口菜单句柄。每个域都被赋予一个缺省值。hInst域包含了程序的实例句柄,后面是一个附加参数(NULL)。
显示和更新窗口
在Windows下,ShowWindow()函数被用来实际显示一个窗口。下面的代码示范了这个函数:
Show Window(hWnd,nCmdShow);
在调用CreateWindow()时生成的窗口句柄被用作hWnd参数。ShowWindow()的第二个参数是nCmdShow,决定了窗口被如何显示。这个显示状态也被称为窗口的可视状态。
显示窗口的最后一步是调用Windows的Update Window()函数。
UpdateWindow(hWnd);
6.消息循环
一旦调用Win-Main()函数并显示了窗口,应用程序就需要一个消息处理循环。最常用的实现方法是使用一个标准的while循环:
while (GetMessage (&lpMsg,NULL,0,0)) { TranslateMessage(&lpMsg); DispatchMessage(&lpMsg); }
GETMESSAGE()函数:应用程序要处理的下一个消息可以通过调用Windows的GetMessage()函数来取得。
NULL参数指示函数取回这个应用程序的任何窗口的任何消息。最后两个参数0和0告诉GetMessage()不要使用任何消息过滤器。消息过滤器能够将接收到的消息限制在一个明确的范围之内,如键盘消息或鼠标消息等。
一般应用程序应该确认通向消息循环的所有步骤都已经正确地执行过了。这包括确认每个窗口类都已经注册过,都已经被创建。否则,一旦进入了消息循环,只有一个消息能够结束这个循环。无论何时处理了WM-QUIT消息,返回值是FALSE。这会引发主循环关闭例程。WM-QUIT消息是应用程序退出消息循环的唯一途径。
TRANSLATEMESSAGE()函数:通过TranslateMessage()函数,虚拟消息可以被转换为字符消息。
DISPATCHMESSAGE()函数:Windows通过DispatchMessage()函数将当前的消息发送到正确的窗口过程。
******* 窗口函数
所有的应用程序都必须包括一个WinMain()函数和一个回调窗口函数。因为一Win-dows应用程序从不直接访问任何窗口函数,每个应用程序都必须向Windows提出请求以执行规定的操作。
一个回调函数在Windows中注册,当Windows要对一个窗口进行操作时,它就被调用。各个应用程序的回调函数的实际代码长度会大不相同。窗口函数本身可以非常小,只处理一个或两个消息,也可以非常大而且复杂。
下面的代码段(不完整的应用程序说明语句)显示了在应用程序中的回调窗口函数WndProc()的一个范例:
LRESULT CALLBACK WndProc(HWND hWnd,UNIT messg, WPARAM wParam,LPARAM 1Param) HDC hdc; PAINTSTRUCT ps; switch(messg) case WM-PAINT: hdc=BeginPaint(hWnd,&ps); . . . ValidateRect(hWnd,NULL); EndPaint(hWnd,&ps); break; case WM-DESTROY: postQuitMessage(0); break; default: return(DefWindowProc(hWnd,messg,wParam,1param)); return(0);
Windows希望窗口类结构定义中wcApp,1pfnWndProc域的名字能够与回调函数的名
字匹配。后面用这个窗口类创建的所有窗口的回调函数都应该用WndProc()的名字。
下面的代码段讨论一个窗口类结构中回调函数名的位置和赋值:
. . . wcApp.1pszClassName=szProgName; wcApp.hInstance=hInst; wcApp.1pfnWndProc=WndProc; . . .
Windows有向百个消息可以发送给窗口函数。这些消息用“WM-”打头的标识符来
标识。
WndProc()的第一个参数是hWnd。hWnd包含了Windows发送消息的窗口句柄。
函数的第二个参数messg按WINUSER.H中的定义指明了即将被处理的实际消息。最后的两个参数wParam以及1Param,指明了处理每个消息所需的附加信息。
WndProc()函数继续定义了两个变量:hdc指明了显示设备句柄,ps指明了存储用户区
信息所需的一个PAINTSTRUCT结构。
回调函数被用于检查将被处理的消息并选择执行适当的动作。这个选择过程通常在一个标准的C语言的 ?
第一节 关于声音
声音是空气的一系列振荡,称为声波,一般可以用二维的波形图来表示。数字音频是指使用某种设备将声波记录下来并保存为一种数字化的文件。播放相应的文件就可以产生某种声音效果。数字音频的音质随着采样频率及所使用的位数不同而有很大的差异。因此,了解所使用音频文件格式的有关标准是很有必要的。例如,CD中的音频是16位,采样频率达到44.1MHz的立体声数字音频。
在所有声音文件的格式中,WAV是最普遍的。这是Windows平台上最常见的格式,由微软公司创造。支持8位和16位的音质、多样本、对立体声和单声道音频均可播放。它还支持多种音频压缩算法。
要在游戏中取得好的声音效果,例如,使用3D音效,可以有两种方法来实现:一是使用一定的工具软件对声音文件进行处理,生成播放效果足够好的文件,然后在游戏程序中直接将这样的文件播放。显然,这样比较简单,但是不灵活。如果需要音效随着游戏场景的变化而不断改变,且不受所具有声音文件数量的限制,就需要进行实时混音了。
第二节DirectSound结构
DirectSound的功能模块包括播放、声音缓冲区、三维音效、音频抓获、属性集等。
DirectSound playback建构于IDirectSound COM接口之上。IDirectSoundBuffer,IDirectSound3DBuffer和IDirectSound3DListener接口则用以实现对声音缓冲区和三维音效的操作。
DirectSound capture建构于IDirectSoundCapture和IDirectSoundCaptureBuffer COM接口之上。
其它的COM接口,如IKsPropertySet,使应用程序能够从声卡的扩展功能中最大地受益。
最后,IDirectSoundNotify接口用于在播放或音频抓获达到一定地方时向产生一个事件。
第三节 播放功能概述
DirectSound缓冲区对象表示一个包含声音数据的缓冲区,这些数据以PCM格式被存储。该对象不仅可以用于开始、停止或暂停声音的播放,还能够设置声音数据中诸如频率和格式等属性。
缓冲区分为主缓冲区和副缓冲区。主缓冲区中是听者将要听到的音频信号,一般是将副缓冲区中信号混音后的结果。而副缓冲区中存放着许多单独的声音信号,有的可以直接播放,有的要混音,有的循环播放。主缓冲区由DirectSound自动创建,而副缓冲区需由应用程序来创建。DirectSound将副缓冲区中的声音混合后,存入主缓冲区,再输出到相应播放设备。
DirectSound中没有解析声音文件的功能,需要您自己在应用程序中将不同格式的声音信号改变过来(PCM)。
缓冲区可以在主板的RAM、波表存储器、DMA通道或虚拟存储器中。
多个应用程序可以用同一声音设备来创建DirectSound对象。当输入焦点在应用程序中发生变化时,音频输出将自动在各个应用程序的流之间切换。于是,应用程序不用在输入焦点改变中反复地播放和停止它们的缓冲区。
通过IDirectSoundNotify接口,当播放到了一个用户指定的地方,或播放结束时,DirectSound将动态地通知拥护这一事件。
第四节 音频抓获概述
DirectSoundCapture对象可以查询音频抓获设备的性能,并为从输入源抓获音频而创建缓冲区。
其实,在Win32中早已经有了抓获音频的功能,而目前的(版本5)DirectSoundCapture与只比较并没有什么新的功能。不过,DirectSoundCapture API使您能够编写使用相同接口的播放和音频抓获程序,而且,这也为将来可能出现的API改进提供了原始模型,使您可以从中受益。
DirectSoundCapture还能够抓获压缩格式的音频。
DirectSoundCaptureBuffer对象表示一个用于抓获音频的缓冲区。它可以循环利用,也就是说,当输入指针达到缓冲区的最后时,它会回到开始的地方。
DirectSoundCaptureBuffer对象的各种方式使您能够设定缓冲区的属性、开始或停止操作、锁定某部分存储器(这样就可以安全地将这些数据保存或用于其它目的)。
与播放类似,IDirectSoundNotify接口使在输入指针到达一定地方时通知用户。
第五节 初始化
对于一些简单的操作,可以使用缺省的首选设备。不过,在游戏的制作中,我们可能还是需要知道一些特定的声音设备。于是,您应该先列举出可用的声音设备。
在此之前,您需要先设定一个回收函数,在每一次DirectSound发现新设备后调用该函数。函数中您可以做任何事情,但您必须将它定义得与DSEnumCallback形式相同。如果希望列举继续,函数应返回真,否则返回假。
下面的例程来自光盘Example目录下的Dsenum.c文件。它列举可用的设备并在一个列表框中增加一条相应的信息。首先是他的回收函数:
BOOL CALLBACK DSEnumProc(LPGUID lpGUID, LPCTSTR lpszDesc, LPCTSTR lpszDrvName, LPVOID lpContext ) { HWND hCombo = *(HWND *)lpContext; LPGUID lpTemp = NULL; if( lpGUID != NULL ) { if(( lpTemp = LocalA